深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

来源:互联网 发布:淘宝电器销售额 编辑:程序博客网 时间:2024/06/07 00:13

转:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/

关于

深入理解 Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)深入理解 Java 8 Lambda(类库篇——Streams API,Collector 和并行)深入理解 Java 8 Lambda(原理篇——Java 编译器如何处理 lambda)

本文是深入理解 Java 8 Lambda 系列的第一篇,主要介绍 Java 8 新增的语言特性(比如 lambda 和方法引用),语言概念(比如目标类型和变量捕获)以及设计思路。

本文是对 Brian Goetz 的 State of Lambda 一文的翻译,那么问题来了:
为什么要翻译这个系列?

工作之后,我开始大量使用 Java公司将会在不久的未来使用 Java 8作为资质平庸的开发者,我需要打一点提前量,以免到时拙计为了学习Java 8(主要是其中的 lambda 及相关库),我先后阅读了Oracle的 官方文档,Cay Horstmann(Core Java的作者)的 Java 8 for the Really Impatient 和Richard Warburton的 Java 8 Lambdas但我感到并没有多大收获,Oracle的官方文档涉及了 lambda 表达式的每一个概念,但都是点到辄止;后两本书(尤其是Java 8 Lambdas)花了大量篇幅介绍 Java lambda 及其类库,但实质内容不多,读完了还是没有对Java lambda产生一个清晰的认识关键在于这些文章和书都没有解决我对Java lambda的困惑,比如:    Java 8 中的 lambda 为什么要设计成这样?(为什么要一个 lambda 对应一个接口?而不是 Structural Typing?)    lambda 和匿名类型的关系是什么?lambda 是匿名对象的语法糖吗?    Java 8 是如何对 lambda 进行类型推导的?它的类型推导做到了什么程度?    Java 8 为什么要引入默认方法?    Java 编译器如何处理 lambda?    等等……之后我在 Google 搜索这些问题,然后就找到 Brian Goetz 的三篇关于Java lambda的文章(State of Lambda,State of Lambda libraries version 和 Translation of lambda),读完之后上面的问题都得到了解决为了加深理解,我决定翻译这一系列文章

警告(Caveats)

如果你不知道什么是函数式编程,或者不了解 map,filter,reduce 这些常用的高阶函数,那么你不适合阅读本文,请先学习函数式编程基础(比如 这本书)。
State of Lambda by Brian Goetz

The high-level goal of Project Lambda is to enable programming patterns that require modeling code as data to be convenient and idiomatic in Java.

关于

本文介绍了 Java SE 8 中新引入的 lambda 语言特性以及这些特性背后的设计思想。这些特性包括:

lambda 表达式(又被成为“闭包”或“匿名方法”)方法引用和构造方法引用扩展的目标类型和类型推导接口中的默认方法和静态方法
  1. 背景

Java 是一门面向对象编程语言。面向对象编程语言和函数式编程语言中的基本元素(Basic Values)都可以动态封装程序行为:面向对象编程语言使用带有方法的对象封装行为,函数式编程语言使用函数封装行为。但这个相同点并不明显,因为Java 对象往往比较“重量级”:实例化一个类型往往会涉及不同的类,并需要初始化类里的字段和方法。

不过有些 Java 对象只是对单个函数的封装。例如下面这个典型用例:Java API 中定义了一个接口(一般被称为回调接口),用户通过提供这个接口的实例来传入指定行为,例如:

1
2
3

public interface ActionListener {
void actionPerformed(ActionEvent e);
}

这里并不需要专门定义一个类来实现 ActionListener,因为它只会在调用处被使用一次。用户一般会使用匿名类型把行为内联(inline):

1
2
3
4
5

button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});

很多库都依赖于上面的模式。对于并行 API 更是如此,因为我们需要把待执行的代码提供给并行 API,并行编程是一个非常值得研究的领域,因为在这里摩尔定律得到了重生:尽管我们没有更快的 CPU 核心(core),但是我们有更多的 CPU 核心。而串行 API 就只能使用有限的计算能力。

随着回调模式和函数式编程风格的日益流行,我们需要在Java中提供一种尽可能轻量级的将代码封装为数据(Model code as data)的方法。匿名内部类并不是一个好的 选择,因为:

语法过于冗余匿名类中的 this 和变量名容易使人产生误解类型载入和实例创建语义不够灵活无法捕获非 final 的局部变量无法对控制流进行抽象

上面的多数问题均在Java SE 8中得以解决:

通过提供更简洁的语法和局部作用域规则,Java SE 8 彻底解决了问题 1 和问题 2通过提供更加灵活而且便于优化的表达式语义,Java SE 8 绕开了问题 3通过允许编译器推断变量的“常量性”(finality),Java SE 8 减轻了问题 4 带来的困扰

不过,Java SE 8 的目标并非解决所有上述问题。因此捕获可变变量(问题 4)和非局部控制流(问题 5)并不在 Java SE 8的范畴之内。(尽管我们可能会在未来提供对这些特性的支持)
2. 函数式接口(Functional interfaces)

尽管匿名内部类有着种种限制和问题,但是它有一个良好的特性,它和Java类型系统结合的十分紧密:每一个函数对象都对应一个接口类型。之所以说这个特性是良好的,是因为:

接口是 Java 类型系统的一部分接口天然就拥有其运行时表示(Runtime representation)接口可以通过 Javadoc 注释来表达一些非正式的协定(contract),例如,通过注释说明该操作应可交换(commutative)

上面提到的 ActionListener 接口只有一个方法,大多数回调接口都拥有这个特征:比如 Runnable 接口和 Comparator 接口。我们把这些只拥有一个方法的接口称为 函数式接口。(之前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method))

我们并不需要额外的工作来声明一个接口是函数式接口:编译器会根据接口的结构自行判断(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。不过API作者们可以通过 @FunctionalInterface 注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。

实现函数式类型的另一种方式是引入一个全新的 结构化 函数类型,我们也称其为“箭头”类型。例如,一个接收 String 和 Object 并返回 int 的函数类型可以被表示为 (String, Object) -> int。我们仔细考虑了这个方式,但出于下面的原因,最终将其否定:

它会为Java类型系统引入额外的复杂度,并带来 结构类型(Structural Type) 和 指名类型(Nominal Type) 的混用。(Java 几乎全部使用指名类型)它会导致类库风格的分歧——一些类库会继续使用回调接口,而另一些类库会使用结构化函数类型它的语法会变得十分笨拙,尤其在包含受检异常(checked exception)之后每个函数类型很难拥有其运行时表示,这意味着开发者会受到 类型擦除(erasure) 的困扰和局限。比如说,我们无法对方法 m(T->U) 和 m(X->Y) 进行重载(Overload)

所以我们选择了“使用已知类型”这条路——因为现有的类库大量使用了函数式接口,通过沿用这种模式,我们使得现有类库能够直接使用 lambda 表达式。例如下面是 Java SE 7 中已经存在的函数式接口:

java.lang.Runnablejava.util.concurrent.Callablejava.security.PrivilegedActionjava.util.Comparatorjava.io.FileFilterjava.beans.PropertyChangeListener

除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

Predicate<T>——接收 T 并返回 booleanConsumer<T>——接收 T,不返回值Function<T, R>——接收 T,返回 RSupplier<T>——提供 T 对象(例如工厂),不接收值UnaryOperator<T>——接收 T 对象,返回 TBinaryOperator<T>——接收两个 T,返回 T

除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction

阅读全文
0 0