Java函数式编程

Java函数式编程

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口(可以有非抽象方法)。函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。

@FunctionalInterface注解

Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface。该注解可用于一个接口的定义上。一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口。

1
2
3
4
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}

函数式编程

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么(What),而不是怎么做(How)。重视的是结果,而不是过程

Lambda表达式

JDK8,必须具有函数式接口才能使用。如:RunnableComparator

Lambda 标准格式

1
(参数类型 参数名称) -> { 代码语句 }

Lambda省略格式

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

Lambda表达式作为参数

如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。

1
2
3
4
5
6
7
8
9
10
11
12
// 实现Runnable接口
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}).start();

// 实现Comparator接口
Student[] arr = new Student[] {
new Student("aa", 18),
new Student("bb", 16)
};

Arrays.sort(arr, (s1, s2) -> s1.age - s2.age);

Lambda表达式作为返回值

如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。

1
2
3
4
5
6
7
8
private static Comparator<String> newComparator() {
return (a, b) ‐> b.length() ‐ a.length();
}

public static void main(String[] args) {
String[] array = { "abc", "ab", "abcd" };
Arrays.sort(array, newComparator());
}

Lambda的延迟

有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。

性能浪费的日志案例

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";

// 无论level为多少都会进行字符拼接操作,但只有level=1时,字符串拼接才是有意义的
log(1, msgA + msgB + msgC);
}

优化

SLF4J在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。

例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS")

Lambda方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface MessageBuilder {
String builderMessage();
}

public class DemoLambda {
public static void main(String[] args) {
String log1 = "log1";
String log2 = "log2";
String log3 = "log3";

// 只有当level等级为1时,才会调用builderMessage方法,进行字符串拼接
System.out.println(getMessage(1, () -> log1 + log2 + log3));
}

private static String getMessage(int level, MessageBuilder mb) {
if (level == 1) {
System.out.println("条件满足才执行");
return mb.builderMessage();
} else {
return null;
}
}
}

常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景, 主要在java.util.function 包中被提供。下面是最简单的几个接口及使用示例。

Supplier接口

java.util.function.Supplier<T> 接口仅包含一个无参的方法: T get() 。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int a = 1;
int b = 2;
System.out.println(sum(() -> a+b));
}

private static int sum(Supplier<Integer> sup) {
return sup.get();
}

Consumer接口

java.util.function.Consumer<T> 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。抽象方法 void accept(T t) ,意为消费一个指定泛型的数据。

默认方法:andThen

如果一个方法的参数和返回值有多个Consumer 类型,那么就可以实现:消费数据的时候,先组合, 再调用accept方法消费数据。

1
2
3
4
5
6
// andThen 源码
default Consumer<T> andThen(Consumer<? super T> after) {
// Objects.requireNonNull: 参数为null时主动抛出NullPointerException 异常
Objects.requireNonNull(after);
return (T t) ‐> { accept(t); after.accept(t); };
}

例子

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
show("AcBBsfasdf",
s -> System.out.println(s.toUpperCase()), // 先转为大写
s -> System.out.println(s.toLowerCase()) // 再转为小写
);
}

public static void show(String data, Consumer<String> s1, Consumer<String> s2) {
s1.andThen(s2).accept(data);
}

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate接口。

  • 抽象方法boolean test(T t)

  • 默认方法:and、or、negate , 对应与、或、非。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
String[] arr = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };

// 选名字为3个字的女性
List list = FilterName(arr,
name -> name.split(",")[1].equals("女"),
name -> name.split(",")[0].length() == 3
);

System.out.println(list);
}

private static List FilterName(String[] arr, Predicate<String> p1, Predicate<String> p2) {

List<String> list = new ArrayList<>();
for (String name : arr) {
if (p1.and(p2).test(name)) {
list.add(name.substring(0, 3));
}
}
return list;
}

Function接口

java.util.function.Function接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。

抽象方法R apply(T t) ,根据类型T的参数获取类型R的结果。

默认方法:andThen 用来进行组合操作。源码如下:

1
2
3
4
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) ‐> after.apply(apply(t));
}

例子: 将一个字符型的数字转为int型的,然后再计算这个数的乘方。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
String num = "99";
int ret = getNumNum(num, s -> Integer.parseInt(s), s -> s * s);
System.out.println(ret);
}

public static int getNumNum(String num, Function<String, Integer> f1, Function<Integer, Integer> f2) {
return f1.andThen(f2).apply(num);
}

Stream流

Java8引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

传统集合的多步遍历的弊端:

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

  • for循环的语法就是“怎么做”
  • for循环的循环体才是“做什么”

流式思想

流式思想类似与工厂车间的“生产流水线。当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案一步步的去执行它。

Stream流

  • 是一个集合元素的函数模型,不是集合,也不是数据结构,其本身并不存储任何元素或者地址值
  • 是一个来自数据源的元素队列,数据源可以是集合,数组 等。

Stream操作特征

  • Pipelining: 中间操作都会返回流对象。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。

获取流

java.util.stream.Stream 是Java 8新加入的最常用的流接口

  • 所有的Collection集合都可以通过stream默认方法获取流;

  • Stream接口的静态方法of可以获取数组对应的流。

    • public static <T> Stream<T> of(T... values)

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Stream.of方法
Stream<String> stream1 = Stream.of("aa", "bb", "cc");

// Map: key, value, entry
Map<Integer, String> map = new HashMap<>();
map.put(1, "aa");
map.put(2, "bb");

Set<Integer> keySet = map.keySet();
Stream<Integer> stream2 = keySet.stream();

Collection<String> values = map.values();
Stream<String> stream3 = values.stream();

Set<Map.Entry<Integer, String>> entryset = map.entrySet();
Stream<Map.Entry<Integer, String>> stream4 = entryset.stream();

常用方法

  • 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。

映射:map

  • <R> Stream<R> map(Function<? super T, ? extends R> mapper);:
  • 将流中的元素映射到另一个流中

逐一处理:forEach: 是一个终结方法

  • void forEach(Consumer<? super T> action);

过滤:filter

  • Stream filter(Predicate<? super T> predicate);
  • 函数返回false过滤

reduce操作

  • reduce(initialValue, accumulator)
  • 例:实现累加操作
    • list.stream().reduce(0, (acc, e) -> acc + e)
    • 第一个参数0,是acc的初始值, acc记录每次计算的结果

统计个数:count: 是一个终结方法

  • long count();

最大值/最小值:max / min

  • 需要传递一个Comparator对象

取用前几个:limit

  • Stream limit(long maxSize);: 对流进行截取,只取用前n个。

跳过前几个:skip

  • Stream skip(long n);: 流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

组合:concat

  • static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

collect

  • collect(Collectors.toList()), 可将流中数据存入一个list集合并返回

例子: 使用Stream流优化下面操作

现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,请依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
  2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
  3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
  4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
  5. 将两个队伍合并为一个队伍;存储到一个新集合中。
  6. 根据姓名创建 Person 对象;存储到一个新集合中。
  7. 打印整个队伍的Person对象信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<String> one = List.of("迪丽热巴", "宋远桥" , "苏星河", "石破天", "石中玉", "老子", "庄子", "洪七公");

List<String > two = List.of("古力娜扎", "张无忌", "赵丽颖", "张三丰", "尼古拉斯赵四", "张天爱");

Stream<String> stream1 = one.stream()
.filter(name -> name.length() == 3).limit(3);

Stream<String> stream2 = two.stream()
.filter(name -> name.startsWith("张")).skip(2);

Stream<String> stream3 = Stream.concat(stream1, stream2);

ArrayList<Person> person = new ArrayList<>();
stream3.forEach(name -> person.add(new Person(name)));

System.out.println(person);

方法引用

在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

方法引用符

双冒号::为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

通过对象名引用成员方法

如果一个类中已经存在一个能过实现我们功能的成员方法,直接引用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@FunctionalInterface
interface printAble { void print(String s);}

class printUpper {
// 将字符串转为大写再打印
public void printUpperCase(String s) {
System.out.println(s.toUpperCase());
}
}

public class Demo1 {
public static void main(String[] args) {
printUpper obj = new printUpper();
print("hello world", obj::printUpperCase);
}

public static void print(String s, printAble p) {
p.print(s);
}
}

通过类名称引用静态方法

如果已经存在了一个能过实现我们功能的静态方法,直接引用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
interface printAble { void print(String s);}

public class Demo1 {
public static void main(String[] args) {
printString("hello world", s -> System.out.println(s));
printString("hello world", System.out::println); // 引用java提供的函数
}

public static void printString(String s, printAble p) {
p.print(s);
}
}

通过super引用成员方法

如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。

  • Lambda表达式:() -> super.父类方法
  • 方法引用: super::父类方法

通过this引用成员方法

如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法的格式来使用方法引用。

类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名::new的格式表示。

  • Lambda表达式: name -> new Person(name)
  • 方法引用: Person::new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
interface PersonBuilder {
Person buildPerson(String name);
}

public class ConStrucorRef {
public static void main(String[] args) {
printPersonName("tan", (name -> new Person(name)));
printPersonName("tan", Person::new);
}

public static void printPersonName(String name, PersonBuilder p) {
System.out.println(p.buildPerson(name).getName());
}
}

数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

  • Lambda表达式:length -> new int[length]
  • 方法引用:int[]::new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionalInterface
interface ArrayBuilder {
int[] buildArray(int length);
}

public class ArrayConStructorRef {
public static void main(String[] args) {
int[] arr1 = createArray(10, (len) -> new int[len]);
int[] arr2 = createArray(10, int[]::new);
System.out.println(arr1.length + ", " + arr2.length);
}

public static int[] createArray(int length, ArrayBuilder arrb) {
return arrb.buildArray(length);
}
}

 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×