Java8新特性
2020-05-11 14:31:50 249 举报
AI智能生成
Java8新特性总结
作者其他创作
大纲/内容
stream
什么是流
从支持数据处理操作的源生成的元素序列
集合与流
流只能消费一次
流操作会改变源数据
内部迭代与外部迭代
使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。
相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
中间操作与终端操作
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚至void。
limit
limit具有短路性,即找到满足需要的数据后不会迭代剩余的数据
中间操作不会执行实际的计算,需要终端操作来触发。
中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,例如返回流中的最大元素。
使用流
1: 一个数据源(如集合)来执行一个查询;
2: 一个中间操作链,形成一条流的流水线;
3: 一个终端操作,执行流水线,并能生成结果。
2: 一个中间操作链,形成一条流的流水线;
3: 一个终端操作,执行流水线,并能生成结果。
流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。
筛选和切片
filter
distinct
limit
skip
映射
map
flatMap
如果map返回的是多个流,使用flatMap会把它们合并成一个流
查找和匹配
另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。StreamAPI通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
具有短路性。
具有短路性。
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
归约
Lambda反复结合每个元素,直到流被归约成一个值。
int sum=numbers.stream().reduce(0,(a,b)->a+b);
首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。
首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21。
reduce
数值流
Java8引入了三个原始类型特化流接口来解决暗含的装箱问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。
原始流转化为数值流
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。
数值流转化为原始流
要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法
数值范围
和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1和100之间的所有数字。Java8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
找出1到1000以内所有的勾股值
构建流
由集合创建
由值创建流
你可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。
Stream<String> stream=Stream.of("Java8","Lambdas","In","Action");
Stream<String> emptyStream=Stream.empty();
由数组创建流
你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
int[] numbers={2,3,5,7,11,13};
int sum=Arrays.stream(numbers).sum();←─总和是41
int sum=Arrays.stream(numbers).sum();←─总和是41
由文件生成流
Java中用于处理文件等I/O操作的NIOAPI(非阻塞I/O)已更新,以便利用StreamAPI。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
由函数生成流:创建无限流
StreamAPI提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
iterate
iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>类型)。
Stream.iterate(0,n->n+2).limit(10).forEach(System.out::println);
打印前20个斐波拉契数列:
Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]}).limit(20).map(t->t[0]).forEach(System.out::println);
Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]}).limit(20).map(t->t[0]).forEach(System.out::println);
单词意思:重复
generate
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。
Stream.generate(Math::random).limit(5).forEach(System.out::println);
使用limit限制大小,避免无限流
用流收集数据
用Collectors类创建和使用收集器(Collector)
collect()方法
它遍历流中的每个元素,并让Collector进行处理。
将数据流归约为一个值
汇总:归约的特殊情况
根据对象的某条属性求和:
int totalCalories=menu.stream().collect(reducing(0, //初始值
Dish::getCalories, //转换函数
Integer::sum));//累积函数
int totalCalories=menu.stream().collect(reducing(0, //初始值
Dish::getCalories, //转换函数
Integer::sum));//累积函数
数据分组和分区
分组
Collectors.groupingBy
Map<Dish.Type,List<Dish>> dishesByType=menu.stream().collect(groupingBy(Dish::getType));
我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。
分组操作的结果是一个Map,把分组函数返回的值作为映射的键,
把流中所有具有这个分类值的项目的列表作为对应的映射值。
分组操作的结果是一个Map,把分组函数返回的值作为映射的键,
把流中所有具有这个分类值的项目的列表作为对应的映射值。
多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准
Map<Dish.Type,Map<CaloricLevel,List<Dish>>>dishesByTypeCaloricLevel=menu.stream().collect(groupingBy(Dish::getType,groupingBy(dish->{if(dish.getCalories()<=400)returnCaloricLevel.DIET;elseif(dish.getCalories()<=700)returnCaloricLevel.NORMAL;elsereturnCaloricLevel.FAT;})));
这里的外层Map的键就是第一级分类函数生成的值:“fish,meat,other”,而这个Map的值又是一个Map,键是二级分类函数生成的值:“normal,diet,fat”。最后,第二级map的值是流中元素构成的List,是分别应用第一级和第二级分类函数所得到的对应第一级和第二级键的值:“salmon、pizza…”这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map。
按子组收集数据
传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy。例如,要数一数菜单中每类菜有多少个,可以传递counting收集器作为groupingBy收集器的第二个参数
Map<Dish.Type,Long> typesCount=menu.stream().collect(groupingBy(Dish::getType,counting()));
把收集器的结果转换为另一种类型
把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen工厂方法返回的收集器
分区
Collectors.partitioningBy
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组,false是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开
使用方法和分组类似
收集器Collector接口
Collector会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如toList),并将结果累积在一个数据结构中,从而产生这一过程的最终输出。
有五个
Supplier<A> supplier();
一个创建并返回新的可变结果容器的函数。
BiConsumer<A, T> accumulator();
将值折叠到可变结果容器中的函数。
BinaryOperator<A> combiner();
一个接受两个部分结果并合并它们的函数。 组合器函数可以将状态从一个参数折叠到另一个参数并返回该参数,或者可以返回新的结果容器。
Function<A, R> finisher();
在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,就像ToListCollector的情况一样,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数
Set<Characteristics> characteristics();
最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。
开发自己的自定义收集器
并行数据处理与性能
并行流
将顺序流转换为并行流
调用流的parallel()方法
LongStream.rangeClosed(1, n).reduce(0, Long::sum)
LongStream.rangeClosed(1, n).parallel().reduce(0, Long::sum)
实现原理
内部分块处理,然后将各块的结果合并
将并行流转换为顺序流
调用流的sequential()方法
如果一个流程里面调用了sequential和parallel方法,以最后调用的方法为准
配置并行流使用的线程池
看看流的parallel方法,你可能会想,并行流用的线程是从哪儿来的?有多少个?怎么自定义这个过程呢?并行流内部使用了默认的ForkJoinPool(7.2节会进一步讲到分支/合并框架),它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().availableProcessors()得到的。但是你可以通过系统属性java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小,如下所示:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
iterate生成的流本质上是顺序的,因为每次计算都会依赖上一次计算的结果
什么时候利用并行流来提高性能
并行流并不总比顺序流快,涉及到装箱拆箱操作
留意装箱
有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成无序流。那么,如果你需要流中的n个元素而不是专门要前n个的话,对无序并行流调用limit可能会比单个有序流(比如数据源是一个List)更高效。
还要考虑终端操作中合并步骤的代价是大是小(例如Collector中的combiner方法)。如果这一步代价很大,那么组合每个子流产生的部分结果所付出的代价就可能会超出通过并行流得到的性能提升。
要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂方法创建的原始类型流也可以快速分解。最后,你将在7.3节中学到,你可以自己实现Spliterator来完全掌控分解过程。
避免共享可变状态
常见数据源的可分解性,可分解性影响是否适合并行
ArrayList 极好
LinkedList 差
IntStream.range 极好
Stream.iterate 差
HashSet 好
TreeSet 好
分支/合并框架
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
Spliterator
Spliterator是Java8中加入的另一个新接口;这个名字代表“可分迭代器”(splitableiterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。
内部方法
boolean tryAdvance(Consumer<? super T> action);
tryAdvance方法的行为类似于普通的Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true,否则返回false。
Spliterator<T> trySplit();
子主题
底层线程数量固定
默认方法
目的
它让类可以自动地继承接口的一个默认实现
api的变化不会影响现有实现类
冲突解决规则
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断
(1)
(1)类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级
(2)
如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体
(3)
最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现
如果显示调用接口的方法
Xxx.supper.hello()
public void hello() {OSO.super.hello(); }
CompletableFuture:组合式异步编程
接口
函数式的思考
命令式编程
告诉程序怎么做
声明式编程
告诉程序要做什么
把最终如何实现的细节留给了函数库。我们把这种思想称之为内部迭代。它的巨大优势在于你的查询语句现在读起来就像是问题陈述,由于采用了这种方式,我们马上就能理解它的功能,比理解一系列的命令要简洁得多。
为什么要采用函数式编程
函数式编程具体实践了前面介绍的声明式编程(“你只需要使用不相互影响的表达式,描述想要做什么,由系统来选择如何实现”)和无副作用计算。
它们能让我们的程序更便于阅读,易于编写。
引用透明性
如果一个函数只要传递同样的参数值,总是返回同样的结果,那这个函数就是引用透明的。
lamda表达式
在哪里可以使用
在使用函数式接口的地方
方法引用
什么是方法引用
方法引用是lamda表达式的快捷写法
这种类型的lamda表达式 (Shapshot.User u)->u.isMan()
这种类型的lamda表达式 (Shapshot.User u)->u.isMan()
有几种
(1)指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
(2)指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。
(3)指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
(2)指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。
(3)指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
构造函数方法引用
Supplier<Apple> c1=Apple::new;←─构造函数引用指向默认的Apple()构造函数
Apple a1=c1.get();←─调用Supplier的get方法将产生一个新的Apple
Apple a1=c1.get();←─调用Supplier的get方法将产生一个新的Apple
Supplier<Apple> c1=()->new Apple();←─利用默认构造函数创建Apple的Lambda表达式
Apple a1=c1.get();←─调用Supplier的get方法将产生一个新的Apple
Apple a1=c1.get();←─调用Supplier的get方法将产生一个新的Apple
上面二者等价
函数式接口
可以有几个抽象方法
只定义一个抽象方法的接口
官方预定义好的函数式接口在哪里
java.util.function包下
复合Lambda表达式
谓词复合
谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。
请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a||b)&&c。
比较器复合
逆序
inventory.sort(comparing(Apple::getWeight).reversed());←─按重量递减排序
比较器链
inventory.sort(comparing(Apple::getWeight).reversed() ←─ 按重量递减排序
.thenComparing(Apple::getCountry)); ←─ 两个苹果一样重时,进一步按国家排序
.thenComparing(Apple::getCountry)); ←─ 两个苹果一样重时,进一步按国家排序
函数复合
你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。
重构/测试和调试
为改善可读性和灵活性重构代码
从匿名类到Lambda表达式的转换
是将实现单一抽象方法的匿名类转换为Lambda表达式
注意点
this supper
匿名类屏蔽类变量
重载
从Lambda表达式到方法引用的转换
Lambda表达式非常适用于需要传递代码片段的场景。不过,为了改善代码的可读性,也请尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。
注意点
抽取lamda表达式的内容到单独的方法里面,然后传递方法引用
尽量考虑使用静态辅助方法,比如comparing、maxBy
很多通用的归约操作,比如sum、maximum,都有内建的辅助方法可以和方法引用结合使用
从命令式的数据处理切换到Stream
将所有使用迭代器这种数据处理模式处理集合的代码都转换成StreamAPI的方式
StreamAPI能更清晰地表达数据处理管道的意图。除此之外,通过短路和延迟载入以及利用第7章介绍的现代计算机的多核架构,我们可以对Stream进行优化。
注意点
考虑控制流语句,比如break、continue、return
找工具转换
增加代码灵活性
采用函数接口
没有函数接口,你就无法使用Lambda表达式
有条件的延迟执行
如果你发现你需要频繁地从客户端代码去查询一个对象的状态(比如前文例子中的日志器的状态),只是为了传递参数、调用该对象的一个方法(比如输出一条日志),那么可以考虑实现一个新的方法,以Lambda或者方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法。
环绕执行
如果你发现虽然你的业务代码千差万别,但是它们拥有同样的准备和清理阶段,这时,你完全可以将这部分代码用Lambda实现。这种方式的好处是可以重用准备和清理阶段的逻辑,减少重复冗余的代码
使用Lambda重构面向对象的设计模式
对设计经验的归纳总结被称为设计模式
策略模式
用lamda表达式代替具体的实现类
模板方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案
接口参数添加函数式接口,方便直接传lamda表达式,而不用通过继承来实现
观察者模式
某些事件发生时(比如状态转变),如果一个对象(通常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案
如果逻辑很简单的观察者模式可以直接用lamda表达式
责任链模式?
责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推
工厂模式
使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建
return new XXX() 的地方换成 XXX::new
测试lamda表达式
判断返回结果和预期结果是否一致
调试
查看栈跟踪
使用lambda表达式和非相同类的方法引用导致栈跟踪不易理解
输出日志
流的peek方法
它不像forEach那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作
用optional取代null
创建optional
空的
Optional<String> optional=Optional.empty();
包含非空值的
Optional<String> optional=Optional.of("hello");
包含可为空值的
Optional<String> optional=Optional.ofNullable("hello");
使用map从Optional对象中提取和转换值
Optional<Integer> optionalInteger=optional.map(String::length);
flatMap
map操作的结果是一个Optional<Optional<Car>>,则flatMap得到Optional<Car>
以不解包的方式组合两个Optional对象
不要写在域模型里面,因为optional对象不支持序列化
optional取值
get()
有则返回,无则报异常
orElse(T other)
有则返回,无则返回给定值
orElseGet(Supplier<? extends T> other)
是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)
T orElseThrow(Supplier<? extends X> exceptionSupplier)
get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型
ifPresent(Consumer<? super T> consumer)
有则用传入的方法消费这个对象
optional filter
filter(Predicate<? super T> predicate)
filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件,filter方法就返回其值;否则它就返回一个空的Optional对象
避免使用optional对象的基础类
因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类最有用的方法
在需要进行空值判断的地方使用有奇效
新的日期和时间api
为什么在java8中引入新的时间和日期库
Date和Calendar都是可变的
Dateformat不是线程安全的
java8的时间和日期api
LocalDate
静态方法,of创建
LocalTime
Instant
Duration
Period
ZonedDateTime
DateTimeFormatter
TemporalAdjuster
你现在可以按照特定的格式需求,定义自己的格式器,打印输出或者解析日期-时间对象。这些格式器可以通过模板创建,也可以自己编程创建,并且它们都是线程安全的。
特点
你可以用绝对或者相对的方式操纵日期和时间,
操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
操作的结果总是返回一个新的实例,老的日期时间对象不会发生变化。
新的API提供了两种不同的时间表示方式,有效地区分了运行时人和机器的不同需求。
新版的日期和时间API中,日期-时间对象是不可变的。
Java8之前老版的java.util.Date类以及其他用于建模日期时间的类有很多不一致及设计上的缺陷,包括易变性以及糟糕的偏移值、默认值和命名。
函数式编程的技巧
高阶函数
能满足下面任一要求就可以被称为高阶函数
接受至少一个函数作为参数
返回的结果是一个函数
科里化
科里化是一种将具备2个参数(比如,x和y)的函数f转化为使用一个参数的函数g,并且这个函数的返回值也是一个函数,它会作为新函数的一个参数。后者的返回值和初始函数的返回值相同,即f(x,y)=(g(x))(y)
持久化数据结构
破坏式更新
函数式更新
没有改动任何现存的数据结构
Stream的延迟计算
Java8的Stream以其延迟性而著称。它们被刻意设计成这样,即延迟操作,有其独特的原因:Stream就像是一个黑盒,它接收请求生成结果。当你向一个Stream发起一系列的操作请求时,这些请求只是被一一保存起来。只有当你向Stream发起一个终端操作时,才会实际地进行计算。这种设计具有显著的优点,特别是你需要对Stream进行多个操作时(你有可能先要进行filter操作,紧接着做一个map,最后进行一次终端操作reduce);这种方式下Stream只需要遍历一次,不需要为每个操作遍历一次所有的元素。
0 条评论
下一页