Java 8 学习笔记
整理自《Java8实战》一书
断断续续半年没有更新了,每次只能写一点然后保存为草稿。因为一直在忙着写毕业论文,过几个月又要上班了,所以趁着这几个月多更新一点基础,等工作了就多更新一些应用遇到的问题。
1.变化:函数、流、默认方法、模式匹配、避免空指针等
2.Collection主要是为了存储和访问数据,Stream主要用于描述对数据的计算
3.lambda表达式
基本语法:(parameters) -> expression 或者是 (parameters) -> {statement;}
当是表达式的时候不能加花括号;当是语句的时候要加上花括号
主要作用于函数式接口:只定义一个抽象方法的接口,默认方法不计入
lambda表达式允许直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。匿名内部类也可以实现,只是要写很多样板代码。
private static void test2(){
//Java8以前的写法,匿名类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("r1");
}
};
//Java8之后的写法,lambda表达式
Runnable r2 = () -> {
System.out.println("r2");
};
test2_1(r1);
test2_1(r2);
test2_1(()->{System.out.println("r3");});
}
private static void test2_1(Runnable r){
r.run();
}
注解@FunctionalInterface用来表示函数式接口
类型检查:根据lambda的上下文推断出来
lambda表达式捕获局部变量同匿名类一样,需要声明为final类型
方法引用:使用 类名::方法名 来表示
方法引用就是根据已有的方法创建lambda表达式
三种形式:指向静态方法的方法应用、指向任意类型实例方法的方法引用、指向现有对象的实例方法的方法引用
方法引用就是替代那些转发参数的Lambda表达式的语法糖
4.流
定义:从支持数据处理操作的源生成的元素序列
粗略地说,流和集合的差异在于什么时候进行计算。流就像一个延迟创建的集合,只有在消费者要求的时候才会计算值
流是内部迭代,集合是外部迭代
<注意>流只能消费一次,遍历一次之后需要重新获取流
流操作分为两种:可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作
中间操作:除非流水线上触发一个终端操作,否则中间操作不会执行任何处理
5.使用流
(1)筛选和切片:在转换为流之后,如下操作都为中间操作
方法名 | 作用 |
---|---|
filter | 接受一个谓词,并根据谓词结果返回结果为true的流 |
distinct | 筛选不同的元素,不需要传参 |
limit | 截断流,传递一个数字参数n,表示只保留前n个数据 |
skip | 跳过元素,传递一个数字参数n,表示跳过前n个元素,如果元素不足n个,则返回空流 |
(2)映射
方法名 | 作用 |
---|---|
map | 接收一个函数,应用到每一个元素上 |
flatmap | 同样的,只是将map的不同的流映射为一个流,最终生成一个流 |
(3)查找和匹配
方法名 | 作用 |
---|---|
anyMatch | 传递谓词,流中任意一个元素匹配,终端操作 |
allMatch | 传递谓词,流中所有元素匹配,终端操作 |
noneMatch | 传递谓词,流中没有元素匹配,终端操作 |
findAny | 无须传参,返回流中任意元素,返回类型Optional |
findFirst | 无须传参,返回流中第一个元素,返回类型Optional |
(4)归约(折叠)
方法名 | 作用 |
---|---|
reduce | 把流中的元素组合起来 |
流的操作分为无状态和有状态,诸如map/filter等从流中获取一个元素并放到刷出流中没有内部状态的称为无状态;诸如reduce/max/sum等需要内部状态来积累结果的则为有状态。
(5)数值流
提供了原始类型流特化,即Integer转为int等。
提供三个IntStream、DoubleStream和LongStream
这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性
映射到数值流,只需要调用mapToInt等方法就可以映射为IntStream流,在调用该流新添加的sum等方法进行数值操作;反过来讲数值流转换为普通流,只需要调用boxed方法即可。
IntStream intStream = a.stream().mapToInt(A::getValue());
Stream<Integer> stream = intStream.boxed();
同样的,对于数值操作,max等会返回OptionalInt类型
对于提供的range和rangeClosed函数,都需要提供起始和终止参数;对于range来讲不包含终止参数
(6)创建流
方法名 | 作用 |
---|---|
Stream.of | 静态方法,接收任意数量参数 |
Arrays.stream | 静态方法,接收一个数组 |
Files.lines | 静态方法,传递文件,返回文件中每一行的流 |
Stream.iterate | 迭代,依次递推,是有序的 |
Stream.generate | 生成,不是依次对值应用函数 |
6.用流收集数据
收集器:对流调用collect方法将会对流中的元素触发一个归约操作
一般来讲,Collectors类提供的工厂方法能适用大多数场景
(1)归约和汇总
方法名 | 作用 |
---|---|
maxBy/minBy | 获取最大值和最小值 |
summingInt等 | 求和 |
averagingInt等 | 求平均值 |
summarizingInt等 | 返回IntSummaryStatistics对象,包含count/sum/average等属性 |
joining | 连接字符串,内部使用StringBuilder;重载版本可以传递分隔符 |
上述讨论的收集器,实际上可以通过Collectors.reducing工厂方法来实现,是特殊情况
一般情况下的收集器,可以利用Collectors中的reducing方法来实现
reducing方法主要分为两个重载版本
方法名 | 作用 |
---|---|
reducing(a,b,c) | a表示起始值,b表示选择的属性,c为lambda,是一个二元操作,最后返回一个结果 |
reducing(a) | a为lambda,为一个二元操作,最后返回Optional对象 |
那Stream里面的reduce和collect又有什么区别?
reduce旨在把两个值结合起来生成一个新值,是不可变的归约,并且无法并行执行;
collect的设计就是要改变容器,从而累计要输出的结果,适合并行操作,适合表达可变容器的归约
(2)分组
groupingBy方法,第一个参数为类型,第二个参数接收Collector类型参数;如果只传递一个参数,则第二个参数默认为toList()
groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入到分组Map中
把收集器的结果转换为另一种类型,使用collectingAndThen方法,其接收两个参数,分别为要转换的收集器以及转换函数,并返回另一个收集器
mapping方法也可以和groupingBy联合,其接收两个参数,一个函数对流中的元素进行变换,另一个则将变换的结果对象收集起来
最后收集的容器类型,可以通过collect(toCollection,HashSet::new)来控制
(3)分区
分区是分组的特殊情况,由一个谓词(返回布尔的函数)作为分类函数,它被称为分区函数;因此该方法得到的Map的键为Boolean类型
partitioningBy方法只接收一个谓词,与groupingBy方法类似,同样可以接收两个参数
(4)收集器接口
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 | 该方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用 |
accumulator | 该方法返回执行归约操作的函数,该函数执行到第n个元素时,有保留归约结果的累加器和第n个元素。该函数返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态用来体现遍历的元素的效果 |
finisher | 遍历完流后,该方法必须返回在累积过程中的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果 |
combiner | 返回一个供归约操作使用的函数,定义了流的各个子部分在进行并行处理时,各个子部分归约所得的累加器的合并方式 |
characteristics | 返回一个不可变的Characteristics集合,定义了收集器的行为,UNORDERED表示归约结果不受流中项目的遍历和累积顺序的影响、CONCURRENT表示accumulator可以从多个线程同时调用,如果没有UNORDERED标记,则仅在用于无序数据源时才可以并行归约、IDENTITY_FINISH表示完成器方法返回的函数是一个恒等函数,这种情况下,累加器对象将会直接用作归约过程的最终结果 |
7.并行数据处理与性能
(1)并行流
通过对收集源调用parallelStream方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
通过调用parallel方法可以把顺序流转换为并行流,通过调用sequential方法可以把并行流转换为顺序流;但是流水线最终会按照最后一次parallel/sequential的调用决定。
并行流使用的线程数量默认为CPU数量,可以通过如下方式进行更改
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","100")
//该方法是全局设置,将影响所有的并行流
并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。
需要注意的是,共享可变状态会影响并行流以及并行计算。
那么如何高效的使用并行流呢?
- 测量性能
- 装箱问题
- 依赖元素顺序的操作在并行流上性能就比顺序流差
- 流的操作流水线总计算成本
- 较小数据量不适合并行流
- 流背后的数据结构是否易于分解
- 流的特点、流水线中间操作修改流的方式等都会改变分解过程的性能
- 考虑终端操作中合并步骤的代价大小
下表所示为流的数据源和可分解性
源 | 可分解性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
(2)分支/合并框架
分支/合并框架的目的是以递归方式将可以并行执行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,把子任务分配给线程池中(ForkJoinPool)的工作线程.
- 对一个任务调用join方法会阻塞调用方,直到该任务做出结果
- 不应该在RecursiveTask内部使用ForkJoinTask的invoke方法,相反应该直接使用compute或者fork方法,只有顺序代码才应该使用invoke来启动并行运算
- 对子任务调用fork方法可以把它排进ForkJoinPool
该框架使用工作窃取技术来解决任务分配问题。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。基于前面所述的原因,某个线程可能早早地完成了分配的任务,此时其线程空而其他线程忙。此时,该线程将随机选择一个忙的线程,并从其尾部去任务开始执行。
(3)spliterator
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
- tryAdvance方法类似于iterator,会按照顺序依次使用Spliterator中的元素,如果有其他元素要遍历则返回true
- trySplit方法用于划分元素,把一部分元素划分给第二个Spliterator,使得其并行处理
- estimateSize方法用于估计还有多少元素需要遍历
- characteristics方法用于表示Spliterator的特性
8.重构、测试和调试
在匿名类中,this指代类自身,再lambda中则指代包含类。
其次,匿名类可以屏蔽包含类中的变量,而lambda则会编译错误。
int a = 10;
Runnable r1 = new Runnable(){
int a = 11;//正常
}
Runnable r2 = () -> {
int a = 11;//编译错误
}
匿名类的类型在初始化时就可以确定,lambda的类型则取决于上下文。
当一个lambda表达式有歧义时,可以利用强制类型转换来解决(比如两个都是Runnable一样的接口)。
9.默认方法
Java 8允许在接口内声明静态方法;也可以通过默认方法指定接口方法的默认实现
引入默认方法的目的:让类自动的继承接口的一个默认实现
默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体
函数式接口只包含一个抽象方法,默认方法是一种非抽象方法
那在Java 8中,抽象类和抽象接口的区别在哪?
一个类只能继承一个抽象类,但是一个类可以实现多个接口
一个抽象类可以通过实例变量保存一个通用状态,而接口不存在实例变量
兼容性 | 描述 |
---|---|
二进制 | 现有的二进制执行文件能无缝持续链接和运行,为接口添加一个方法就是二进制级的兼容 |
源代码 | 表示引入变化之后,现有的程序依然能够成功编译通过 |
函数行为 | 表示引入变化之后,程序接收同样的输入能得到同样的结果 |
Java 8中引入的默认方法就是源代码级别的兼容
解决冲突问题的规则:
- 类中的方法优先级最高。类或者父类中声明的方法的优先级高于任何声明为默认方法的优先级
- 第一条无法判断时,子接口的优先级更高;函数签名相同时,优先选择拥有最具体实现的默认方法的接口
- 以上依旧无法判断时,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式的选择使用哪一个默认方法的实现
显式调用默认方法的方式:X.super.func();其中X为对应的父接口
10.用Optional取代null
为了更好地解决null问题,提出了java.util.Optional<T>类
方法 | 描述 |
---|---|
empty | 返回一个空的Optional实例 |
filter | 如果值存在且满足条件,则返回包含该值的Optional;否则返回空的Optional |
flatMap | 如果值存在,就调用对应的mapping方法并返回Optional类型的值,否则返回空的Optional |
get | 如果值存在则返回包含该值的Optional,否则抛出NoSuchElementException异常 |
ifPresent | 如果值存在则执行使用该值的方法调用,否则什么都不做 |
isPresent | 如果值存在就返回true,否则返回false |
map | 如果值存在,则调用mapping方法 |
of | 将指定值包装为Optional返回,如果为null则抛出NullPointerException |
ofNullable | 将指定值包装为Optional返回,如果为null返回空Optional |
orElse | 如果有值则返回,否则返回一个默认值 |
orElseGet | 如果有值则返回,否则返回由Supplier生成的值 |
orElseThrow | 如果有值则返回,否则返回由Supplier生成的异常 |
需要注意的是。Optional并没有实现Serializable接口,所以无法实现序列化和反序列化
11.CompletableFuture组合式异步编程
通过工厂方法CompletableFuture.supplyAsync创建相应对象,该方法接收一个生产者Supplier作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由ForkJoinPool池中的某个执行线程运行,同时重载版本第二个参数可以指定线程
进行计算密集型操作时,推荐使用stream编程;当并行的工作单元还涉及到IO操作时,使用CompletableFuture效果更好。
针对工厂方法,Async结尾的方法会将后续的任务提交到一个线程池;没有Async结尾的方法会和前一个方法在同一个线程中运行
thenApply执行CompletableFuture的同步操作
thenCompose允许对两个异步操作进行流水线操作,当第一个操作完成时,将其结果作为参数传递给第二个操作
thenCombine允许将两个完全不相关的异步操作整合起来,其第二个参数为BiFunction,定义了当两个CompletableFuture对象完成计算后的合并操作
thenAccpet定义如何处理CompletableFuture返回的结果,一旦CompletableFuture计算得到结果,就返回一个CompletableFuture<Void>
12.新的日期和时间API
java.time是Java 8 提供的日期相关的包
LocalDate、LocalTime、LocalDateTime、Instant、Duration、Period等类提供了不同的时间表示方法
其他部分:超越Java 8
略
主要内容就是以上所写的内容。这里都是复述,并没有一些思考在里面。还需要多一些实践操作才可以。