很多人认为 java 冗余,啰嗦。在 Java 8 之前也许的确是这样,但是Java8 14年到现在,Java11马上就要来了,Java已经有了巨大的改变,lambda, Stream,Flow api 等等,Spring5 完全基于Java8 开发。因此现在到了必须学习Java 8新特性的时候。

现在就来详解Java8 最重要的特性,lambda和函数接口,走进函数式编程大门,本片文章主要详解使用,对原理不会深究。

Java8 之前,一般都是通过匿名类实现回调接口。

1
2
3
4
5
6
7
8
9
10
11
public class Hello {
public static void main(String[] args) throws Exception{
Runnable run = new Runnable() { //匿名类 编译时会生成class文件
@Override
public void run() {
System.out.println("hello world!");
}
};
new Thread(run).start(); //匿名对象
}
}

看看匿名类生成的文件

1
2
3
4
5
6
7
class Hello$1 implements Runnable {
Hello$1() {
}
public void run() {
System.out.println("hello world!");
}
}

Java8 之后lambda和方法引用很简单美观实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
lambda 的实现通过 java7 新增的invokedynamic指令实现的,不必在编译的时候确定。
而且新增 java.lang.invoke 包(JSR 292)
*/
Runnable run = () -> System.out.println("hello world!"); //lambda
Runnable run1 = Hello::say; //方法引用 和 lambda 效果一样
Runnable run2 = sayHello("hello world!");
new Thread(run).start();

public static void say() {
System.out.println("hello world!");
}
public static Runnable sayHello(String content) {
return () -> System.out.println(content); //lambda
}

我们先来学习一下什么是函数接口,Java最大缺点就是方法(函数)无法独立存在,除了基本类型就只有对象了,不像其他语言可以直接把方法当参数传递(如C#委托),只能通过变通的方式如demo中的匿名内部类,因此Java8引入函数接口来表示方法类型,函数接口概念非常简单,只有一个抽象方法的接口就是函数接口。注:Java8 接口可以有任意多个静态方法和默认方法,Java9 又添加了私有方法,因此接口甚至可以完全取代抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FunctionalInterface //标识一个接口是不是函数接口,只起标识作用,有没有并不影响是不是函数接口的判定
public interface Consumer<T> {
void accept(T t); //抽象方法

default Consumer<T> andThen(Consumer<? super T> after) { //默认方法,属于接口
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
/*
Runnable,Consumer 都是函数接口。现在来看看函数接口的特点。
1.接口只有一个抽象方法。
2.确定方法的入参(形参),Consumer中泛型T.
3.确定方法的返回值。
事实上,一个函数接口完全可以确定一类方法。
如Consumer类型的方法,都是只有一个入参没有返回值,Runnable是不需要参数和没有返回值的一类方法。
*/

Java8 新增 java.util.function 包表示常用的函数接口,我们来看一下 这些接口具体表示的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
大致分为下面4类  <T> 代表泛型对象,不能为基本类型,Java 这一点还是很蛋疼
1.Consumer //代表只有一个参数<T>没有返回值的一类方法
2.Function //代表只有一个参数<T>有返回值<V>的一类方法
3.Predicate //代表只有一个参数<T>有返回值<boolean>的一类方法
4.Supplier //代表没有一个参数,有返回值<T>的一类方法
5.UnaryOperator<T> extends Function<T, T> //只有一个参数<T>有返回值<T>的一类方法
同理其他的一看就知道是什么了
BiConsumer // Bi代表Binary两个, 参数<T, U> -> void, 没有返回值
BiFunction // <T, U> -> R
BinaryOperator<T> extends BiFunction<T,T,T> //<T, T> -> T
BiPredicate // <T, T> -> boolean

因为Java泛型不能表示基本类型,所以提供了基本类型的函数接口。
IntBinaryOperator // <int, int> -> int
IntConsumer // int -> void
IntFunction // int -> R *注意* 接受的是int,返回的是对象 (diff to) ToIntFunction
IntPredicate // int -> boolean
IntSupplier // void -> int
IntUnaryOperator // int -> int
To 开头代表返回
ToDoubleBiFunction // <T, U> -> double
ToDoubleFunction // T -> double
ToIntBiFunction // <T, U> -> int
ToIntFunction // T -> int
ToLongBiFunction // <T, U> -> long
ToLongFunction // T -> long
其他的想 ObjDoubleConsumer, ObjIntConsumer, ObjLongConsumer 之类的也可以望文生义了。这里再一次吐槽Java 基本类型的类类型的差别。所以 Stream 也有了与基本类型对应 IntStream,LongStream,DoubleStream

介绍完函数接口后,我们再来介绍一下 lambda 表达式 和 方法引用。

所谓 lambda 表达式简单理解就是一个匿名方法,可以简化方法的定义。

1
2
3
4
5
6
7
// Js C++, C# 等语法 使用 => 连接参数和返回值, Java不同,使用 ->
let add = (a, b) => a + b;
add(1, 2); // 3

// Java
IntBinaryOperator intAdds = (a, b) -> a + b; // intAdds 是一个函数对象
intAdds.applyAsInt(1, 2); //3 调用 intAdds的方法,这也是函数接口只能有一个抽象方法的原因。

看到什么了吗?虽然 lambda 没有 Js 那么灵活。但是 没有lambda 之前,Java 方法只能所属于具体类,不能单独存在(即所谓的方法不是一级公民),方法无法临时创建,也无法传递。而动态语言和其他支持 lambda 的静态语言,就很灵活,因为Java 与时俱进,在Java 8 中支持了此特性。

没有lambda 之前,Java 无法创建局部方法[即在方法中定义方法]想要实现回调方法一般只能定义一个匿名类,生成其对象,然后传入参数,如文章最开始的时候 。Java8 之后 集合框架加入了大量 函数式支持。

1
2
3
4
5
6
7
8
9
10
List<String> list = Arrays.asList("hello", "world", "!!!", null);
//void forEach(Consumer<? super T> action) 接受一个Consumer对象,即 T -> void
list.forEach(s -> System.out.println(s)); //输出每一项,如果我们求每一项长度,lambda可能就没那么美观了
list.forEach(s -> { //超过一行的 lambda 方法体用要用 {} 括起来
if (s == null) {
System.out.println("字符串为null");
return;
}
System.out.println(s.length());
});

如果每次都要写很长的 lambda,那其实没省效率,而且代码重用率极低。因此,方法引用登场。

1
2
3
4
5
/* 所谓方法引用 即方法是一种类型,是函数接口的引用,fun 就是方法的引用, 右边就是实际方法对象[lambda]
eg: Object obj = "hello"; obj 是引用, "hello" 是实际对象
方法引用相当于 对现有所有方法的重复利用 */
Function<String, Integer> fun = s -> s.length(); // 相当于下面
Function<String, Integer> fun1 = String::length;

:: 方法引用运算符,标准形式是:类名::方法名 有下面四种形式引用静态方法

  1. Class::staticMethodName 引用静态方法[类名::静态方法名]

  2. obj::instanceMethodName 引用某个对象的实例方法[对象::实例方法名]

  3. ClassName::new 引用构造方法

  4. Type::methodName 引用某个类型的任意对象的实例方法

下面重点介绍一下,尤其是令人困惑第四种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 下面以 String 的方法为例
Function<Object, String> fun = String::valueOf; //形式1, 等价于 obj -> String.valueOf(obj)

private void sayHello(String content) { // Hello 类有 sayHello实例方法
System.out.println();
}
Hello hello = new Hello();
Consumer<String> con = hello::sayHello; //形式2 通用实例方法引用 等价于 s -> hello.sayHello(s)
Consumer<String> con1 = this::sayHello; //形式2 在此类(Hello)实例方法可以使用 s -> this.sayHello(s)

Supplier<Hello> supplier = Hello::new; // 形式3 () -> new Hello();
Hello hello1 = supplier.get(); //每次调用产生新的Hello对象,所以叫供应商

BiConsumer<Hello, String> biCon = Hello::sayHello; //形式4, 相当于 hello::sayHello
sayHello.accept(hello, "hello"); //不过运行是需要传递这个对象
/* *某个类型的任意对象的实例方法*, 不像形式2,此方法应经绑定了对象,
在引用是并不知道是哪个对象,必须传入,而且一定是第一个参数,如 hello,也可以传入Hello子类对象 */

public int length(); //String length方法签名,按照之前定义的无参,有返回值
//void -> int 明明是 Supplier<Integer>/IntSupplier 为什么变成了 Function T -> R
//这是因为 形式4 并不能知道调用方法的是哪个对象,必须传入【第一个参数】才知道
Function<String, Integer> length = String::length;
int len = length.apply("hello world"); //求任意String类型对象的长度

介绍了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
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 除了 java.util.function 包之外的 常用的函数接口
Runnable //@since 1.0 线程任务接口
Callable<V> //@since 1.5 有返回值的线程任务接口
Comparable<T> //@since 1.2 内比较器 int compareTo(T o)
Comparator<T> //@since 1.2 外比较器 常用于排序
Thread.UncaughtExceptionHandler //@since 1.5 线程异常处理接口

//lambda 组合,我们以 Function 为例子,看一下 Function 源码
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

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

static <T> Function<T, T> identity() {
return t -> t;
}
}
// 抽象方法apply,两个默认方法compose,andThen,静态方法identity
Function<Integer, Integer> square = e -> e * e; // e的平方
Function<Integer, Integer> dou = e -> 2 * e; // e的双倍
int a = square.compose(dou).apply(3); //36
int b = square.andThen(dou).apply(3); //18
int c = dou.compose(square).apply(3); //18
int d = dou.andThen(square).apply(3); //36
//compose 在本身之前先算 andThen 在本身之后再算
(V v) -> apply(before.apply(v)); //先计算 before.apply(v)
(T t) -> after.apply(apply(t)); //apply(t)
//如果能理解源码,当然最好,初学者可以记住,compose从最后往前算,andThen重前往后算
// ((4 * 3)^2 * 2 * 3)^2 * 2 = 1492992
int e = dou.andThen(tripe).compose(square).andThen(square).compose(tripe).andThen(dou).apply(4);

//lambda 组合 常用的场景之一就是排序 Comparator
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
****多个 default method;
}
/* Comparator 竟然有两个抽象方法 compare,equals 不是说函数接口只能有一个抽象方法吗?equals 其实是 Object 类的方法,**所有引用类型都继承Object,当然包括接口**,所以函数接口中可以重新声明Object类的方法。*/
@FunctionalInterface
interface Compare<T> extends Comparable{
int add(T a, T b); //这个就不是函数接口,无法通过编译
}

List<String> list = new ArrayList<>(Arrays.asList("he", "b hello", "a hello world!", "world", "hello", "ha"));
//多条件组合排序 先根据长度排序再根据自然排序 [ha, he, hello, world, b hello, a hello world!]
list.sort(Comparator.comparing(String::length).thenComparing(Comparator.naturalOrder()));

Java8 支持 lambda之后, 9,10,11 版本都进行了扩展,虽然 lambda 效率低于匿名类,但是获得更加简洁的写法,更强的语义。Java Collection, Map, Arrays 添加了大量lambda api,大家在写代码的时候学以致用,下面一篇文章将详解Java8新增的 Optional 和 Stream。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 先看看 Stream 的强大作用
// List 去除 null, 去重后求和
List<Integer> nums = Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10);
int sum = nums.stream().filter(Objects::nonNull).distinct().mapToInt(Integer::intValue).sum();

// List 分组 根据城市分组
public class Employee { // 员工
private String name;
private String city;
private int number;
****
}
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("张三", "杭州", 100));
employees.add(new Employee("小兰", "嘉兴", 85));
employees.add(new Employee("李四", "杭州", 80));
employees.add(new Employee("王二", "兰州", 95));
// Stream 一行代码搞定, Stream 就是对集合的增强,我们下篇见
Map<String, List<Employee>> result =
employees.stream().collect(Collectors.groupingBy(Employee::getCity));


Java SE      java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!