目录
第一章:为什么要关心 Java 8?
几乎免费的并行:
写代码时不能访问共享的可变数据。这些函数有时被称为“纯 函数”或“无副作用函数”或“无状态函数”,
方法引用:
内部迭代:
用for-each循环一个个去迭代元素,然后再处理元素。我们把这种 数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处 理完全是在库内部进行的。我们把这种思想叫作内部迭代
默认方法:
简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
接口如今可以包含实现类没有提供实现的方法签名 了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类 提供。——这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明 中使用新的default关键字来表示这一点
默认方法(from 知乎):
在 java 8 之前,接口与其实现类之间的 耦合度 太高了(tightly coupled),当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在 lambda 表达式作为 java 8 语言的重要特性而出现之际,为升级旧接口且保持向后兼容(backward compatibility)提供了途径。
- 接口可以被类多实现(被其他接口多继承),抽象类只能被单继承。
- 接口中没有 this,没有构造函数,不能拥有实例字段(实例变量)或实例方法,无法保存 状态(state),抽象方法中可以。
- 抽象类不能在 java 8 的 lambda 表达式中使用。
- 从设计理念上,接口反映的是 “like-a” 关系,抽象类反映的是 “is-a” 关系。
Java中的并行与无共享可变状态
虽然函数式编程中 的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在 元素之间无互动”。
第二章:通过行为参数化传递代码
可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰嗦,因为 你需要声明很多只要实例化一次的类。让我们来看看可以怎样改进。
匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。
2.5 小结
以下是你应从本章中学到的关键概念。
-
行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。
-
行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。
-
传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。
-
Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。
第三章:Lambda 表达式
接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
Lambda 环绕执行
原文:https://blog.csdn.net/jwq201288888888/article/details/89633186 |
在java8推出来后,针对环绕执行模式(即资源处理时,常见的模式是打开一个资源,做一些处理,然后关闭资源。这些准备和清理资源阶段总是很类似,并且会围绕执行处理的那些重要代码。) 重复代码的问题,可以通过泛型解决入参出参差异的问题,通过lambda表达式(函数式编程)传递方法解决执行过程不同的问题。这样环绕代码只需写一次。 |
3 设计模式 在遵循“开放闭合”原则下,采用各种模式组织代码,譬如工厂模式,策略模式,模板模式等,都会将差异化部分封装在新类(其中很多都是接口或抽象类的子类)或新方法中。此时和容易想到将这些类或方法用lambda函数替代。 譬如在工厂模式中就不需要定义很多接口的子类了。当然比较复杂逻辑,为了保证代码的可读性和可维护性,还是需要针对具体情况做具体拆分。 //定义接口 |
泛型(比如Consumer<T>中的T)只能绑定到 引用类型。这是由泛型内部的实现方式造成的。1因此,在Java里有一个将原始类型转换为对应 的引用类型的机制。这个机制叫作装箱(boxing)。
——————————
1 C#等其他语言没有这一限制。Scala等语言只有引用类型
异常、Lambda,还有函数式接口又是怎么回事呢?请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda
表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。
特殊的void兼容规则
特殊的void兼容规则 然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void: // Predicate返回了一个boolean Predicate<String> p = s -> list.add(s); // Consumer返回了一个void Consumer<String> b = s -> list.add(s); |
Lambda 使用局部变量、闭包、线程共享(栈/堆)...
简化代码:
静态辅助方法
方法引用 (好理解,略)
复合函数:f.andThen(g) --> g(f(x));f.compose(g) --> g(f(x))
第四章:引入流
总结一下,Java 8中的Stream API可以让你写出这样的代码:
声明性——更简洁,更易读
可复合——更灵活
可并行——性能更好
其他库:Guava、Apache和lambdaj 为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是 谷歌创建的一个很流行的库。它提供了multimaps和multisets等额外的容器类。Apache Commons Collections库也提供了类似的功能。最后,本书作者Mario Fusco编写的lambdaj受到 函数式编程的启发,也提供了很多声明性操作集合的工具。 如今Java 8自带了官方库,可以以更加声明性的方式操作集合了。 |
集合&流:
集合是一个内存中的数据结构, 它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中
相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计 算的。
流只能消费一次!
哲学中的流和集合
对于喜欢哲学的读者,你可以把流看作在时间中分布的一组值。相反,集合则是空间(这 里就是计算机内存)中分布的一组值,在一个时间点上全体存在——你可以使用迭代器来访问for-each循环中的内部成员。
流利用了内部迭代:替你把迭代做了。但 是,只有你已经预先定义好了能够隐藏迭代的操作列表,例如filter或map,这个才有用。
4.5 小结
以下是你应从本章中学到的一些关键概念。
-
流是“从支持数据处理操作的源生成的一系列元素”。
-
流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了。
-
流操作有两类:中间操作和终端操作。
-
filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果。
-
forEach和count等终端操作会返回一个非流的值,并处理流水线以返回结果。
-
流中的元素是按需计算的
第五章:使用流
flatMap(Arrays::stream)
flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接 起来成为一个流
短路求值:
某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny、limit) 不用处理整个流就能得到结果
rangeClosed:比较一下,如果改用IntStream.range(1, 100),则结果 将会是49个偶数,因为range是不包含结束值的
示例:勾股数:
实用的~
由函数生成流:iterate 生成无限流
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
Fib 数列:
Stream.iterate(new int[]{0,1},
t -> new int[]{t[1],t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次 对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。我们先来 看一个简单的用法:
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
5.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等特定方法创建。
-
无限流是没有固定大小的流
第六章:用流收集数据
导入Collectors类的所有静态工厂方法:import static java.util.stream.Collectors.*;
这样就可以写counting()而用不着写Collectors.counting()之类的了。
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream().collect(maxBy(dishCaloriesComparator));
averagingInt、summarizingInt、
IntSummaryStatistics{count=9, sum=4300, min=120,
average=477.777778, max=800}
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
int totalCalories = menu.stream().collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
reduce v.s. collect:
这个解决方案有两个问题:一个语义问题和一个实际问题。语义问题在于,reduce方法
旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设 计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方 法,因为它在原地改变了作为累加器的List。你在下一章中会更详细地看到,以错误的语义 使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发 修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每 次分配一个新的List,而对象分配又会影响性能。这就是collect方法特别适合表达可变容 器上的归约的原因,更关键的是它适合并行操作
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创 建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。
把收集器的结果转换为另一种类型:Collectors.collectingAndThen
partitioningBy需要一个谓词,也就是返回一个布尔值的函数
Collector接口
public interface Collector<T, A, R> {
Supplier<A> supplier(); //建立新的结果容器
BiConsumer<A, T> accumulator(); //将元素添加到结果容器
Function<A, R> finisher(); //对结果容器应用最终转换
BinaryOperator<A> combiner(); //合并两个结果容器
Set<Characteristics> characteristics(); //
}
??
素数优化:
Java 8 自定义收集器
6.7 小结
以下是你应从本章中学到的关键概念。
-
collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称
为收集器)。
-
预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
这些收集器总结在表6-1中。
-
预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区。
-
收集器可以高效地复合起来,进行多级分组、分区和归约。
-
你可以实现Collector接口中定义的方法来开发你自己的收集器。
第七章:并行数据处理与性能
并行流内部使用了默认的ForkJoinPool(7.2节会进一步讲到分支/合并框架),它默认的 线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().available- Processors()得到的。
但 是 你 可 以 通 过 系 统 属 性 java.util.concurrent.ForkJoinPool.common. parallelism来改变线程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个 并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值, 除非你有很好的理由,否则我们强烈建议你不要修改它。
这就说明了并行编程可能很复杂,有时候甚至有点违反直觉。如果用得不对(比如采用了一 个不易并行化的操作,如iterate),它甚至可能让程序的整体性能更差,所以在调用那个看似 神奇的parallel操作时,了解背后到底发生了什么是很有必要的。
使用更有针对性的方法
那到底要怎么利用多核处理器,用流来高效地并行求和呢?我们在第5章中讨论了一个叫LongStream.rangeClosed的方法。这个方法与iterate相比有两个优点。
LongStream.rangeClosed直接产生原始类型的long数字,没有装箱拆箱的开销。
LongStream.rangeClosed会生成数字范围,很容易拆分为独立的小块。例如,范围1~20可分为1~5、6~10、11~15和16~20。
选择适当的数据结构往往比并 行化算法更重要
共享可变状态会影响并行流以及并行计算。
避免共享可变状态,确保并行Stream得到正 确的结果。
接下来,一些实用建议,你可以由此判断什么时候可以利用并行流来提升 性能。
7.1.4 高效使用并行流 一般而言,想给出任何关于什么时候该用并行流的定量建议都是不可能也毫无意义的,因为 任何类似于“仅当至少有一千个(或一百万个或随便什么数字)元素的时候才用并行流)”的建 议对于某台特定机器上的某个特定操作可能是对的,但在略有差异的另一种情况下可能就是大错 特错。尽管如此,我们至少可以提出一些定性意见,帮你决定某个特定情况下是否有必要使用并 行流。
|
ForkJoinPool:静态单例
availableProcessors方法虽然看起来是处理器, 但它实际上返回的是可用内核的数量,包括超线程生成的虚拟内核
工作窃取(work stealing)
子项目数的确定、多CPU/项目负载均衡:
Spliterator接口
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方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值 也有助于让拆分均匀一点。
7.4 小结
在本章中,你了解了以下内容。
-
内部迭代让你可以并行处理一个流,而无需在代码中显式使用和协调不同的线程。
-
虽然并行处理一个流很容易,却不能保证程序在所有情况下都运行得更快。并行软件的
行为和性能有时是违反直觉的,因此一定要测量,确保你并没有把程序拖得更慢。
-
像并行流那样对一个数据集并行执行操作可以提升性能,特别是要处理的元素数量庞大,
或处理单个元素特别耗时的时候。
-
从性能角度来看,使用正确的数据结构,如尽可能利用原始流而不是一般化的流,几乎
总是比尝试并行化某些操作更为重要。
-
分支/合并框架让你得以用递归方式将可以并行的任务拆分成更小的任务,在不同的线程
上执行,然后将各个子任务的结果合并起来生成整体结果。
-
Spliterator定义了并行流如何拆分它要遍历的数据。