Java 8 实战 读后日志
- 基础知识
- 为什么要关心Java8
- 编程语言生态系统的气候正在变化。程序员越来越多地要处理所谓的大数据(数百万兆甚至更多字节的数据集),并希望利用多核计算机或计算集群来有效地处理。
- Java 8可以透明地把2输入的不相关部分拿到几个CPU内核上去分别执行你的Stream操作流水线——这几乎是免费的并行。
- 行为参数化:java 8增加了把方法(你的代码)作为参数传递给另一个方法的能力。
- 没有共享的可变数据、将方法和函数即代码传递给其他方法的能力是函数式编程范式的基石。
- Lambda的长度多于几行(它的行为也不是一目了然)的话,那你应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。你应该以代码的清晰度为准绳。
- Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。
- Java中从函数式编程中引入的两个思想:将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。
- 模式匹配可以比if-then-else更简明地表达编程思想。可以把模式匹配看作switch的扩展形式,可以同时将一个数据类型分解成元素。
- 通过行为参数化传递代码
- 行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。
- 行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。
- 好的代码应该是一目了然的。
- lambda表达式
- 可以把lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可抛出的异常列表。
- 匿名
- 函数
- 传递
- 简洁
- Lambda表达式的三个部分:
- 参数列表
- 箭头: ->
- Lambda主体
- Lambda基本语句:
(parameters) -> expression
(parameters) -> { statements; }
- 函数式接口就是只定义一个抽象方法的接口。(哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。)
- 函数式接口的抽象方法的签名基本上就是lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
- @FunctionalInterface标注用于表示该接口会设计成一个函数式接口。该注解标签不是必须的,但对于为此设计的接口而言,使用它是比较好的做法。
- 函数式接口:
- java.util.function.Predicate<T>接口定义了一个名为名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
- java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回值(void)。
- java.util.function.Function<T,R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。
- Java 8中的常用函数式接口:
函数式接口 |
函数描述符 |
原始类型特化 |
Predicate<T> |
T -> boolean |
IntPredicate,LongPredicate,DoublePredicate |
Consumer<T> |
T -> void |
IntConsumer,LongConsumer,DoublePredicate |
Function<T,R> |
T -> R |
IntFunction<R>,IntToDoubleFunction,IntToLongFunction, LongFunction<R>,LongToDoubleFunction,LongToIntFunction, DoubleFunction<R>,ToIntFunction<T>,ToDoubleFunction<T>, ToLongFunction<T> |
Supplier<T> |
() -> T |
BooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier |
UnaryOperator<T> |
T -> T |
IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator<T> |
(T,T) -> T |
IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
BiPredicate<L,R> |
(L,R)->boolean |
|
BiConsumer<T,U> |
(T,U) -> void |
ObjIntConsumer<T>,ObjLongConsumer<T>, ObjDoubleConsumer<T> |
BiFunction<T,U,R> |
(T,U) -> R |
ToIntBiFunction<T,U>,ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
- 任何函数式接口都不允许抛出受检异常。Lambda抛出异常的两种方法:
- 定义一个自己的函数式接口,并声明受检异常。
- 把Lambda包在一个try/catch块中。
- Lambda的类型是从使用Lambda的上下文推断出来的。
- 上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
- 类型推断:
当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。
- Lambda中使用局部变量:
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但是局部变量必须显式声明为final或事实上是final。
- Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以看作捕获final修饰的局部变量this)
- Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。
- 闭包:
闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。
可以认为Lambda是对值的封闭,而不是对变量的封闭。
- 方法引用:
- 指向静态方法的方法引用(如Integer::parseInt)
- 指向任意类实例方法的方法引用(String::length)
- 指向现有对象的实例方法的方法引用object::getValue)
- 编译器会进行一种与lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。
- 构造函数的引用:ClassName::new
- 复合Lambda表达式的有用方法:
- 比较器复合 Comparator.comparing
- 逆序:reversed()
- 比较器链:thenComparing()
- 谓词复合
- 非:negate()
- 与:and()
- 或:or()
- 函数复合
- andThen():方法返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。 g(f(x))
- Compose():先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于计算结果。 f(g(x))
- 函数式数据处理
- 引入流
- 流是什么?
流是java api的新成员,它允许你以声明性方式处理数据集合。此外,流可以透明地并行处理。
- Stream API可以使你写出的代码:
- 声明性——更简洁、更易读
- 可复合——更灵活
- 可并行——性能更好
- 流的简短定义:从支持数据处理操作的源生成的元素序列。
- 元素序列——和集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。
- 源——流会使用一个提供数据的源。
- 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作。
- 流水线——很多流操作本身会返回一个流,这样多个操作就可以连接起来,形成一个大的流水线。
- 内部迭代——流的迭代操作实在背后进行的。
- 集合与流之间的差异就在于什么时候进行计算。
- 集合是内存中的数据结构。它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。
- 流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。
- 和迭代器类似,流只能遍历一次。
- 哲学中的流和集合:
可以把流看作在时间中分布的一组值。集合则是在空间中分布的一组值。
- 流操作分为:中间操作、终端操作
- 流的使用一般包括三件事:
- 一个数据源(如集合)来执行一个查询
- 一个中间操作链,形成一条流的流水线
- 一个终端操作,执行流水线,并能生成结果
- 使用流
- 筛选和切片
- 用谓词筛选:filter(),接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流。
- 筛选各异的元素:distinct(),它返回一个元素各异(根据流所生成元素的hashcode和equals方法实现)的流。
- 截断流:limit(n),该方法返回一个不超过给定长度的流。
- 跳过元素:skip(n),返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。
- 映射
- 对流中每个元素应用函数:map(),它接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新元素。
- 流的扁平化:flatMap(),把一个流中的每个值都换换成另一个流,然后把所有的流连接起来成为一个流。
- 查找和匹配
- 检查谓词是否匹配至少匹配一个元素:anyMatch(),返回一个boolean。
- 检查谓词是否匹配所有元素:allMatch()
- 检查流中没有任何元素与给定谓词匹配:noneMatch()
- 查找元素:findAny(),返回当前流中的任意元素
- 查找第一个元素:findFirst()
- 归约
- 元素求和:reduce(初始值,BinaryOperator<T>),Lambda反复结合每个元素,直到流被归约成一个值。无初始值时,返回一个optional<T>对象。
- 最大值和最小值:numberStream.reduce(Integer::min) numberStream.reduce(Integer::max)
- 流操作:无状态和有状态
诸如map或filter等操作会从输入流中获取每个元素,并在输出流中得到0或1个结果。这些操作一般是无状态的:它们没有内部状态(假设用户提供的lambda或方法引用没有内部可变状态)。
诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在例子中就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。
诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但是有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能出现问题。我们把这些操作叫作有状态操作。
- 中间操作和终端操作
操作 |
类型 |
返回类型 |
使用的类型/函数式接口 |
函数描述符 |
filter |
中间 |
Stream<T> |
Predicate<T> |
T -> boolean |
distinct |
中间 (有状态-无界) |
Stream<T> |
|
|
skip |
中间 (有状态-有界) |
Stream<T> |
long |
|
limit |
中间 (有状态-有界) |
Stream<T> |
long |
|
map |
中间 |
Stream<T> |
Function<T,R> |
T -> R |
flatMap |
中间 |
Stream<T> |
Function<T,Stream<R>> |
T -> Stream<R> |
sorted |
中间 (有状态-无界) |
Stream<T> |
Comparator<T> |
(T,T) -> int |
anyMatch |
终端 |
boolean |
Predicate<T> |
T -> boolean |
noneMatch |
终端 |
boolean |
Predicate<T> |
T -> boolean |
allMatch |
终端 |
boolean |
Predicate<T> |
T -> boolean |
findAny |
终端 |
Optional<T> |
|
|
findFirst |
终端 |
Optional<T> |
|
|
forEach |
终端 |
void |
Consumer<T> |
T -> void |
collect |
终端 |
R |
Collector<T,A,R> |
|
reduce |
终端 (有状态-有界) |
Optional<T> |
BinaryOperator<T> |
(T,T) -> T |
count |
终端 |
long |
|
|
- 数值流
- 原始类型流特化:IntStream、DoubleStream、LongStream,分别将流中的元素特化为int、long、double,从而避免了暗含的装箱成本。
- 映射到数值流:mapToInt、mapToDouble、mapToLong
- 转换回对象流:boxed()
- 默认值Optional:
Optional原始类型特化版本:OptionalInt、OptionalDouble、OptionalLong
- 数值范围
- range(起始值,结束值) 不包含结束值
- rangeClosed(起始值,结束值) 包含结束值
- 构建流
- 由值创建流:Stream.of,通过显式值创建一个流
- 由数组创建流:Arrays.stream,从数组创建一个流
- 由文件生成流:Files.lines,它会返回一个由指定文件中的各行构成的字符串流,其中每个元素都是给定文件中的一行。
- 由函数生成流:创建无限流
- Stream.iterate(初始值,UnaryOperator<T>):一般来说,需要依次生成一系列值的时候应该使用iterate
- Stream.generate(Supplier<T>):使用的供应源应该是无状态的。
- 用流收集数据
- 预定义收集器的三大功能:
- 将流元素归约和汇总为一个值
- 元素分组
- 元素分区(分区是特殊的分组,按谓词进行的)
- 归约和汇总
- 查找流中的最大值和最小值:Collectors.maxBy、Collectors.minBy
- 汇总:
Collectors.summingInt,接受一个把对象映射为求和所需int的函数,并返回一个收集器。
Collectors.averagingInt/averagingLong/averagingDouble
Collectors.sumarizingInt 返回IntSummaryStatistics ,包含count、sum、min、max、average
- 连接字符串
joining(),重载版本接受元素之间的分界符。
- 收集与归约(reduce()对比collect(reducing()))
- reduce方法把两个值结合起来生成一个新值,它是一个不可变的归约。
- collect方法的设计就是改变容器,从而累积要输出的结果。
- 分组
- Collectors.groupingBy,接受一个Function,我们把这个Function叫作分类函数。分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。
- 多级分组:Collectors.groupingBy,可以接受collector类型的第二个参数。要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个流中项目分类的二级标准。
- 按子组收集数据:groupingBy(f)实际上是groupingBy(f,toList())的简便写法。
- groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中。
- 分区
- 分区是分组的特殊情况:partitioningBy,由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。存在重载版本,接受第二个收集器。
- 分区的优势:分区的好在于保留了分区函数返回true或false的两套流元素列表。
- Collectors类的静态工厂方法
工厂方法 |
返回类型 |
用于 |
例子 |
toList |
List<T> |
把流中所有项目收集到一个List |
List<Dish> dishes = menuStream.collect(toList()) |
toSet |
Set<T> |
把流中所有项目收集到一个Set,删除重复项 |
Set<Dish> dishes = menuStream.collect(toSet()) |
toCollection |
Collection<T> |
把流中所有项目收集到给定的供应源创建的集合 |
Collection<Dish> dishes = menuStream.collect(toCollection,ArrayList::new) |
counting |
Long |
计算流中元素的个数 |
long howManyDishes = menuStream.collect(counting()) |
summingInt |
Integer |
对流中项目的一个整数属性求和 |
int totalCalories = menuStream.collect(summingInt(Dish::getCalories)) |
averagingInt |
Double |
计算流中项目Integer属性的平均值 |
double averageCalories = menuStream.collect(averagingInt(Dish::getCalories)) |
sumarizingInt |
IntSummaryStatistics |
收集关于流中项目Integer属性的统计值,例如最大、最小、总和、平均值 |
IntSummaryStatisstics menuStatistics = menuStream.collect(summarizingInt(Dish::getCaloriess)) |
joining |
String |
连接对流中每个项目调用toString方法所生成的字符串 |
String shortMenu = menuStream.map(Dish::getName).collect(joining(“,”)) |
maxBy |
Optional<T> |
一个包裹了流中按照给定比较器选出的最大元素的Optional,或如果流为空则为Optional.empty() |
Optional<Dish> fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories))) |
minBy |
Optional<T> |
一个包裹了流中按照给定比较器选出的最小元素的Optional,或如果流为空则为Optional.empty() |
Optional<Dish> lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories))) |
reducing |
归约操作产生的类型 |
从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值 |
Int totalCalories = menuStream.collect(reducing(0,Dish::getCalories,Integer::sum)) |
collectingAndThen |
转换函数返回的类型 |
包裹另一个收集器,对其结果应用转换函数 |
Int howManyDishes = menuStream.collect(collectingAndThen(toList(),List::size)) |
groupingBy |
Map<K,List<T>> |
根据项目的一个属性的值对流中的项目作分组,并将属性值作为结果Map的键 |
Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(groupingBy(Dish::getType)) |
partitionBy |
Map<Boolean,List<T>> |
根据对流中每个项目应用谓词的结果来对项目进行分区。 |
Map<Boolean,List<T>> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian)) |
- 理解Collector接口
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是收集操作得到的对象(通常但并不一定是集合)的类型。
- 建立新的结果容器:supplier在调用时会创建一个空的累加器实例,供数据收集过程使用。
- 将元素添加到结果容器:accumulator方法
- 对结果容器应用最终转换:finisher方法
- 合并另个结果容器:combiner方法
- characteristics方法:返回一个不可变的Characteris集合,定义了收集器的行为。
- UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
- CONCCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。
- IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。
- 重载版本:Stream.collect(supplier,accumulator,combiner)
- 并行数据处理与性能
- 并行流就是把内容分成对个数据块,并用不同的线程分别处理每个数据块的流。
- parallel方法:将顺序流转换为并行流。它内部实际上就是设置了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行。
- sequential方法:将并行流变为顺序流
- 并行流内部使用了默认的ForkJoinPool,它默认线程数就是你的处理器核数,这个值是由Runtime.getRuntime().availableProcessors()得到的。
- 选择适当的数据结构往往比并行化算法更加重要。
- 使用正确的数据结构然后使其并行工作能够保证最佳的性能。
- 保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。
- 共享可变状态会影响并行流以及并行计算。
- 高效使用并行流:
- 如有疑问,测量。
- 留意装箱。
- 有些操作本事在并行流上的性能就比顺序流差。
- 还要考虑流的操作流水线的总计算成本。
- 对于较小的数据量,选择并行流几乎从来都不是一个好的决定。
- 要考虑流背后的数据结构是否易于分解。
- 流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能。
- 还要考虑终端操作中合并步骤的代价是大是小。
流的数据源和可分解性
源 |
可分解性 |
ArrayList |
极佳 |
LinkedList |
差 |
IntStream.range |
极差 |
Stream.iterate |
差 |
HashsSet |
好 |
TreeSet |
好 |
- 分支/合并框架
分支/合并框架的目的就是以递归方式将可并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。他是ExecutorService接口的一个实现,他把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
- RecursiveTask<R> R是产生结果的类型;不返回结果的是RecursiveAction类型
protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。
if(任务足够小或不可分){
顺序计算该任务
}else{
将任务分成两个子任务
递归调用该方法,拆分每个子任务,等待所有子任务完成
合并每个子任务的结果
}
- 使用分支/合并框架的最佳做法
- 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。
- 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。
- 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。你这样做可以为其中一个子任务重用同一个线程,从而避免在线程池中多分配一个任务造成的开销。
- 调试使用分支/合并框架的并行计算可能有点棘手。
- 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。
- 你必须选择一个标准,来决定任务是要进一步拆分还是已经小到可以顺序求值。
- 分支/合并框架工程用工作窃取的技术解决多线程执行效率不一致问题。
- Spliterator 可分迭代器(splitable iterator)
public interface Spliterator<T>{
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
- tryAdvance方法的行为类似于普通的Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true。
- trySplit是专门为Spliterator接口设计的,它可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理。
- estimateSize方法估计还剩多少元素要遍历,即使不准确,能快速算出来一个值也有助于让拆分均匀点。
- characteristics方法返回一个int,代表Spliterator本身特性集的编码。
Spliterator的特性
特性 |
含义 |
ORDERED |
元素有既定的顺序(如List),因此Spliterator在遍历和划分时也会遵循这一顺序 |
DISTINCT |
对于任意一对遍历过的元素x和y,x.equals(y)返回false |
SORTED |
遍历的元素按照一个预定义的顺序排序 |
SIZED |
该Spliterator由一个已知大小的源建立(如Set),因此estimatedSize()返回的是准确值 |
NONNULL |
保证遍历的元素不会为null |
IMUTABLE |
Spliterator的数据源不能修改。这意味着在遍历时不能添加、删除或修改任何元素 |
CONCURRENT |
该Spliterator的数据源可以被其他线程同时修改而无需同步 |
SUBSIZED |
该Spliterator和所有从它拆分出来的Spliterator都是SIZED |
- 高效JAVA 8编程
- 重构、测试和调试
- 为改善可读性和灵活性重构代码
- 改善代码可读性
- 重构代码,用lambda表达式取代匿名类
- 用方法引用重构Lambda表达式
- 用Stream Api重构命令式的数据处理
- 从匿名类到Lambda表达式的转换
- 匿名类和lambda中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但是在lambda中,它代表的是包含类。
- 匿名类可以屏蔽包含类的变量,而lambda表达式不能(它们会导致编译错误)
- 从lambda表达式到方法引用的转换
- 尽量考虑使用静态辅助方法,如comparing、maxBy
- 很多通用的归约操作,比如sum、maximum,都有内建的辅助方法可以和方法引用结合使用
- 从命令式的数据处理切换到Stream
- 将命令式的代码结构转换为Stream API的形式是个困难的任务,因为你需要考虑控制流语句,比如break、continue、return,并选择使用恰当的流操作。
- 增加代码的灵活性
- 采用函数接口
- 有条件的延迟执行
- 环绕执行
- 使用Lambda重构面向对象的设计模式
- 策略模式
- 策略模式包括三个部分:
- 一个代表 某个算法的接口
- 一个或多个该接口的具体实现,它们代表了算法的多种实现
- 一个或多个使用策略对象的客户
- 模板方法
- 观察者模式
- 某些事件发生时(比如状态转变),如果一个对象(通常称之为主题)需要自动地通知其它多个对象(称之为观察者),就会采用这种方案。
- 责任链模式
① 责任链模式是一种创建对象序列(比如操作序列)的通用方案。
- 工厂模式
- 测试Lambda表达式
- 测试可见lambda函数的行为:可以通过添加静态字段的方式声明出来
- 测试使用lambda的方法的行为
- 将复杂的lambda表达式分到不同的方法:可以将lambda表达式转换为方法引用
- 高阶函数的测试:把它当成一个函数接口,对它的功能进行测试。
- 调试
- 查看栈跟踪
- 如果方法引用指向的是同一个类中声明的方法,那么它的名称是可以在栈跟踪中显示的。
- 涉及lambda表达式的栈跟踪可能非常难理解。
- 输出日志
- 使用peek方法:peek的设计初衷就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像foreach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会顺承到流水线中的下一个操作。
- 默认方法
- Java 8在声明方法的同时提供实现,可以通过两种方式完成:
- 声明静态方法
- 默认方法:主要目标是类库的设计者,目的是让类可以自动地继承接口的一个默认实现。
- 不同类型的兼容性:
- 二进制兼容性:表示现有的二进制执行文件能无缝持续集成(包括验证、准备和解析)和运行。
- 源代码级兼容性:表示引入变化之后,现有的程序依然能成功编译通过。
- 函数行为的兼容性:表示变更发生之后,程序接受同样的输入能得到同样的结果。
- 默认方法是一种以源码兼容方式向接口内添加实现的方法。
- 默认方法的使用:
- 可选方法:无需在实现类中显式的实现空方法
- 行为的多继承:
- 类型的多继承
- 利用正交方法的精简接口
- 组合接口
- 关于继承的一些错误观点:
- 继承不应该成为你一谈到代码复用就试图依靠的万金油,你完全可以靠代理有效的规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。
- 通过精简的接口,你能获得最有效的组合,因为你可以只选择你需要的实现。
- 解决冲突的三条原则:
- 首先,类或父类中显式声明的方法,其优先级高于所有的默认方法
- 如果第一条无法判断 ,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
- 最后,如果冲突无法解决,你就只能在你的额类中覆盖该默认方法,显示地指定在你的类中使用哪一个接口中的方法。(java8中引入新语法:X.super.m(...),其中X是你希望调用的m方法所在的父接口)
- 用Optional取代null
- 代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是逆算法上的缺陷,抑或是你数据中的问题。
- 创建Optional对象
- 声明一个空的Optional:Optional.empty()
- 依据一个非空值创建Optional:Optional.of(car)
- 可接受null的Optional:Optional.ofNullable(car)
- 使用map从Optional对象中提取和转换值
- 如果Optional包含一个值,那函数就将该值作为参数传递给map。对该值进行转换。如果Optional为空,就什么也不做。
- 使用flatMap链接Optional对象:该方法棘手一个函数作为参数,这个函数的返回值就是另一个流。(该方法生成的各个流会被合并或者扁平化为一个单一的流)
- Optional无法序列化。
- Optional类的方法
方法 |
描述 |
empty |
返回一个空的Optional实例。 |
filter |
如果有值存在并且满足提供的谓词,就返回包含该值的Optional对象;否则返回一个空的Optional对象。 |
flatMap |
如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象。 |
get |
如果值存在,就将被Optional封装的值返回,否则抛出一个NoSuchElementsException异常 |
ifPresent |
如果值存在,就执行使用该值的方法调用,否则什么也不做 |
isPresent |
如果值存在就返回true,否则返回false |
map |
如果值存在,就对该值执行提供的mapping函数调用 |
of |
将指定值用Optional封装之后返回,如果该值为null,则抛出一个NullPointerException异常 |
ofNullable |
将指定值用Optional封装之后返回,如果值为null,则返回一个空的Optional对象 |
orElse |
如果有值就将其返回,否则返回一个默认值 |
orElseGet |
如果有值则将其返回,否则返回一个由指定的Supplier接口生的值 |
orElseThrow |
如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常 |
- 基础类型的Optional对象:Optionalint、OptionalLong、OptionalDouble。基础类型的Optional不支持map、flatMap、filter方法。
- CompletableFuture:组合式异步编程
- CompletableFuture.completeExceptionally()方法将导致导致CompletableFuture内发生问题的异常抛出。
- CompletableFuture.supplyAsync()方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。
- 并行:使用流还是CompletableFuture
- 计算密集型的操作,并且没有I/O,推荐使用Stream,因为实现简单,同时效率也可能是最高的。
- 并行的工作单元设计等待I/O的操作,使用CompletableFuture灵活性更好
- CompletableFuture.thenCompose方法允许对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。
- 通常而言名称中不带Async方法和它的前一个任务一样,在同一个线程中运行;而名称以Async结尾的方法会将后续的任务提交到一个线程池,所以每个任务是由不同的线程处理的。
- CompletableFuture.thenCombine方法,接受名为BiFunction的第二个参数,这个参数定义了当两个CompletableFuture对象完成计算后,结果如何合并。
- CompletableFuture.thenAccept方法,接受CompletableFuture执行完毕后的返回值做参数。
- CompletableFuture.anyOf方法,接受一个CompletableFuture对象构成的数组,返回由第一个执行完毕的CompletableFuture对象的返回值构成的CompletableFuture<Object>。
- 新的日期和时间API
- LocalDate:该类实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息。另外,它也不附带任何与时区相关的信息。
- LocalTime:提供了时分秒
- LocalDateTime:它同时表示了日期和时间,但是不带有时区信息。
- java.time.Instant:是以unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算。Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。
- Temporal接口定义了如何读取和操纵为时间建模的对象的值。
- Duration.between:接受LocalTime、LocalDateTime、Instant
- Period.between:接受LocalDate
- LocalDate对象 调用withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。
- 表示时间点的日期-时间类的通用方法
方法名 |
是否是静态方法 |
描述 |
from |
是 |
依据传入的Temporal对象创建对象实例 |
now |
是 |
依据系统时钟创建Temporal对象 |
of |
是 |
由Temporal对象的某个部分创建该对象的实例 |
parse |
是 |
由字符串创建Temporal对象的实例 |
atOffset |
否 |
将Temporal对象和某个时区偏移相结合 |
atZone |
否 |
将Temporal对象和某个时区相结合 |
format |
否 |
使用某个指定格式器将Temporal对象转换为字符串(Instant类不提供该方法) |
get |
否 |
读取Temporal对象的某一部分的值 |
minus |
否 |
创建Temporal对象的一个副本,通过将当前Temporal对象的值减去一定时长创建副本 |
plus |
否 |
创建Temporal对象的一个副本,通过将当前Temporal对象的值加上一定时长创建副本 |
with |
否 |
以Temporal对象为模板,对某些状态进行修改创建该对象的副本 |
- TemporalAdjuster类中的工厂方法
方法名 |
描述 |
dayOfWeekInMonth |
创建一个新的日期,它的值为同一个月中每一周的第几天 |
firstDayOfMonth |
创建一个新的日期,它的值为当月的第一天 |
firstDayOfNextMonth |
创建一个新的日期,它的值为下月的第一天 |
firstDayOfNextYear |
创建一个新的日期,它的值为明年的第一天 |
firstDayOfYear |
创建一个新的日期,它的值为当年的第一天 |
firstInMonth |
创建一个新的日期,它的值为同一个月中,第一个符合星期几要求的值 |
lastDayOfMonth |
创建一个新的日期,它的值为当月的最后一天 |
lastDayOfNextMonth |
创建一个新的日期,它的值为下月的最后一天 |
lastDayOfNextYear |
创建一个新的日期,它的值为明年的最后一天 |
lastDayOfYear |
创建一个新的日期,它的值为今年的最后一天 |
lastInMonth |
创建一个新的日期,它的值为同一个月中,最后一个符合星期几要求的值 |
next/previous |
创建一个新的日期,并将其值设定为日期调整后或调整前,第一个符合指定星期几要求的日期 |
nextOrSame/previousOrSame |
创建一个新的日期,并将其值设定为日期调整后或调整前,第一个符合指定星期几要求的日期,如果该日期已经符合要求,直接返回该对象。 |
- DateTimeFormatter:实例都是线程安全的。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(“dd/MM/yyyy”);
LocalDate date1 = LocalDate.of(2014, 3 , 18);
String formattedDatte = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate,formatter);
- DateTimeFormatterBuilder:提供了非常强大的解析功能,比如区分大小写的解析、柔性解析(允许解析器使用启发式的机制去解析输入,不精确的匹配指定的模式)、填充,以及在格式器中指定可选节。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder();
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(“. ”)
.appendText(ChronoFeild.MONTH_OF_YEAR)
.appendLiteral(“ ”)
.appendText(ChronoFeild.YEAR)
.parseCaseIntensive()
.toFormatter(Locale.ITALIAN);
- 处理不同的时区和历法
- 时区是按照一定的规则将区域划分成的标准时间相同的区间。
- java.time.ZoneId是java.util.TimeZone的替代品。
ZoneId zone = ZoneId.of(“{区域}/{城市}”);
这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。
- TimeZone、TimeId转换
ZoneId zoneId = TimeZone.getDefault().toZoneId();
- 将ZoneId对象与LocalDate、LocalDateTime或是Instant对象整合起来,构造成为一个ZonedDateTime实例,它代表了相对于指定时区的时间点。
- 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);
- LocalDateTime datetime = LocalDateTime.of(2014,Month.MARCH,18,13,45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
- Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant,romeZone);
- 利用和UTC/格林尼治时间的固定偏差计算时区
使用ZoneOffset类,表示当前时间和伦敦格林尼治子午线时间的差异。
ZoneOffset newYorkOffset = ZoneOffset.of(“-05:00”);
OffsetDateTime使用ISO-8601历法系统,以相对于UTC格林尼治时间的偏差方式表示日期时间。
LocalDateTime datetime = LocalDateTime.of(2014,Month.MARCH,18,13,45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date,newYorkOffset );
- 使用别的日历系统
1)ISO-8601日历系统是世界文明日历系统的事实标准。
2)Java 8中提供了四种日历系统:ThaiBuddhistDate、MinguoDate、JapaneseDate、HijrahDate。
3)如果你需要将程序的输入或者输出本地化,这时你需要使用ChronoLocalDate类。
4)伊斯兰教日历
- 超越Java 8
- 函数式的思考
- 实现和维护系统
系统应该具有良好的结构,最好类的结构应该反映出系统的结构,这样能便于理解;甚至软件工程中还提供了指标,对结构的合理性进行评估,比如耦合性、内聚性。
- 共享的可变数据:
如果一个方法既不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为纯粹的或者无副作用的。
- 声明式编程:
声明式编写的代码更加接近问题陈述。
- 为什么要采用函数式编程:
一些语言的特性,比如构造操作和传递行为对于自然的方式实现声明式编程时必要的,它们能让我们的程序更便于阅读,易于编写。
- 什么是函数式编程;
- 在函数式编程的上下文中,一个“函数”,对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用。
- 像数学函数那样没有副作用,称为纯粹的函数式编程。
- 如果程序有一定的副作用,不过副作用不会为其他的调用者感知,称为函数式编程。
- 函数式java编程
- 我们的准则是,被称为“函数式”的函数或方法都只能修改本地地变量。除此之外,它引用的对象都应该是不可修改的对象。
- 函数或者方法不应该抛出任何异常。
- 局部函数式:对于某些输入值,甚至是大多数的输入值都返回一个确定的结果;不过对另一些输入值,它的结果是未定义的,甚至是不返回任何结果。
- 引用透明性:如果一个函数只要传递同样的参数值,总是返回同样的结果,那么这个函数就是引用透明的。
- 可以使用递归替代迭代,递归能更好的运用函数式编程。
- 函数式编程的技巧
- 能够像普通变量一样使用的函数称为一等函数。
- 高阶函数满足以下任一条件:
- 接受至少一个函数作为参数
- 返回的结果是一个函数
- 将所有你愿意接受的作为参数的函数可能带来的副作用以文档的方式记录下来是一个不错的设计原则,最理想的情况下你接收的函数参数应该没有任何副作用。
- 科里化:科里化是一种将具备2个参数(比如x,y)的函数f转化为使用一个参数的函数g,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的返回值相同,即f(x,y)= (g(x))(y)。
- 部分应用:一个函数使用所有参数仅有部分被传递时,通常我们说这个函数式部分应用的(partially applied)。
- 函数式方法不允许修改任何全局数据结构或者任何作为参数传入的结构。
- 函数式数据结构的持久化:数据结构的值始终保持一致,不受其他部分变化的影响。
- Sream的延迟计算
- 模式匹配(java 未实现)
- 遵守“引用透明性”原则的函数,其计算结构可以进行缓存。
- 接合器是一种函数式思想,它指的是将两个或多个函数或者数据结构进行合并。
- 面向对象和函数式编程的混合:JAVA 8和SCALA的比较
- 结论以及Java的未来
- 两种趋势:
- 要让代码运行得更快,需要代码具备并行运算的能力。
- 以声明方式简洁的操作数据集合来处理数据,这一趋势不断增长。
- 行为参数化:lambda表达式、方法引用
- 流(Stream API)
- CompletableFuture
- Optional
- 默认方法
- Java的未来
- 集合
- 类型系统的改进:
- 声明位置变量
- 更多的类型推断
- 模式匹配
- 更加丰富的泛型形式
- 具化泛型
- 泛型中特别为函数类型增加灵活性
- 原型特化和泛型
- 对不变性的更深层支持
- java 8只支持三种值:
简单类型值
指向对象的引用
指向函数的引用
- 值类型
- 为什么编译器不能对Integer和int一视同仁
- 值对象——无论简单类型还是对象类型都不能包打天下
- 装箱、泛型、值类型——互相交织的问题
附录:
- 语言特性的更新:
- 重复注解(repeated annotation):@Repeatable注解标签
- 类型注解(type annotation)
- 通用目标类型推断(generalized target-type inference)
- 类库的更新:
- 集合类可接口中新增的方法
类/接口 |
新方法 |
Map |
getOrDefault,forEach,compute,computeIfAbsent,computeIfPresent,merge, putIfAbsent,remove(key,value),replace,replaceAll |
Iteratable |
foreach,spliterator |
Iterator |
forEachReaming |
Collection |
removeIf,stream,parallelStream |
List |
replaceAll,sort |
BitSet |
stream |
- 并发
- 原子操作
新增方法
方法名 |
作用 |
getAndUpdate |
以原子方式用给定的方法更新当前值,并返回变更之前的值。 |
updateAndGet |
以原子方式用给定的方法更新当前值,并返回变更之后的值。 |
getAndAccumulate |
一原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。 |
accumulateAndGGet |
以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。 |
- Adder和Accumulator
- LongAdder、DoubleAccumulator类支持加法操作。
- LongAccumulator、DoubleAccumulator可以使用给定的方法整合多个值。
- ConcurrentHashMap
- 性能:当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree),新的数据结构具有更好的查询性能(排序树的查询复杂度为O(log(n)))。这种优化只有当键可以比较的时候才会发生。
- 类流的操作:
- forEach:对每个键值对进行特定操作
- reduce:使用给定的精简函数(reduce function),将所有的键值对整合出一个结果
- search:对每个键值对执行一个函数,直到函数的返回值为一个非空值
- 计数:mappingCount方法,以长整型long返回map中映射的数目。
- 集合视图:keySet方法,该方法以Set的形式返回ConcurrentHashMap的一个视图。静态方法newKeySet,由ConcurrentHashMap创建一个Set。
- Arrays
- 使用parallelSort
- 使用setAll和parallelSetAll
- 使用parallelPrefix
- Number和Math
提供了新的方法
- Files
- Files.list:生成由指定目录中所有条目构成的Stream<Path>。这个列表不是递归包含的
- Files.walk:生成包含给定目录中所有条目的Stream<Path>。这个列表是递归的,你可以设定递归的深度。
- Files.find:通过递归地遍历一个目录找到符合条件的条目,并生成一个Stream<Path>对象。
- Reflection
- 新增java.lang.reflect.Parameter类查询方法参数的名称和修饰符。
- String
- 增加静态方法join
- Lambda表达式和JVM
- 编译时,匿名类和lambda表达式使用了不同的字节码指令。
- 使用了invokeDynamic指令。