时光飞逝,还记得大二开始接触 java 时,那时主流的是 jdk1.6 与 eclipse 的天下。没想到转眼间 java8 也发布快六年了,其中 Lambda、方法引用、stream 这些新特性,每次看到都云里来雾里去。正好拜读一下《Java 8 实战》这本书一探究竟~
p.s. 本文更多的是个人的笔记,完整代码请参考这个 repo: https://github.com/daya0576/java8_practice
第一部分:基础知识
第一章:Why java8
- Stream API:灵感源自 linux 命令的管道流(|) -> 好处:天然的并行(执行的时候分块)
- 函数式编程:方法引用 -> lambda
第二章:通过行为参数化传递代码
了解过「策略设计模式」的同学,都知道将「行为」作为参数,可以增加代码的灵活性与可读性,但代码看上去还是有一丝累赘🤔
...
参考《在 java & python 中,如何优雅的筛选一堆苹果》
第三章:lambda 表达式
基本语法如下:
1 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); |
主要分为三部分:
- 参数:两个 Apple
- 箭头:将「参数列表」与 「Lambda 主体」区分开
- 主体:比较两个苹果的重量(注意控制流语句需用大括号包围:例如
return "Hello" + i
)
使用场景
函数式接口?
原名叫做 functional interface,为只有一个抽象方法的接口。
java8 还专门给抽象方法 @FunctionalInterface
注解,在编译阶段做检查。
举个例子:
1 | // 官方的 Runnable 接口 |
函数式接口
java.util.function
中几个函数式的接口:
- Runnable(
()->void
) - Predict(
T->boolean
) -> 输入一个参数,返回 boolean,用于例如列表中元素的筛选 - Compare(
(U,T)->R
) -> 输入两个参数,返回 boolean,用于排序 - Consumer(
T->void
) -> 返回 void,用于例如打印一个列表中的所有元素
和 Runnable 的区别??Runnable 是没有参数的 - Supplier(
()->T
): 用于实例化多个对象 - Function(
T->R
) -> 返回任意泛型的结果,例如用于获取一堆苹果对应的重量
方法引用
以下两种写法是等价的:
1 | // lambda |
引用又分为三种:
- 静态方法,e.g. Integer::parseInt
- 类型 - 实例方法,e.g. String::length
- 实例对象 - 实例方法,e.g. expensiveTransaction::getValue
谓词复合
可以将多个 lambda 通过 and
/or
/negative
关联起来,例如筛选出又红又大的苹果:
1 | Predicate<Apple> greenApplePredict = (Apple apple) -> "green".equals(apple.getColor()); |
函数组合
利用 andThen
和 compose
两个默认方法,将函数组合成自定义的 pipeline
1 | // g(f(x)) |
第四章:引入流 & 使用流
以前以为 java 中的流只是为了让代码看起来更加 简洁优雅,但另一个非常重要的优势在于自动的多核并行处理,提升性能,同时不用再担心如何处理线程与锁(在书的第七章中会重点介绍)。
⚠️注意:stream
与 python 中的 generator
类似,产生后只能被消费一次。
Stream API 总共有几种呢?
一、筛选:
- filter
- distinct
- limit
- skip: 跳过前 n 个元素
二、映射:
- map
- flatMap: 将多个 list 直接打平
flatMap 的例子:
1 | List<String> words = Arrays.asList("hello", "world"); |
以及如何生成笛卡尔积(但有一句说一句,感觉不是很直观。。):
1 | // 给定列表 [1, 2, 3] 和表[3, 4],返回笛卡尔积: |
三、查找和匹配
anyMatch/allMatch
findAny: 注意这个方法返回的是 Optional 容器类(代表一个值存在或不存在),目标为有效的避免 NPE 的情况。
findFirst: 故名思义,与 findAny 的区别在于是否有序的第一个
sorted
distinct
四、归约
- reduce: 举几个例子
- reduce(0, (a, b) -> a + b): 0 代表初始的指
- reduce(0, Integer::sum) / reduce(0, Integer::min)
但如果你想找到多个交易中,金额最大的拿笔交易:
1 | // 找到最大金额的交易 |
总结
用文中的一张图总结一下:
第六章:用流收集数据
collector 的三大功能:
- 将流元素规约和汇总为一个值
- 元素分组
- 元素分区
1. 汇总
- 最大值 / 最小值
1 | Comparator<Dish> dishCaloriesComparator = |
- 总数
1 | int totalCalories = menu.stream().collect(summingInt(Dish::getCalories)); |
- 连接字符串
1 | String shortMenu = menu.stream().map(Dish::getName).collect(joining()); |
2. 元素分组
其实就是 groupby
1 | Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); |
也可以直接计算 count
1 | Map<Dish.Type, Long> typesCount = menu.stream().collect( groupingBy(Dish::getType, counting())); |
还可以多级的分组。。。不太喜欢,就不列例子了
3. 分区
以下的返回结果分别为 true/false 对应的 map
1 | Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian)); |
第七章:并行数据处理与性能
好复杂,跳过
第三部分:高效 java8 编程
罗列了一些设计模式,跳过。
第八章:重构、测试和调试
如何调试
一、堆栈
Q: 这个挺有意思的,下面堆栈中的 $0
是什么意思呢?
A: 这些表示错误发生在 Lambda 表达式内部。由于 Lambda 表达式没有名字,所以编译器只能为 它们指定一个名字。
1 | Exception in thread "main" java.lang.NullPointerException |
二、日志
利用 peek
插入打印日志的动作:
1 | List<Integer> result = numbers.stream() |
第九章:默认方法
Q: 首先回答为什么需要默认方法呢?
A: nterface 新增一个方法的时候,不让已有的代码报错,从而有了默认方法。
举个例子:java 中 extend 只能继承一个父类,但可以 implement 多个接口。好处在于默认方法的复用
1 | public interface Rotatable { |
但假如 Rotatable 和 Moveable 接口,都存在一个相同名字的默认方法,造成的冲突要如何解决呢?
第十章:用 Optional 取代 null
为避免 NPE 引入了 Optional 对象
创建 Optional 对象
空的 Optional
1 | Optional<Car> optCar = Optional.empty(); |
非空值创建 Optional, 注意如果 car 为 null,则立刻抛出一个 NPE
1 | Optional<Car> optCar = Optional.of(car); |
可接受 null 的 Optional
1 | Optional<Car> optCar = Optional.ofNullable(car); |
获取 Optional 中的值
如何重构下面的代码呢?
1 | person.getCar().getInsurance().getName(); |
假设 person 是一个 Optional 的对象:
1 | person.flatMap(Person::getCar) |
上面 flatMap 与 map 的区别:
都是在不为 null 的情况下,根据约定 flatMap 返回的是 Optional 对象,而 map 则直接返回对应的值。
将 Insurance 定义为 Optional 类型的另一个好处在于,告诉未来的同事,它很明显可能是一个空值。
实战
case1: 用 Optional 可能为 null 的结果
1 | // 优化前 |
case2: 巧用异常处理
1 | // 优化前 |
第十一章:CompletableFuture: 组合式异步编程
使用 异步线程 + future,避免一些 IO 操作的时候阻塞当前线程:
p.s. 完整可运行的代码:链接
1 | public class Main { |
如果需要对多个商品发起查询呢?
可以考虑使用 parallelStream 并行运行,提高性能(因为运行不要求是有序的)
等待所有 查询 执行完毕后,进行汇总。当然也可以使用 anyOf
方法,意味着任意一个查询结束即返回。
1 | CompletableFuture[] futures = findPricesStream("myPhone") |
p.s. 说实话有点过于复杂了,不太喜欢。。
第十二章:新的日期与时间 API
(略)
第四部分:超越 Java 8
(略)