1.为什么要关心 Java 8
Java 8 不但对软件有影响,对硬件也有影响:平常用的 CPU 都是多核的—笔记本电脑或台式机上的处理器可能有四个 CPU 甚至更多内核。但是,绝大多数现有的 Java 程序都只使用其中一个内核, 其他三个都闲着,或只是用一小部分的处理能力来运行操作系统或杀毒程序。
在Java 8之前,专家们可能会说必须利用线程才能使用多个内核。但是线程用起来不容易。
Java 8 特点:
- Stream API
- 向方法传递代码的技巧
- 接口中的默认方法
Java 8 提供了一个新的 API(称为“流”,Stream),它支持许多处理数据的并行操作,其思路和在数据库查询语言中的思路类似—用更高级的方式表达想要的东西,而由“实现”(在这里是Streams库)来选择最佳低级执行机制。可以避免用 synchronized 编写代码,且效率更高。
Java 8 中的主要变化反映了它开始远离常侧重于改变现有值的经典面向对象思想,而向函数式编程领域转变,在大面上考虑做什么被认为是头等大事,并和如何实现区分开来。
编程语言的整个目的就在于操作值,要是按照历史 上编程语言的传统,这些值因此被称为一等值。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间不能传递,因而是二等公民。人们发现,在运行时传递方法能将方法变成一等公民。
谓词(predicate) 在数学上常常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。
for-each 循环一个个去迭代元素,然后再处理元素。我们把这种 数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。
2.通过行为参数化传递代码
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被程序的其他部分调用,这意味可以推迟这块代码的执行。
2.1 方法参数化
功能需求:根据传入的 Apple 属性,对 List
inventory 进行筛选,完成收集后的 List。
1 |
|
2.2 阶段 1
1 | public static List<Apple> filterGreenApples(List<Apple> inventory){ |
如果想改变过滤条件,比如把过滤颜色从 “green” 变成 “red” 该怎么办?重新添加一个方法,但是改动的代码只有一两行吗?
2.3 阶段 2
1 | interface ApplePredicate{ |
2.4 阶段 3
1 | public static boolean isGreenApple(Apple apple) { |
3.Lambda 表达式
3.1 Lambda 管中窥豹
可以把 Lambda 表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名:它不像普通方法那样有一个确切的名称。
- 函数: Lambda 函数不像方法(Method)那样属于某个特定的类。但和方法一样, Lambda 有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递: Lambda 表达式可以作为参数传递给方法或存储在变量中。
- 简洁:无需像匿名类那样写很多模板代码。
普通方法:
1 | // 等号右边的就是 匿名函数类。 |
Lambda 函数
1 | Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight()); |
需要注意的是 Comparator<T>
是一个接口。
1 |
|
Lambda 表达式有三个部分:参数列表(可以为空,即无参)、箭头、 Lambda 主体(可以无返回值,即
void
)。
一些 Lambda
示例
使用案例 | Lambda 示例 |
---|---|
布尔表达式 | (List<String> list) -> list.isEmpty(); |
创建对象 | () -> new Apple(10); |
消费一个对象 | (Apple a) -> { System.out.println(a.getWeight()) }; |
从一个对象中选择/抽取 | (String s) -> s.length(); |
组合两个值 | (int a, int b) -> a * b; |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
3.2 Lambda 使用场景
在函数式接口上使用 Lambda 表达式。
3.2.1 函数式接口
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。比如常见的 Comparator<T>、Runnable、Callable
。
1 | // java.util.Comparator |
另外,在 Java 8 中已经允许接口中有方法的默认实现,也就是接口可以拥有默认方法(即在类没有对方法进行实现时, 其主体为方法提供默认实现的方法)。即使一个接口有很多默认方法,但是只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
3.2.2 函数描述符
函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。这种抽象方法叫作函数描述符。例如,Runnable 接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作 run 的抽象方法,这个方法什么也不接受,什么也不返回(void)。
Lambda 表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,但是这个 Lambda 表达式的签名要和函数式接口的抽象方法一样。
@FunctionalInterface 是怎么回事?
如果查看新的 Java API,会发现函数式接口带有 @FunctionalInterface 的标注,这个标注用于表示该接口会设计成 一个函数式接口。不是必需的,但对于为此设计的接口而言,使用它是比较推荐的,就像是 @Override 标注表示方法被重写了。
3.3 环绕执行模式
资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理, 然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式。
在以下代码中,是从一个文件中读取一行所需的模板代码。
1 | public static String processFile() throws IOException { |
3.3.1 行为参数化
上面那段代码是有局限的。只能读文件的第一行,如果想要返回头两行,甚至是返回使用最频繁的词,该怎么办?理想情况下,要重用执行设置和清理的代码,并告诉 processFile
方法对文件执行不同的操作。也就是需要把 processFile
的行为参数化。需要一种方法把行为传递给 processFile
,以便它可以利用 BufferedReader
执行不同的行为。
需要一个接收 BufferedReader
并返回 String
的 Lambda
,下面就是从 BufferedReader
中打印两行的写法:
1 | String result = processFile((BufferedReader br) -> br.readLine() + br.readLine()); |
3.3.2 使用函数式接口来传递行为
前面提到过, Lambda 仅可用于上下文是函数式接口的情况。因此需要创建一个能匹配 BufferedReader -> String
,还可以抛出 IOException
异常的接口。暂且称此接口为 BufferedReaderProcessor
。
1 |
|
接下来就可以把这个接口作为新的 processFile
方法的参数了:
1 | public static String processFile(BufferedReaderProcessor p) throws IOException { |
3.3.3 执行一个行为
任何 BufferedReader -> String
形式的 Lambda
都可以作为参数来传递,因为它们符合 BufferedReaderProcessor
接口中定义的 process
方法的签名。
Lambda 表达式允许直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
1 | public static String processFile(BufferedReaderProcessor p) throws IOException { |
3.3.4 传递 Lambda
接下来就可以通过传递不同的 Lambda
重用 processFile
方法,并以不同的方式处理文件了。
1 | // 处理一行 |
3.4 使用函数式接口
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述 Lambda 表达式的签名。函数式接口的抽象方法的签名称为函数描述符。
3.4.1 Predicate
java.util.function.Predicate<T>
接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean。在需要表示一个涉及类型 T 的布尔表达式时,就可以使用这个接口。
1 |
|
3.4.2 Consumer
java.util.function.Consumer<T>
定义了一个名叫 accept 的抽象方法,它接受泛型 T 的对象,没有返回(void)。如果需要访问类型 T 的对象,并对其执行某些操作,可以使用这个接口。
1 |
|
3.4.3 Function
java.util.function.Function<T, R>
接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。如果需要定义一个 Lambda ,将输入对象的信息映射到输出,可以使用这个接口。
1 |
|
3.4.4 原始类型特化
Java 类型要么是引用类型(比如 Byte、Integer、Object、List),要么是原始类型(比如 int、double、byte、char)。但是泛型(比如 Consumer<T>
中的 T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
- 自动装箱:将原始类型转换为对应的引用类型。
- 自动拆箱:将引用类型转换为对应的原始类型。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆(Heap)里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8 为前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
比如,在下面的代码中,使用 IntPredicate 就避免了对值 1000 进行装箱操作,但要是用 Predicate<Integer>
就会把参数 1000 装箱到一个 Integer 对象中:
1 | public interface IntPredicate { |
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function 接口还有针对输出参数类型的变种:ToIntFunction
、IntToDoubleFunction等。
Java 8 中常用的函数式接口。
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
占位 | 占位 | 占位 |
任何函数式接口都不允许抛出受检异常(checked exception)。如果需要 Lambda 表达式来抛出异常,有两种办法:
- 定义一个自己的函数式接口,并声明受检异常。
- 或者把 Lambda 包在一个
try/catch
块中。
3.5 类型检查、类型推断以及限制
Lambda 表达式时,说它可以为函数式接口生成一个实例。然而, Lambda 表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解 Lambda 表达式,你应该知 道 Lambda 的实际类型是什么。
3.5.1 类型检查
Lambda 的类型是从使用 Lambda 的上下文推断出来的。上下文中 Lambda 表达式需要的类型称为目标类型。下图概述了下列代码的类型检查过程。
1 | List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150); |
3.5.2 相同 Lambda ,不同函数式接口。
有了目标类型的概念,同一个 Lambda 表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,
1 | Callable<Integer> c = () -> 42; |
它们都代表着什么也不接受且返回一个泛型 T 的函数。
特殊的 void 兼容规则
如果一个 Lambda 的主体是一个语句表达式, 它就和一个返回 void 的函数描述符兼容(当然需要参数列表也兼容)。
3.5.3 类型推断
Java 编译器会从上下文(目标类型)推断出用什么函数式接口来配合 Lambda 表达式,这意味着它也可以推断出适合 Lambda 的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解 Lambda 表达式的参数类型,这样就可以在 Lambda 语法中省去标注参数类型。比如:
1 | // 显示参数类型 |
3.5.4 使用局部变量
迄今为止所介绍的所有 Lambda 表达式都只用到了其主体里面的参数。但 Lambda 表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获 Lambda 。比如:
1 | int portNumber = 1337; |
Lambda 可以没有限制地捕获实例变量和静态变量。但局部变量必须显式声明为 final, 或事实上是 final。换句话说, Lambda 表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量 this。)
实例变量都存储在堆中,而局部变量则保存在栈上。
闭包
闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8 的 Lambda 和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义 Lambda 的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为 Lambda 是对值封闭,而不是对变量封闭。
3.6 方法引用
方法引用允许重复使用现有的方法定义,并像 Lambda 一样传递它们。在一些情况下, 比起使用 Lambda 表达式,它们更易读,也更自然。
1 | // Lambda |
3.6.1 管中窥豹
方法引用可以被看作仅仅调用特定方法的 Lambda 的一种快捷写法。它的基本思想是,如果一个 Lambda 代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是根据已有的方法实现来创建 Lambda 表达式。但是,显式地指明方法的名称,代码的可读性会更好。
当需要使用方法引用时,目标引用放在分隔符 ::
前,方法名称放在后面(不需要括号,因为并没有实际调用这个方法)。可以把方法引用看作针对仅仅涉及单一方法的 Lambda 的语法糖。
Lambda 及其等效方法引用的例子。
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() |
Apple::getWeight |
() -> Thread.currentThread().dumpStack() |
Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) |
String::substring |
(String s) -> System.out.println(s) |
System.out::println |
方法引用主要有三类。
- 指向静态方法的方法引用(例如 Integer 的 parseInt 方法,写作
Integer::parseInt
)。 - 指向任意类型实例方法的方法引用(例如 String 的 length 方法,写作
String::length
)。 - 指向现有对象的实例方法的方法引用(假设有一个局部变量 expensiveTransaction 用于存放 Transaction 类型的对象,它支持实例方法 getValue,那么可以写
expensiveTransaction::getValue
)。
依照一些简单的方法,可以将 Lambda 表达式重构为等价的方法引用。
3.6.2 构造函数引用
对于一个现有构造函数,可以利用它的名称和关键字 new
来创建它的一个引用:ClassName::new
。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。 它适合 Supplier 的签名 () -> Apple
。可以这样做:
1 | // 构造函数引用指向默认的 Apple() 构造函数 |
如果构造函数的签名是 Apple(Integer weight)
,那么它就适合 Function 接口的签名,于是可以这样写:
1 | // 等价于 Function<Integer, Apple> c2 = (weight) -> new Apple(weight); |
如果构造函数具有两个参数,签名是 Apple(String color, Integer weight)
,它适合 BiFunction 接口的签名,于是可以这样写:
1 | // 等价于 BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight); |
3.7 Lambda 和方法引用实战
用不同的排序策略给一个 Apple 列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。
最后结果:
1 | inventory.sort(comparing(Apple::getWeight)); |
3.8 复合方法
可以把多个简单的 Lambda 复合成复杂的表达式。可以让两个谓词之间做一个 or 操作,组合成一个更大的谓词,还可以让一个函数的结果成为另一个函数的输入。
3.8.1 比较器复合
可以使用静态方法 Comparator.comparing
,根据提取用于比较的键值的 Function 来返回一个 Comparator,如下所示:
1 | // 原始 |
3.8.2 谓词复合
谓词接口包括三个方法:negate、and 和 or,可以重用已有的 Predicate 来创建更杂的谓词。
1 | Predicate<Apple> notRedApple = redApple.negate(); // 苹果不是红的 |
3.8.3 函数复合
可以把 Function 接口所代表的 Lambda 表达式复合起来。Function 接口为此配了 andThen 和 compose 两个默认方法,它们都会返回 Function 的一个实例。
andThen
:先对输入应用一个给定函数,再对输出应用另一个函数。意味着g(f(x))
。compose
:先把给定的函数用作 compose 的参数里面给的那个函数,然后再把函数本身用于结果。意味着f(g(x))
。
3.9 数学中的类似思想
3.10 小结
- Lambda 表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda 表达式可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用 Lambda 表达式。
- Lambda 表达式允许直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8 自带一些常用的函数式接口,放在
java.util.function
包里。 - 为了避免装箱操作,对
Predicate<T>
和Function<T, R>
等通用函数式接口的原始类型特化。 - Lambda 表达式所需要代表的类型称为目标类型。
- 方法引用可以重复使用现有的方法实现并直接传递它们。
- Comparator、Predicate 和 Function 等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。