第1章:Java8、9、10、11的变化
简洁:利用Java8,可以书写更加简洁的代码。
多核:当前计算机硬件大多都是多核CPU,而Java并没有充分利用多核的优势,即使是多线程也是在单个CPU上进行。利用synchronized成本会比想象中的更高。
行为参数化:方法引用(::语法)和Lambda表达式。将方法作为参数来传递,实现函数式编程。Lambda表达式其实就是一个匿名函数,但没有传统的匿名函数有那么多繁琐的模板代码,形式上更加简洁。
接口的默认方法:为了解决接口平滑演进,比如如何改变已发布的接口而不破坏已有的实现,可使用这一技术实现。
为什么Java还在变:即使现在很活跃,并不代表以后还会一直活跃。只有一直变化,才能不被时代抛弃。要么改变,要么衰亡。COBOL的前车之鉴。
流处理:Unix或Linux中,可以通过管道(|)将多个命令连接在一起,其实就是一个流处理,这些命令是同时处理的。Java8借鉴的这一思想,推出了Stream API,这样就可以把输入的不相关部分拿到几个CPU上分别执行,这是几乎免费的并行,用不着费劲搞Thread了。Collection主要是为了存储和访问数据,Stream主要用于描述对数据的计算,前者迭代是外部迭代,开发者自己控制,而后者迭代是内部迭代,开发者无需关心如何迭代,内部迭代可并行迭代,这是外部迭代不好处理的地方。
谓词:predicate,一个返回boolean值的函数。
Optional类:避免出现NullPointerException。
(结构化的)模式匹配:Switch的扩展形式,能够同时将一个数据类型分解成元素。
第2章:通过行为参数化传递代码
应对不断变化的需求
第1次尝试:直接定义一个判断绿苹果的方法,无扩展性。
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor() == Color.GREEN) {
result.add(apple);
}
}
return result;
}
第2次尝试:在参数中增加颜色或重量,可以判断颜色或重量,有一定扩展性。
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor() == color) {
result.add(apple);
}
}
return result;
}
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
第3次尝试:在参数中增加能想到的所有属性,这样的代码可读性和可维护性会非常差,非常不推荐。代码省略。
第4次尝试:抽象行为,利用谓词定义一个接口ApplePredicate来对选择标准建模,不同的选择实现内容不同。然后将接口作为参数传入filter方法。但可惜的是filter方法只能接受对象,所以必须把代码包裹在ApplePredicate对象里面,有点像策略模式,我们还想更加简洁。
interface ApplePredicate {
boolean test(Apple a);
}
static class AppleWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
static class AppleColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getColor() == Color.GREEN;
}
}
static class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getColor() == Color.RED && apple.getWeight() > 150;
}
}
public static List<Apple> filter(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
第5次尝试:使用匿名类,避免声明实现ApplePredicate接口的类,还要实例化。虽然比之前少了很多代码,但还是有很多模板代码。
List<Apple> redApples2 = filter(inventory, new ApplePredicate() {
@Override
public boolean test(Apple a) {
return a.getColor() == Color.RED;
}
});
第6次尝试:使用Lambda表达式,相比匿名类更加简洁,将所有的模板代码都省去。
List<Apple> redApples3 = filter(inventory, (Apple apple) -> Color.RED.equals(apple.getColor()));
第7次尝试:将List类型抽象化,之前的代码只适用于Apple,List类型抽象化,从而超越眼前要处理的问题。在灵活性和简洁性之前找到了最佳平衡点。
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for (T e : list){
if (p.test(e)){
result.add(e);
}
}
return result;
}
List<Integer> numbers = Arrays.asList(1, 2, 4);
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
第3章:Lambda表达式
Lambda表达式可以看成是匿名函数,也是没有声明名称的方法,也可以像匿名函数一样,作为参数传递给一个方法。Lambda表达式没有名称,但有参数列表、函数主体、返回类型和可能的异常列表。
Lambda表达式的有个两种风格:
- 表达式风格:(parameters) -> expression
- 块风格:(parameters) -> { statements;},块中是一个个语句。
函数式接口:只定义一个抽象方法的接口(多个默认实现方法不影响)
可以使用注解@FunctionalInterface来表示这个接口是函数式接口,但不是强制要求一定需要此注解。
函数描述符:函数式接口中的方法签名。
Java8库中常见的函数式接口
- Predicate接口:T -> boolean,布尔表达式
- Consumer接口:T -> void,消费一个对象
- Function接口:T -> R,从一个对象中选择/提取
- Supplier接口:() -> T,创建对象
- UnaryOperator接口:T -> T
- BinaryOperator接口:(T, T) -> T,合并两个值
- BiPredicate接口:(T, U) -> boolean
- BiConsumer接口:(T, U) -> void
- BiFunction接口:(T, U) -> R,比较两个对象
异常处理
- 自定义函数式接口,并声明受检异常
- 把Lambda包在一个try/catch块中
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。因为Lambda表达式将名称都省略掉了,所以只要Lambda表达式的参数列表和出参跟某个函数式接口的方法签名兼容,那么就可以对应传递。
显式二义性问题
两个不同的函数式接口有相同的函数描述符,那么就可能出现一个Lambda表达式对应两个函数式接口。为解决这种问题,可使用显式将Lambda表达式强制转换成对应的接口。
类型推断
Lambda表达式的入参列表可以显式表明类型,也可以不显式表明,后者就是通过类型推断而来。
使用局部变量
Lambda表达式可以没有限制的捕获实例变量和静态变量,但是局部变量必须显式声明为final,或事实上是final。也就是说,Lambda表达式只能捕获指派给它们的局部变量一次。
方法引用
语法::,符号前面是类型,符号后面是方法,针对仅仅涉及单一方法的Lambda的语法糖。
三类方法引用:
- 指向静态方法的方法引用,如Integer::parseInt
- 指向任意类型实例方法的方法引用,引用一个对象的方法,这个对象是Lambda表达式的一个参数,如String::length
- 指向现存对象或表达式实例方法的方法引用,需要传递一个私有辅助方法时特别有用。
构造函数引用,ClassName::new
- 无参构造函数,适合Supplier接口
- 一个参数的构造函数,适合Function接口
复合Lambda表达式的有用方法
- 比较器复合,Comparator接口支持逆序reversed和比较器链thenComparing两个方法
- 谓词复合,Predicate接口支持negate、and和or三个方法
- 函数复合,Function接口有andThen和compose两个默认方法,方法返回Function的一个实例