目录
第11章:CompletableFuture:组合式、异步编程
第15章:面向对象和函数式编程的混合:Java 8 和Scala 的比较
第三部分
Part 3
高效 Java 8 编程
本书第三部分将探究如何结合现代程序设计方法利用Java 8的各种特性更有效地改善代码 质量。
第8章会介绍如何利用Java 8的新特性及一些技巧,改进现有代码。除此之外,还会探讨 一些非常重要的软件开发技术,譬如设计模式、重构、测试以及调试。
第9章中,你会了解什么是默认方法,如何以兼容的方式使用默认方法改进API,一些实 用的使用模式,以及有效地利用默认方法的规则。
第10章围绕Java 8中全新引入的java.util.Optional类展开。java.util.Optional类能帮助我们设计出更优秀的API,同时降低了空指针异常发生的几率。
第11章着重介绍CompletableFuture类。通过CompletableFuture类,我们能以声 明性方式描述复杂的异步计算,即并行Stream API的设计。
第12章探讨了新的Date和Time接口,这些新接口极大地优化了之前处理日期和时间时极 易出错的API。
第8章:重构、测试和调试
我们会介绍几种方法,帮助你重构代码,以适配使用Lambda表达式,让你维护的代码具备更好的可读性和灵活性。除此之外,我们还会讨论目前比较流行的 几种面向对象的设计模式,包括策略模式、模板方法模式、观察者模式、责任链模式,以及工厂 模式,在结合Lambda表达式之后变得更简洁的情况。最后,我们会介绍如何测试和调试使用Lambda表达式和Stream API的代码。
匿名 类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但 是在Lambda中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不 能(它们会导致编译错误)
这篇文章对转换的整个过程进行了深入细致的描述,值得一读:http://dig.cs.illinois.edu/papers/lambda-Refactoring.pdf
在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦 涩
可以尝试使用显式的类型转换来解决这种模棱两可的情况
多用方法引用,避免晦涩
应该尽量考虑使用静态辅助方法,比如comparing、maxBy。这些方法设 计之初就考虑了会结合方法引用一起使用
不幸的是,将命令式的代码结构转换为Stream API的形式是个困难的任务,因为你需要考虑 控制流语句,比如break、continue、return,并选择使用恰当的流操作。好消息是已经有一 些工具可以帮助我们完成这个任务
请参考http://refactoring.info/tools/LambdaFicator/
有条件的延迟执行 & 环绕执行
如果你发现你需要频繁地从客户端代码去查询一个对象 11的状态(比如前文例子中的日志器的状态),只是为了传递参数、调用该对象的一个方法(比如
输出一条日志),那么可以考虑实现一个新的方法,以Lambda或者方法表达式作为参数,新方法 在检查完该对象的状态之后才调用原来的方法。你的代码会因此而变得更易读(结构更清晰), 封装性更好(对象的状态也不会暴露给客户端代码了)。
使用Lambda重构面向对象的设计模式:
策略模式 模板方法 观察者模式 责任链模式 工厂模式
Lambda表达式避免了采用策略设计模式时僵化的模板代码。Lambda表达式实际已经对部分代码(或策略)进行了封装,而 这就是创建策略设计模式的初衷。强烈建议尽量使用Lambda表达式来解决。
Subject使用registerObserver方法可以注册一个新的观察者,使用notifyObservers方法通知它的观察者一个新闻的到来。让我们更进一步,实现Feed类:
Class Feed implements Subject{
private final List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
} }
Observer接口的所有实现类都提供了一个方法:notify。新闻到达时,它们都只是对同一 段代码封装执行
但是,观察者的逻辑有可能十分复杂,它们可能还持有状态,抑或定义了多个方法,诸如此 类。在这些情形下,你还是应该继续使用类的方式。
责任链模式:Lambda将两个方法结合起来,结果就是一个操作链
Lambda表达式会生成函数接口的一个实例。由此,你可以测试该实例的行为
使用日志调试:
.forEach(System.out::println);
8.5 小结
下面回顾一下这一章的主要内容。
-
Lambda表达式能提升代码的可读性和灵活性。
-
如果你的代码中使用了匿名类,尽量用Lambda表达式替换它们,但是要注意二者间语义
的微妙差别,比如关键字this,以及变量隐藏。
-
跟Lambda表达式比起来,方法引用的可读性更好 。
-
尽量使用Stream API替换迭代式的集合处理。
-
Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的
比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
-
即使采用了Lambda表达式,也同样可以进行单元测试,但是通常你应该关注使用了
Lambda表达式的方法的行为。
-
尽量将复杂的Lambda表达式抽象到普通方法中。
-
Lambda表达式会让栈跟踪的分析变得更为复杂。
-
流提供的peek方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有
用的工具。
第9章:默认方法
向接口添加方法是诸多问题的罪恶之源;一旦接口发生变化,实现这些接口的类 往往也需要更新,提供新添方法的实现才能适配接口的变化。
这就是引入默认方法的目的: 它让类可以自动地继承接口的一个默认实现。
静态方法及接口
同时定义接口以及工具辅助类(companion class)是Java语言常用的一种模式,工具类定 义了与接口实例协作的很多静态方法。比如,Collections就是处理Collection对象的辅 助类。由于静态方法可以存在于接口内部,你代码中的这些辅助类就没有了存在的必要,你可 以把这些静态方法转移到接口内部。为了保持后向的兼容性,这些类依然会存在于Java应用程 序的接口之中。
默认方法在Java 8的API中已经大量地使用了。本章已经介绍过我们前一章 中大量使用的Collection接口的stream方法就是默认方法。List接口的sort方法也是默认方 法。第3章介绍的很多函数式接口,比如Predicate、Function以及Comparator也引入了新的 6默认方法,比如Predicate.and或者Function.andThen(记住,函数式接口只包含一个抽象 方法,默认方法是种非抽象方法)。
Java 8中的抽象类和抽象接口那么抽象类和抽象接口之间的区别是什么呢?它们不都能包含抽象方法和包含方法体的实现吗?
首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。 其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。
默认方法是一种以源码兼容方 式向接口内添加实现的方法。这样实现Collction的所有类(包括并不隶属Collection API的 用户扩展类)都能使用removeIf的默认实现。
默认方法:减少无效模板代码——实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。
行为的多继承。这是一种让 类从多个来源重用代码的能力
保持接口的精致性和正交性 能帮助你在现有的代码基上最大程度地实现代码复用和行为组合
组合接口:需要给出所有抽象方法的实 现,但无需重复实现默认方法
尽量不要选择继承——通过精简的接口,你能获得最有效的组合, 因为你可以只选择你需要的实现
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条 规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优 先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择 拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法——
B.super.hello();
小结:
下面是本章你应该掌握的关键概念。
-
Java 8中的接口可以通过默认方法和静态方法提供方法的代码实现。
-
默认方法的开头以关键字default修饰,方法体与常规的类方法相同。
-
向发布的接口添加抽象方法不是源码兼容的。
-
默认方法的出现能帮助库的设计者以后向兼容的方式演进API。
-
默认方法可以用于创建可选方法和行为的多继承。
-
我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
第10章:用Optional 取代 null
Optional:链式调用,用flatMap
Optional的设计初衷仅仅是要支持能返回Optional对象的语法。由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现Serializable接口
如果你一定要实现序列化的域模型,作为替代方案, 我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,代码清单如下:
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
get() orElse() orElseGet() orElseThrow() ifPresent()
filter()
以不解包的方式组合两个Optional对象结合本节中介绍的map和flatMap方法,用一行语句重新实现之前出现的nullSafeFind-
CheapestInsurance()方法。 答案:你可以像使用三元操作符那样,无需任何条件判断的结构,以一行语句实现该方法,代码如下。
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
第11章:CompletableFuture:组合式、异步编程
CompletableFuture和Future的关系就跟Stream和Collection的关系一样
使用工厂方法supplyAsync创建CompletableFuture对象
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由ForkJoinPool池中的某个执行线程(Executor)运行,但是你也可以使用supplyAsync方法的重载版本,传 递第二个参数指定不同的执行线程执行生产者方法。
并行——使用流还是CompletableFutures?
目前为止,你已经知道对集合进行并行计算有两种方式:要么将其转化为并行流,利用map这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在Completable- Future内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助 你确保整体的计算不会因为线程都在等待I/O而发生阻塞。
我们对使用这些API的建议如下。
❑如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。
❑反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用CompletableFuture灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的 流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。
thenAccept():它接收CompletableFuture执行完毕后的返回值做参数
thenXXX() 函数都有 Async 版本
第12章:新的日期和时间API
DateFormat方法也有它自己的问题。比如,它不是线程安全的。这意味着两个线程如果尝 试使用同一个formatter解析日期,你可能会得到无法预期的结果。
最后,Date和Calendar类都是可以变的。
Joda-time,Java8 中进行了整合;
LocalDate、LocalTime、Instant、Duration以及Period
这一章中,你应该掌握下面这些内容。
Java 8之前老版的java.util.Date类以及其他用于建模日期时间的类有很多不一致及
设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
-
新版的日期和时间API中,日期时间对象是不可变的。
-
新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。 9
-
你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老
的日期时间对象不会发生变化。
TemporalAdjuster让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
-
你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。
这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
-
你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表
示时区,并将其应用到日期时间对象上,对其进行本地化。
-
你现在可以使用不同于ISO-8601标准系统的其他日历系统了。
第四部分
Part 4
在本书的最后一部分,我们简单地介绍Java中的函数式编程,并对Java 8和Scala中相关 的特性进行比较。
第13章中,我们会全面地介绍函数式编程,介绍它的术语,并详细介绍如何在Java 8中 进行函数式编程。
第14章会讨论函数式编程的一些高级技术,包括高阶函数、科里化、持久化数据结构、 延迟列表,以及模式匹配。你可以将这一章看作一道混合大餐,它既包含了能直接应用到你 代码中的实战技巧,也囊括了一些学术性的知识,帮助你成为知识更加渊博的程序员。
第15章讨论Java 8和Scala语言的特性比较——Scala是一种新型语言,它和Java有几分相 似,都构建于JVM之上,最近一段时间发展很迅猛,在编程生态系统中已经对Java某些方面 的固有地位造成了威胁。
最后,我们在第16章回顾了学习Java 8的旅程,以及向函数式编程转变的潮流。除此之 外,我们还展望了会有哪些改进以及重要的新的特性可能出现在Java 8之后的版本里。
第13章:函数式的思考
下面是这一章中你应该掌握的关键概念。
-
从长远看,减少共享的可变数据结构能帮助你降低维护和调试程序的代价。
-
函数式编程支持无副作用的方法和声明式编程。
-
函数式方法可以由它的输入参数及输出结果进行判断。
-
如果一个函数使用相同的参数值调用,总是返回相同的结果,那么它是引用透明的。采
用递归可以取得迭代式的结构,比如while循环。
-
相对于Java语言中传统的递归,“尾-递”可能是一种更好的方式,它开启了一扇门,让我 们有机会最终使用编译器进行优化。
方法factorialHelper属于“尾递”类型的函数,原因是递归调用发生在方法的最后。对 比我们前文中factorialRecursive方法的定义,这个方法的最后一个操作是乘以n,从而得到 递归调用的结果。
这种形式的递归是非常有意义的,现在我们不需要在不同的栈帧上保存每次递归计算的中间 值,编译器能够自行决定复用某个栈帧进行计算。实际上,在factorialHelper的定义中,立 即数(阶乘计算的中间结果)直接作为参数传递给了该方法。再也不用为每个递归调用分配单独 的栈帧用于跟踪每次递归调用的中间值——通过方法的参数能够直接访问这些值。
第14章:函数式编程的技巧
Stream:“懒”
https://blog.csdn.net/dm_vincent/article/details/40503685
Supplier接口的 get方法
Java8改进了Map接口,提供了一个名为computeIfAbsent的方法处理这样的情况
一旦并发和可变状态的对象揉到 一起,它们引起的复杂度要远超我们的想象,而函数式编程能从根本上解决这一问题。当然,这 也有一些例外,比如出于底层性能的优化,可能会使用缓存,而这可能会有一些影响。另一方面, 如果不使用缓存这样的技巧,如果你以函数式的方式进行程序设计,那就完全不必担心你的方法 是否使用了正确的同步方式,因为你清楚地知道它没有任何共享的可变状态。
函数式编程通常不使用==(引用相等),而是使用equal对数据结构值进行比 较,由于数据没有发生变更,所以这种模式下fupdate是引用透明的。
下面是本章中你应该掌握的重要概念。
一等函数是可以作为参数传递,可以作为结果返回,同时还能存储在数据结构中的函数。
高阶函数接受至少一个或者多个函数作为输入参数,或者返回另一个函数的函数。Java中典型的高阶函数包括comparing、andThen和compose。如果n的值为0,直接返回 “什么也不做”的标识符
科里化是一种帮助你模块化函数和重用代码的技术。
-
持久化数据结构在其被修改之前会对自身前一个版本的内容进行备份。因此,使用该技 术能避免不必要的防御式复制。
-
Java语言中的Stream不是自定义的。
-
延迟列表是Java语言中让Stream更具表现力的一个特性。延迟列表让你可以通过辅助方法
(supplier)即时地创建列表中的元素,辅助方法能帮忙创建更多的数据结构。
-
模式匹配是一种函数式的特性,它能帮助你解包数据类型。它可以看成Java语言中switch
语句的一种泛化。
-
遵守“引用透明性”原则的函数,其计算结构可以进行缓存。
-
结合器是一种函数式的思想,它指的是将两个或多个函数或者数据结构进行合并。
-