思维导图

Java8实战

一 为什么使用Java8

在Java 8中加入 Streams 可以看作把另外两项扩充加入Java 8的直接原因:把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法

在与硬件无关的内存模型里,多核处理器上的并发线程可能比在单核处理器上出现的意外行为更多

流处理

流是一系列数据项,一次只生成一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。

Stream<T> 就是一系列 T 类型的项目。

语言需要不断改进以跟进硬件的更新或满足程序员的期待

Java中的函数

我们都知道软件工程中复制粘贴的危险——给一个做了更新和修正,却忘了另一个。

通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。

线程可能会同时访问并更新共享变量。因此,如果没有协调好,数据可能会被意外改变。

Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。

默认方法

使用default进行修饰

以下是你应从本章中学到的关键概念。

  • 请记住语言生态系统的思想,以及语言面临的“要么改变,要么衰亡”的压力。虽然Java可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命运,如COBOL。
  • Java 8中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。
  • 现有的Java编程实践并不能很好地利用多核处理器。
  • 函数是一等值;记得方法如何作为函数式值来传递,还有Lambda是怎样写的。
  • Java 8中 Streams 的概念使得 Collections 的许多方面得以推广,让代码更为易读,并允许并行处理流元素。
  • 你可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。
  • 其他来自函数式编程的有趣思想,包括处理 null 和使用模式匹配。

二 通过行为参数化传递代码

行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。

行为参数化:

  1. 传递代码行为
  2. 多种行为,一个参数

匿名内部类的坏处:

  1. 往往很笨重,因为它占用了很多空间
  2. 难以让人理解

行为参数化与值参数化

小结:

  • 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
  • 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
  • Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理

三 Lambda表达式

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

lambda表达式的基本语法:

1
(parameters) -> expression

或者:

1
(parameters) -> { statements; }

函数式接口就是只定义一个抽象方法的接口,哪怕有很多默认方法,只要接口只定义了一个抽象
方法它就仍然是一个函数式接口。

java.util.function.Predicate 接口定义了一个名叫 test 的抽象方法,它接受泛型T 对象,并返回一个 boolean 。

java.util.function.Consumer 定义了一个名叫 accept 的抽象方法,它接受泛型 T的对象,没有返回( void )。

java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。

常用函数式接口1

常用函数式接口2

ava编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final ,或事实上是 final 。

对局部变量的限制:

  1. 实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。
  2. 这一限制不鼓励你使用改变外部变量的典型命令式编程模式

方法引用:

  1. 指向静态方法的方法引用
  2. 指向任意类型实例方法的方法应用
  3. 指向 现有对象的实例方法引用

对于一个现有构造函数,你可以利用它的名称和关键字 new 来创建它的一个引用:ClassName::new

以下是你应从本章中学到的关键概念。

  • Lambda 表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
  • Lambda 表达式让你可以简洁地传递代码。
  • 函数式接口就是仅仅声明了一个抽象方法的接口。
  • 只有在接受函数式接口的地方才可以使用 Lambda 表达式。
  • Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  • Java 8 自带一些常用的函数式接口,放在 java.util.function 包里,包括 Predicate 、 Function<T,R> 、 Supplier 、 Consumer 和 BinaryOperator ,如表
  • 为了避免装箱操作,对 Predicate 和 Function<T, R> 等通用函数式接口的原始类型特化: IntPredicate 、 IntToLongFunction 等。
  • 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合 Lambda 提高灵活性和可重用性。
  • Lambda 表达式所需要代表的类型称为目标类型。
  • 方法引用让你重复使用现有的方法实现并直接传递它们。
  • Comparator 、 Predicate 和 Function 等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。

四 使用流

好处:

  • 代码是以声明性方式写的
  • 可以将几个基础操作链接起来,来表达复杂的数据处理流水线

Java 8中的Stream API可以让你写出这样的代码:

  • 声明性——更简洁,更易读
  • 可复合——更灵活
  • 可并行——性能更好

集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。

流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样。

Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。与此相反,一旦通过写 for-each 而选择了外部迭代,那你基本上就要自己管理所有的并行问题了。

总而言之,流的使用一般包括三件事:

* 一个数据源(如集合)来执行一个查询;
* 一个中间操作链,形成一条流的流水线;
* 一个终端操作,执行流水线,并能生成结果。

流操作

小结:

  • 流是“从支持数据处理操作的源生成的一系列元素”。
  • 流利用内部迭代:迭代通过 filter 、 map 、 sorted 等操作被抽象掉了。
  • 流操作有两类:中间操作和终端操作。
  • filter 和 map 等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果。
  • forEach 和 count 等终端操作会返回一个非流的值,并处理流水线以返回结果。
  • 流中的元素是按需计算的

五 使用流

Streams 接口支持 filter 方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的hashCode 和 equals 方法实现)的流。

流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给 limit 。如果流是有序的,则最多会返回前 n 个元素。

流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。

使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。

一言以蔽之, flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”。

allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。

noneMatch 它可以确保流中没有任何元素与给定的谓词匹配。

reduce 接受两个参数:

  • 一个初始值,这里是0;
  • 一个 BinaryOperator<T> 来将两个元素结合起来产生一个新值

中间操作和终端操作

Java 8引入了三个原始类型特化流接口来解决这个问题: IntStream 、 DoubleStream 和LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。

将流转换为特化版本的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。

Optional 可以用Integer 、 String 等参考类型来参数化。对于三种原始流特化,也分别有一个 Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。

Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。

小结:

  • Streams API可以表达复杂的数据处理查询
  • 你可以使用 filter 、 distinct 、 skip 和 limit 对流做筛选和切片。
  • 你可以使用 map 和 flatMap 提取或转换流中的元素。
  • 你可以使用 findFirst 和 findAny 方法查找流中的元素。你可以用 allMatch 、noneMatch 和 anyMatch 方法让流匹配给定的谓词。
  • 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
  • 你可以利用 reduce 方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
  • filter 和 map 等操作是无状态的,它们并不存储任何状态。 reduce 等操作要存储状态才能计算出一个值。 sorted 和 distinct 等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。
  • 流有三种基本的原始类型特化: IntStream 、 DoubleStream 和 LongStream 。它们的操作也有相应的特化。
  • 流不仅可以从集合创建,也可从值、数组、文件以及 iterate 与 generate 等特定方法创建。
  • 无限流是没有固定大小的流

六 用流收集数据

一般来说, Collector 会对元素应用一个转换函数,并将结果累积在一个数据结构中,从而产生这一过程的最终输出。

预定义收集器的功能,也就是那些可以从 Collectors类提供的工厂方法(例如 groupingBy )创建的收集器。它们主要提供了三大功能:

  • 将流元素归约和汇总为一个值
  • 元素分组
  • 元素分区

![Collectors 类的静态工厂方法](\images\Java8实战\Collectors 类的静态工厂方法.jpg)

![Collectors 类的静态工厂方法2](\images\Java8实战\Collectors 类的静态工厂方法2.jpg)

Collector接口的声明:

  1. 建立新的结果容器:supplier方法
  2. 将元素添加到结果容器: accumulator 方法
  3. 对结果容器应用最终转换: finisher 方法
  4. 合并两个结果容器: combiner 方法
  5. characteristics 方法

characteristics 会返回一个不可变的 Characteristics 集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics 是一个包含三个项目的枚举。

  • UNORDERED ——归约结果不受流中项目的遍历和累积顺序的影响。
  • CONCURRENT —— accumulator 函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为 UNORDERED ,那它仅在用于无序数据源时才可以并行归约。
  • IDENTITY_FINISH ——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器 A 不加检查地转换为结果 R 是安全的。

小结:

  • collect 是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。
  • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
  • 预定义收集器可以用 groupingBy 对流中元素进行分组,或用 partitioningBy 进行分区。
  • 收集器可以高效地复合起来,进行多级分组、分区和归约。
  • 你可以实现 Collector 接口中定义的方法来开发你自己的收集器。

七 并行数据处理与性能

并行流

并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

对顺序流调用 parallel 方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个 boolean 标志,表示你想让调用 parallel 之后进行的所有操作都并行执行。类似地,你只需要对并行流调sequential 方法就可以把它变成顺序流。

特别是在优化性能时,你应该始终遵循三个黄金规则:测量,测量,再测量。

用传统 for 循环的迭代版本执行起来应该会快很多,因为它更为底层,更重要的是不需要对原始类型做任何装箱或拆箱操作。

**并行化并不是没有代价的。**并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。

决定使用并行流的条件:

  • 如果有疑问,测量
  • 留意装箱
  • 有些操作本身在并行流上执行的代建非常大
  • 考虑流的操作流水线的总计算成本
  • 对于较小的数据量,选择并行流几乎从来都不是一个号的决定
  • 考虑流背后的数据结构是否易于分解
  • 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
  • 还要考虑终端操作中合并步骤的代价是大是小

流的数据源和可分解性

分支/合并框架

分支合并过程

**在实际应用时,使用多个 ForkJoinPool 是没有什么意义的。**一般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任何部分方便地重用了。

使用分支/合并框架的最佳做法:

  • 对一个任务调用 join 方法会阻塞调用方,直到该任务做出结果。
  • 不应该在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法。相反,你应该始终直接调用 compute 或 fork 方法,只有顺序代码才应该用 invoke 来启动并行计算。
  • 对子任务调用 fork 方法可以把它排进 ForkJoinPool 。
  • 调试使用分支/合并框架的并行计算可能有点棘手。
  • 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。

分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应用中,这意味着这些任务差不多被平均分配到 ForkJoinPool 中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。

Spliterator

Spliterator 是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitableiterator)。

Spliterator的特性

小结:

  • 内部迭代让你可以并行处理一个流,而无需在代码中显式使用和协调不同的线程。
  • 虽然并行处理一个流很容易,却不能保证程序在所有情况下都运行得更快。并行软件的行为和性能有时是违反直觉的,因此一定要测量,确保你并没有把程序拖得更慢。
  • 像并行流那样对一个数据集并行执行操作可以提升性能,特别是要处理的元素数量庞大,或处理单个元素特别耗时的时候。
  • 从性能角度来看,使用正确的数据结构,如尽可能利用原始流而不是一般化的流,几乎总是比尝试并行化某些操作更为重要。
  • 分支/合并框架让你得以用递归方式将可以并行的任务拆分成更小的任务,在不同的线程上执行,然后将各个子任务的结果合并起来生成整体结果。
  • Spliterator 定义了并行流如何拆分它要遍历的数据。

八 重构、测试和调试

在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩。

策略模式包含三部分内容:

  • 一个代表某个算法的接口(它是策略模式的接口)。
  • 一个或多个该接口的具体实现,它们代表了算法的多种实现
  • 一个或多个使用策略对象的客户。

这些表示错误发生在Lambda表达式内部。由于Lambda表达式没有名字,所以编译器只能为它们指定一个名字。

如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。

这就是流操作方法 peek 大显身手的时候。 peek 的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像 forEach 那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。

小结:

  • Lambda表达式能提升代码的可读性和灵活性。
  • 如果你的代码中使用了匿名类,尽量用Lambda表达式替换它们,但是要注意二者间语义的微妙差别,比如关键字 this ,以及变量隐藏。
  • 跟Lambda表达式比起来,方法引用的可读性更好 。、
  • 尽量使用Stream API替换迭代式的集合处理。
  • Lambda表达式有助于避免使用面向对象设计模式时容易出现的僵化的模板代码,典型的比如策略模式、模板方法、观察者模式、责任链模式,以及工厂模式。
  • 即使采用了Lambda表达式,也同样可以进行单元测试,但是通常你应该关注使用了Lambda表达式的方法的行为。
  • 尽量将复杂的Lambda表达式抽象到普通方法中。
  • Lambda表达式会让栈跟踪的分析变得更为复杂。
  • 流提供的 peek 方法在分析Stream流水线时,能将中间变量的值输出到日志中,是非常有用的工具。

九 默认方法

其一,Java 8允许在接口内声明静态方法。其二,Java 8引入了一个新功能,叫默认方法,通过默认方法
你可以指定接口方法的默认实现。

默认方法的主要目标用户是类库的设计者啊

变更对Java程序的影响大体可以分成三种类型的兼容性,分别是:二进制级的兼容、源代码级的兼容,以及函数行为的兼容。

String 类被声明为 final ,因为我们不希望有人对这样的核心功能产生干扰。

解决继承问题的三条规则:

  1. 类中的方法优先级最高
  2. 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果 B 继承了 A ,那么 B 就比 A 更加具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

Java 8中引入了一种新的语法 X.super.m(…) ,其中 X 是你希望调用的 m方法所在的父接口。

菱形继承问题

你只需要遵守下面这三条准则就能解决所有可能的冲突:

  1. 首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。
  2. 如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
  3. 最后,如果冲突依旧无法解决,你就只能在你的类中覆盖该默认方法,显式地指定在你的类中使用哪一个接口中的方法。

小结:

  • Java 8中的接口可以通过默认方法和静态方法提供方法的代码实现。
  • 默认方法的开头以关键字 default 修饰,方法体与常规的类方法相同。
  • 向发布的接口添加抽象方法不是源码兼容的。
  • 默认方法的出现能帮助库的设计者以后向兼容的方式演进API。
  • 默认方法可以用于创建可选方法和行为的多继承。
  • 我们有办法解决由于一个类从多个接口中继承了拥有相同函数签名的方法而导致的冲突。
  • 类或者父类中声明的方法的优先级高于任何默认方法。如果前一条无法解决冲突,那就选择同函数签名的方法中实现得最具体的那个接口的方法。
  • 两个默认方法都同样具体时,你需要在类中覆盖该方法,显式地选择使用哪个接口中提供的默认方法。

![Optional 类的方法1](\images\Java8实战\Optional 类的方法1.jpg)

![Optional 类的方法2](\images\Java8实战\Optional 类的方法2.jpg)

小结:

  • null 引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8中引入了一个新的类 java.util.Optional<T> ,对存在或缺失的变量值进行建模。
  • 你可以使用静态工厂方法 Optional.empty 、 Optional.of 以及 Optional.ofNull-able 创建 Optional 对象。
  • Optional 类支持多种方法,比如 map 、 flatMap 、 filter ,它们在概念上与 Stream 类中对应的方法十分相似。
  • 使用 Optional 会迫使你更积极地解引用 Optional 对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
  • 使用 Optional 能帮助你设计更好的API,用户只需要阅读方法签名,就能了解该方法是否接受一个 Optional 类型的值。

十一 CompletableFuture:组合式异步编程

两种趋势不断地推动我们反思我们设计软件的方式。第一种趋势和应用运行的硬件平台相关,第二种趋势与应用程序的架构相关,尤其是它们之间如何交互。

并发和并行

![使用 Future 以异步方式执行长时间的操作](\images\Java8实战\使用 Future 以异步方式执行长时间的操作.jpg)

CompletableFuture 具有一定的优势,因为它允许你对执行器( Executor )进行配置,尤其是线程池的大小,让它以更适合应用需求的方式进行配置,满足程序的要求。

如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。

N threads = N CPU * U CPU * (1 + W/C)
其中:

  • N CPU 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProce-ssors() 得到
  • U CPU 是期望的CPU利用率(该值应该介于0和1之间)
  • W/C是等待时间与计算时间的比率

使用并行API的建议:

  • 如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用 Stream 接口,因为实现简单,同时效率也可能是最高的
  • 反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用CompletableFuture 灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。

小结:

  • 执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可以改善程序的性能,加快程序的响应速度。
  • 你应该尽可能地为客户提供异步API。使用 CompletableFuture 类提供的特性,你能够轻松地实现这一目标。
  • CompletableFuture 类还提供了异常管理的机制,让你有机会抛出/管理异步任务执行中发生的异常。
  • 将同步API的调用封装到一个 CompletableFuture 中,你能够以异步的方式使用其结果。
  • 如果异步任务之间相互独立,或者它们之间某一些的结果是另一些的输入,你可以将这些异步任务构造或者合并成一个。
  • 你可以为 CompletableFuture 注册一个回调函数,在 Future 执行完毕或者它们计算的结果可用时,针对性地执行一些程序。
  • 你可以决定在什么时候结束程序的运行,是等待由 CompletableFuture 对象构成的列表中所有的对象都执行完毕,还是只要其中任何一个首先完成就中止程序的运行。

十二 新的日期和时间API

TemporalField 是一个接口,它定义了如何访问 temporal 对象某个字段的值。 ChronoField 枚举实现了这一接口,所以你可以很方便地使用 get 方法得到枚举元素的值。

日期时间类1

日期时间类2

表示时间点的日期_时间类的通用方法

![TemporalAdjuster 类中的工厂方法](\images\Java8实战\TemporalAdjuster 类中的工厂方法.jpg)

小结:

  • Java 8之前老版的 java.util.Date 类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
  • 新版的日期和时间API中,日期时间对象是不可变的。
  • 新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
  • 你可以用绝对或者相对的方式操纵日期和时间,操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
  • TemporalAdjuster 让你能够用更精细的方式操纵日期,不再局限于一次只能改变它的一个值,并且你还可按照需求定义自己的日期转换器。
  • 你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
  • 你可以用相对于某个地区/位置的方式,或者以与UTC/格林尼治时间的绝对偏差的方式表示时区,并将其应用到日期时间对象上,对其进行本地化。
  • 你现在可以使用不同于ISO-8601标准系统的其他日历系统了。

十三 函数式思考

在函数式编程的上下文中,一个“函数”对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用。你可以把它看成一个黑盒,它接收输入并产生一些输出。

一个没有任何副作用的函数

要被称为函数式,函数或者方法不应该抛出任何异常。

如果一个函数只要传递同样的参数值,总是返回同样的结果,那这个函数就是引用透明的。

小结:

  • 从长远看,减少共享的可变数据结构能帮助你降低维护和调试程序的代价。
  • 函数式编程支持无副作用的方法和声明式编程。
  • 函数式方法可以由它的输入参数及输出结果进行判断。
  • 如果一个函数使用相同的参数值调用,总是返回相同的结果,那么它是引用透明的。采用递归可以取得迭代式的结构,比如 while 循环。
  • 相对于Java语言中传统的递归,“尾递”可能是一种更好的方式,它开启了一扇门,让我们有机会最终使用编译器进行优化。

十四 函数式编程技巧

满足下面任一要求就可以被称为高阶函数(higher-order function):

  • 接受至少一个函数作为参数
  • 返回的结果是一个函数

科里化是一种将具备2个参数(比如, x 和 y )的函数 f 转化为使用一个参数的函数 g ,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的返回值相同,即 f(x,y) = (g(x))(y) 。

我希望对树结构的更新对某些用户可见(当然,这句话的潜台词是其他人看不到这些更新)。”那么,要实现这一目标,你可以通过两种方式:第一种是典型的Java解决方案(对对象进行更新时,你需要特别小心,慎重地考虑是否需要在改动之前保存对象的一份副本)。另一种是函数式的解决方案:逻辑上,你在做任何改动之前都会创建一份新的数据结构(这样一来就不会有任何的对象发生变更),只要确保按照用户的需求传递给他正确版本的数据结构就好了。

小结:

  • 一等函数是可以作为参数传递,可以作为结果返回,同时还能存储在数据结构中的函数。
  • 高阶函数接受至少一个或者多个函数作为输入参数,或者返回另一个函数的函数。Java中典型的高阶函数包括 comparing 、 andThen 和 compose 。
  • 科里化是一种帮助你模块化函数和重用代码的技术。
  • 持久化数据结构在其被修改之前会对自身前一个版本的内容进行备份。因此,使用该技术能避免不必要的防御式复制。
  • Java语言中的Stream不是自定义的。
  • 延迟列表是Java语言中让Stream更具表现力的一个特性。延迟列表让你可以通过辅助方法( supplier )即时地创建列表中的元素,辅助方法能帮忙创建更多的数据结构。
  • 模式匹配是一种函数式的特性,它能帮助你解包数据类型。它可以看成Java语言中 switch语句的一种泛化。
  • 遵守“引用透明性”原则的函数,其计算结构可以进行缓存。
  • 结合器是一种函数式的思想,它指的是将两个或多个函数或者数据结构进行合并。

十五 面向对象和函数式编程的混合:Java 8和Scala的比较

Java 8和Scala都是整合了面向对象编程和函数式编程特性的编程语言,它们都运行于JVM之上,在很多时候可以相互操作。
Scala支持对集合的抽象,支持处理的对象包括 List 、 Set 、 Map 、 Stream 、 Option ,这些和Java 8非常类似。不过,除此之外Scala还支持元组。
Scala为函数提供了更加丰富的特性,这方面比Java 8做得好,Scala支持:函数类型、可以不受限制地访问本地变量的闭包,以及内置的科里化表单。
Scala中的类可以提供隐式的构造器、getter方法以及setter方法。
Scala还支持trait,它是一种同时包含了字段和默认方法的接口。

其它新特性

Java 8在两个方面对注解机制进行了改进,分别为:

  • 你现在可以定义重复注解
  • 使用新版Java,你可以为任何类型添加注解

从Java 8开始,注解已经能应用于任何类型

集合类和接口中新增的方法

Files 类最引人注目的改变是,你现在可以用文件直接产生流。