Java8 Stream API

朴灿烈づ我的快乐病毒、 2022-05-31 11:59 257阅读 0赞
  • Java8 Stream API

    • Stream是啥
    • 创建流

      • 创建一个空的流
      • 通过集合创建
      • 通过数组创建
      • 直接创建
      • 通过builder创建
      • generate()和iterate()
      • 合并多个Stream
      • 从文件创建
    • 操作流

      • distinct()
      • filter()
      • map()与flatMap()
      • mapToT()与flatMapToT()
      • limit()
      • peek()
      • skip()
      • sorted()和sorted(Comparator)
    • 收集结果

      • allMatch()
      • anyMatch()
      • collect(collector)和collect(supplier, accumulator, combiner)
      • count()
      • findAny()和findFirst()
      • forEach()和forEachOrdered()
      • max()和min()
      • noneMatch()
      • reduce()
      • toArray()和toArray(generator)
    • IntegerStream、DoubleStream、LongStream
    • 一些使用中遇到的问题

      • Exception
      • Collectors.toMap()

Java8 Stream API

Stream是啥

  • 用一段时间Stream API之后,会发现“流”这个称呼非常的贴切,不抄书上的解释的话,流很像一个流水线:先把集合(暂时忽略IntStream等)拆成一个一个的放到一个流水线上,然后在流水线上有很多工人或机械臂(比如筛选、重新映射、去重等等),最后在流水线末端有一个收集装置(比如重新收集成集合、取出最大值、分组等等),将产品包装成我们想要的样子。
  • 所以一个Stream API的使用过程一般分为三个步骤:创建流->操作流->收集结果,这次学习笔记主要从这三个方面记录和完善。
  • 主线只针对集合的流式操作,IntStream LongStream DoubleStream单独学习并在一个独立的模块中记录。
  • 需要先了解lambda expression

创建流

创建一个空的流

正常情况下不会创建一个空的流,一般用来预防NPE.

  1. public Stream streamOf(Collection collection){
  2. if(collection != null && !collection.isEmpty()){
  3. return collection.stream();
  4. }
  5. return Stream.empty();
  6. }

通过集合创建

Collection接口有一个stream()方法并且有default实现,任何继承自Collection的类都能直接创建流。

  1. //Collection.class
  2. default Stream<E> stream() {
  3. return StreamSupport.stream(spliterator(), false);
  4. }
  5. List<String> list = new ArrayList<>();
  6. Stream stream = list.stream();

通过数组创建

Arrays.stream()有很多重载方法,可以按需使用。

  1. String[] array = new String[]{
  2. "A","B","C"};
  3. Stream stream = Arrays.stream(array);

直接创建

Stream.of()方法参数是可变长的。

  1. Stream stream = Stream.of("A","B","C");

通过builder创建

  1. Stream stream = Stream.builder()
  2. .add("A")
  3. .add("B")
  4. .add("C")
  5. .build();

generate()和iterate()

两个都是生成一个无限的流,通常跟limit()一起使用,限制流中元素的个数。不同的是前者可以根据任何计算方式来生成,后者只能根据给定的seed来生成,自我感觉这个两个方法在处理一些数学公式或时非常实用,下面的例子用generate()打印前10个斐波那契数列项。

  1. Stream stream = Stream.generate(new Supplier<Long>() {
  2. long a = 0,b = 1;
  3. @Override
  4. public Long get() {
  5. long tmp = a + b;
  6. a = b;
  7. b = tmp;
  8. return a;
  9. }
  10. });
  11. stream.limit(10).forEach(System.out::println);

接下来是用itrate()打印20-210。itrate()生成的元素与seed(第一个参数)密切相关,相当于是f(seed)f(f(seed))f(f(f(seed)))……

  1. Stream stream = Stream.iterate(1, n -> n * 2);
  2. stream.limit(11).forEach(System.out::println);

合并多个Stream

  1. Stream stream1 = Stream.builder()
  2. .add("A")
  3. .add("B")
  4. .add("C")
  5. .build();
  6. Stream stream2 = Stream.builder()
  7. .add("D")
  8. .add("E")
  9. .add("F")
  10. .build();
  11. Stream stream = Stream.concat(stream1,stream2);

从文件创建

  1. try(Stream<String> stream = Files.lines(Paths.get("C:\\Windows\\System32\\drivers\\etc\\hosts"), Charset.defaultCharset())){
  2. stream.forEach(System.out::println);
  3. }catch (IOException e){
  4. e.printStackTrace();
  5. }

操作流

操作流的结果依然是一个流,就像是从一个分区转移到另一个分区一样,不到收集结果阶段,所有元素都依然在流水线上生存,只是不同分区有不同的功能而已。

distinct()

去重,把流水线上相同的元素去掉,只保留不同的元素。

  1. Stream.of(1,2,1).distinct().forEach(System.out::println);//1,2

filter()

过滤,把满足指定条件的元素留在流水线上,其他的删掉。

  1. Stream.of(10,3,9,5).filter(n -> n > 5).forEach(System.out::println);//10,9

map()与flatMap()

map()是把流水线上的产品挨个挨个做相同的处理,比如给每个产品贴个标签,每个数字+1flatMap更像是拆箱,放到流水线上的产品是被箱子包装起来的,先要把箱子拆开把里面的产品放到流水线上再做后续处理。map()是直接对流水线上的产品做处理,即使有“箱子”也会被忽略,标签会直接贴在箱子上;flatMap()目的是对箱子里的产品做处理。因此map()的参数是具体操作,而flatMap()的参数是一个Stream,即Stream就是箱子。

  1. Stream.of(new ArrayList<>(Arrays.asList(1,2,3)),new ArrayList<>(Arrays.asList(10,20,30)))
  2. .map(item -> item.subList(0,1))
  3. .collect(Collectors.toList())
  4. .forEach(System.out::println);//[1] [10]
  5. Stream.of(new ArrayList<>(Arrays.asList(1,2,3)),new ArrayList<>(Arrays.asList(10,20,30)))
  6. .flatMap(item -> item.stream())
  7. .map(item -> item + 1)
  8. .collect(Collectors.toList())
  9. .forEach(System.out::println);//2 3 4 11 21 31

mapToT()与flatMapToT()

这两类方法包括flatMapToDouble() flatMapToInt() flatMapToLong()以及mapToDouble() mapToInt() mapToLong(),功能大体上和map()flatMap()相同,只不过针对的产品不同:对于Double型的产品可以放到DoubleStream流水线上处理,这个流水线上可能包含新的功能区,当然产品放到DoubleStream流水线前,必须保证产品是Double类型的(参数的返回值必须是Double类型)。DoubleStream会单独记录,现在只考虑如何放到流水线上,不考虑流水线的任何功能与操作。

  1. Stream.of(1,2,3).mapToDouble(Double::new).forEach(System.out::println);

limit()

限制产品线上的产品数量,不超过指定的数量。

  1. Stream.of(1,2,3).limit(1).forEach(System.out::println);//1
  2. Stream.of(1,2,3).limit(10).forEach(System.out::println);//1 2 3

peek()

给产品安装一个监听器,当产品下线被收集时,将触发所有的监听器,这个监听器可以拿到产品当时的状态,注意是当时的状态哦,不是最终的状态,然后就可以在这个监听器里为所欲为了。因为流水线上的产品只能被消费一次,因此监听器只会被触发一次,不可能多次被触发。

  1. Stream.of(1,2,3)
  2. .peek(item -> System.out.println("consumer1 [" + item + "]"))
  3. .map(item -> item + 1)
  4. .peek(item -> System.out.println("consumer2 [" + item + "]"));
  5. //没有消费(收集)过程,输出空
  6. //因此不消费是不会触发peek
  7. Stream.of(1,2,3)
  8. .peek(item -> System.out.println("consumer1 [" + item + "]"))
  9. .map(item -> item + 1)
  10. .peek(item -> System.out.println("consumer2 [" + item + "]"))
  11. .forEach(System.out::println);
  12. //最终输出
  13. /* consumer1 [1]//第一个peek()的时候还没+1 consumer2 [2]//第一个peek()的时候已经+1,因此是当时的状态 2 //是在正真消费之前触发的 consumer1 [2] consumer2 [3] 3 consumer1 [3] consumer2 [4] 4 */

可见每一次peek()都是存了快照的,Java API文档里都说了:可以利用这个特性来做调试。

skip()

limit()恰好相反,skip()是跳过流水线上前n个产品,保留剩下的产品。

  1. Stream.of(1,2,3).skip(2).forEach(System.out::println);//3
  2. Stream.of(1,2,3).skip(5).forEach(System.out::println);//空

sorted()和sorted(Comparator)

明显,前者是根据元素自然排序,后者是根据指定的策略排序。自定义的排序规则可以调用Comparator的静态方法,也可以自己写。Comparator的静态方法几乎都是按照自然排序排的,即使是自己写的比较器,也是“小的”放在前面,可以使用reverseOrder()reversed()反序。

  1. Stream.of("5","7","0","a","z","^").sorted().forEach(System.out::println);//0 5 7 ^ a z
  2. Stream.of("4444","22","333","1","55555").sorted(Comparator.comparingInt(String::length).reversed()).forEach(System.out::println);//按字符串长度倒序排
  3. Stream.of("4444","22","333","1","55555").sorted((a,b) -> b.length() - a.length()).forEach(System.out::println);//跟上面的效果一样

收集结果

allMatch()

返回流中的元素是否全部满足给定的条件,相当有用。

  1. List<Integer> list = Arrays.asList(1,2,3,4,5);
  2. System.out.println(list.stream().allMatch(s-> s > 0));//true
  3. System.out.println(list.stream().allMatch(s-> s > 1));//false

anyMatch()

返回流中的元素是否有任意一个满足给定的条件,也很有用的。

  1. List<Integer> list = Arrays.asList(1,2,3,4,5);
  2. System.out.println(list.stream().anyMatch(s-> s > 4));//true
  3. System.out.println(list.stream().anyMatch(s-> s > 10));//false

collect(collector)和collect(supplier, accumulator, combiner)

Collector来收集结果,包括转换成各种集合、总数、求和、求均值、分组、分区等等。

  1. System.out.println(Stream.of(4444,22,333,1,55555)
  2. .collect(Collectors.summarizingInt(item -> item)));
  3. //输出:IntSummaryStatistics{count=5, sum=60355, min=1, average=12071.000000, max=55555}
  4. System.out.println(Stream.of(4444, 22, 333, 1, 55555)
  5. .collect((Supplier<ArrayList>) ArrayList::new, ArrayList::add, ArrayList::addAll));
  6. //输出:[4444, 22, 333, 1, 55555]
  7. System.out.println(Stream.of(4444, 22, 333, 1, 55555)
  8. .collect(Collectors.toList()));//跟上面一样

count()

返回流中元素个数.

  1. System.out.println(Stream.of(4444, 22, 333, 1, 55555).count());//5

findAny()和findFirst()

这两个方法其实是一样的,findAny() java doc这样写的:

The behavior of this operation is explicitly nondeterministic; it is free to select any element in the stream. This is to allow for maximal performance in parallel operations; the cost is that multiple invocations on the same source may not return the same result. (If a stable result is desired, use findFirst() instead.)

看起来是说findAny()是返回任意一个元素,但是实际情况并不是这样:

  1. Stream.of(4444, 22, 333, 1, 55555).findFirst().ifPresent(System.out::println);//4444
  2. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444
  3. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444
  4. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444
  5. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444
  6. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444
  7. Stream.of(4444, 22, 333, 1, 55555).findAny().ifPresent(System.out::println);//4444

这里有一个解释:Java 8 Stream.findAny() vs finding a random element in the stream
结合java docstackoverflow上的第一个回答翻译过来就是:

findAny()实际上是findFirst()另一个更灵活的选择,在某些情况下(并行流操作)findAny()的开销更少,但是代价是同一个数据源多次调用findAny()可能结果不一样。简单的说就是:findFirst()一定是第一个元素,findAny()能取出某个元素,但不保证是第一个,也不能保证每次取到是同一个。

原来只是在并行流(parallel stream)的时候,两个方法才有区别的。

  1. List list = Arrays.asList(4444, 22, 333, 1, 55555);
  2. IntStream.iterate(0,i -> i + 1).limit(10).forEach(i -> list.parallelStream().findAny().ifPresent(System.out::println));
  3. //多运行几次可能会输出:
  4. /* 333 333 333 333 333 333 22 333 22 333 */
  5. //但是findFirst()无论如何都是第一个
  6. //并行遍历时查找第一个应该需要更多的代价,findAny()可以用更少的代价从流中去取一个元素,而且也没有明显的随机效果
  7. //在列表中所有元素等价并且是并行流的时候,用findAny()开销比findFirst()低,其他情况还是findFirst()吧,稳一些
  8. //注意:两个方法中的任意一个方法、在任何情况下都没有很好的随机效果

forEach()和forEachOrdered()

这两个方法就是遍历,前面用了好多次,可以每个元素调用一个方法,比如打印。forEachOrdered()forEach()的关系和findAny()findFirst()的关系相似,前者是在并行流的情况下依然按输入顺序遍历,当然单价是更大的开销。

  1. List list = Arrays.asList(1, 2, 3);
  2. IntStream.iterate(0,i -> i + 1).limit(3).forEach(i -> {
  3. synchronized (StreamTeat.class){
  4. list.parallelStream().forEach(System.out::print);
  5. System.out.println();
  6. }
  7. });
  8. //可能的输出
  9. /* 123 321 213 */
  10. //即并行遍历的时候输出顺序是不定的,如果用forEachOrdered()那么肯定是按照输入顺序遍历的

max()和min()

sorted()方法结合起来看,Comparator是必须的。

  1. Stream.of(4444, 22, 333, 1, 55555).max(Comparator.naturalOrder()).ifPresent(System.out::println);//55555
  2. Stream.of(4444, 22, 333, 1, 55555).min(Comparator.naturalOrder()).ifPresent(System.out::println);//1

noneMatch()

anyMatch()相反。

  1. System.out.println(Stream.of(4444, 22, 333, 1, 55555).noneMatch(item -> item < 0));//true
  2. System.out.println(Stream.of(4444, 22, 333, 1, 55555).noneMatch(item -> item > 3));//false

reduce()

规约,把流中的元素前两个执行一个方法,再把结果和第三个元素执行同样的方法,直至最后一个元素,最后得出结果:可以定义初始值,也可以定义返回类型和规约操作,比如可以用规约实现一个sum(),和collect()Collectors.reduce()很像的,有三个重载方法。

  1. System.out.println(Stream.of(1, 2, 3, 4).reduce(((sum,item) -> sum += item)));//10
  2. System.out.println(Stream.of(1, 2, 3, 4).reduce(656,((sum,item) -> sum += item)));//666
  3. System.out.println(Stream.of(1, 2, 3, 4).parallel().reduce(new StringBuilder(), StringBuilder::append, StringBuilder::append));//1234
  4. //第一个方法:把流中的元素前两个执行一个方法,再把结果和第三个元素执行同样的方法,直至最后一个元素,返回类型和元素类型一致
  5. //第二个方法:把初始值656和流中的第一个元素执行一个方法,再把结果和第二个元素执行同样的方法,直至最后一个元素,返回类型和元素类型一致
  6. //第三个方法:定义返回类型,定义规约操作,定义并行流结果合并方式

第三个方法可以参开这里:
java8中3个参数的reduce方法怎么理解?

意思就是并行的时候,流被分成多段,每段会产生一个同样类型的结果,比如有100个产品在流水线上,被分配给10个工人,最终要装在盒子里;10个工人每个人都会把自己的10个产品装在一个盒子里,最终这10个盒子要被合并在一个盒子里,那么盒子与盒子之间要定义合并规则,所以第三个参数在并行流的时候才会用到。

注意:并行流时第三个参数可能有重复元素,这里没有做太深入的了解,应该需要注意排重

toArray()和toArray(generator)

都能返回一个流中所有元素组成的array,后者可以有自定义数组元素类型。

  1. System.out.println(Arrays.toString(Stream.of(1, 2, 3, 4).toArray()));//[1, 2, 3, 4]
  2. System.out.println(Arrays.toString(Stream.of(1, 2, 3, 4).toArray(Integer[]::new)));//[1, 2, 3, 4]
  3. System.out.println(Arrays.toString(Stream.of(1, 2, 3, 4).toArray(size -> new Integer[size])));//上面的方法就是把数组的size传进来了
  4. //第一个方法是返回Object[],第二个方法是返回Integer[]

IntegerStream、DoubleStream、LongStream

全都是Stream的一些特殊实现。

  • 约束性更强,元素类型固定,一些特殊方法比如summaryStatistics()可以直接调用,而不用在Collect()里面才能调用。
  • 一些新的方法比如range()rangeClosed()方法来生成一个流,类似于fork i++fork i--
  • 可以用IntStream.mapToObj()转换成Stream;同样可以用Stream.mapToInt转换成Stream

一些使用中遇到的问题

Exception

因为lambda表达式和匿名内部类有些相似,可以看做一个闭包,Exception必须在内部catch住而不能throw出来让外层处理,所以在Stream中的lambda表达式调用一个声明throw Exception的方法时很不友好,直接编译错误。这样就不能让外层中断,外层甚至不能轻易地获取错误(可以用一个全局变量保存错误,但只有循环完毕才能拿到错误信息,可以再给这个全局变量加个监听,保证出错时能第一时间获取错误),这时就不必强行使用Stream了。
还有一种解决方案是,让方法抛出RuntimeException,编译肯定能通过,内层出错也能立即终止,但是如果方法无法更改,那也无能为力。

Collectors.toMap()

这个方法有点坑的,如果某个valuenull,会报错的。因为toMap()方法虽然有三个重载方法,但是都没有包含所有的参数,底层的java.util.stream.Collectors.CollectorImpl构造函数是有5个参数的,其中有个BinaryOperator<A> combiner参数,这个方法是用来解决key冲突的,默认会调用Map.merge()方法,但对用户不可见,无法直接传入,这个方法要求value不能为空,否者报NPE
这个其实也有解决方法的,因为merge()不是用来解决key冲突的嘛,自己写个类实现java.util.stream.Collectorcombiner开放出来就好了。

  1. System.out.println(Stream.of("1","2","3",null).collect(Collectors.toMap(k -> k,v -> v)));
  2. //Exception in thread "main" java.lang.NullPointerException
  3. System.out.println(Stream.of("1","2","3","3").collect(Collectors.toMap(k -> k,v -> v)));
  4. //Exception in thread "main" java.lang.IllegalStateException: Duplicate key 3
  5. //默认解决冲突的方法是直接抛出错误

自己写个toMap()方法解决这个问题。

  1. public class DbaasCollectors {
  2. static class ToMapCollector<T,K,V> implements Collector<T,Map<K,V>,Map<K,V>>{
  3. private Function<? super T, ? extends K> keyMapper;
  4. private Function<? super T, ? extends V> valueMapper;
  5. private BinaryOperator<Map<K, V>> combiner;
  6. public ToMapCollector(Function<? super T, ? extends K> keyMapper,
  7. Function<? super T, ? extends V> valueMapper,
  8. BinaryOperator<Map<K, V>> combiner) {
  9. super();
  10. this.keyMapper = keyMapper;
  11. this.valueMapper = valueMapper;
  12. this.combiner = combiner;
  13. }
  14. @Override
  15. public Supplier<Map<K, V>> supplier() {
  16. return HashMap::new;
  17. }
  18. @Override
  19. public BiConsumer<Map<K, V>, T> accumulator() {
  20. return (map, element) -> map.put(keyMapper.apply(element), valueMapper.apply(element));
  21. }
  22. @Override
  23. public BinaryOperator<Map<K, V>> combiner() {
  24. return combiner;
  25. }
  26. @Override
  27. public Function<Map<K, V>, Map<K, V>> finisher() {
  28. return (kvMap -> (Map<K, V>) kvMap);
  29. }
  30. @Override
  31. public Set<Characteristics> characteristics() {
  32. return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
  33. }
  34. }
  35. public static <T, K, V> Collector<T, ?, Map<K, V>> toMap(Function<T, K> keyMapper, Function<T, V> valueMapper, BinaryOperator<Map<K, V>> combiner) {
  36. return new ToMapCollector<>(keyMapper,valueMapper,combiner);
  37. }
  38. private static <K, V> Map<K, V> merge(Map<K, V> result1, Map<K, V> result2) {
  39. result2.forEach((key, value) -> {
  40. if (result1.containsKey(key)) {
  41. result1.put(key, (V) (String.valueOf(result1.get(key)) + String.valueOf(value)));
  42. } else {
  43. result1.put(key, value);
  44. }
  45. });
  46. return result1;
  47. }
  48. public static void main(String[] args){
  49. System.out.println(Stream.of("1","2","3","3",null).parallel().collect(toMap(k -> k, v -> v, DbaasCollectors::merge)));
  50. }
  51. }
  52. //输出:{null=null, 1=1, 2=2, 3=33}
  53. //既解决了value不能为空的问题,又可以自定义merge方法,还解决了key重复报错的问题

发表评论

表情:
评论列表 (有 0 条评论,257人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java 8 Stream API实战

    Java 8的Stream API提供了一种新的、更加简洁和高效的方式来处理集合数据。以下是一些使用Stream API实战的例子: 1. **过滤**:保留满足条件的元素。

    相关 Java 8-Stream API

    流处理 流是一系列数据项,一次只生产一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。 流,简

    相关 Java8Stream API

    Stream (java.util.stream.\)是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。