将之前《Java8实战》笔记 pdf 版本转换为网页博客,方便查看。本文主要体现《Java8实战》书籍的主体结构,具体细节可以翻书查阅。
为什么要关心Java 8
Java怎么还在变
流处理
尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。
可以在一个更高的抽象层次上写Java 8程序了:思路变成了把这样的流变成了那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。
Java 8可以透明地把输入的不相关部分拿到几个CPU内核上分别执行你的Stream操作流水线——这是几乎免费的并行,用不着费劲搞Thread了。
用行为参数化把代码传递给方法
Java 8 增加了把方法(你的代码)作为参数传递给另一个方法的能力,把这一概念称为行为参数化。
并行与共享的可变数据
没有共享的可变数据,将方法和函数即代码传递给其他方法的能力是我们平常所说的函数式编程范式的基石。
Java中的函数
方法和Lambda作为一等公民
Java8 的方法引用 ::
语法(把这个方法作为值)。例如:File::isHidden
。
Lambda(或匿名函数),例如:(int x)-> x+1 。
流
我们把 for-each
循环一个个去迭代元素称为外部迭代,有了 Stream API 数据处理全是在库内进行的,这种思想叫作内部迭代。
函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。
默认方法
如何改变已发布的接口而不破坏已有的实现呢?为了解决这个问题,Java 8中加入了接口的默认方法。加入默认方法后,Java有了某种形式的多重继承,Java 8用一些限制来避免出现类似C++中凑名昭著的菱形继承问题。
通过行为参数化传递代码
行为参数化就是可以帮你处理频繁变更的需求的一种软件开发模式。
匿名类的不足之处:占用了很多空间,显得很笨重;用起来可能会让人费解。
场景:Comparator排序,用Runnable执行一个代码块,以及GUI事件处理。
Lambda表达式
基本语法:1
2(parameters)-> expression // 表达式
(parameters)-> { statements; } // 语句
在需要函数接口的地方可以使用Lambda表达式。
函数式接口就是只定义一个抽象方法的接口。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就任然是一个函数式接口。
@FunctionalInterface
这个注解表示该接口会设计成一个函数式接口。不是必须的,类似于@Override。
任何函数式接口都不允许抛出受检查异常(checked exception
)。如果你需要Lambda表达式来抛出异常,有两种办法:定义自己的函数式接口,并声明受检查异常;或者把Lambda包在一个 try/catch
块中。
引入流
Stream API 可以让你写出这样的代码:
- 声明性——更简洁,更易读
- 可复合——更灵活
- 可并行——性能更好
Java 8引入流的理由:Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。
Java 8需要一个类似于Collection却没有迭代器的接口,与是就有了Stream!
和迭代器类似,流只能遍历一次。
使用流
- 筛选和切片
filter
、distinct
、limit(n)
、skip(n)
- 映射
map
、flatMap
- 查找和匹配
allMatch
、anyMatch
、noneMatch
、findFirst
、findAny
- 归约
reduce
- 构建流
由值创建流、由数组创建流、由文件创建流、由函数生成流(创建无限流)
函数生成流:iterate
、generate
用流收集数据
Collectors
类提供工厂方法创建收集器,提供了三大功能:
- 将元素规约和汇总为一个值
- 元素分组
- 元素分区
并行数据处理与性能
Java 7引入了一个叫作“分支/合并”的框架,让并行处理数据操作更加稳定,更不易出错。
parallel
、sequential
调用会影响整个流水线。
优化性能时,你应该始终遵循三个黄金规则:测量,测量,测量。
很重要的的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。总而言之,很多情况下不可能或不方便进行并行化。
记住要避免共享可变状态,确保并行Stream得到正确的结果。
留意装箱。自动装箱和拆箱操作会大大降低性能。
对于较少的数据量,选择并行流几乎从来都不是一个好的决定。
“分支/合并”框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成结果。它是ExecutorService
接口的一个实现,它把子任务分配给线程池(称为 ForkJoinPool
)中的工作线程。
“分支/合并”框架工程用一种称为工作窃取(work stealing
)的技术来解决这个问题。每个线程池都为分配给它的任务保存一个双向链式队列,完成任务的线程可以从其他线程任务队列的尾端“偷走”一个任务。
Spliterator
(可分迭代器)是Java 8中加入的另一个新接口,可以让你控制划分数据结构的策略。
重构、测试和调试
如果你使用了匿名类,尽量使用Lambda表达式替换它们,但是要注意二者间语义的微妙差别,比如关键字 this
,以及变量隐藏。
尽量使用Stream API替换迭代式的集合处理。
尽量将复杂的Lambda表达式抽象到普通方法中。
Lambda表达式会让栈跟踪的分析变得更为复杂。
流提供的 peek
方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。
默认方法
Java 8允许在接口内声明静态方法;Java 8引入了默认方法,通过默认方法你可以指定接口方法的默认实现。
引入默认方法的目的:它让类可以自动地继承接口的一个默认实现。
抽象类与抽象接口的区别:
- 一个类只能继承一个抽象类,但一个类可以实现多个接口。
- 一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。
默认方法的使用模式:可选方法和行为的多继承。
多个接口默认方法冲突的解决机制(继承菱形问题):
- 首先,类或父类中显示声明的方法,其优先级高于所有的默认方法。
- 如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
- 最后,如果冲突依旧无法解决,你就只能在你的类中覆盖默认方法,显示地指定在你的类中使用哪一个接口中的方法。
用Optional取代null
Java 8 中引入了一个新的类 java.util.Optional<T>
,对存在或缺失的变量进行建模。
可以使用 Optional
中的 empty
、of
、ofNullable
创建 Optional
对象。
Optional
类支持多种方法,比如:map
、flatMap
、filter
,它们在概念上与Stream类中对应的方法十分相似。
CompletableFuture 组合式异步编程
同步API中调用方在被调用方运行的过程中会等待,异步API会直接返回。
如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那么没有必要创建比处理器核数更多的线程)。反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用 CompletableFuture
灵活性更好。
CompletableFuture
类还提供了异常管理的机制,让你有机会抛出/管理异步任务执行中发生的异常。
如果异步任务之间相互独立,或者它们之间某一些的结果是另一些的输入,你可以将这些异步构造或者合并一个。
你可以为 CompletableFuture
注册一个回调函数;你可以决定在什么时候结束程序的运行,是等待由 CompletableFuture
对象构成的列表中所有对象都执行完毕,还是只要其中任何一个首先完成就中止程序的运行。
新的日期和时间API
Java 8之前老版的 java.util.Date
类以及其他用于建模日期时间的类有很多不一致设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
新的日期和时间API中,日期—时间对象是不可变的。
新类包括:LocalDate
、LocalTime
、Instant
、Duration
以及Period
。
函数式的思考
从长远看,减少共享的可变数据能帮你降低维护和调试程序的代价。
如果一个函数使用相同的参数值调用,总是返回相同的结果,那么它是引用透明的。
相对于Java语言中传统的递归,“尾-递”(递归调用发生在方法的最后)可能是一种更好的方式,它开启了一扇门,让我们有机会最终使用编译器进行优化。
函数式编程的技巧
高阶函数接受至少一个或多个函数作为输入参数,或者返回另一个函数的函数。Java中典型的高阶函数包括 comparing
、andThen
、compose
。
科里化是一种将具备2个参数的函数f转换为使用一个参数的函数g,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。例如:1
f ( x,y ) = ( g ( x ) ) ( y )
持久化数据结构在其被修改之前会对自己前一个版本的内容进行备份。
遵守“引用透明性”原则的函数,其计算结构可以进行缓存。
面向对象和函数式编程的混合:Java 8 和 Scala 的比较
Java 8 和Scala都是整合了面向对象编程和函数式编程特性的语言,它们都运行于JVM之上,在很多时候可以互相操作。
结论以及Java的未来
TODO
附录
附录A 其他语言特性的更新
重复注解(repeated annotation
)、类型注解(type annotation
)、通用目标类型推断(generalized target-type inference
)。
附录B 类库的更新
略
附录C 如何以并发的方式在同一个流上执行多种操作
略
附录D Lambda表达式和JVM字节码
略