Java8,准确地说应该是JavaSE 8,发布挺长时间了(废话!Java9今年上半年都要出了!),但除了实习那会听同事做了场分享,自己一直没有系统地学习一下。这些天整了本《写给大忙人看的JavaSE 8》,抽点时间读一读,这个系列的博客当是读书笔记了。这第一部分是lambda表达式。

何为lambda表达式

lambda表达式是一段可以传递的代码。

过去在多线程、回调等情境(大多是内部类?)下,我们都会把一段代码传递给其他调用者,而这段代码稍后才会被调用。而纯面向对象的Java是不支持传递代码块的,但在Java8中,可以。

第一个lambda表达式,包括两部分——一是代码块本身,二是传递给代码块的参数,注意lambda的返回类型永远是推导来的,不需要显式指定。

1
2
// 格式:参数+箭头+表达式
(int a, int b) -> foo.bar(a, b);

但lambda的格式也不是说就这么死板,有几种特殊情况:如果代码块不是一个表达式(多行代码、处理异常等),那么就用大括号包裹起来;如果没有参数,那么将小括号置空;如果参数的类型是可以推导的,那么参数类型可以省略;如果有且只有一个可以被推导的参数,那么甚至可以省略小括号。

lambda表达式能做什么

lambda表达式有且只能用于函数式接口转换

对只包含一个抽象方法的接口,我们可以用lambda表达式来创建该接口的实例,这种接口也被称作函数是接口。而所谓“一个抽象方法”,则是因为Java8允许在接口中定义非抽象方法。

以java.util.Comparator接口为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Comparator<T> {
//抽象方法
int compare(T var1, T var2);
//Object类中有实现,不是抽象方法
boolean equals(Object var1);
//有默认实现,不是抽象方法
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
//以下省略
}

符合上面对函数式接口的定义,因此Comparator是函数式接口,可以通过lambda表达式实例化

1
Comparator comparator = (a, b) -> foo.bar(a, b);

至于注解@FunctionalInterface,意识可以让编译器帮着检查这个接口符不符合函数式接口的定义,二是生成JavaDoc时对这个接口有一个标记,因此推荐使用。

方法引用

对于类似这样的lambda表达式,我们有更方便的写法

1
2
3
//下面两者是等价的
(x) -> foo.bar(x)
foo::bar

实际上就是将foo这个对象中的boo方法,整个传递给某个函数式接口。方法引用又分成三类

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

前两个没有什么特殊之处,注意对象可以是自己或者自己的父类(即this或super),但最后一个,类如何调用实例方法呢?实践后会发现,这种情况下总要求函数式接口中那个抽象方法的参数要比引用的方法的参数多一个,而且这多出来的参数就是类的实例。

更进一步地,我们还可以引用类的构造方法,不过方法名不是我们在反射时见到的,就是简单明了的new了~再特殊一点,如果引用的是数组的构造方法,则需要隐含这一个参数,即数组长度

1
2
3
4
5
6
7
//引用类的实例方法,下面两者也是等价的
String::equals
(x, y) -> x.equals(y)
//引用构造方法
String::new
int[]::new

捕获

在Java8以前,这样的代码会产生编译错误:要求将count声明为final类型,因为我们在一个内部类中引用了外部类的局部变量count,很可能当内部类要用到count时,外部类已经不存在了

1
2
3
4
5
6
7
8
9
10
11
public void runnableWithoutLambda(int count) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < count; i++) {
System.out.println(i);
}
}
};
new Thread(runnable).start();
}

但到了Java8,这样做却被“允许”了,当然不可能是无条件的放开,只是不一定要显式地声明final——“等效于final”即可。

1
2
3
4
5
6
7
8
public void runnableWithLambda(int count) {
Runnable runnable = () -> {
for (int i = 0; i < count; i++) {
System.out.println(i);
}
};
new Thread(runnable).start();
}

事实上,像count这样既不是lambda表达式的参数,也不是定义在lambda表达式的代码块中的变量,被称为“自有变量”,而这个特性被称为“捕获”,含有自有变量的代码块被称为“闭包”。为了实现这个特性,lambda表达式会存储这些变量,既然这些变量等效于final,它们就不允许被修改,即使编译器不报错,尤其在多线程条件下,结果是完全不可预估的。