Java 8 Lambda函数编程入门(二)

来源:互联网 发布:淘宝商城女装卫衣套装 编辑:程序博客网 时间:2024/06/07 11:46

类库

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中,仅对整型、 长整型和双浮点型做了特殊处理,因为它们在数值计算中用得最多,特殊处理后的系统性能提升效果最明显。

ToLongFunction

对基本类型做特殊处理的方法在命名上有明确的规范。

  1. 如果方法返回类型为基本类型,则在基本类型前加To,如上图中的 ToLongFunction
  2. 如果参数是基本类型,则不加前缀只需类型名即可,如下图中的 LongFunction
  3. 如果高阶函数使用基本类型,则在操作后加后缀 To 再加基本类型,如 mapToLong

LongFunction

这些基本类型都有与之对应的 Stream,以基本类型名为前缀,如 LongStream。事实上, mapToLong 方法返回的不是一个一般的 Stream,而是一个特殊处理的 Stream。在这个特 殊的 Stream 中,map 方法的实现方式也不同,它接受一个 LongUnaryOperator 函数,将 一个长整型值映射成另一个长整型值,如下图所示。通过一些高阶函数装箱方法,如 mapToObj,也可以从一个基本类型的 Stream 得到一个装箱后的 Stream,如 Stream<Long>

LongUnaryOperator

重载解析

在 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.Comparablejava.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 表达式作为循环体。

完整的继承体系图:

完整的继承体系图

简言之,类中重写的方法胜出。这样的设计主要是由增加默认方法的目的决定的,增加默认方法主要是为了在接口上向后兼容。让类中重写方法的优先级高于默认方法能简化很多继承问题。

多重继承

接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。

三定律
如果对默认方法的工作原理,特别是在多重继承下的行为还没有把握,如下三条简单的定律可以帮助大家。

  1. 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义 的方法。
  2. 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法, 那么子类中定义的方法胜出。
  3. 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明 为抽象方法。

其中第一条规则是为了让代码向后兼容。

接口的静态方法

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.csdn.net/wsyw126/article/details/52649380
作者:WSYW126

0 0