Java8 之 Lambda

lambda即匿名函数,使用它可以简洁的表示一个行为。由于这个“行为”是可以传递的,Java8的世界变得妙极了~

引言

简单地看个例子:

1
2
3
4
5
6
@Data
@AllArgsConstructor
public class Apple {
private Integer weight;
private String color;
}
1
2
3
4
5
6
7
appleList.sort(new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight() - a2.getWeight();
}
});
System.out.println(appleList);

sort方法需要传入一个Comparator接口以便进行排序。

在Java8以前,显然需要传入一个Comparator的实现类,就像上面的代码。

我们已经习惯了使用匿名类来实现接口,但是这种方式有几个显而易见的缺点:

  • 代码量太多,我们真正要关心的只有o1.getWeight() - o2.getWeight()一句,其他的代码都是一些不得不写,但是很繁琐的样板化代码。而且对于阅读代码来说也是写无用信息。

  • 如果需要不同的比较器,需要书写多个匿名类。而且,为了复用和清晰,我们就不能使用匿名类,只得将他们提取出来,成为一个个的比较器实现类,这又增加了许多代码的书写,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class AppleComparatorByWeight implements Comparator<Apple>{
    public int compare(Apple a1, Apple a2) {
    return xxx
    }
    }
    class AppleComparatorByColor implements Comparator<Apple>{
    public int compare(Apple a1, Apple a2) {
    return xxx
    }
    }

    appleList.sort(new AppleComparatorByWeight());
    appleList.sort(new AppleComparatorByColor());

看看Java8是如何解决的:

1
appleList.sort((a1, a2) -> a1.getWeight() - a2.getWeight());

使用Lambd后,只需要关注真正的业务代码,它自动实现了Comparator接口,并复写了compare方法,我们再也不用写那些讨厌的样板化代码了。代码的语言也变得清晰易懂。

除了Lambda,使用方法引用,甚至可以进一步将代码简化如下:

1
appleList.sort(comparingInt(Apple::getWeight));

要复用比较器的情景下,也不需要编写实现类,只需要用lambda。

1
2
3
4
Comparator<Apple> c1 = (a1, a2) -> a1.getWeight() - a2.getWeight();
Comparator<Apple> c2 = (a1, a2) -> a1.getColor().compareTo(a2.getColor());
appleList.sort(c1);
appleList.sort(c2);

函数式接口 @FunctionalInterface

@FunctionalInterface是Java8新引入的一个注解,它可以标记在接口/抽象类上,表明此接口/抽象类是一个函数式接口。如果该接口/抽象类不满足函数式接口的要求,那么编译器将报错。

函数式接口: 该接口/抽象类只包含一个抽象方法。(default方法不算抽象方法)

比如刚才的sort方法接收的Comparator接口,就是一个函数式接口,它只有一个compare()抽象方法。

注:如果一个接口复写了Object的方法,并不影响它成为函数式接口。比如Comparator接口还复写了equals方法。

在比如Runnable接口,在JDK8中也被标记上了@FunctionalInterface,因为它只有一个抽象方法:

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

那么,函数式接口有什么用呢?

传递行为,而不是传递值

Java8中,终于将函数/lambda作为一等公民,这意味着,我们可以将“行为”保存到一个变量里;也可以将"行为"作为参数,在方法之间传递。

我们先自己写一段代码来感受下,如何接收一个行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FunctionalInterface
interface BufferedReaderProcessor { //step 1: 定义一个函数式接口,可以理解为“行为”接口
String process(BufferedReader b) throws IOException;
}

public class ProcessFileDemo {

// step 2: 定义一个方法,来接收“行为”
static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br); //调用这个"行为"p,这个“行为”需要一个参数br
}
}

public static void main(String[] args) throws IOException {
//step 3: 向processFile()传递行为
String online = processFile(br -> br.readLine());
String twoLines = processFile(br -> br.readLine() + br.readLine());

System.out.println("online:\n" + online);
System.out.println("twoLines:\n" + twoLines);
}
}

显然,lambda自动地实现了BufferedReaderProcessor接口并override了process方法。

在看下其他的例子,

1
2
3
4
//执行一个行为是“打印haha"的线程
new Thread(()-> System.out.println("haha")).start();
//对于每个apple,执行打印操作
appleList.forEach(apple -> System.out.println(apple));

我们说过Runnable接口是一个FunctionalInterface,那么第一个例子中,lambda显然实现了Runnable的run方法。

那么,传递给forEach方法的lambda又实现了什么接口呢?

1
2
3
4
5
6
// public interface Iterable<T> 
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this)
action.accept(t);
}

可以看到,forEach方法接收一个consumer,那么显然我们传递的apple -> System.out.println(apple)实现了该接口。

1
2
3
4
5
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// ....
}

Consumer只有一个抽象方法accept。它接收一个t,返回void。

结合forEach()方法的定义来思考,当我们调用appleList.forEach(apple -> System.out.println(apple));方法时,t其实是list中的每个apple元素,action是打印apple。

于是,对于每个apple,Consumer(即“打印行为”) accpet了这个apple。

这里有可能有点绕,其实就像下面:

1
2
3
4
5
6
appleList.forEach(new Consumer<Apple>() {
@Override
public void accept(Apple apple) {
System.out.println(apple);
}
});

lambda相当于实现了Cousumer接口的accpet方法,然后forEach方法中,对每个元素,都调用accept方法。

Consumer其实是JDK为我们抽象出了一个通用的函数接口,他将所有接收一个对象,返回void的行为,叫做Consumer

又比如Predicate接口

1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

接收一个对象,返回boolean的行为,叫做Predicate。

比如我们可以这样:

1
2
3
4
Object[] objects = appleList.stream()
.filter(apple -> "red".equals(apple.getColor()))
.toArray();
// 关于stream()请参考下一讲《Java8-Stream》

filter方法接收一个Predicate。apple为红色时,返回true。

那么这条代码将返回appleList中所有红色的苹果。

JDK中还有许多抽象的行为接口:

函数式接口 函数描述符 说明
Predicate T->boolean 接收T,返回boolean
Consumer T->void 接收T,返回void
Function<T,R> T->R 接收T,返回R
Supplier ()->T 什么也不接收,返回一个T
UnaryOperator T->T 接收一个T,返回一个T
BinaryOperator (T,T)->T 接收两个T类型对象,返回一个T类型对象
BiPredicate<L,R> (L,R)->boolean 接收T、R,返回boolean
BiConsumer<T,U> (T,U)->void 接收T、U,返回void
BiFunction<T,U,R> (T,U)->R 接收T、U,返回一个R

还记得上面我们实现的BufferedReaderProcessor吗?

1
2
3
4
@FunctionalInterface
interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

它不正是一个Function<BufferedReader,String>吗。TODO 试着修改下之前的代码?

lambda书写语法

Lambda基本语法是

(parameters) -> expression

或(请注意语句的花括号)

(parameters) -> { statements; }

1
2
3
4
5
6
7
8
9
10
11
12
13
//第一个Lambda具有一个String类型的参数并返回一个int。 没有写return语句,但是已经隐含了return
(String s) -> s.length() // 它相当于(String s) -> {return s.length();}
//第二个Lambda有一个Apple类型的参数并返回一个boolean
(Apple a) -> a.getWeight() > 150
//第三个Lambda有两个int类型的参数而没有返回值(void返回)。注意Lambda可以包含多行语句,这里是两行
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x+y);
}
//第四个Lambda没有参数,返回一个int
() -> 42
//第五个Lambda有两个Apple类型的参数,返回一个int
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

方法引用

我们已经知道了,可以通过lambda来implement一个函数式接口,从而省去很多样板化代码。

另外,通过方法引用的方式,也可以implement一个函数式接口,而且这种方式能进一步简化代码、提高代码易读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 例一:静态方法引用
List<String> strings = Arrays.asList("12", "14", "3", "2");
// 使用lambda:
strings.stream()
.map(str->Integer.parseInt(str)) //(map方法的意思是将每个str映射成一个int)
.toArray();
// 使用方法引用
strings.stream()
.map(Integer::parseInt) // 引用Integer类的静态方法
.toArray();

// 例二:任意类型实例方法引用
// 使用lambda:
appleList.stream().map(apple -> apple.toString()).forEach(System.out::println);
// 使用方法引用
appleList.stream()
.map(Apple::toString) // 引用appleList中每个元素的toString
.forEach(System.out::println);

// 例三:现有对象的实例方法引用
// 使用lambda:
appleList.forEach(apple -> System.out.println(apple));
// 使用方法引用:
appleList.forEach(System.out::println); //引用System.out对象的实例方法println。 传入的参数是每个遍历到的apple

以上展示了总共的三种方法引用。

  1. 静态方法的引用
  2. 任意类型实例方法的引用
  3. 现有对象的方法的引用

需要区别的是后两种,正如实例代码中,第二种方式时,appleList中的每个元素是方法的调用者;而第三种方式时,由于引用的是其他对象的方法,所以appleList中的每个元素成为了该方法的入参。

ps:实例代码中的map方法传入一个Function<T,R>,作用是将一个T类型的流映射成一个R类型的流。关于流的内容请参考下一讲《Java8-Stream》

进阶:复合lambda

类比于数学中的函数组合

1
2
3
f(x) = x+1
g(x) = x*2
则 g(f(x)) = (x+1)*2

可以把多个简单的lambda复合成复杂的表达式。

函数复合

1
2
3
4
5
6
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h1 = f.andThen(g); //h1 = (x+1)*2
Function<Integer, Integer> h2 = f.compose(g); //h2 = 2*x+1
int result1 = h1.apply(1); // 4
int result2 = h2.apply(1); // 3

谓词复合

1
2
3
4
5
6
7
8
9
Predicate<Apple> redApple = apple -> "red".equals(apple.getColor());

Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor())); // (red && >150)||green

//请注意, and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此, a.or(b).and(c)可以看作(a || b) && c

比较器复合

逆序

1
appleList.sort( comparing(Apple::getWeight).reversed() );

比较器链

1
2
3
4
5
appleList.sort( 
comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)
);

lambda的注意事项

lambda与异常

请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。

  1. 对于自己的函数式接口

比如,我们介绍了一个新的函数式接口BufferedReaderProcessor,它显式声明了一个IOException:

1
2
3
4
5
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
  1. 把Lambda在一个try/catch块中

如果你使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己声明抛出Exception。这种情况下,你可以显式捕捉受检异常:

1
2
3
4
5
6
7
8
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};

同样的 Lambda,不同的函数式接口

同一个Lambda表达式就可以对应不同的函数式接口,只要它们的方法签名能够兼容 。

比如我们自己写的BufferedReaderProcessor其实就是一个Function<BufferedReader, String>

1
2
3
4
@FunctionalInterface
interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}

于是相同的lambda可以同时对应BufferedReaderProcessor和Function<BufferedReader, String>

1
2
BufferedReaderProcessor p1 = br -> br.readLine();
Function<BufferedReader, String> p2 = br -> br.readLine();

再比如:

1
2
3
4
5
6
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());的方法签名为(Apple, Apple) -> Integer,所以它同时符合三种函数式接口的签名

  • Comparator: ( T, T ) ->int
  • ToIntBiFunction: (T, U)-> int
  • BiFunction: ( T, U, R ) -> R

另外,如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:

1
2
3
4
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

类型推断

编译器可以了解Lambda表达式的参数类型,这样就可在Lambda语法中省去标注参数类型

1
2
3
4
5
6
7
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

// 当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略
appleList.forEach( a -> System.out::println);
// forEach(Consumer<T>) -> appleList -> Apple -> T is Apple -> a为Apple

局部变量

Lambda表达式引用的局部变量必须是 final,或事实上是final 的

下面的代码无法编译,因为portNumber被赋值两次

1
2
3
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; //如果去掉这一句,则可以正常编译

你可能会问自己,为什么局部变量有这些限制。

第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后解释,这种模式会阻碍很容易做到的并行处理)

测验

函数式接口

布尔表达式 (List list) -> list.isEmpty() Predicate<List>
创建对象 () -> new Apple(10) Supplier
消费一个对象 (Apple a) -> System.out.println(a.getWeight()) Consumer
从一个对象中选择/提取 (String s) -> s.length() Function<String, Integer>或ToIntFunction
合并两个值 (int a, int b) -> a * b IntBinaryOperator
比较两个对象 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) Comparator或BiFunction<Apple, Apple, Integer> 或 ToIntBiFunction<Apple, Apple

下面哪些接口是函数式接口?

1
2
3
4
5
6
7
8
public interface Adder{
int add(int a, int b);
}
public interface SmartAdder extends Adder{
int add(double a, double b);
}
public interface Nothing{
}

答案:只有Adder是函数式接口。 SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从 Adder那里继承来的)。 Nothing也不是函数式接口,因为它没有声明抽象方法。

Lambda语法

根据上述语法规则,以下哪个不是有效的Lambda表达式?

1
2
3
4
5
(1) () -> {}
(2) () -> "Raoul"
(3) () -> {return "Mario";}
(4) (Integer i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}

答案:只有4和5是无效的Lambda。 (1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}

(2) 这个Lambda没有参数,并返回String作为表达式。

(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。

(4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i) -> {return "Alan" + i;}

(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示: (String s) -> "Iron Man"。或者如果你喜欢,可以使用显式返回语句,如下所示:(String s)->{return "IronMan";}

Comment