《Java 8 in Action》Chapter 7:并行数据处理与性能

在Java 7之前,并行处理数据集合非常麻烦。第一,你得明确地把包含数据的数据结构分成若干子部分。第二,你要给每个子部分分配一个独立的线程。第三,你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成,最后把这些部分结果合并起来。Java 7引入了一个叫作分支/合并的框架,让这些操作更稳定、更不易出错。
Stream接口让你不用太费力气就能对数据集执行并行操作。它允许你声明性地将顺序流变为并行流。此外,你将看到Java是如何变戏法的,或者更实际地来说, 流是如何在幕后应用Java 7引入的分支/合并框架的。

1. 并行流

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

1
2
3
4
5
6
7
8
9
10
11
12
13
public static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, Long::sum);
}
传统写法:
public static long iterativeSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}

1.1 将顺序流转换为并行流

可以把流转换成并行流,从而让前面的函数归约过程(也就是求和)并行运行——对顺序流调用parallel方法:

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

在现实中,对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行。类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。请注意,你可能以为把这两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要顺序执行。

配置并行流使用的线程池
看看流的parallel方法,你可能会想,并行流用的线程是从哪来的?有多少个?怎么自定义这个过程呢?
并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().available- Processors()得到的。
但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小,如下所示:
System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”,”12”);
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值,
除非你有很好的理由,否则我们强烈建议你不要修改它。

1.2 测量流性能

并行编程可能很复杂,有时候甚至有点违反直觉。如果用得不对(比如采用了一 个不易并行化的操作,如iterate),它甚至可能让程序的整体性能更差,所以在调用那个看似神奇的parallel操作时,了解背后到底发生了什么是很有必要的。
并行化并不是没有代价的。并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。总而言之,很多情况下不可能或不方便并行化。然而,在使用 并行Stream加速代码之前,你必须确保用得对;如果结果错了,算得快就毫无意义了。

1.3 正确使用并行流

错用并行流而产生错误的首要原因,就是使用的算法改变了某些共享状态。下面是另一种实现对前n个自然数求和的方法,但这会改变一个共享累加器:

1
2
3
4
5
6
7
8
9
public static long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).forEach(accumulator::add)
return accumulator.total;
}
public class Accumulator {
public long total = 0;
public void add(long value) { total += value; }
}

这段代码本身上就是顺序的,因为每次访问total都会出现数据竞争。接下来将这段代码改为并行:

1
2
3
4
5
6
7
8
9
10
11
12
public static long sideEffectParallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;}
System.out.println("SideEffect parallel sum done in: " + measurePerf(ParallelStreams::sideEffectParallelSum, 10_000_000L) +" msecs" );
Result: 5959989000692
Result: 7425264100768
Result: 6827235020033
Result: 7192970417739
Result: 6714157975331
Result: 7715125932481
SideEffect parallel sum done in: 49 msecs

这回方法的性能无关紧要了,唯一要紧的是每次执行都会返回不同的结果,都离正确值50000005000000差很远。这是由于多个线程在同时访问累加器,执行total += value,而这一句􏱵然看似简单,却不是一个原子操作。问题的根源在于,forEach中调用的方法有副作用,它会改变多个线程共享的对象的可变状态。要是你想用并行Stream又不想引发类似的意外,就必须避免这种情况。现在你知道了,共享可变状态会影响并行流以及并行计算。

1.4 高效使用并行流

  • 如果有疑问,测量。把顺序流转成并行流轻而易举,但却不一定是好事。我们在本节中已经指出,并行流并不总是比顺序流快。此外,并行流有时候会和你的直觉不一致,所以在考虑选择顺序流还是并行流时,第一个也是最重要的建议就是用适当的基准来检查其性能。
  • 留意装箱。自动装箱和拆箱操作会大大降低性能。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方法)。 如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通过并行流得到的性能提升。

并行流背后使用的基础架构是Java 7中引入的分支/合并框架。

2. 分支/合并框架

分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。

2.1 使用RecursiveTask

要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以 及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当然它可能会更新其他非局部机构)。要定义RecursiveTask,只需实现它唯一的抽象方法 compute:

1
protected abstract R compute();

这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。下图表示了递归任务的拆分过程:

让我们试着用这个框架为一个数字范围(这里用一个 long[]数组表示)求和。如前所述,你需要先为RecursiveTask类做一个实现,就是下面代码清单中的ForkJoinSumCalculator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;

public static final long THRESHOLD = 10_000;

public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}

public ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
leftTask.fork();

ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
return leftResult + rightResult;
}

private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
}

这里用了一个LongStream来生成包含前n个自然数的数组,然后创建一个ForkJoinTask (RecursiveTask的父类),并把数组传递给代码清单7-2所示ForkJoinSumCalculator的公共构造函数。最后,你创建了一个新的ForkJoinPool,并把任务传给它的调用方法 。在ForkJoinPool中执行时,最后一个方法返回的值就是ForkJoinSumCalculator类定义的任务结果。
请注意在实际应用时,使用多个ForkJoinPool是没有什么意义的。正是出于这个原因,一般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任何部分方便地重用了。这里创建时用了其默认的无参数构造函数,这意味着想让线程池使用JVM能够使用的所有处理器。更确切地说,该构造函数将使用Runtime.availableProcessors的返回值来决定线程􏶈使用的线程数。请注意availableProcessors方法虽然看起来是处理器, 但它实际上返回的是可用内核的数量,包括超线程生成的虚拟内核。
当把ForkJoinSumCalculator任务传给ForkJoinPool时,这个任务就由􏶈中的一个线程 执行,这个线程会调用任务的compute方法。该方法会检查任务是否小到足以顺序执行,如果不够小则会把要求和的数组分成两半,分给两个新的ForkJoinSumCalculator,而它们也由ForkJoinPool安排执行。因此,这一过程可以递归重复,把原任务分为更小的任务,直到满足不方便或不可能再进一步拆分的条件(本例中是求和的项目数小于等于10000)。这时会顺序计算每个任务的结果,然后由分支过程创建的(隐含的)任务二叉树遍历回到它的根。接下来会合并每个子任务的部分结果,从而得到总任务的结果。这一过程如下图所示。

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

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

2.3 工作窃取

实际中,每个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如磁盘访问慢,或是需要和外部任务协调执行。分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。
在实际应用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程之间平衡负载。一般来说,这种工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。

3. Spliterator

Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。

1
2
3
4
5
6
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}

与往常一样,T是Spliterator遍历的元素的类型。tryAdvance方法的行为类似于普通的 Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true。但trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理。Spliterator还可通过 estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值也有助于让拆分均匀一点。

3.1 拆分过程

将Stream拆分成多个部分的算法是一个递􏰒过程,如图7-6所示。第一步是对第一个 Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用 trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割,如第三步所示。最后,这个递归拆分过程到第四步就终止了,这时所有的Spliterator在调用trySplit时都返回了null。

Spliterator的特性
Spliterator接口声明的最后一个抽象方法是characteristics,它将返回一个int,代 表Spliterator本身特性集的编码。
使用Spliterator的客户可以用这些特性来更好地控制和优化它的使用。
表7-2总结了这些特性。(不幸的是,虽然它们在概念上与收集器的特性有重叠,编码却不一样。)

3.2 实现自定义Spliterator

4. 小结

在本章中,你了解了以下内容。

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 6:用流收集数据

1. 收集器简介

collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中。Collectors 类包含了大量常用收集器的工厂方法,toList() 和 toSet() 就是其中最常见的两个,除了它们还有很多收集器,用来对数据进行对复杂的转换。

指令式代码和函数式对比:

要是做多级分组,指令式和函数式之间的区别就会更加明显:由于需要好多层嵌套循环和条件,指令式代码很快就变得更难阅读、更难维护、更难修改。相比之下,函数式版本只要再加上 一个收集器就可以轻松地增强

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

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

2. 使用收集器

在需要将流项目重组成集合时,一般会使用收集器(Stream方法collect 的参数)。再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。这个结果可以是任何类型,可以复杂如代表一棵树的多级映射,或是简单如一个整数。

3. 收集器实例

3.1 流中最大值和最小值

Collectors.maxBy和 Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素。你可以创建一个Comparator来根据所含热量对菜肴进行比较:

1
2
3
4
5
6
System.out.println("找出热量最高的食物:");
Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
collect.ifPresent(System.out::println);
System.out.println("找出热量最低的食物:");
Optional<Dish> collect1 = DataUtil.genMenu().stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
collect1.ifPresent(System.out::println);

3.2 汇总求和

Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。举个例子来说,你可以这样求出菜单列表的总热量:

1
2
3
4
5
6
Integer collect = DataUtil.genMenu().stream().collect(Collectors.summingInt(Dish::getCalories));
System.out.println("总热量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summingDouble(Double::doubleValue));
System.out.println("double和:" + collect1);
Long collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summingLong(Long::longValue));
System.out.println("long和:" + collect2);

3.3 汇总求平均值

Collectors.averagingInt,averagingLong和averagingDouble可以计算数值的平均数:

1
2
3
4
5
6
Double collect = DataUtil.genMenu().stream().collect(Collectors.averagingInt(Dish::getCalories));
System.out.println("平均热量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.averagingDouble(Double::doubleValue));
System.out.println("double 平均值:" + collect1);
Double collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.averagingLong(Long::longValue));
System.out.println("long 平均值:" + collect2);

3.4 汇总合集

你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用summarizingInt工厂方法返回的收集器。例如,通过一次summarizing操作你可以就数出菜单中元素的个数,并得到热量总和、平均值、最大值和最小值:

1
2
3
4
5
6
IntSummaryStatistics collect = DataUtil.genMenu().stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println("int:" + collect);
DoubleSummaryStatistics collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summarizingDouble(Double::doubleValue));
System.out.println("double:" + collect1);
LongSummaryStatistics collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summarizingLong(Long::longValue));
System.out.println("long:" + collect2);

3.5 连接字符串

joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。

1
String collect = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining());

请注意,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。幸好,joining工厂方法有一个重载版本可以接受元素之间的分界符,这样你就可以得到一个都好分隔的名称列表:

1
String collect1 = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining(","));

4. 广义的归约汇总

所有收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。
它需要三个参数:

  • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
  • 第二个参数就是你在6.2.2节中使用的函数,将菜肴转换成一个表示其所含热量的int。
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。

下面两个是相同的操作:

1
2
Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

5. 分组

用Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成任务:

1
Map<Dish.Type, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType));

给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每 一道Dish的Dish.Type。我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。

5.1 多级分组

要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准:

1
2
3
4
5
6
7
8
9
10
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(
Collectors.groupingBy(Dish::getType,
Collectors.groupingBy(dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else return CaloricLevel.FAT;
}))
);

5.2 按子组收集数据

传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数:

1
Map<Dish.Type, Long> collect2 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));

还要注意,普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f, toList())的简便写法。
把收集器返回的结果转换为另一种类型,你可以使用 Collectors.collectingAndThen工厂方法返回的收集器,接受两个参数:要转换的收集器以及转换函数,并返回另一个收集器。

1
2
3
4
5
Map<Dish.Type, Dish> collect3 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
Optional::get
)));

这个操作放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。

常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收􏰁起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收􏰁器适应不同类型的对象。我们来看一个使用这个收集器的实际例子。比方说你想要知道,对于每种类型的Dish, 菜单中都有哪些CaloricLevel。

1
2
3
4
5
6
7
8
9
10
11
Map<Dish.Type, Set<CaloricLevel>> collect4 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(
Dish::getType, Collectors.mapping(
dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else return CaloricLevel.FAT;
}, Collectors.toSet()
)
));

6. 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分类函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以 分为两组——true是一组,false是一组。例如,如果想要把菜按照素食和非素食分开:

1
2
3
4
5
6
Map<Boolean, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println(collect.get(true));
partitioningBy 工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(
Dish::isVegetarian, Collectors.groupingBy(Dish::getType)
));

分区看作分组一种特殊情况。

7. Collectors类的静态工厂方法


8. 收集器接口

1
2
3
4
5
6
7
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}

本列表适用以下定义:

  • T是流中要收集的项目的泛型。
  • A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  • R是手机操作得到的对象(通常但并不一定是集合)的类型。

8.1 建立新的结果容器:supplier方法

supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。

8.2 将元素添加到结果容器:accumulator方法

accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前n-1个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。

8.3 对结果容器应用最终转换:finisher方法

在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
顺序归约过程的逻辑步骤:

8.4 合并两个结果容器:combiner方法

四个方法中的最后一个——combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并:

  • 原始流会以递归方式拆分为子流,直到定义流是否需要进一步拆分的一个条件为非(如果分布式工作单位太小,并行计算往往比顺序计算要慢,而且要是生成的并行任务比处理器内核数多很多的话就毫无意义了)。
  • 现在,所有的子流都可以并行处理,即对每个子流应用图6-7所示的顺序归约算法。
  • 最后,使用收集器combiner方法返回的函数,将所有的部分结果两两合并。这时会把原始流每次拆分时得到的子流对应的结果合并起来

8.5 characteristics方法

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

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

9. 小结

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 5:使用流

流让你从外部迭代转向内部迭代,for循环显示迭代不用再写了,流内部管理对集合数据的迭代。这种处理数据的方式很有用,因为你让Stream API管理如何处理数据。这样Stream API就可以在背后进行多种优化。此外,使用内部迭代的话,Stream API可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。

1. 筛选和切片

1.1 用谓词筛选

该操作会接受一个谓词(一个返回 boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。筛选出所有素菜

1
2
3
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());

1.2 筛选各异的元素

返回一个元素各异(根据流所生成元素的 hashCode和equals方法实现)的流。筛选出列表中所有的偶数,并确保没有重复。

1
2
3
4
5
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);

1.3 截短流

流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。选出热量超过300卡路里的头三道菜

1
2
3
4
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(toList());

limit也可以用在无序流上,比如源是一个Set。这种情况下,limit的结果不会以 任何顺序排列。

1.4 跳过元素

流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。跳过超过300卡路里的头两道菜,并返回剩下的。

1
2
3
4
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());

2. 映射

2.1 对流中每一个元素应用函数

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。提取流中菜肴的名称:

1
2
3
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());

2.2 流的扁平化

flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。单个流都被合并起来,即扁平化为一个流。例如,给定单词列表 [“Hello”,”World”],你想要返回列表[“H”,”e”,”l”, “o”,”W”,”r”,”d”]。

1
2
3
4
5
6
List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action”);
List<String> uniqueCharacters = words.stream()
.map(w -> w.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());

3. 查找和匹配

3.1 检查谓词是否至少匹配一个元素

anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。anyMatch方法返回一个boolean,因此是一个终端操作。比如,你可以用它来看看菜单里面是否有素食可选择:

1
2
3
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

3.2 检查谓词是否匹配所有元素

allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):

1
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如, 你可以用noneMatch重写前面的例子:

1
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉 的Java中&&和||运算符短路在流中的版本。

3.3 查找元素

findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。比如,你可能想找到一道素食菜肴。你可以结合使用filter和findAny方法来实现这个查询:

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

Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。

  • isPresent()将在Optional包含值的时候返回true, 否则返回false。
  • ifPresent(Consumer block)会在值存在的时候执行给定的代码块。
  • T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
  • T orElse(T other)会在值存在时返回值,否则返回一个默认值。

3.4 查找第一个元素

为此有一个findFirst 方法,它的工作方式类似于findany。 例如,给定一个数字列表,下面的代码能找出第一个平方 能被3整除的数:

1
2
3
4
5
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

4. 归约

归约操作 (将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

4.1 元素求和

reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。
reduce接受两个参数:

  • 一个初始值,这里是0;
  • 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是 lambda (a, b) -> a + b。
1
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:

1
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

4.2 最大值和最小值

使用reduce来计算流中的最大值

1
Optional<Integer> max = numbers.stream().reduce(Integer::max);

要计算最小值,你需要把Integer.min传给reduce来替换Integer.max:

1
Optional<Integer> min = numbers.stream().reduce(Integer::min);

4.3 小结

5. 实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ExecDemo {
public static void main(String[] args) {
Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario","Milan");
Trader alan = new Trader("Alan","Cambridge");
Trader brian = new Trader("Brian","Cambridge");
List<Transaction> transactions = Arrays.asList(
new Transaction(brian, 2011, 300),
new Transaction(raoul, 2012, 1000),
new Transaction(raoul, 2011, 400),
new Transaction(mario, 2012, 710),
new Transaction(mario, 2012, 700),
new Transaction(alan, 2012, 950)
);

System.out.println("(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。");
List<Transaction> collect = transactions.stream()
.filter(t -> t.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList());
System.out.println(collect);

System.out.println("\n(2) 交易员都在哪些不同的城市工作过?");
List<String> collect1 = transactions.stream()
.map(transaction -> transaction.getTrader().getCity())
.distinct()
.collect(Collectors.toList());
System.out.println(collect1);
// [Cambridge, Milan]

System.out.println("\n(3) 查找所有来自于剑桥的交易员,并按姓名排序。");
List<Trader> collect2 = transactions.stream()
.map(Transaction::getTrader)
.filter(trader -> trader.getCity().equals("Cambridge"))
.distinct()
.sorted(Comparator.comparing(Trader::getName))
.collect(Collectors.toList());
System.out.println(collect2);

System.out.println("\n(4) 返回所有交易员的姓名字符串,按字母顺序排序。");
String reduce = transactions.stream()
.map(transaction -> transaction.getTrader().getName())
.distinct()
.sorted()
.reduce("", (n1, n2) -> n1 + n2);
System.out.println(reduce);

System.out.println("\n(5) 有没有交易员是在米兰工作的?");
boolean b = transactions.stream()
.anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan"));
System.out.println(b);

System.out.println("\n(6) 打印生活在剑桥的交易员的所有交易额。");
transactions.stream()
.filter(transaction -> transaction.getTrader().getCity().equals("Cambridge"))
.map(Transaction::getValue)
.forEach(System.out::println);

System.out.println("\n(7) 所有交易中,最高的交易额是多少?");
transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max)
.ifPresent(System.out::println);

System.out.println("\n(8) 找到交易额最小的交易。");
transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::min)
.ifPresent(System.out::println);
}

6. 数值流

6.1 原始类型流特化

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

6.1.1 映射到数值流

将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前 面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。例如,你 可以像下面这样用mapToInt对menu中的卡路里求和:

1
2
3
int calories = menu.stream()                        // 返回一个 Stream<Dish>
.mapToInt(Dish::getCalories) // 返回一个 IntStream
.sum();

请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。

6.1.2 转换回对象流

要把原始流转换成一般流(每个int都会装箱成一个 Integer),可以使用boxed方法,如下所示:

1
2
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); //将Stream转换为数值流
Stream<Integer> stream = intStream.boxed(); // 将数值流转换为Stream

6.1.3 默认值OptionalInt

对于三种原始流特化,也分别有一个Optional原始类 型特化版本:OptionalInt、OptionalDouble和OptionalLong。例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:

1
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();

6.2 数值范围

Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围: range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。

1
2
IntStream evenNumbers = IntStream.range(1, 100) .filter(n -> n % 2 == 0); // 一个从1到100的偶数流   表示范围[1, 100)
IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0); // 一个从1到100的偶数流 表示范围[1, 100]

7. 构建流

7.1 由值创建流

你可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例如,以下代码直接使用Stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再一个个打印出来:

1
2
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); 
stream.map(String::toUpperCase).forEach(System.out::println);

你可以使用empty得到一个空流,如下所示:

1
Stream<String> emptyStream = Stream.empty();

7.2 由数组创建流

你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如,你可以将一个原始类型int的数组转换成一个IntStream,如下所示:

1
2
int[] numbers = {2, 3, 5, 7, 11, 13}; 
int sum = Arrays.stream(numbers).sum();

7.3 由文件生成流

Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines,它会返回一个由指定文件中的各行构成的字符串流。

7.4 由函数生成流:创建无限流

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

7.4.1 迭代

1
2
3
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);

此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。

7.4.2 生成

与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次 对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。我们先来看一个简单的用法:这段代码将生成一个流,其中有五个0到1之间的随机双精度数。

1
2
3
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);

8. 小结

这一章很长,但是很有收获!现在你可以更高效地处理集合了。事实上,流让你可以简洁地表达复杂的数据处理查询。此外,流可以透明地并行化。以下是你应从本章中学到的关键概念。

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 4:引入流

1. 流简介

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理。让我们来看一个实例返回低热量(<400)的菜肴名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Java7版本:
List<Dish> lowCaloricDishes = new ArrayList<>();
// 用累加器筛选元素
for(Dish d: menu){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
// 用匿名类对菜肴排序
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
// 处理排序后的菜名列表
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName());
}
Java8版本:
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = menu.stream()
.filter(d -> d.getCalories() < 400) // 选出400卡路里以下的菜肴
.sorted(comparing(Dish::getCalories)) // 按照卡路里排序
.map(Dish::getName) // 提取菜肴名称
.collect(toList()); // 将所有的名称保存在List中
利用多核架构并行执行,只需要把stream()换成parallelStream()

Java 8中的Stream API特性:

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

流定义:

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序 值。
  • 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询。
  • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

2. 流与集合

集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。集合和流的另一个关键区别在于它们遍历数据的方式。

2.1 只能遍历一次

和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。以下代码会抛出一个异常,说流已被消费掉了:

1
2
3
4
5
6
7
8
9
List<String> title = Arrays.asList(“Java8”,”In”, “Action”);
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at com.lujiahao.learnjava8.chapter4.StreamAndCollection.main(StreamAndCollection.java:16)

2.2 外部迭代与内部迭代

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Streams库使用内部迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
集合:用for-each循环外部迭代
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}

集合:用背后的迭代器做外部迭代
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}

流:内部迭代
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());

3. 流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
中间操作:除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。
终端操作:会从流的流水线生成结果。其结果是任何不是流的值。

流的使用一般包括三件事:

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

流的流水线背后的理念类似于构建器模式。

常见流操作:

4. 小结

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

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 3:Lambda表达式

1. Lambda简介

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

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁——无需像匿名类那样写很多模板代码。

2. Lambda写法

(parameters) -> expression 或 (parameters) -> { statements; }
eg:(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Lambda表达式有三个部分:

  • 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
  • 箭头——箭头->把参数列表与Lambda主体分隔开。
  • Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

3. 函数式接口和函数描述符

函数式接口就是只定义一个抽象方法的接口。接口上标有@FunctionalInterface表示该接口会设计成 一个函数式接口,如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。接口现在还可以拥有默认方法(即在类没有对方法进行实现时, 其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

函数式接口的抽象方法的签名就是Lambda表达式的签名。我们将这种抽象方法叫作:函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。

4. 三种常用的函数式接口

4.1 Predicate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents a predicate (boolean-valued function) of one argument.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #test(Object)}.
* @param <T> the type of the input to the predicate
* @since 1.8
*/
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
}

Predicate的英文示意是:谓词。
Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

4.2 Consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, {@code Consumer} is expected
* to operate via side-effects.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #accept(Object)}.
* @param <T> the type of the input to the operation
* @since 1.8
*/
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
* @param t the input argument
*/
void accept(T t);
}

Consumer的英文示意是:消费者。
Consumer接口定义了一个名叫accept的抽象方法,它接受泛型T对象,并没有返回任何值。

4.3 Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Represents a function that accepts one argument and produces a result.
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
* @param t the function argument
* @return the function result
*/
R apply(T t);
}

Function的英文示意是:功能。
Function接口定义了一个名叫apply的抽象方法,它接受泛型T对象,并返回一个泛型R的对象。

Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。

4.4 Java 8中的常用函数式接口

5. 类型检查、类型推断以及限制

5.1 类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

类型检查过程可以分解为如下所示。

  • 首先,你要找出filter方法的声明。
  • 第二,要求它是Predicate(目标类型)对象的第二个正式参数。
  • 第三,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
  • 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean.
  • 最后,filter的任何实际参数都必须匹配这个要求。

这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个 boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必 须与之匹配。有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它 们的抽象方法签名能够兼容。比如,前面提到的Callable和PrivilegedAction,这两个接口都代表着什么也不接受且返回一个泛型T的函数。 因此,下面两个赋值是有效的:

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

特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。
例如,以下两行都是合法的,尽管List的add方法返回了一个 boolean,而不是Consumer上下文(T -> void)所要求的void:

1
2
3
4
// Predicate返回了一个boolean 
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

5.2 类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接 口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。

1
2
3
4
// 没有类 型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

5.3 使用局部变量

Lambda表达式 也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被 称作捕获Lambda。
Lambda捕获的局部变量必须显式声明为final, 或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。

  • 第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  • 第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中 解释,这种模式会阻碍很容易做到的并行处理)。

6. 方法引用

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷 写法,方法引用看作针对仅仅涉及单一方法的Lambda的语法糖。目标引用放在分隔符::前,方法的名称放在后面。方法引用主要有三类:

  • (1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
  • (2) 指向任意类型实例方法的方法引用(例如String的length方法,写作 String::length)。
  • (3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction 用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive- Transaction::getValue)。
1
2
3
4
5
6
7
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用: ClassName::new。它的功能与指向静态方法的引用类似。
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

这就等价于:
Supplier<Apple> c1 = () -> new Apple(); // 利用默认构造函数创建 Apple的Lambda表达式
Apple a1 = c1.get(); // 调用Supplier的get方法 将产生一个新的Apple

7. 复合Lambda表达式的有用方法

7.1 比较器复合

1
2
3
4
5
6
7
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
// 逆序 按重量递 减排序
inventory.sort(comparing(Apple::getWeight).reversed());
// 比较器链 按重量递减排序;两个苹果一样重时,进一步按国家排序
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));

7.2 谓词复合

1
2
3
4
5
6
7
8
// 产生现有Predicate 对象redApple的非
Predicate<Apple> notRedApple = redApple.negate();
// 链接两个谓词来生成另 一个Predicate对象 一个苹果既是红色又比较重
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
// 链接Predicate的方法来构造更复杂Predicate对象 表达要么是重(150克以上)的红苹果,要么是绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
请注意,and和or方法是按照在表达式链中的位置,从左向右确定优 先级的。因此,a.or(b).and(c)可以看作(a || b) && c。

7.3 函数复合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。 比如,
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);
数学上会写作g(f(x))或(g o f)(x)
这将返回4

compose方法,先把给定的函数用作compose的参数里面给的那个函 数,然后再把函数本身用于结果。
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);
数学上会写作f(g(x))或(f o g)(x)
这将返回3

8. 小结

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

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 2:通过行为参数化传递代码

你将了解行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入 Lambda表达式的主要原因。行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味 着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用。本章通过筛选苹果这个实际需求来一步步引出Lambda表达式,同时我也会把代码贴出来,读完你会看到代码是如何一步一步的向Lambda转化。多代码来袭,保护我方ADC!!

代码演化

1.实习生版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 筛选绿色苹果
* @author lujiahao
* @date 2019-02-19 18:28
*/
public class FilterAppleV0 {
public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
List<Apple> greenAppleList = new ArrayList<>();

for (Apple apple : appleList) {
if ("green".equals(apple.getColor())) {
greenAppleList.add(apple);
}
}

System.out.println("原集合:" + appleList);
System.out.println("绿苹果集合:" + greenAppleList);
}
}

这种之所以称之为实习生版本,是因为此种写法比较初级,所有代码在一个方法中实现。没有进行方法的抽取,不符合面向对象的理念,希望大家在编码工作时避免这种写法。

2.方法抽取版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 筛选绿色苹果
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV1 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterGreenApples(appleList);
System.out.println("绿苹果集合:" + filterGreenApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterGreenApples(List<Apple> appleList) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if ("green".equals(apple.getColor())) {
resultList.add(apple);
}
}
return resultList;
}
}

此版本对筛选绿色苹果的方法进行了简单的抽取,相较于上个版本有了很大的提升。然而,如果需求方改变想法,想筛选红色的苹果。复制filterGreenApples() 方法并将其中的绿色筛选条件改为红色,确实可以实现。但是,这样有太多重复的模板代码,不是良好的编码规范。因此,我们将筛选条件颜色进一步抽象化。

3.筛选条件作为参数传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 需要判断的属性作为参数传入
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV2 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, "green");
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterRedApples = filterApples(appleList, "red");
System.out.println("筛选红色苹果:" + filterRedApples);
}

/**
* 筛选特定颜色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, String color) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if (color.equals(apple.getColor())) {
resultList.add(apple);
}
}
return resultList;
}
}

满足了颜色的筛选条件,然而需求方又灵光一闪,筛选大于150克的苹果。无论是复制filterApples() 方法,还是增加重量作为参数传入,都是不推荐的编码习惯。第一种方法复制了大部分的代码来实现遍历,它打破了DRY(Don’t Repeat Yourself)的软件工程原则;第二种方法并不能考虑到所有情况,并且每次修改都对原有代码产生了影响,无法做到修改对外封闭的原则。

4.行为参数化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 行为参数化
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV3 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, new AppleGreenColorPredicate());
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, new AppleHeavyWeightPredicate());
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}

public static class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean filter(Apple apple) {
return apple.getWeight() > 150;
}
}

public static class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean filter(Apple apple) {
return "green".equals(apple.getColor());
}
}
}

我们对苹果的所有属性进行更高一个层次的抽象建模,通过定义ApplePredicate 接口,AppleHeavyWeightPredicate 和 AppleGreenColorPredicate 分别实现该接口来达到进行不同的筛选功能。客户端调用中创建不同的实现类,对于filterApple() 方法而言,是传入了不同的行为,即行为参数化。行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
其原理如下图所示:

5.匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 使用匿名类
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV4 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, new ApplePredicate() {
@Override
public boolean filter(Apple apple) {
return "green".equals(apple.getColor());
}
});
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, new ApplePredicate() {
@Override
public boolean filter(Apple apple) {
return apple.getWeight() > 150;
}
});
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}
}

当每次有新的查询需求提出,都要新建一个实现类,随着条件越来越多,实现类的数量也在急剧上升。此时,通过使用匿名内部类的方式,来减少实现类过多的模板代码。然而,匿名内部类并非完美,第一,它往往很笨重,因为它占用了很多空间;第二,很多程序员觉得它用起来很让人费解。

6.使用 Lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.List;

/**
* 使用Lambda表达式
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV5 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filterApples(appleList, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println("筛选绿色苹果:" + filterGreenApples);

List<Apple> filterHeavyApples = filterApples(appleList, (Apple apple) -> apple.getWeight() > 150);
System.out.println("筛选重量大于150苹果:" + filterHeavyApples);
}

/**
* 筛选绿色苹果
* @param appleList
* @return
*/
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
// 谓词对象封装了条件
if (predicate.filter(apple)) {
resultList.add(apple);
}
}
return resultList;
}

public interface ApplePredicate {
boolean filter(Apple apple);
}
}

不得不承认这代码看上去比先前干净很多,而且它看起来更像是在陈述问题本身,更加通俗易懂。

7.List 类型抽象化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.lujiahao.learnjava8.chapter2;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

/**
* List类型抽象话
*
* @author lujiahao
* @date 2019-02-19 18:30
*/
public class FilterAppleV6 {

public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

List<Apple> filterGreenApples = filter(appleList, (Apple apple) -> "green".equals(apple.getColor()));
System.out.println("筛选绿色苹果:" + filterGreenApples);

System.out.println("=============================================");

List<Integer> numberList = Arrays.asList(1, 2, 3);
System.out.println("原集合:" + numberList);

List<Integer> numbers = filter(numberList, (Integer i) -> i % 2 == 0);
System.out.println("能被2整除的数:" + numbers);
}

/**
* 筛选绿色苹果
*/
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> resultList = new ArrayList<>();
for (T t : list) {
// 谓词对象封装了条件
if (predicate.filter(t)) {
resultList.add(t);
}
}
return resultList;
}

public interface Predicate<T> {
boolean filter(T t);
}
}

在通往抽象的路上,我们还可以更进一步。目前,filterApples方法还只适用于Apple。还可以将List类型抽象化,从而支持所有类型。

8.演化小结

这一路演化中我们可以看出代码是如何一步一步转化的更加简洁更加优雅,对此我们进行总结:

实例

1.用 Comparator 排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.lujiahao.learnjava8.chapter2;

import java.util.Comparator;
import java.util.List;

/**
* 用 Comparator 排序
* @author lujiahao
* @date 2019-03-02 18:34
*/
public class ComparatorDemo {
public static void main(String[] args) {
List<Apple> appleList = DataUtil.generateApples();
System.out.println("原集合:" + appleList);

appleList.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
System.out.println("按重量升序:" + appleList);

appleList.sort((Apple a1, Apple a2) -> a1.getColor().compareTo(a2.getColor()));
System.out.println("按颜色字典排序:" + appleList);
}
}

2.用 Runnable 执行代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.lujiahao.learnjava8.chapter2;

/**
* 用 Runnable 执行代码块
* @author lujiahao
* @date 2019-03-02 18:42
*/
public class RunnableDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello Java 8!");
}
});
t.start();

Thread t1 = new Thread(() -> System.out.println("Hello Lambda!"));
t1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

3.GUI 事件处理

1
2
3
4
5
6
7
8
Button button = new Button(“Send”);
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
lable.setText(“Send!!”);
}
}

button.setOnAction((ActionEvent event) -> lable.setText(“Send!!”));

小猿之前搞安卓开发的,各种控件的监听都是这个样子,想想以前各种代码啊啊啊~

总结

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

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

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!

《Java 8 in Action》Chapter 1:为什么要关心Java 8

自1998年 JDK 1.0(Java 1.0) 发布以来,Java 已经受到了学生、项目经理和程序员等一大批活跃用户的欢迎。这一语言极富活力,不断被用在大大小小的项目里。从 Java 1.1(1997年) 一直到 Java 7(2011年),Java 通过增加新功能,不断得到良好的升级。Java 8 则是在2014年3月发布的。Java 8 所做的改变,在许多方面比 Java 历史上任何一次改变都深远,而且极大的提高了 Java 代码的简洁性。

1. lambda 表达式

本文通过筛选苹果的需求引入 Java 8 ,对 inventory 中的苹果按照重量进行排序。
Java 8 之前的版本:

1
2
3
4
5
Collections.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});

Java 8 版本:

1
inventory.sort(comparing(Apple::getWeight));

通过对比我们不难发现,使用 Java 8 可以编写更为简洁的代码,而且代码读起来更接近问题的描述。

2. 方法引用

在 Java 8 之前类(Class)是Java中的一等公民,Java8中将方法和lambda增加为一等公民。方法和lambda作为一等公民,是Java8中方法引用的基础。除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括 Lambda1(或匿名函数)。

筛选一个目录中的所有隐藏文件,Java 8 之前版本:

1
2
3
4
5
File[] hiddenFiles = new File(“.”).listFiles(new FileFilter() {
public boolean accept (File file) {
return file.isHidden();
}
}

Java 8 版本:

1
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

3. 流

在Java8之前,遍历处理集合元素,你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。

Java 8 中对于大数据量的集合,用Stream API(java.util.stream)解决了:集合处理时的套路和晦涩,以及难以利用多核这两个问题。

如下展示 Java 8 中使用 Stream API 并行处理数据:

1
2
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150) .collect(toList());

4. 默认方法

Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。同时,普通开发者也可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容,进一步方便开发。

1
2
List<Apple> heavyApples1 = inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
List<Apple> heavyApples2 = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());

在Java 8之前,List并没有stream或parallelStream方法,它实现 的Collection接口也没有。Java 8 给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明 中使用新的default关键字来表示这一点。这样就实现了改变已发布的接口而不破坏已有的实现。

总结

本章主要总结Java 8 的主要变化(Lambda表达式、方法引用、流和默认方法),为后面更进一步学习打下坚实基础。

资源获取

  • 公众号回复 : Java8 即可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注我:后端小哥,专注后端开发,希望和你一起进步!