Java8 新特性(一) - Lambda 表达式
近些日子一直在使用和研究 golang,很长时间没有关心 java 相关的知识,前些天看到 java9 已经正式发布,意识到自己的 java 知识已经落后很多,心里莫名焦虑,决定将拉下的知识补上。
Lambda 表达式的渊源
Java8 作为近年来最重要的更新之一,为开发者带来了很多新特性,可能在很多其他语言中早已实现,但来的晚总比不来好。Lambda 表达式就是 Java8 带来的最重要的特性之一。
Lambda 表达式为 Java8 带来了部分函数式编程的支持。Lambda 表达式虽然不完全等同于闭包,但也基本实现了闭包的功能。和其他一些函数式语言不一样的是,Java 中的 Lambda 表达式也是对象,必须依附于一类特别的对象类型,函数式接口。
为什么需要 Lambda 表达式
内循环 VS. 外循环
先看一个非常简单的例子, 打印 list 内所有元素:
List<Interger> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)
for (int number: bumbers) {
System.out.println(number)
}
作为一个 Java 开发者,你这一生可能已经写过无数次类似代码。看上去好像挺好的,没有什么需要改进的,我们显式的在外部迭代遍历 list 内元素,并挨个处理其中元素。那为什么提倡内部迭代呢,因为内部迭代有助于 JIT 的优化,JIT 可以将处理元素的过程并行化。
在 Java8 之前,需要借助 Guava 或其他第三方库来实现内部迭代,而在 Java8 中, 我们可以用以下代码实现:
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
以上代码还是稍显繁琐,需要创建一个匿名类,使用 lambda 表达式后,可以大大简化代码
list.forEach((a) -> System.out.println(a));
Java 8 中 还引入了双冒号运算符,用于类方法引用,以上方法可以进一步简化为
list.forEach(System.out::println);
内循环描述你要干什么,更符合自然语言描述的逻辑
passing behavior,not only value
通过 lambda 表达式,我们可以在传参时,不仅可以将值传入,还可将相关行为也传入,这样可以实现更加抽象和通用,更易复用的 API。看一下代码例子,需要实现一个求 list 内所有元素和的方法,嗯,看上去很简单。
public int sumAll(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
这个时候,又有需求实现一个 list 内所有偶数和的方法,简单,代码复制一遍,稍作修改。
public int sumAllEven(List<Integer> numbers) {
int total = 0;
for (int number : numbers) {
if (number % 2 == 0) {
total += number;
}
}
return total;
}
也没发多少功夫,还需要改进么,这个时候又需要所有奇数和呢,不同的需求过来,你需要一遍又一遍的复制代码。有没有更加优雅的解决方法呢?我们又想起了我们的 lambda 表达式,java 8 引入了一个新的函数接口 Predicate<T>
, 使用它来定义 filter,代码如下
public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
int total = 0;
for (int number : numbers) {
if (p.test(number)) {
total += number;
}
}
return total;
}
这样以上两个方法都可以通过这个方法实现,并且可以非常容易的扩展,当你需要用其他条件实现元素筛选求和时,只需要实现筛选条件的 lambda 表达式,如下
System.out.println(sumAll(list, (a)-> true)); \\ 所有元素和
System.out.println(sumAll(list, (a) -> a % 2 == 0)); \\ 所有偶数和
System.out.println(sumAll(list, (a) -> a % 2 != 0)); \\ 所有奇数和
有同学会说,以前不用 lambda 表达式我们用接口也能实现。没错,用接口 + 匿名类也能实现类似效果,但 lambda 表达式更加直观,代码简捷,可读性也强,开发者也更有动力使用类似代码。
利于写出优雅可读性更高的代码
先看一段代码:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
for (int number : list) {
if (number % 2 == 0) {
int n2 = number * 2;
if (n2 > 5) {
System.out.println(n2);
break;
}
}
}
这个代码也不难理解,取了 list 中的偶数,乘以 2 后 大于 5 的第一个数,这个代码看上去不难,但是当你在实际业务代码中添加更多的逻辑时,就会显得可读性较差。使用 Java 8 新加入的 stream api 和 lambda 表达式重构这段代码后,如下
System.out.println(
list.stream()
.filter((a) -> a % 2 == 0)
.map((b) -> b * 2)
.filter(c -> c > 5)
.findFirst()
);
一行代码就实现了以上功能,并且可读性也好,从做至右依次读过去,先筛选 偶数,在乘以 2, 再筛选大于 5 的数,取第一个数。并且 stream api 都是惰性的api,且不占用多余的空间,比如上面这段代码,并不会把list 中所有元素都遍历,当找到第一个符合要求的元素后就会停止。
Lambda 表达式语法
Lambda 表达式的语法定义在 Java 8 规范 15.27 中,并给出了一些例子
() -> {} // 无参数,body 为空
() -> 42 // 无参数,表达式的值作为返回
() -> {return 42;} // 无参数,block 块
() -> {System.gc();}
() -> {
if (true) return 23;
else {
return 14
}
}
(int x) -> {return x + 1;} // 有参数,且显式声明参数类型
(int x) -> x + 1
(x) -> x + 1 // 有参数,未显式声明参数类型,编译器推断参数类型
x -> x + 1
(int x, int y) -> x + y
(x, y) -> x + y
(x, int y) -> x + y // 非法, 参数类型显示指定不能混用
总结一下:
- Lambda 表达式可以具有零个,一个或多个参数。
- 可以显式声明参数的类型,也可以由编译器自动从上下文推断参数类型。
- 参数用小括号括起来,用逗号分隔。例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)
- 空括号用于表示一组空的参数。
- 当仅有一个参数时,且不显式指明类型,则可省略小括号
- Lambda 表达式的正文可以包含零条,一条或多条语句。
- 如果 Lambda 表达式的正文只有一条语句,则大括号可不用写
- 如果 Lambda 表达式的正文有一条以上的语句必须包含在代码块中
Functional Interface (函数接口)
还有一个问题,在上面的内容没有提到,怎样在声明的时候表示 Lambda 表达式呢?比如函数可以接受一个Lambda表达式作为输入。Java 8 引入了一种新的概念,叫函数接口。其实说起来也不是什么新鲜东西,函数接口就是一种只包含一个抽象方法的接口(可以包含其他默认方法),同时 Java 8 引入一个新的注解 @FunctionalInterface
,虽然不使用 FunctionalInterface
注解也可以使用,但是使用注解可以显式的声明该接口为函数接口,并且当接口不符合函数接口要求时,在编译期间抛出错误。之前 Java 已有的很多接口加上了该注解,最常见的比如 Runnable
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
也就是说,现在启动一个线程时,可以采用新的 Lambda 表达式
new Thread(
() -> System.out.println("hello world")
).start()
之前已经存在的接口还有
java.lang.Comparable
java.util.concurrent.Callable
Java 8 中还新加了一些函数接口
java.util.function.Consumer<T> // 消费一个元素,无返回
java.util.function.Supplier<T> // 每次返回一个 T 类型的对象
java.util.function.Predicate<T> // 输入一个元素,返回 boolean 值,常用于 filter
java.util.function.Function<T,R> // 输入一个 T 类型元素,返回一个 R 类型对象
Lambda 表达式与匿名类
看上面的内容,一定会有人认为这些功能我使用匿名类也可以实现,那 Lambda 表达式和匿名类有什么区别呢。最明显的区别就是 this 指针,this 指针在匿名类中代表是匿名类,而在 Lambda 表达式中为包含 Lambda 表达式的类。同时,匿名类可以实现多个方法,而 Lambda 表达式只能有一个方法。
直观上,很多人会觉得 Lambda 表达式可能只是一个语法糖,最终转换为一个匿名类。事实上,考虑到实现效率问题,和向前兼容问题,Java 8 并没有采用匿名类语法糖,也没有和其他语言一样,采用专门的函数处理类型来实现 lambda 表达式。
lambda 实现
既然 lambda 表达式并未用匿名类的方式实现,那其原理到底是什么呢,之前我们分析泛型的时候都是分析字节码,这里也一样。我们先看一段代码和字节码。
public class LambdaStudy004 {
public void print() {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.forEach(x -> System.out.println(x));
}
}
javap -p 结果
public class lambda.LambdaStudy004 {
public lambda.LambdaStudy004();
public void print();
private static void lambda$print$0(java.lang.Integer);
}
很明显,lambda 表达式编译后,会生成类的一个私有静态方法,然而,事情并没有那么简单,虽然生成了一个静态方法,lambda 表达式本身又由什么表示呢,java 中没有函数指针,总要有一个类作为载体调用该静态方法。
javap -p -v 查看字节码
...
37: invokedynamic #5, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
42: invokeinterface #6, 2 // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
47: return
...
和普通的 static 方法调用采用 invokestatic
指令不一样,lambda 表达式的调用采用了 java 7 新引入的 invokedynamic
指令,该指令是为了加强 java 的动态语言特性引入,当 invokedynamic
指令被调用时,会调用 metafactory
函数动态生成一个实现了函数接口的对象,该对象实现的方法实际调用了之前生成的 static 方法,这个对象才是 lambda 表达式的实际翻译后的表示,翻译代码如下
class LambdaStudy004Inner {
private static void lambda$print$0(Integer x) {
System.out.println(x);
}
private class lambda$1 implements Consumer<Integer> {
@Override
public void accept(Integer x) {
LambdaStudy004Inner.lambda$print$0(x);
}
}
public void print() {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.forEach(new LambdaStudy004Inner().new lambda$1());
}
}
具体引入 invokedynamic
实现 Lambda 表达是的原因可以看 R 大的解释, 传送门: Java 8的Lambda表达式为什么要基于invokedynamic