hangscer

Lambda表达式 in Java8

2017/02/11

Lambda表达式

行为参数化

Lambda表达式鼓励使用行为参数化风格。比如,利用Lambda表达式,可以简洁地定义一个Comparator对象。
java8inaction3——1

先前:
1
2
3
4
5
Comparator<Apple> byWeight=new Comparator(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
之后(用了Lambda表达式):
1
2
Comparator<Apple> byWeight=
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

20170106java8inaction3-2
下表提供了一些Lambda的例子和使用案例:
20170106java8inaction3-3

3.2 在哪里以及如何使用Lambda

简而言之,可以在函数式接口上使用Lambda表达式。

3.2.1 函数式接口

为了参数化filter方法的行为而创建的Predicate<T>接口吗?它就是一个函数式接口!为什么?因为Predicate仅仅定义了一个抽象方法:

20170106java8inaction3-4
一言以蔽之,函数式接口就是只定义一个抽象方法的接口,比如ComparatorRunnable
20170106java8inaction3-5
注意,java8之后,接口还可以有*default*方法,即默认方法(即在类没有对方法进行实现时,其主体位方法提供默认实现的方法)。哪怕有很多默认方法,只要接口定义了一个抽象方法,它仍然还是一个函数式接口。

用函数式接口可以干什么?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。

3.2.2 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将把这种抽象方法叫做函数描述符。

我们在本章中使用了一个特殊表示法来描述Lambda和函数式接口的签名。()->void代表了参数列表为空,且返回void的函数。这正是Runnable接口所代表的。另外一个例子,(Appale a1,Apple a2)->int 代表接受两个Apple作为参数且返回int的函数。

为什么只在需要函数式接口的时候菜可以传入Lambda呢?

3.3 把Lambda付诸实践:环绕执行模式

例如,在以下代码中,高亮显示的就是从一个文件中读取一行所需的模块代码:
20170106java8inaction3-6
任务A和任务B周围都环绕着进行准备/清理的同一段冗余代码

3.3.1 第1步:记得行为参数化

在理想情况下,你要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。这听起来是不是很耳熟?是的,需要把processFile的行为参数化。你需要把一种方法行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。

传递行为正是Lambda拿手好戏。那要是想一次读两行,这个新的processFile方法看起来又该是什么样子呢?基本上,你需要一个接收BufferedReader并返回String的Lambda。例如,下面就是从BufferedReader中打印两行的写法:
20170106java8inaction3-7

3.3.2 第2步:使用函数式接口传递行为

前面解释过了,Lambda仅可用于上下文是函数式接口的情况。你需要一个能匹配BufferedReader->String,还可以抛出IOException的接口。让我们把这一接口叫做BufferedReaderProcessor吧。
20170106java8inaction3-8
现在你可以把这个接口作为新的processFile方法的参数了:
20170106java8inaction3-9

3.3.3 第3步:执行一个行为

任何BufferedReader -> String形式的Lambda都可以作为参数来传递,因为它符合BufferedReaderProcessor接口中定义的process方法的签名。请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实现。因此,你可以在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理。
20170106java8inaction3-10

3.3.4 传递Lambda

现在你可以通过传递不同的Lambda重用processFile方法,并以不同方法以处理文件了。

处理一行:
20170106java8inaction3-11
处理两行:
20170106java8inaction3-12

3.4 使用函数式接口

Java8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。下面来介绍PredicateConsumerFunction

3.4.1 Predicate

Predicate<T>接口定义了test的抽象方法,它接受泛型T对象,并返回一个boolean。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义了一个接受String对象的Lambda表达式,如图所示:
20170106java8inaction3-13

3.4.2 Consumer

Consumer<T>定义了名叫accept的抽象方法,它接受泛型对象T,没有返回值(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以创建forEach方法,接受一个Integer的列表,并对其中每个元素执行操作。
20170106java8inaction3-14

3.4.3 Function

Function<T,R>接口定义了名叫apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量、字符串映射为它的长度等等)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表中。
20170106java8inaction3-15

原始类型特化

由于介绍了三个泛型函数接口:Predicate<T>Consumer<T>Function<T,R>,泛型只能绑定引用类型。装箱和拆箱需要花费大量的资源。Java8为我们带来了一些专门的版本的函数,比如,使用IntPredicate避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把1000装箱到一个Integer对象中:
20170106java8inaction3-16
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始的类型前缀,比如DoublePredicateIntConsumerLongBinaryOperatorIntFunction等。Function接口还有针对输入参数类型的变种:TonIntFunction<T>IntToDoubleFunction等。
表3-2总结了Java API提供的常用的函数式接口以及其函数描述符。如果需要,自己可以设计一个。请记住, ( T , U) -> R的表达方式展示了应当如何思考一个函数描述符。
20170106java8inaction3-17

3.5 类型检查、类型推断以及限制

然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息,应该知道Lambda的实际类型是什么。

3.5.1 类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或者接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。下图概述了代码的类型检查过程。
20170106java8inaction3-18

特殊的void兼容规则

如果lambd的主体是一个语句表达式,它就和一个返回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参数的类型。

3.5.3 类型推断

还可以进一步简化你的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出合适Lambda的签名。这样做的好处是在Lambda中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:
20170106java8inaction3-19

3.5.4 使用局部变量(捕获Lambda)

Lambda允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就想匿名内部类一样。它们被称为捕获Lambda。

对局部变量的限制final

为什么会有这些限制?

  1. 实例变量和局部变量的实现有一个关键不同:实例变量在堆中,局部变量在栈中。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量回收后才去访问变量。
  2. 这一限制不鼓励使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。

    3.6 方法引用

    方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。下面就是借助Java8 API,用方法引用写的一个排序的例子:
    先前:
    20170106java8inaction3-20
    之后:
    20170106java8inaction3-21

    3.6.1 管中窥豹

    它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那么还是用名称来调用它,而不是描述如何调用它。
    20170106java8inaction3-22

    3.8 复合Lambda表达式的有用方法

    Java8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法。在实践中,可以把多个简单的的Lambda复合成复杂的表达式。两个谓词之间做一个OR操作,组成更大的谓词。函数式接口中怎么可能有更多的方法呢?。窍门在于这些方法都是默认方法。

也就是说它们不抽象。

3.8.1 比较器复合

可以使用静态方法Comparator.comparing来返回一个Comparator,如下所示:
20170106java8inaction3-23

1. 逆序

如果想对苹果按重量递减排序怎么办?用不着再去建立另一个Comparator的实例。
20170106java8inaction3-24

2. 比较器链

但是如果两个苹果一样重怎么办?哪个苹果应该放在前面呢?可能需要再提供一个Comparator来比较了。比如,在按重量比较之后,可能需要安原产国排序。thenComparing方法就是这个用的。它接受一个函数作为参数,就像comparing一样。如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator
20170106java8inaction3-25

3.8.2 谓词复合

谓词接口包括三个方法:negateandor,让开发者可以重用已有的Predicate来创建更复杂的谓词。

3.8.3 函数复合

Function接口具有andThencompose两个默认方法。