java 8实战

第一部分 基础知识

第一章 为什么要关心Java8

1.1.2 流处理
1.1.3用行为参数化把代码传递给方法
1.1.4并行与共享的可变数据(几乎免费的并行)
1.1.5Java需要演变
1.2.1方法和Lambda作为一等公民 方法引用::语法
Lambda——匿名函数

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

本章内容
应对不断变化的需求
打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:go,它可以接受不同的新行为作为参数,然后去执行。
1、建模

publicinterfaceApplePredicate{
  boolean  test(Appleapple);
}

2、实现

public class AppleHeavyWeightPredicate implements ApplePredicate{
 publicbooleantest(Appleapple){
 return apple.getWeight()>150;
}
}
publicclass AppleGreenColorPredicateimplementsApplePredicate{
publicbooleantest(Appleapple){
return"green".equals(apple.getColor());
}
}

3、以前的实现方式

List<Apple> redAndHeavyApples=
filterApples(inventory,new AppleRedAndHeavyPredicate());

4、匿名内部类

List<Apple>redApples=filterApples(inventory,newApplePredicate(){publicbooleantest(Appleapple){
return"red".equals(apple.getColor());
}
});

5、使用Lambda表达式

List<Apple> result=
filterApples(inventory,(Apple apple)->"red".equals(apple.getColor()));

第三章 Lambda表达式

1、Lambda
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
请注意,任何函数式接口都不允许抛出受检异常(checkedexception)。如果你需要Lambda
表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
比如,在3.3节我们介绍了一个新的函数式接口BufferedReaderProcessor,它显式声明了一个IOException:

@FunctionalInterface
public interface BufferedReaderProcessor{
    String process(BufferedReaderb)throwsIOException;
}

但是你可能是在使用一个接受函数式接口的API,比如Function<T,R>,没有办法自己创建一个(你会在下一章看到,StreamAPI中大量使用表3-2中的函数式接口)。这种情况下你可以显式捕捉受检异常:
在这里插入图片描述
3.5类型检查、类型推断以及限制
3.5.1类型检查
在这里插入图片描述
类型检查过程可以分解为如下所示。
首先,你要找出filter方法的声明。
第二,要求它是Predicate(目标类型)对象的第二个正式参数。
第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。最后,filter的任何实际参数都必须匹配这个要求。
这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

3.5.2同样的Lambda,不同的函数式接口

Callable<Integer> c=()->42;
PrivilegedAction<Integer> p=()->42;


 Comparator<Apple> c1 = (Applea1, Applea2) -> a1.getWeight().compareTo(a2.getWeight());
    ToIntBiFunction<Apple, Apple> c2 = (Applea1, Applea2) -> a1.getWeight().compareTo(a2.getWeight());
    BiFunction<Apple, Apple, Integer> c3 = (Applea1, Applea2) -> a1.getWeight().compareTo(a2.getWeight());

3.5.3类型推断
在这里插入图片描述

3.5.4使用局部变量

在这里插入图片描述

3.6方法引用

在这里插入图片描述
在这里插入图片描述
3.6.2 构造函数引用
在这里插入图片描述
3.8.1 比较器复合

在这里插入图片描述
3.8.2 谓词复合
在这里插入图片描述
3.8.3 函数复合

在这里插入图片描述
在这里插入图片描述

第二部分 函数式数据处理

第四章 引入流

4.1 流是什么
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不
是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。
在这里插入图片描述

在这里插入图片描述
4.2 流简介

那么,流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”。让
我们一步步剖析这个定义。
 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序
值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元
素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的
filter、sorted和map。集合讲的是数据,流讲的是计算。我们会在后面几节中详细解
释这个思想。
 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集
合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中
的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执
行,也可并行执行。
此外,流操作有两个重要的特点。
 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大
的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以
看作对数据源进行数据库式查询。
 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。我们
在第1章中简要地提到了这个思想,下一节会再谈到它。
在这里插入图片描述

第5章 使用流

5.2 映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选
择一列。Stream API也通过map和flatMap方法提供了类似的工具。

5.2.2 流的扁平化

在这里插入图片描述
在这里插入图片描述

5.3 查找和匹配

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

Optional<Dish> dish = 
 menu.stream() 
 .filter(Dish::isVegetarian) 
 .findAny();

3、查找第一个元素

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5); 
Optional<Integer> firstSquareDivisibleByThree = 
 someNumbers.stream() 
 .map(x -> x * x) 
 .filter(x -> x % 3 == 0) 
 .findFirst(); // 9

5.4 归约

1、 元素求和

int sum = numbers.stream().reduce(0, (a, b) -> a + b);


在这里插入图片描述
2、Optional max = numbers.stream().reduce(Integer::max);

在这里插入图片描述
在这里插入图片描述

第六章 用流收集数据

1、收集框架的灵活性:以不同的方法执行同样的操作

int totalCalories = menu.stream().collect(reducing(0, 
 Dish::getCalories,
 Integer::sum));

在这里插入图片描述
2、 分组

public enum CaloricLevel { DIET, NORMAL, FAT } 
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect( 
 groupingBy(dish -> { 
 if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return 
 CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 } ));

现在,你已经看到了如何对菜单中的菜肴按照类型和热量进行分组,但要是想同时按照这两
个标准分类怎么办呢?分组的强大之处就在于它可以有效地组合。让我们来看看怎么做。

3、多级分组

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = 
menu.stream().collect( 
 groupingBy(Dish::getType, 
 groupingBy(dish -> { 
 if (dish.getCalories() <= 400) return CaloricLevel.DIET; 
 else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
 else return CaloricLevel.FAT; 
 } ) 
 ) 
);

这个二级分组的结果就是像下面这样的两级Map:


{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, 
 FISH={DIET=[prawns], NORMAL=[salmon]}, 
 OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}

4、按子组收集数据

Map<Dish.Type, Long> typesCount = menu.stream().collect( 
 groupingBy(Dish::getType, counting())); 

其结果是下面的Map:
{MEAT=3, FISH=2, OTHER=4}

还要注意,普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f,
toList())的简便写法。

Map<Dish.Type, Optional<Dish>> mostCaloricByType = 
 menu.stream() 
 .collect(groupingBy(Dish::getType, 
 maxBy(comparingInt(Dish::getCalories))));

这个分组的结果显然是一个map,以Dish的类型作为键,以包装了该类型中热量最高的Dish
的Optional作为值:
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}

5、查找每个子组中热量最高的Dish

Map<Dish.Type, Dish> mostCaloricByType = 
 menu.stream() 
 .collect(groupingBy(Dish::getType,
 collectingAndThen( 
 maxBy(comparingInt(Dish::getCalories)), 
 Optional::get)));

在这里插入图片描述

第七章 并行数据处理与性能

在Java 7之前,并行处理数据集合非常麻烦。第一,你得明确地把包含数据的数据结
构分成若干子部分。第二,你要给每个子部分分配一个独立的线程。第三,你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成,最后把这些部分结果合并起来。Java 7引入了一个叫作分支/合并的框架,让这些操作更稳定、更不易出错。

1、 将顺序流转换为并行流

public static long parallelSum(long n) { 
 return Stream.iterate(1L, i -> i + 1) 
 .limit(n) 
 .parallel() 
 .reduce(0L, Long::sum); 
}

在这里插入图片描述
并行流内部使用了默认的ForkJoinPool(7.2节会进一步讲到分支/合并框架),它默认的
线程数量就是你的处理器数量,这个值是由 Runtime.getRuntime().availableProcessors()得到的。但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common.
parallelism来改变线程池大小,如下所示:
System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”,“12”);
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个
并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值,
除非你有很好的理由,否则我们强烈建议你不要修改它。

尽管如此,请记住,并行化并不是没有代价的。并行化过程本身需要对流做递归划分,把每
个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。总而言之,很多情况下不可能或不方便并行化。然而,在使用并行Stream加速代码之前,你必须确保用得对;如果结果错了,算得快就毫无意义了。让我们来看一个常见的陷阱。

尽管如此,我们至少可以提出一些定性意见,帮你决定某个特定情况下是否有必要使用并
行流。
 如果有疑问,测量。把顺序流转成并行流轻而易举,但却不一定是好事。我们在本节中
已经指出,并行流并不总是比顺序流快。此外,并行流有时候会和你的直觉不一致,所
以在考虑选择顺序流还是并行流时,第一个也是最重要的建议就是用适当的基准来检查
其性能。
 留意装箱。自动装箱和拆箱操作会大大降低性能。Java 8中有原始类型流(IntStream、
LongStream、DoubleStream)来避免这种操作,但凡有可能都应该用这些流。
 有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元
素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性
能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成
无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用
limit可能会比单个有序流(比如数据源是一个List)更高效。
 还要考虑流的操作流水线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过
流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高就意味
着使用并行流时性能好的可能性比较大。
 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行处理少数几个元素
的好处还抵不上并行化造成的额外开销。
 要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList
高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂
方法创建的原始类型流也可以快速分解。最后,你将在7.3节中学到,你可以自己实现
Spliterator来完全掌控分解过程。
 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
例如,一个SIZED流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处
理,但筛选操作可能丢弃的元素个数却无法预测,导致流本身的大小未知。
 还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。
如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通
过并行流得到的性能提升。
表7-1按照可分解性总结了一些流数据源适不适于并行。
表7-1 流的数据源和可分解性
源 可分解性
ArrayList 极佳
LinkedList 差
IntStream.range 极佳
Stream.iterate 差
HashSet 好
TreeSet 好
最后,我们还要强调并行流背后使用的基础架构是Java 7中引入的分支/合并框架。并行汇总
的示例证明了要想正确使用并行流,了解它的内部原理至关重要,所以我们会在下一节仔细研究

7.2 分支/合并框架

在这里插入图片描述

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

虽然分支/合并框架还算简单易用,不幸的是它也很容易被误用。以下是几个有效使用它的
最佳做法。
 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子
任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,
因为每个子任务都必须等待另一个子任务完成才能启动。
 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直
接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用
它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为
其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面
看栈跟踪(stack trace)来找问题,但放在分支合并计算上就不行了,因为调用compute
的线程并不是概念上的调用方,后者是调用fork的那个。
 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计
算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时
有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方
法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出
同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就
像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编
译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么
做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死
码分析——删去从未被使用的计算)。
对于分支/合并拆分策略还有最后一点补充:你必须选择一个标准,来决定任务是要进一步
拆分还是已小到可以顺序求值。

第三部分 高效 Java 8 编程

第8章 重构、测试和调试

第九章 默认方法

解决问题的三条规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。
(1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
(2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
(3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,猜猜打印输出的是什么?

第十章 用Optional取代null

1、

public String getCarInsuranceName(Optional<Person> person) { 
 return person.flatMap(Person::getCar) 
 .flatMap(Car::getInsurance) 
 .map(Insurance::getName) 
 .orElse("Unknown"); 
}

在这里插入图片描述

2、在域模型中使用Optional,以及为什么它们无法序列化
在代码清单10-4中,我们展示了如何在你的域模型中使用Optional,将允许缺失或者暂
无定义的变量值用特殊的形式标记出来。然而,Optional类设计者的初衷并非如此,他们构
思时怀揣的是另一个用例。这一点,Java语言的架构师Brian Goetz曾经非常明确地陈述过,
Optional的设计初衷仅仅是要支持能返回Optional对象的语法。
由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现
Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在
域模型中使用Optional,有可能引发应用程序故障。然而,我们相信,通过前面的介绍,你
已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全
部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案,
我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,
代码清单如下:

public class Person { 
 private Car car; 
 public Optional<Car> getCarAsOptional() { 
 return Optional.ofNullable(car); 
 } 
}

3、使用场景
10.4.1 用 Optional 封装可能为 null 的值

Optional<Object> value = Optional.ofNullable(map.get("key"));

10.4.2 异常与 Optional 的对比

public static Optional<Integer> stringToInt(String s) { 
 try { 
 return Optional.of(Integer.parseInt(s)); 
 } catch (NumberFormatException e) {
 return Optional.empty(); 
 } 
}

我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用OptionalUtility.stringToInt方法,将String转换为一个Optional对象,而不再需要记得你在其中封装了笨拙的
try/catch的逻辑了。

第十一章 CompletableFuture:组合式异步编程

需求:
最近这些年,两种趋势不断地推动我们反思我们设计软件的方式。第一种趋势和应用运行的硬件平台相关,第二种趋势与应用程序的架构相关,尤其是它们之间如何交互。我们在第7章中已经讨论过硬件平台的影响。我们注意到随着多核处理器的出现,提升应用程序处理速度最有效的方式是编写能充分发挥多核能力的软件。你已经看到通过切分大型的任务,让每个子任务并行运行,这一目标是能够实现的;你也已经了解相对直接使用线程的方式,使用分支/合并框架(在
Java 7中引入)和并行流(在Java 8中新引入)能以更简单、更有效的方式实现这一目标。第二种趋势反映在公共API日益增长的互联网服务应用。著名的互联网大鳄们纷纷提供了自己的公共API服务,比如谷歌提供了地理信息服Facebook提供了社交信息服务,Twitter提供了新闻服务。现在,很少有网站或者网络应用会以完全隔离的方式工作。更多的候,我们看到的下一代网络应用都采用“混聚”(mash-up)的方式:它会使用来自多个来源的内容,将这些内容聚合在一起,方便用户的生活。比如,你可能希望为你的法国客户提供指定主题的热点报道。为实现这一功能,你需要向谷歌或者Twitter的API请求所有语言中针对该主题最热门的评论,可能还需要依据你的内部算法对它们的相关性进行排序。之后,你可能还需要使用谷歌的翻译服务把它们翻译成法语,甚至利用谷歌地图服务定位出评论作者的位置信息,最终将所有这些信息聚集起来,呈现在你的网站上。当然,如果某些外部网络服务发生响应慢的情况,你希望依旧能为用户提供部分信息,比如提供带问号标记的通用地图,以文本的方式显示信息,而不是呆呆地显示一片空白屏幕,直到地图服务器返回结果或者超时退出。图11-1解释了这种典型的“混聚”应用如何与所需的远程服务交互。
要实现类似的服务,你需要与互联网上的多个Web服务通信。可是,你并不希望因为等待某些服务的响应,阻塞应用程序的运行,浪费数十亿宝贵的CPU时钟周期。比如,不要因为等待Facebook的数据,暂停对来自Twitter的数据处理。
这些场景体现了多任务程序设计的另一面。第7章中介绍的分支/合并框架以及并行流是实现并行处理的宝贵工具;它们将一个操作切分为多个子操作,在多个不同的核、CPU甚至是机器上并行地执行这些子操作。

1、 Future 接口

ExecutorService executor = Executors.newCachedThreadPool(); 
Future<Double> future = executor.submit(new Callable<Double>() { 
 public Double call() {
 return doSomeLongComputation(); 
 }}); 
doSomethingElse(); 
try { 
 Double result = future.get(1, TimeUnit.SECONDS); 
} catch (ExecutionException ee) {
 // 计算抛出一个异常
} catch (InterruptedException ie) { 
 // 当前线程在等待过程中被中断
} catch (TimeoutException te) { 
 // 在Future对象完成之前超过已过期
}

11.1.1 Future 接口的局限性

 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
 等待Future集合中的所有任务都完成。
 仅等待Future集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果。
 通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)。
 应对Future的完成事件(即当Future的完成事件发生时会收到通知,并能使用Future计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)。

11.1.2 使用 CompletableFuture 构建异步应用
11.2.1 将同步方法转换为异步方法

public Future<Double> getPriceAsync(String product) {
 CompletableFuture<Double> futurePrice = new CompletableFuture<>(); 
 new Thread( () -> { 
 double price = calculatePrice(product); 
 futurePrice.complete(price); 
 }).start(); 
 return futurePrice; 
}

11.2.2 错误处理

如果没有意外,我们目前开发的代码工作得很正常。但是,如果价格计算过程中产生了错误会怎样呢?非常不幸,这种情况下你会得到一个相当糟糕的结果:用于提示错误的异常会被限制在试图计算商品价格的当前线程的范围内,最终会杀死该线程,而这会导致等待get方法返回结果的客户端永久地被阻塞。

public Future<Double> getPriceAsync(String product) { 
 CompletableFuture<Double> futurePrice = new CompletableFuture<>(); 
 new Thread( () -> {
 try { 
 double price = calculatePrice(product); 
 futurePrice.complete(price); 
 } catch (Exception ex) { 
 futurePrice.completeExceptionally(ex); 
 }
 }).start(); 
 return futurePrice; 
}

客户端现在会收到一个ExecutionException异常,该异常接收了一个包含失败原因的Exception参数,即价格计算方法最初抛出的异常。所以,举例来说,如果该方法抛出了一个运行时异常“product not available”,客户端就会得到像下面这样一段ExecutionException:java.util.concurrent.ExecutionException: java.lang.RuntimeException: product
not available at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2237)
如果价格计算正常结束,完成Future操作并设置商品价格否则就抛出导致失败的异常,完成这次Future操作
at lambdasinaction.chap11.AsyncShopClient.main(AsyncShopClient.java:14)
… 5 more
Caused by: java.lang.RuntimeException: product not available
at lambdasinaction.chap11.AsyncShop.calculatePrice(AsyncShop.java:36)
at lambdasinaction.chap11.AsyncShop.lambda$getPrice 0 ( A s y n c S h o p . j a v a : 23 ) a t l a m b d a s i n a c t i o n . c h a p 11. A s y n c S h o p 0(AsyncShop.java:23) at lambdasinaction.chap11.AsyncShop 0(AsyncShop.java:23)atlambdasinaction.chap11.AsyncShop$Lambda$1/24071475.run(Unknown Source)
at java.lang.Thread.run(Thread.java:744)

11.3 让你的代码免受阻塞之苦

11.3.1 使用并行流对请求进行并行操作

public List<String> findPrices(String product) { 
 return shops.parallelStream() 
 .map(shop -> String.format("%s price is %.2f", 
 shop.getName(), shop.getPrice(product)))
 .collect(toList()); 
} 
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price 
 is 214.13, BuyItAll price is 184.74] 
Done in 1180 msecs

相当不错啊!看起来这是个简单但有效的主意:现在对四个不同商店的查询实现了并行,所以完成所有操作的总耗时只有1秒多一点儿。

11.3.2 使用 CompletableFuture 发起异步请求

List<CompletableFuture<String>> priceFutures = 
 shops.stream() 
 .map(shop -> CompletableFuture.supplyAsync( 
 () -> String.format("%s price is %.2f", 
 shop.getName(), shop.getPrice(product)))) 
 .collect(toList());

使用这种方式,你会得到一个List<CompletableFuture>,列表中的每个CompletableFuture对象在计算完成后都包含商店的String类型的名称。但是,由于你用CompletableFutures实现的findPrices方法要求返回一个List,你需要等待所有的future执行完毕,将其包含的值抽取出来,填充到列表中才能返回。为了实现这个效果,你可以向最初的List<CompletableFuture>施加第二个map操作,对List中的所有future对象执行join操作,一个接一个地等待它们运行结束。注意CompletableFuture类中的join方法和Future接口中的get有相同的含义,并且也声明在Future接口中,它们唯一的不同是join不会抛出任何检测到的异常。使用它你不再需要使用try/catch语句块让你传递给第二个map方法的Lambda表达式变得过于臃肿。所有这些整合在一起,你就可以重新实现findPrices了,具体代码如下。

public List<String> findPrices(String product) { 
 List<CompletableFuture<String>> priceFutures = 
 shops.stream() 
 .map(shop -> CompletableFuture.supplyAsync( 
 () -> shop.getName() + " price is " +
 shop.getPrice(product))) 
 .collect(Collectors.toList()); 
 return priceFutures.stream() 
 .map(CompletableFuture::join) 
 .collect(toList()); 
}

在这里插入图片描述
在这里插入图片描述

11.3.3 寻找更好的方案

在这里插入图片描述

11.3.4 使用定制的执行器

调整线程池的大小
《Java并发编程实战》(http://mng.bz/979c)一书中,Brian Goetz和合著者们为线程池大小的优化提供了不少中肯的建议。这非常重要,如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。Brian Goetz建议,线程池大小与处理器的利用率之比可以使用下面的公式进行估算:
Nthreads = NCPU * UCPU * (1 + W/C)
其中:
❑NCPU是处理器的核的数目,可以通过Runtime.getRuntime().availableProcessors()得到
❑UCPU是期望的CPU利用率(该值应该介于0和1之间)
❑W/C是等待时间与计算时间的比率

你的应用99%的时间都在等待商店的响应,所以估算出的W/C比率为100。这意味着如果你期望的CPU利用率是100%,你需要创建一个拥有400个线程的线程池。实际操作中,如果你创建的线程数比商店的数目更多,反而是一种浪费,因为这样做之后,你线程池中的有些线程根本没有机会被使用。出于这种考虑,我们建议你将执行器使用的线程数,与你需要查询的商店数目设定为同一个值,这样每个商店都应该对应一个服务线程。不过,为了避免发生由于商店的数目过
多导致服务器超负荷而崩溃,你还是需要设置一个上限,比如100个线程。代码清单如下所示。

private final Executor executor = 
 Executors.newFixedThreadPool(Math.min(shops.size(), 100), 
 new ThreadFactory() { 
 public Thread newThread(Runnable r) { 
 Thread t = new Thread(r); 
 t.setDaemon(true); 
 return t; 
 }
}); 

改进之后,使用CompletableFuture方案的程序处理5个商店仅耗时1021秒,处理9个商店时耗时1022秒。一般而言,这种状态会一直持续,直到商店的数目达到我们之前计算的阈值400。这个例子证明了要创建更适合你的应用特性的执行器,利用CompletableFutures向其提交任务执行是个不错的主意。处理需大量使用异步操作的情况时,这几乎是最有效的策略。

并行——使用流还是CompletableFutures?
目前为止,你已经知道对集合进行并行计算有两种方式:要么将其转化为并行流,利用map这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在CompletableFuture内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待I/O而发生阻塞。
我们对使用这些API的建议如下。
❑如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。
❑反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用CompletableFuture灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。

11.4 对多个异步任务进行流水线操作

 public List<String> findPrices(String product) {
        List<CompletableFuture<String>> priceFutures =
            shops.stream()
                .map(shop -> CompletableFuture.supplyAsync(
                    () -> shop.getPrice(product), executor))
                .map(future -> future.thenApply(Quote::parse))
                .map(future -> future.thenCompose(quote ->
                    CompletableFuture.supplyAsync(
                        () -> Discount.applyDiscount(quote), executor)))
                .collect(toList());
        return priceFutures.stream()
            .map(CompletableFuture::join)
            .collect(toList());
    }

在这里插入图片描述

你所进行的这三次map操作和代码清单11-5中的同步方案没有太大的区别,不过你使用CompletableFuture类提供的特性,在需要的地方把它们变成了异步操作。

  1. 获取价格
    这三个操作中的第一个你已经在本章的各个例子中见过很多次,只需要将Lambda表达式作为参数传递给supplyAsync工厂方法就可以以异步方式对shop进行查询。第一个转换的结果是一个Stream<CompletableFuture>,一旦运行结束,每个CompletableFuture对象中都会包含对应shop返回的字符串。注意,你对CompletableFuture进行了设置,用代码清单11-12中的方法向其传递了一个订制的执行器Executor。
  2. 解析报价
    现在你需要进行第二次转换将字符串转变为订单。由于一般情况下解析操作不涉及任何远程服务,也不会进行任何I/O操作,它几乎可以在第一时间进行,所以能够采用同步操作,不会带来太多的延迟。由于这个原因,你可以对第一步中生成的CompletableFuture对象调用它的thenApply,将一个由字符串转换Quote的方法作为参数传递给它。注意到了吗?直到你调用的CompletableFuture执行结束,使用的thenApply方法都不会阻塞你代码的执行。这意味着CompletableFuture最终结束运行时,你希望传递Lambda表达式给thenApply方法,将Stream中的每个CompletableFuture对象转换为对应的CompletableFuture对象。你可以把这看成是为处理CompletableFuture的结果建立了一个菜单,就像你曾经为Stream的流水线所做的事儿一样。
  3. 为计算折扣价格构造Future
    第三个map操作涉及联系远程的Discount服务,为从商店中得到的原始价格申请折扣率。这一转换与前一个转换又不大一样,因为这一转换需要远程执行(或者,就这个例子而言,它需
    要模拟远程调用带来的延迟),出于这一原因,你也希望它能够异步执行。为了实现这一目标,你像第一个调用传递getPrice给supplyAsync那样,将这一操作以Lambda表达式的方式传递给了supplyAsync工厂方法,该方法最终会返回另一个CompletableFuture对象。到目前为止,你已经进行了两次异步操作,用了两个不同的CompletableFutures对象进行建模,你希望能把它们以级联的方式串接起来进行工作。
     从shop对象中获取价格,接着把价格转换为Quote。
     拿到返回的Quote对象,将其作为参数传递给Discount服务,取得最终的折扣价格。
    Java 8的 CompletableFuture API提供了名为thenCompose的方法,它就是专门为这一目的而设计的,thenCompose方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。换句话说,你可以创建两个CompletableFutures对象,对第一个CompletableFuture对象调用 thenCompose,并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一
    个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。使用这种方
    式,即使Future在向不同的商店收集报价,主线程还是能继续执行其他重要的操作,比如响应
    UI事件。

将这三次map操作的返回的Stream元素收集到一个列表,你就得到了一个List<CompletableFuture>,等这些CompletableFuture对象最终执行完毕,你就可以像代码清单11-11中那样利用join取得它们的返回值。代码清单11-18实现的新版findPrices方法产生的
输出如下:
[BestPrice price is 110.93, LetsSaveBig price is 135.58, MyFavoriteShop price
is 192.72, BuyItAll price is 184.74, ShopEasy price is 167.28]
Done in 2035 msecs
你在代码清单11-16中使用的thenCompose方法像CompletableFuture类中的其他方法一样,也提供了一个以Async后缀结尾的版本thenComposeAsync。通常而言,名称中不带Async的方法和它的前一个任务一样,在同一个线程中运行;而名称以Async结尾的方法会将后续的任务提交到一个线程池,所以每个任务是由不同的线程处理的。就这个例子而言,第二个CompletableFuture对象的结果取决于第一个CompletableFuture,所以无论你使用哪个版
本的方法来处理CompletableFuture对象,对于最终的结果,或者大致的时间而言都没有多少
差别。我们选择thenCompose方法的原因是因为它更高效一些,因为少了很多线程切换的开销。

11.4.4 将两个 CompletableFuture 对象整合起来,无论它们是否存在依赖

 Future<Double> futurePriceInUSD =
        CompletableFuture.supplyAsync(() -> shop.getPrice(product))
            .thenCombine(
                CompletableFuture.supplyAsync(
                    () -> exchangeService.getRate(Money.EUR, Money.USD)),
                (price, rate) -> price * rate
            );

在这里插入图片描述
11.5 响应 CompletableFuture 的 completion 事件

代码清单11-19 一个模拟生成0.5秒至2.5秒随机延迟的方法

 private static final Random random = new Random();
    public static void randomDelay() {
        int delay = 500 + random.nextInt(2000);
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

代码清单11-20 重构findPrices方法返回一个由Future构成的流

    public Stream<CompletableFuture<String>> findPricesStream(String product) {
        return shops.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                () -> shop.getPrice(product), executor))
            .map(future -> future.thenApply(Quote::parse))
            .map(future -> future.thenCompose(quote ->
                CompletableFuture.supplyAsync(
                    () -> Discount.applyDiscount(quote), executor)));
    }

现在,你为findPricesStream方法返回的Stream添加了第四个map操作,在此之前,你已经在该方法内部调用了三次 map 。这个新添加的操作其实很简单,只是在每个
CompletableFuture上注册一个操作,该操作会在CompletableFuture完成执行后使用它的返回值。Java 8的CompletableFuture通 过thenAccept方法提供了这一功能,它接收CompletableFuture执行完毕后的返回值做参数。在这里的例子中,该值是由Discount服务返回的字符串值,它包含了提供请求商品的商店名称及折扣价格,你想要做的操作也很简单,只是将结果打印输出:
findPricesStream(“myPhone”).map(f -> f.thenAccept(System.out::println));
注意,和你之前看到的thenCompose和thenCombine方法一样,thenAccept方法也提供了一个异步版本,名为thenAcceptAsync。异步版本的方法会对处理结果的消费者进行调度,从线池中选择一个新的线程继续执行,不再由同一个线程完成CompletableFuture的所有任务。因为你想要避免不必要的上下文切换,更重要的是你希望避免在等待线程上浪费时间,尽快响应CompletableFuture的completion事件,所以这里没有采用异步版本。由 于thenAccept方法已经定义了如何处理CompletableFuture返回的结果,一旦CompletableFuture计算得到结果,它就返回一个CompletableFuture。所以,map操作返回的是一Stream<CompletableFuture>。对这个<CompletableFuture- >对象,你能做的事非常有限,只能等待其运行结束,不过这也是你所期望的。你还希望能给最慢的商店一些机会,让它有机会打印输出返回的价格。为了实现这一目的,你可以把构成Stream的所有CompletableFuture对象放到一个数组中,等待所有的任务执行完成,
代码如下所示。

CompletableFuture[] futures = findPricesStream("myPhone") 
 .map(f -> f.thenAccept(System.out::println)) 
 .toArray(size -> new CompletableFuture[size]); 
CompletableFuture.allOf(futures).join();

allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有CompletableFuture对象执行完成之后,它返回一个CompletableFuture对象。这意味着,如果你需要等待最初Stream中的所有 CompletableFuture对象执行完毕,对 allOf方法返回的CompletableFuture执行join操作是个不错的主意。这个方法对“最佳价格查询器”应用也是有用的,因为你的用户可能会困惑是否后面还有一些价格没有返回,使用这个方法,你可以在执行完毕之后打印输出一条消息“All shops returned results or timed out”。然而在另一些场景中,你可能希望只要CompletableFuture对象数组中有任何一个执行完毕就不再等待,比如,你正在查询两个汇率服务器,任何一个返回了结果都能满足你的需求。在这种情况下,你可以使用一个类似的工厂方法anyOf。该方法接收一个CompletableFuture对象构成的数组,返回由第一个执行完毕的CompletableFuture对象的返回值构成的CompletableFuture。

long start = System.nanoTime();
    CompletableFuture[] futures = findPricesStream("myPhone27S")
        .map(f -> f.thenAccept(
            s -> System.out.println(s + " (done in " +
                ((System.nanoTime() - start) / 1_000_000) + " msecs)")))
        .toArray(size -> new CompletableFuture[size]); 
CompletableFuture.allOf(futures).join(); 
System.out.println("All shops have now responded in "
    + ((System.nanoTime() - start) / 1_000_000) + " msecs");
BuyItAll price is 184.74 (done in 2005 msecs) 
MyFavoriteShop price is 192.72 (done in 2157 msecs) 
LetsSaveBig price is 135.58 (done in 3301 msecs) 
ShopEasy price is 167.28 (done in 3869 msecs) 
BestPrice price is 110.93 (done in 4188 msecs) 
All shops have now responded in 4188 msecs

我们看到,由于随机延迟的效果,第一次价格查询比最慢的查询要快两倍多。

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

第 12 章 新的日期和时间API

在这里插入图片描述
代码清单12-3 创建LocalTime并读取其值

LocalTime time = LocalTime.of(13, 45, 20); 
int hour = time.getHour(); 
int minute = time.getMinute(); 
int second = time.getSecond();

LocalDate和LocalTime都可以通过解析代表它们的字符串创建。使用静态方法parse,你
可以实现这一目的:

LocalDate date = LocalDate.parse("2014-03-18"); 
LocalTime time = LocalTime.parse("13:45:20");

12.1.2 合并日期和时间

代码清单12-4 直接创建LocalDateTime对象,或者通过合并日期和时间的方式创建

LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); 
LocalDateTime dt2 = LocalDateTime.of(date, time); 
LocalDateTime dt3 = date.atTime(13, 45, 20); 
LocalDateTime dt4 = date.atTime(time); 
LocalDateTime dt5 = time.atDate(date);
LocalDate date1 = dt1.toLocalDate(); 
LocalTime time1 = dt1.toLocalTime();

12.1.3 机器的日期和时间格式

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); 
Instant.ofEpochSecond(4, -1_000_000_000);

12.1.4 定义 Duration 或 Period

Duration d1 = Duration.between(time1, time2); 
Duration d1 = Duration.between(dateTime1, dateTime2); 
Duration d2 = Duration.between(instant1, instant2);

如果你需要以年、月或者日的方式对多个时间单位建模,可以使用Period类。使用该类的
工厂方法between,你可以使用得到两个LocalDate之间的时长,如下所示:

 Period tenDays = Period.between(LocalDate.of(2014, 3, 8), 
 LocalDate.of(2014, 3, 18));

Duration threeMinutes = Duration.ofMinutes(3); 
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); 
Period tenDays = Period.ofDays(10); 
Period threeWeeks = Period.ofWeeks(3); 
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

Duration类和Period类共享了很多相似的方法,参见表12-1所示。
在这里插入图片描述

12.2 操纵、解析和格式化日期

LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.withYear(2011); 
LocalDate date3 = date2.withDayOfMonth(25); 
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.plusWeeks(1); 
LocalDate date3 = date2.minusYears(3); 
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

在这里插入图片描述

12.2.1 使用 TemporalAdjuster

截至目前,你所看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,你可
以使用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。

import static java.time.temporal.TemporalAdjusters.*; 
LocalDate date1 = LocalDate.of(2014, 3, 18); 
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); 
LocalDate date3 = date2.with(lastDayOfMonth());

在这里插入图片描述
请设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天的日期,同时过滤掉周六和周日这些节假日。格式如下所示:

TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster( 
 temporal -> { 
 DayOfWeek dow = 
 DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK)); 
 int dayToAdd = 1; 
 if (dow == DayOfWeek.FRIDAY) dayToAdd = 3; 
 if (dow == DayOfWeek.SATURDAY) dayToAdd = 2; 
 return temporal.plus(dayToAdd, ChronoUnit.DAYS); 
 }); 
date = date.with(nextWorkingDay);

12.2.2 打印输出及解析日期时间对象

LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); 
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 

LocalDate date1 = LocalDate.of(2014, 3, 18); 
String formattedDate = date1.format(formatter); 
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

代码清单12-11 创建一个本地化的DateTimeFormatter

DateTimeFormatter italianFormatter = 
 DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN); 
LocalDate date1 = LocalDate.of(2014, 3, 18); 
String formattedDate = date.format(italianFormatter); // 18. marzo 2014 
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

代码清单12-12 构造一个DateTimeFormatter

DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() 
 .appendText(ChronoField.DAY_OF_MONTH) 
 .appendLiteral(". ") 
 .appendText(ChronoField.MONTH_OF_YEAR) 
 .appendLiteral(" ") 
 .appendText(ChronoField.YEAR) 
 .parseCaseInsensitive() 
 .toFormatter(Locale.ITALIAN);

12.3 处理不同的时区和历法

ZoneId romeZone = ZoneId.of("Europe/Rome");
ZoneId zoneId = TimeZone.getDefault().toZoneId();

//为时间点添加时区信息

LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 
ZonedDateTime zdt1 = date.atStartOfDay(romeZone); 
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45); 
ZonedDateTime zdt2 = dateTime.atZone(romeZone); 
Instant instant = Instant.now(); 
ZonedDateTime zdt3 = instant.atZone(romeZone);


12.3.1 利用和 UTC/格林尼治时间的固定偏差计算时区

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

第四部分 超越 java 8

第十四章 函数式编程的技巧

函数式编程的世界里,如果函数,比如Comparator.comparing,能满足下面任一要求就
可以被称为高阶函数(higher-order function):
 接受至少一个函数作为参数
 返回的结果是一个函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山巅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值