类库
Lambda 表达式,接下来将详细阐述另一个重要方面:如何使用 Lambda 表达式。即使不需要编写像 Stream 这样重度使用函数式编程风格的类库,学会如何使用 Lambda 表达式也是非常重要的。即使一个最简单的应用,也可能会因为代码即数据的函数式编程风格而受益。
在代码中使用Lambda表达式
从调用Lambda 表达式的代码的角度来看,它和调用一个普通接口方法没什么区别。例子:
传统的写法:
Logger logger = new Logger();
if (logger.isDebugEnabled()) {
logger.debug("Look at this: " + expensiveOperation());
}
Lambda表达式的写法:
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());
基本类型
在 Java 中,有一些相伴的类型,比如 int 和 Integer —— 前者是基本类型,后者是装箱类型。基本类型内建在语言和运行环境中,是基本的程序构建模块;而装箱类型属于普通的 Java 类,只不过是对基本类型的一种封装。
将基本类型转换为装箱类型,称为装箱,反之则称为拆箱,两者都需要额外的计算开销。 对于需要大量数值运算的算法来说,装箱和拆箱的计算开销,以及装箱类型占用的额外内存,会明显减缓程序的运行速度。
为了减小这些性能开销,Stream 类的某些方法对基本类型和装箱类型做了区分。下图所示的高阶函数LongFunction和其他类似函数即为该方面的一个尝试。在Java 8中,仅对整型、 长整型和双浮点型做了特殊处理,因为它们在数值计算中用得最多,特殊处理后的系统性能提升效果最明显。
对基本类型做特殊处理的方法在命名上有明确的规范。
- 如果方法返回类型为基本类型,则在基本类型前加
To
,如上图中的ToLongFunction
。 - 如果参数是基本类型,则不加前缀只需类型名即可,如下图中的
LongFunction
。 - 如果高阶函数使用基本类型,则在操作后加后缀
To
再加基本类型,如mapToLong
。
这些基本类型都有与之对应的 Stream,以基本类型名为前缀,如 LongStream
。事实上, mapToLong
方法返回的不是一个一般的 Stream,而是一个特殊处理的 Stream。在这个特 殊的 Stream 中,map 方法的实现方式也不同,它接受一个 LongUnaryOperator
函数,将 一个长整型值映射成另一个长整型值,如下图所示。通过一些高阶函数装箱方法,如 mapToObj
,也可以从一个基本类型的 Stream 得到一个装箱后的 Stream,如 Stream<Long>
。
重载解析
在 Java 中可以重载方法,造成多个方法有相同的方法名,但签名确不一样。这在推断参数 类型时会带来问题,因为系统可能会推断出多种类型。这时,javac 会挑出最具体的类型。
例:
两个重载方法可供选择
private void overloadedMethod(Object o) {
System.out.print("Object");
}
private void overloadedMethod(String s) {
System.out.print("String");
}
overloadedMethod("abc");
方法调用在选择定义的重载方法时,输出 String,而不是 Object。
总而言之,Lambda 表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循 如下规则:
- 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
- 如果有多个可能的目标类型,由最具体的类型推导得出;
- 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。
@FunctionalInterface
每个用作函数接口的接口都应该添加这个注释。
这究竟是什么意思呢? Java 中有一些接口,虽然只含一个方法,但并不是为了使用 Lambda 表达式来实现的。比如,有些对象内部可能保存着某种状态,使用带有一个方法的接口可能纯属巧合。java.lang.Comparable
和 java.io.Closeable
就属于这样的情况。
如果一个类是可比较的,就意味着在该类的实例之间存在某种顺序,比如字符串中的字母顺序。人们通常不会认为函数是可比较的,如果一个东西既没有属性也没有状态,拿什么比较呢?
一个可关闭的对象必须持有某种打开的资源,比如一个需要关闭的文件句柄。同样,该接口也不能是一个纯函数,因为关闭资源是更改状态的另一种形式。
和 Closeable 和 Comparable 接口不同,为了提高 Stream 对象可操作性而引入的各种新接 口,都需要有 Lambda 表达式可以实现它。它们存在的意义在于将代码块作为数据打包起 来。因此,它们都添加了 @FunctionalInterface
注释。
该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举 类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时, 使用它能很容易发现问题。
二进制接口的兼容性
Java 8中对API最大的改变在于集合类。虽然Java在持续演进,但它一直在保持着向后二进制兼容。具体来说,使用Java 1到Java 7编译的类库或应用,可以直接在 Java 8 上运行。
事实上,修改了像集合类这样的核心类库之后,这一保证也很难实现。我们可以用具体的例子作为思考练习。Java 8中为Collection接口增加了stream方法,这意味着所有实现了 Collection 接口的类都必须增加这个新方法。对核心类库里的类来说,实现这个新方法
(比如为 ArrayList 增加新的 stream 方法)就能就能使问题迎刃而解。
缺憾在于,这个修改依然打破了二进制兼容性,在 JDK 之外实现 Collection 接口的类, 例如MyCustomList,也仍然需要实现新增的stream方法。这个MyCustomList在Java 8中 无法通过编译,即使已有一个编译好的版本,在 JVM 加载 MyCustomList 类时,类加载器 仍然会引发异常。
这是所有使用第三方集合类库的梦魇,要避免这个糟糕情况,则需要在Java 8中添加新的语言特性:默认方法
默认方法
Collection 接口中增加了新的 stream 方法,如何能让 MyCustomList 类在不知道该方法的情况下通过编译?Java 8通过如下方法解决该问题:Collection接口告诉它所有的子类: “如果你没有实现 stream 方法,就使用我的吧。”接口中这样的方法叫作默认方法,在任何
接口中,无论函数接口还是非函数接口,都可以使用该方法。
Iterable 接口中也新增了一个默认方法:forEach,该方法功能和 for 循环类似,但是允许
用户使用一个 Lambda 表达式作为循环体。
完整的继承体系图:
简言之,类中重写的方法胜出。这样的设计主要是由增加默认方法的目的决定的,增加默认方法主要是为了在接口上向后兼容。让类中重写方法的优先级高于默认方法能简化很多继承问题。
多重继承
接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。
三定律
如果对默认方法的工作原理,特别是在多重继承下的行为还没有把握,如下三条简单的定律可以帮助大家。
- 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义 的方法。
- 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法, 那么子类中定义的方法胜出。
- 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明 为抽象方法。
其中第一条规则是为了让代码向后兼容。
接口的静态方法
Stream 是个接口, Stream.of是接口的静态方法。这也是Java 8中添加的一个新的语言特性,旨在帮助编写类库的开发人员,但对于日常应用程序的开发人员也同样适用。
Optional
reduce 方法的一个重点尚未提及:reduce 方法有两种形式,一种如前面出现的需要有一个初始值,另一种变式则不需要有初始值。没有初始值的情况下,reduce 的第一步使用 Stream 中的前两个元素。有时,reduce 操作不存在有意义的初始值,这样做就是有意义 的,此时,reduce 方法返回一个 Optional 对象。
Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。
人们常常使用 null 值表示值不存在,Optional 对象能更好地表达这个概念。使用 null 代表值不存在的最大问题在于 NullPointerException。一旦引用一个存储 null 值的变量,程序会立即崩溃。使用 Optional 对象有两个目的:首先,Optional 对象鼓励程序员适时检查变量是否为空,以避免代码缺陷;其次,它将一个类的 API 中可能为空的值文档化,这比 阅读实现代码要简单很多。
使用工厂方法 of,可以从某个值创建出一个 Optional 对象。Optional 对象相当于值的容器,而该值可以 通过 get 方法提取。例如创建某个值的 Optional 对象:
Optional<String> a = Optional.of("a");
assertEquals("a", a.get());
Optional 对象也可能为空,因此还有一个对应的工厂方法 empty,另外一个工厂方法 ofNullable 则可将一个空值转换成 Optional 对象。第三个方法 isPresent 的用法(该方法表示一个 Optional 对象里是否有值)。例如创建一个空的 Optional 对象,并检查其是否有值:
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());
// 例 4-22 中定义了变量 a
assertTrue(a.isPresent());
使用 Optional 对象的方式之一是在调用 get() 方法前,先使用 isPresent 检查 Optional 对象是否有值。使用 orElse 方法则更简洁,当 Optional 对象为空时,该方法提供了一个 备选值。如果计算备选值在计算上太过繁琐,即可使用 orElseGet 方法。该方法接受一个 Supplier 对象,只有在 Optional 对象真正为空时才会调用。例如使用 orElse 和 orElseGet 方法:
ssertEquals("b", emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));
参考资料:
Java 8函数式编程 作者:(英)沃伯顿著
备注:
转载请注明出处:http://blog.youkuaiyun.com/wsyw126/article/details/52649380
作者:WSYW126