文档章节

死磕Java 8特性系列---流的深入

心中的理想乡
 心中的理想乡
发布于 2018/09/19 16:35
字数 4169
阅读 81
收藏 26

本次,读了两本书,一本是《Beginning Java 8 Language Features》,一本是《Java 8 实战》,有感。感觉平时我们都是使用了个Java8相关特性的皮毛。加上以前面试被人问:你知道一个列表我想同时根据不同字段,一次进行分组,怎么做。我觉得有必要开一个深入使用Java8的系列文章,来总结总结。本次主要是对流的一次性深入。其中涉及了我们很多没有使用过的点。针对常用的,我在此就不在总结

一、从基本的使用去理解流

这里我先举个我们日常使用流的一个例子,然后根据这个例子进行几幅图的解说,能够完全把流内部的原理理解清楚,不仅仅局限于表现上面的使用

1、基本使用

请看代码:

import java.util.ArrayList;
import java.util.List;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
public class StramMain {
    private class Dash{
        private int calories;
        private String name;
        public int getCalories() {
            return calories;
        }
        public void setCalories(int calories) {
            this.calories = calories;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    public static void main(String[] args) {
        List<Dash> menus = new ArrayList<>();
        //List<String> lowColoricDishesName = menus.parallelStream() 并行流
        List<String> lowColoricDishesName = menus.stream()
                .filter(d -> d.getCalories() < 400)//过滤低于400卡路里
                .sorted(comparing(Dash::getCalories))//根据过滤之后进行排序
                .map(Dash::getName)//对象映射成String类型
                .collect(toList());//结束操作,转成list输出
        System.out.println(lowColoricDishesName.toString());

    }
}

2、理解流

对于上面代码中使用的流的过程,大致总结成下面的流程:

流执行过程1

类似于一个流水线,每经历一个节点,都会对原有的集合数据进行一个”加工“的过程,最后加工完了再集中输出成成品。这里有几个感念要理解下:

  • filter、sorted、map这些方法(操作),叫做中间操作
  • collect这中方法(操作),叫做终端操作
  • 重要的一点:除非流程线上触发一个终端操作,否则中间操作不会执行任何处理。所以说每个元素都只会被遍历一次!

整个流的过程就像一个流水线,collect就像是这个流水线的开关:我们首先要把这个流水线要做的工序,都安排好,然后最后,我们一开开关(collect),集合中的每一个数据,挨个的一个接一个从流水线上面流过,经过一个中间操作的节点,就会进行一个加工,最后流入一个新的集合里面。这就是整个过程。下面是一个更细化的图:

流执行过程2

这样做的优点是:

  • 可以进行短路:(如上图)在一个一个Dash经过流水线的过程中,到了limit(3)这里,发现,现在元素已经够了三个了,就不会进行下面元素的遍历了。这一点算是一种优化。这一点还可以运用到后面的anyMatch等终端操作中
  • 只遍历一次:上面做过介绍,看似很长很长的流写法,其实对元素只内部遍历一次,甚至有时候不遍历,这个很牛逼
  • 能够做内部优化:因为内部迭代,所以看似先后书写的流水线操作代码,其实不是按照书写顺序进行摆放的,内部会是有最优的顺序进行处理
  • 能够并行去做迭代:这也是流的一大优势,如果使用并行流,内部迭代会自动分配不同任务到不同cpu上面,这种是我们自己写迭代器非常困难的

二、一些“风骚”的中间操作

除开我们常用的一些中间操作:

  • filter
  • map
  • limit

找一些平时想不太到的中间操作讲讲

1、flatMap:“拍扁”操作

传统操作将一个字符串数组中的所有字符以一个List输出,不能有重复,例如:

String[] words = {"jicheng","gufali"}

变成:List<String> = {"j","i","c","h","e","n","g","u","f","a","l"}

《Java 8 实战》里面尝试了两种方式,我觉得,很有助于我们思考这个拍扁操作的原理

第一种尝试

public class StramMain {
    public static void main(String[] args) {
        String[] words = {"jicheng", "gufali"};
        List<String[]> list = Arrays.stream(words)
            .map(value -> value.split(""))
            .distinct()
            .collect(toList());
        System.out.println(list);
    }
}

结果如下图:

拍扁 操作的结果图1

其实map中间操作里面把源变成了两个Stream<String[]>这种类型,最后输出成list的时候,就成了List<String[]>,显然和我们想要的十万八千里

第二种尝试

代码如下:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

public class StramMain {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("jicheng", "gufali");
        List<Stream<String>> collect = words.stream()
                .map(value -> value.split(""))
                .map(Arrays::stream)
                .collect(toList());
        System.out.println(collect);
    }
}
  • 第一个map:将原始的流转成了Stream<String[]>类型
  • 第二个map:分别将原String数组合并成了两个Stream<String>这样一个Stream流
  • 最后:输出的就是List<Stream<String>>类型

显然,也不是我们要的

最终形态

代码如下,利用了上面的合并数组为一个流的操作public static <T> Stream<T> stream(T[] array)

import static java.util.stream.Collectors.toList;

public class StramMain {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("jicheng", "gufali");
        List<String> collect = words.stream()
                .map(value -> value.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(toList());
        System.out.println(collect);
        // result:[j, i, c, h, e, n, g, u, f, a, l]
    }
}

下面是过程的流程图:

流中间操作flapMap

2、findFirst/findAny:查找

使用代码:

Optional<Dish> dish =menu.stream()
    .filter(Dish::isVegetarian)
    .findAny();
boolean isPresent = dish.isPresent();

查到一顿流操作之后的其中一个或者任意一个。几点值得注意:

  • 返回的是一个Optional,接下来可以做几个处理:
    • 直接使用isPresent方法,写一个if逻辑判断
    • 或者直接在流的后面接ifPresent(Consumer<T> block),如果值存在的话,会执行block
  • findAny和findFirst的区别在于,findFirst返回集合中的第一个,findAny返回任意一个,对于使用并行流的时候,findFirst非常不好优化,有可能还是使用findAny

3、reduce:归约

这东西,类似于把集合里面的所有元素进行一个大汇总(求和、最大最小值、平均值等),下面是源码中的reduce方法:

Optional<T> reduce(BinaryOperator<T> accumulator);//①
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);//②
T reduce(T identity, BinaryOperator<T> accumulator);//③

a、详细解说一个的过程

代码如下:

public class StramMain {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
        System.out.println(sum);
        // result:55
    }
}

解说:首先,0作为Lambda(a)的 第一个参数,从流中获得1作为第二个参数(b)。0 + 1得到1,它成了新的累积值。然后再用累 积值和流中下一个元素2调用Lambda,产生新的累积值3。接下来,再用累积值和下一个元素3 调用Lambda,得到6。以此类推,得到最终结果21。

b、没有初始值的版本

public class StramMain {
    public static void main(String[] args) {
		List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Optional<Integer> reduce = numbers.stream().reduce((a, b) -> a + b);
        System.out.println(reduce.get());
        // result:55
    }
}

结果是一样的,表示:如果没有初始值,流操作会取第一个数组的值,作为初始值,由于不确定列表是不是有值的,如果没值,第一个数值就去不到,那求和就不成功,就没有值。所以返回一个Optional的对象

c、最大最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

同样的,这个同样也可以有个初试的值

public class StramMain {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer max = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);
        Integer min = numbers.stream().reduce(Integer.MAX_VALUE, Integer::min);
        System.out.println("max number:"+max+",min number:"+min);
        // result:max number:10,min number:1
    }
}

4、IntStream/LongStream:数值流

Java 8引入了三个原始类型: IntStream 、 DoubleStream 和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每 个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。 此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

a、映射到数值流

public class StramMain {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1,2,3,4,5);
        int sum = numbers.stream()
            .mapToInt(value -> value)
            .sum();
        System.out.println(sum);
        // result:15
    }
}

b、转换回去

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

c、最大最小值

public class StramMain {
    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1,2,3,4,5);
        OptionalInt max = numbers.stream()
                .mapToInt(value -> value)
                .max();
        OptionalInt min = numbers.stream()
                .mapToInt(value -> value)
                .min();
        int maxValue = max.orElse(Integer.MAX_VALUE);//如果没有最大值默认给一个最大值
        int minValue = min.orElse(Integer.MIN_VALUE);//如果没有最小值默认给一个最小值
    }
}

d、生成范围值

Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围: range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但 range是不包含结束值的,而rangeClosed则包含结束值:

IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0);//偶数流
System.out.println(evenNumbers.count());

c、一个风骚的操作:求勾股数

public class StramMain {
    public static void main(String[] args) {

        Stream<double[]> pythagoreanTriples = IntStream.rangeClosed(1, 100)
                .boxed()
                .flatMap(a -> IntStream.rangeClosed(a, 100)
                        .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)})
                        .filter(t -> t[2] % 1 == 0));
        pythagoreanTriples.limit(3).forEach(value->{
            System.out.println(value[0]+","+value[1]+","+value[2]);
        });
        /**
         * 结果:
         * 3.0,4.0,5.0
         * 5.0,12.0,13.0
         * 6.0,8.0,10.0
         */
    }
}

5、Stream.iterate/Stream.generate:无限流

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。

public class StramMain {
    public static void main(String[] args) {

        Stream.iterate(0, n -> n + 2)
                .limit(10)//注释掉这一行,就会无限循环的生成下去
                .forEach(System.out::println);
    }
}

解释:流的第一个元素是初始值0。然后加 上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的, 因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是 按需计算的,可以永远计算下去。

下面的是generate的无限流:

public class StramMain {
    public static void main(String[] args) {

        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
    }
}

三、其实你不知道的终端操作

细细读了《Java8 实战》,发现其实终端操作才是真正的大杀器!哪怕是一些中间操作的功能,再终端操作也是可以完成的。包括里面的很多设计理念,更是错中复杂。我这回集中讲讲下面的几个点:

  • 在终端操作也能完成的操作:汇总与规约
  • 分组,分组在分组,分组再分组再分组。。。。。
  • List<Object>变换成Map<Object,Object>,常用操作

1、在终端操作也能完成的操作:汇总与规约

其实在中间操作中,一样可以完成此操作

a、基本示例

下面是一系列归约汇总的代码示例片段,其实不难:

import com.alibaba.fastjson.JSON;

import java.util.Arrays;
import java.util.Comparator;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.function.Function;

import static java.util.stream.Collectors.*;

public class StramMain {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        //计算一共有多少个值
        Long collect = intList.stream().collect(counting());
        //同上
        Long sameWithCollect = intList.stream().count();
        System.out.println("一共有多少个数字:" + collect);
        //查找最大值
        intList.stream()
                .collect(maxBy(Comparator.comparing(Function.identity())))
                .ifPresent(integer -> {
                    System.out.println("数字中的最大值:" + integer);
                });

        Integer integer = intList.stream()
                .collect(summingInt(value -> value.intValue()));
        System.out.println("所有数字的和是:"+integer);
        Double averageNumber = intList.stream()
                .collect(averagingInt(value -> value.intValue()));
        System.out.println("平均数是:"+averageNumber);
        IntSummaryStatistics intSummaryStatistics = intList.stream()
                .collect(summarizingInt(value -> value.intValue()));
        System.out.println("所有的归约汇总的结果对象是:"
                + JSON.toJSONString(intSummaryStatistics));
        /**
         * result:
         * 一共有多少个数字:4
         * 数字中的最大值:54
         * 所有数字的和是:123
         * 平均数是:30.75
         * 所有的归约汇总的结果对象是:{"average":30.75,"count":4,"max":54,"min":12,"sum":123}
         */

    }
}

b、连接字符串

joining()工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符 串连接成一个字符串。另外,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。

import static java.util.stream.Collectors.*;
public class StramMain {
    public static void main(String[] args) {
		List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        String stringJoin = intList.stream()
                .map(value -> value.toString())
                .collect(joining(","));
        System.out.println(stringJoin);
        // result: 12,23,34,54
    }
}

c、广义的归约汇总

上面两小节的归约操作,其实都是基于一个底层的操作进行的,这个底层的归约操作就是:reducing(),可以说上面所有的归约操作都是当前reducing操作的特殊化,仅仅是方便程序员罢了。当然,方便程序员可是头等大事儿。说白了,特殊化的归约,是便于阅读与书写的一种模板。

import java.util.Arrays;
import java.util.List;
import static java.util.stream.Collectors.reducing;
public class StramMain {
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(12, 23, 34, 54);
        Integer sumNumber = intList.stream()
            	//注意这里的reducing方法
                .collect(reducing(0, value -> value.intValue(), Integer::sum));
        System.out.println("求和:"+sumNumber);
        // result: 123
    }
}

三个参数的意义:

  • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
  • 第二个参数是Function函数式接口,用于定位我们要返回的具体值类型
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值,就是归约过程执行函数

2、分组,分组在分组,分组再分组再分组。。。。。

这个话题,是我曾经的一次面试中经历过的问题:我们如何实现首先通过一个字段分组之后,在通过另外一个字段进行再次的分组呢?当时自己只经常操作一个字段分组的样子,并没有继续的研究如何通过一个以上字段进行连续分组。所以最终答得也不是很好。其实就是想用流这东西做到类似于数据库里面:group by col1,col2,这种操作。最终的结果数据结构,大体上是:Map<K,Map<T,List<O>>>。要实现很简单,我们从的源码中进行分析:

public final class Collectors {
    ...
        
    //①
	public static <T, K> Collector<T, ?, Map<K, List<T>>>
    	groupingBy(Function<? super T, ? extends K> classifier) {
		...
    }
    
    //②
    public static <T, K, A, D>
    	Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        ...
    }

    
    ...
}
  • 三个方法都返回同一个类型Collector
  • ①方法是我们最常使用,最终使用collect方法能够返回Map<K,List<O>>类型
  • 其中②方法就是实现多级分组的,可见第二个参数是一个Collector类型,我们可以再第二个参数地方调用①方法,如此递进下去
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream()
                .collect(groupingBy(Dish::getType, groupingBy(dish -> {
                    // 这里进行二次分组的实现函数
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT; 
                })));

3、toMap的操作

这个操作也是很常用的,并且经常被我们忽视的方法。如果一个List是我们从数据库里面查出来的对象,里面有id和其他的值,我们往往想快速通过id定位到一个具体的对象,那就需要将这个List装换成一个以id为key的map。以往我们竟然自己手写map,有了toMap操作,简直不能再简单了!我们来看看源码中的toMap的几种重载:

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}//①

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}//②

public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                             Function<? super T, ? extends U> valueMapper,
                             BinaryOperator<U> mergeFunction,
                             Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
        = (map, element) -> map.merge(keyMapper.apply(element),
                                      valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}//③

我们发现所有方法其实底层都死调用了③这个方法的。先解说下如何使用:

  • ①方法能够直接将List映射成一个Map,第一个参数是key,第二个参数是value,key值重复会抛出IllegalStateException异常
  • ②方法的第三个参数是避免如果出现了key值重复,如何选择的问题
  • ③方法的第四个参数是决定具体返回是一个什么类型的map
Map<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId,Funtion.identity()));

Map<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId
    						,Funtion.identity()
                            ,(existValue,newValue->existValue)));


TreeMap<Integer,Person> idToPerson = persons.stream()
    .collect(Collectors.toMap(Person::getId
    						,Funtion.identity()
                            ,(existValue,newValue->existValue)
                            ,TreeMap::new));

© 著作权归作者所有

心中的理想乡

心中的理想乡

粉丝 25
博文 83
码字总数 138010
作品 0
深圳
程序员
私信 提问
加载中

评论(1)

吕兵阳
吕兵阳
写的不错。
死磕 java同步系列之CountDownLatch源码解析

问题 (1)CountDownLatch是什么? (2)CountDownLatch具有哪些特性? (3)CountDownLatch通常运用在什么场景中? (4)CountDownLatch的初始次数是否可以调整? 简介 CountDownLatch,可以...

彤哥读源码
06/16
0
0
死磕 java同步系列之Semaphore源码解析

问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaphore如何实现限流? 简介 Semaph...

彤哥读源码
06/16
0
0
死磕 java同步系列之CyclicBarrier源码解析——有图有真相

问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier,回环栅栏,它会阻塞一组线程直到这些线程同时达到某个...

彤哥读源码
06/28
0
0
死磕 java同步系列之ReentrantLock VS synchronized——结果可能跟你想的不一样

问题 (1)ReentrantLock有哪些优点? (2)ReentrantLock有哪些缺点? (3)ReentrantLock是否可以完全替代synchronized? 简介 synchronized是Java原生提供的用于在多线程环境中保证同步的...

彤哥读源码
06/11
0
0
【死磕Java并发】—– 死磕 Java 并发精品合集

【死磕 Java 并发】系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览。 先来一个总览图: 【高清图,请关注“Java技术驿站”公众号,回复:脑...

chenssy
2018/07/22
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Bootstrap(三)文本排版

排版前的基础 必须是HTML5文档类型 <!DOCTYPE html><html> <head> <meta charset="utf-8"> </head> <body></body></html> 移动设备优先(viewport的设置) <meta name="viewport"......

ZeroBit
20分钟前
0
0
编写高质量代码:改善Java程序的151个建议(第3章:类、对象及方法___建议41~46)

建议41:让多继承成为现实 在Java中一个类可以多重实现,但不能多重继承,也就是说一个类能够同时实现多个接口,但不能同时继承多个类。 Java中提供的内部类可以曲折的解决此问题。 建议42:...

青衣霓裳
21分钟前
2
0
实例解说AngularJS在自动化测试中的应用

7月25日晚8点,线上直播,【AI中台——智能聊天机器人平台】,点击了解详情。 一、什么是AngularJS ? 1、AngularJS是一组用来开发web页面的框架、模板以及数据绑定和丰富UI的组件; 2、Angul...

宜信技术学院
25分钟前
2
0
网站安全防护加固discuz漏洞修复方案

近期我们SINE安全在对discuz x3.4进行全面的网站渗透测试的时候,发现discuz多国语言版存在远程代码执行漏洞,该漏洞可导致论坛被直接上传webshell,直接远程获取管理员权限,linux服务器可以...

网站安全
26分钟前
0
0
彻底弄懂UTF-8、Unicode、宽字符、locale

结论 宽字符类型wchar_t locale 为什么需要宽字符类型 多字节字符串和宽字符串相互转换 最近使用到了wchar_t类型,所以准备详细探究下,没想到水还挺深,网上的资料大多都是复制粘贴,只有个...

linux服务器架构
26分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部