Java8 Stream API

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

Stream API与接口默认方法、静态方法

之前学习函数式接口时,曾经提到过Java8新出的接口静态方法、默认方法:

为什么Java8要引入静态方法和默认方法呢?

原因可能有多种,但其中一种肯定是为了“静默升级”(我自己造的一个词)。打开Collection接口:

我们发现有3个default方法,而且都是JDK1.8新增的:

这下明白了吧,list.stream().filter()...用的这么爽,其实都直接继承自顶级父接口Collection。

这和引入default有啥关系呢?

试想一下,要想操作Stream必须先获取Stream,获取流的方法理应放在Collection及其子接口、实现类中。但如果作为抽象方法抽取到Collection中,那么原先的整个继承链都会产生较大的震动:

JDK官方要从Collection接口沿着继承链向下都实现一遍stream()方法。这还不是最大的问题,最致命的是全球各地保不齐就有人直接实现了Collection,比如MyArrayList啥的,此时如果贸然往Collection增加一个抽象方法,那么当他们升级到JDK1.8后就会立即编译错误,强制他们自己实现stream()...

到这,大家应该恍然大悟了:

害...什么静默升级,不就是向前兼容嘛!

所以JDK的做法是,把获取Stream的一部分方法封装到StreamSupport类,另一部分封装到Stream类,StreamSupport用来补足原先的集合体系,比如Collection,然后引入default方法包装一下,内部调用StreamSupport完成偷天换日。而得到Stream后的一系列filter、map操作是针对Stream的,已经封装在Stream类中,和原来的集合无关。

所以,StreamSupport+default就是原先集合体系和Stream之间的“中介”:

调用者-->Collection-->StreamSupport+default-->Stream

外部调用者还以为集合体系新增了Stream API呢。

接口静态方法也有很多使用场景,大家会在后续学习中看到:

总之,引入接口默认方法和静态方法后,接口越来越像一个类。从某个角度来说,这种做法破坏了Java的单继承原则。Java原本的特点是“单继承、多实现”,假设接口A和接口B都有methodTest(),那么class Test implements interfaceA, interfaceB时,就不得不考虑使用哪个父接口的methodTest()方法。JDK的做法是,编译时强制子类覆盖父接口同名的方法。

Steam API

我们最常用的集合其实来自两个派系:Collection和Map,实际开发时使用率大概是这样的:

  • ArrayList:50%
  • HashMap:40%
  • 其他:10%

而用到Stream的地方,占比就更极端了:

  • List:90%
  • Set:5%
  • Map:5%

对于一般人来说,只要学好List的Stream用法即可。

认识几个重要的接口与类

Stream的方法众多,不要期望能一次性融会贯通,一定要先了解整个API框架。有几个很重要的接口和类:

  • Collection
  • Stream
  • StreamSupport
  • Collector
  • Collectors

Collection

之前介绍过了,为了不影响之前的实现,JDK引入了接口默认方法,并且在Collection中提供了一系列将集合转为Stream的方法:

要想使用Stream API,第一步就是获取Stream,而Collection提供了stream()和parallelStream()两个方法,后续Collection的子类比如ArrayList、HashSet等都可以直接使用顶级父接口定义好的默认方法将自身集合转为Stream。

Collection:定义stream()/parallelStream()
    |--List
        |--ArrayList:list.stream().filter().map().collect(...)
        |--LinkedList:list.stream().filter().map().collect(...)
        |--Vector
    |--Set
        |--HashSet
        |--TreeSet

Stream

Java的集合在设计之初就只是一种容器,用来存储元素,内部并没有提供处理元素的方法。更多时候,我们其实是使用集合提供的遍历方法,然后手动在外部进行判断并处理元素。

Stream是什么呢?简单来说,可以理解为更高级的Iterator,把集合转为Stream后,我们就可以使用Stream对元素进行一系列操作。

来,感受一下,平时使用的filter()、map()、sorted()、collect()都来自哪:

方法太多了,记忆时需要带点技巧,比如分类记忆。

常用的有两种分类方法:

  • 终止/中间操作
  • 短路/非短路操作

前一种分类和我们关系大一些,所以我们按终止、中间操作分类记忆。

所谓中间操作和终止操作,可以粗略地理解为后面还能不能跟其他的方法。比如filter后面还可以跟map等操作,那么filter就是中间操作,而collect后返回的就是元素了,而不是流,无法继续使用。就好比一缕山泉,经过小草、小花、竹林,还是一缕水,但到了你的锅里煮成一碗粥,就没法继续使用了。

还有几个不是特别常用的操作就不放在思维导图里了,这里简要介绍一下。比如,除了Collection接口中定义的stream()和parallelStream(),Stream也定义了创建流的方法(不常用):

还有一个合并流的方法(知道即可):

StreamSupport

没啥好介绍的,一般不会直接使用StreamSupport,Collection接口借助它实现了stream()和parallelStream()。

collect()、Collector、Collectors

我们先不说这几个是什么、有什么关系,直接通过代码演示哪里会用到:

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("甲", 18, "杭州", 999.9));
        list.add(new Person("乙", 19, "温州", 777.7));
        list.add(new Person("丙", 21, "杭州", 888.8));
        list.add(new Person("丁", 17, "宁波", 888.8));

    }

    public static void main(String[] args) {
        List<Person> result = list.stream()
                .filter(person -> person.getAge() > 20)
                .collect(Collectors.toList());
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }

}

collect()是用来收集处理后的元素的,它有两个重载的方法:

我们暂时只看下面那个,它接收一个Collector对象,而我们一般不会自己去new Collector对象,因为JDK给我提供了Collectors,可以调用Collectors提供的方法返回Collector对象:

所以collect()、Collector、Collectors三者的关系是:

collect()通过传入不同的Collector对象来明确如何收集元素,比如收集成List还是Set还是拼接字符串?而通常我们不需要自己实现Collector接口,只需要通过Collectors获取。

这倒颇有点像Executor和Executors的关系,一个是线程池接口,一个是线程池工具类。

如何高效学习Stream API

和几个重要接口、类混个脸熟后,我们来谈谈如何高效学习Stream API。很多同学应该已经被上面的内容吓到了,不要怕,通过后面的实操,整个知识脉络很快就会清晰起来。但还是那句话,一开始不要扣细节,先抓主干:

初学者如果对stream的流式操作感到陌生,可以暂时理解为外部迭代(实际不是这样的,后面会大家一起观察):

特别注意reduce():

特别适合做累加、累乘啥的。

铺垫结束,接下来我们通过实战来正式学习Stream API。学习其他技术都可以追求理论深度,唯独Stream API就是一个字:干!

基础操作

map/filter

先来个最简单:

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // 我们先学中间操作

        // 1.先获取流(不用管其他乱七八糟的创建方式,记住这一个就能应付95%的场景)
        Stream<Person> stream = list.stream();
        // 2.过滤得到年纪大于18岁的(filter表示过滤【得到】符合传入条件的元素,而不是过滤【去除】)
        Stream<Person> filteredByAgeStream = stream.filter(person -> person.getAge() > 18);
        // 3.只要名字,不需要整个Person对象(为什么在这个案例中,filter只能用Lambda,map却可以用方法引用?)
        Stream<String> nameStream = filteredByAgeStream.map(Person::getName);
        // 4.现在返回值是Stream<String>,没法直接使用,帮我收集成List<String>
        List<String> nameList = nameStream.collect(Collectors.toList());
        
        // 现在还对collect()为什么传递Collectors.toList()感到懵逼吗?
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

再来一个:

public static void main(String[] args) {
    // 直接链式操作
    List<String> nameList = list.stream()
            .filter(person -> person.getAge() > 18)
            .map(Person::getName)
            .collect(Collectors.toList());
}

sorted

试着加入sorted()玩一下。

在此之前,我们先来见见一位老朋友:Comparator。这个接口其实早在JDK1.2就有了,但当时只有两个方法:

  • compare()
  • equals()

JDK1.8通过默认方法的形式引入了很多额外的方法,比如reversed()、Comparing()等。

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // JDK8之前:Collections工具类+匿名内部类。Collections类似于Arrays工具类,我经常用Arrays.asList()
        Collections.sort(list, new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getName().length()-p2.getName().length();
            }
        });
        
        // JDK8之前:List本身也实现了sort()
        list.sort(new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getName().length()-p2.getName().length();
            }
        });
        
        // JDK8之后:Lambda传参给Comparator接口,其实就是实现Comparator#compare()。注意,equals()是Object的,不妨碍
        list.sort((p1,p2)->p1.getName().length()-p2.getName().length());
        
        // JDK8之后:使用JDK1.8为Comparator接口新增的comparing()方法
        list.sort(Comparator.comparingInt(p -> p.getName().length()));
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

大家不好奇吗?sort()需要的是Comparator接口的实现,调用Comparator.comparing()怎么也可以?

好家伙,Comparator.comparing()返回的也是Comparator...

OK,铺垫够了,来玩一下Stream#sorted(),看看和List#sort()有啥区别。

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // 直接链式操作
        List<String> nameList = list.stream()
                .filter(person -> person.getAge() > 18)
                .map(Person::getName)
                .collect(Collectors.toList());
        System.out.println(nameList);

        // 我想按姓名长度排序
        List<String> sortedNameList = list.stream()
                .filter(person -> person.getAge() > 18)
                .map(Person::getName)
                .sorted()
                .collect(Collectors.toList());
        System.out.println(sortedNameList);

        // 你:我擦,说好的排序呢?

        // Stream:别扯淡,你告诉我排序规则了吗?(默认自然排序)

        // 明白了,那就按照长度倒序吧(注意细节啊,str2-str1才是倒序)
        List<String> realSortedNameList = list.stream()
                .filter(person -> person.getAge() > 18)
                .map(Person::getName)
                .sorted((str1, str2) -> str2.length() - str1.length())
                .collect(Collectors.toList());
        System.out.println(realSortedNameList);

        // 优化一下:我记得在之前那张很大的思维导图上看到过,sorted()有重载方法,是sorted(Comparator)
        // 上面Lambda其实就是调用sorted(Comparator),用Lambda给Comparator接口赋值
        // 但Comparator还供了一些方法,能返回Comparator实例
        List<String> optimizeNameList = list.stream()
                .filter(person -> person.getAge() > 18)
                .map(Person::getName)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
        System.out.println(optimizeNameList);

        // 又是一样的套路,Comparator.reverseOrder()返回的其实是一个Comparator!!

        // 但上面的有点投机取巧,来个正常点的,使用Comparator.comparing()
        List<String> result1 = list.stream()
                .filter(person -> person.getAge() > 18)
                .map(Person::getName)
                .sorted(Comparator.comparing(t -> t, (str1, str2) -> str2.length() - str1.length()))
                .collect(Collectors.toList());
        System.out.println(result1);

        // 我去,更麻烦了!!
        // 不急,我们先来了解上面案例中Comparator的两个参数
        // 第一个是Function映射,就是指定要排序的字段,由于经过上一步map操作,已经是name了,就不需要映射了,所以是t->t
        // 第二个是比较规则
        
        // 我们把map和sorted调换一下顺序,看起来就不那么别扭了
        List<String> result2 = list.stream()
                .filter(person -> person.getAge() > 18)
                .sorted(Comparator.comparing(Person::getName, String::compareTo).reversed())
                .map(Person::getName)
                .collect(Collectors.toList());
        System.out.println(result2);

        // 为什么Comparator.comparing().reversed()可以链式调用呢?
        // 上面说了哦,因为Comparator.comparing()返回的还是Comparator对象~
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

limit/skip

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        List<String> result = list.stream()
                .filter(person -> person.getAge() > 17)
                // peek()先不用管,它不会影响整个流程,就是打印看看filter操作后还剩什么元素
                .peek(person -> System.out.println(person.getName()))
                .skip(1)
                .limit(2)
                .map(Person::getName)
                .collect(Collectors.toList());
        System.out.println(result);
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

结果

==== 过滤后的元素有3个 ====

i

am

iron

==== skip(1)+limit(2)后的元素 ====

[am, iron]

所谓的skip(N)就是跳过前面N个元素,limit(N)就是只取N个元素。

collect

collect()是最重要、最难掌握、同时也是功能最丰富的方法。

最常用的4个方法:Collectors.toList()、Collectors.toSet()、Collectors.toMap()、Collectors.joining()

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // 最常用的4个方法

        // 把结果收集为List
        List<String> toList = list.stream().map(Person::getAddress).collect(Collectors.toList());
        System.out.println(toList);
        
        // 把结果收集为Set
        Set<String> toSet = list.stream().map(Person::getAddress).collect(Collectors.toSet());
        System.out.println(toSet);
        
        // 把结果收集为Map,前面的是key,后面的是value,如果你希望value是具体的某个字段,可以改为toMap(Person::getName, person -> person.getAge())
        Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, person -> person));
        System.out.println(nameToPersonMap);

        // 把结果收集起来,并用指定分隔符拼接
        String result = list.stream().map(Person::getAddress).collect(Collectors.joining("~"));
        System.out.println(result);
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

关于collect收集成Map的操作,有一个小坑需要注意:

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("iron", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, person -> person));
        System.out.println(nameToPersonMap);
    }

    @Getter
    @Setter
    @AllArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", address='" + address + '\'' +
                    ", salary=" + salary +
                    '}';
        }
    }
}

尝试运行上面的代码,会观察到如下异常:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key Person{name='iron', age=21, address='杭州', salary=888.8}

这是因为toMap()不允许key重复,我们必须指定key冲突时的解决策略(比如,保留已存在的key):

public static void main(String[] args) {
    Map<String, Person> nameToPersonMap = list.stream()
            .collect(Collectors.toMap(Person::getName, person -> person, (preKey, nextKey) -> preKey));
    System.out.println(nameToPersonMap);
}

如果你希望key覆盖,可以把(preKey, nextKey) -> preKey)换成(preKey, nextKey) -> nextKey)。

你可能会在同事的代码中发现另一种写法:

public static void main(String[] args) {
    Map<String, Person> nameToPersonMap = list.stream().collect(Collectors.toMap(Person::getName, Function.identity());
    System.out.println(nameToPersonMap);
}

Function.identity()其实就是v->v:

但它依然没有解决key冲突的问题,而且对于大部分人来说,相比person->person,Function.identity()的可读性不佳。

聚合:max/min/count

max/min

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // 匿名内部类的方式,实现Comparator,明确按什么规则比较(所谓最大,必然是在某种规则下的最值)
        Optional<Integer> maxAge = list.stream().map(Person::getAge).max(new Comparator<Integer>() {
            @Override
            public int compare(Integer age1, Integer age2) {
                return age1 - age2;
            }
        });
        System.out.println(maxAge.orElse(0));

        Optional<Integer> max = list.stream().map(Person::getAge).max(Integer::compareTo);
        System.out.println(max.orElse(0));
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

count

public static void main(String[] args) {
    long count = list.stream().filter(person -> person.getAge() > 18).count();
    System.out.println(count);
}

去重:distinct

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        long count = list.stream().map(Person::getAddress).distinct().count();
        System.out.println(count);
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

所谓“去重”,就要明确怎样才算“重复”。那么,distinct()是基于什么标准呢?

还是那两样:hashCode()和equals(),所以记得重写这两个方法(一般使用Lombok的话问题不大)。

distinct()提供的去重功能比较简单,就是判断对象重复。如果希望实现更细粒度的去重,比如根据对象的某个属性去重,可以怎么做呢?可以参考:分享几种 Java8 中通过 Stream 对列表进行去重的方法

一般来说,学到已经覆盖实际开发90%的场景了,后面的可以不用学了。

高阶操作

两部分内容:

  • 深化一下collect()方法,它还有很多其他玩法
  • 介绍flatMap、reduce、匹配查找、peek、forEach等边角料

collect高阶操作

聚合

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    /**
     * 演示用collect()方法实现聚合操作,对标max()、min()、count()
     * @param args
     */
    public static void main(String[] args) {
        // 方式1:匿名对象
        Optional<Person> max1 = list.stream().collect(Collectors.maxBy(new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getAge() - p2.getAge();
            }
        }));
        System.out.println(max1.orElse(null));

        // 方式2:Lambda
        Optional<Person> max2 = list.stream().collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
        System.out.println(max2.orElse(null));

        // 方式3:方法引用
        Optional<Person> max3 = list.stream().collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
        System.out.println(max3.orElse(null));

        // 方式4:IDEA建议直接使用 max(),不要用 collect(Collector)
        Optional<Person> max4 = list.stream().max(Comparator.comparingInt(Person::getAge));
        System.out.println(max4.orElse(null));
        
        // 特别是方式3和方式4,可以看做collect()聚合和max()聚合的对比
        
        // 剩下的minBy和counting

        Optional<Person> min1 = list.stream().collect(Collectors.minBy(Comparator.comparingInt(Person::getAge)));
        Optional<Person> min2 = list.stream().min(Comparator.comparingInt(Person::getAge));

        Long count1 = list.stream().collect(Collectors.counting());
        Long count2 = list.stream().count();
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

分组

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    /**
     * 按字段分组
     * 按条件分组
     *
     * @param args
     */
    public static void main(String[] args) {
        // GROUP BY address
        Map<String, List<Person>> groupingByAddress = list.stream().collect(Collectors.groupingBy(Person::getAddress));
        System.out.println(groupingByAddress);

        // GROUP BY address, age
        Map<String, Map<Integer, List<Person>>> doubleGroupingBy = list.stream()
                .collect(Collectors.groupingBy(Person::getAddress, Collectors.groupingBy(Person::getAge)));
        System.out.println(doubleGroupingBy);

        // 简单来说,就是collect(groupingBy(xx)) 扩展为 collect(groupingBy(xx, groupingBy(yy))),嵌套分组

        // 解决了按字段分组、按多个字段分组,我们再考虑一个问题:有时我们分组的条件不是某个字段,而是某个字段是否满足xx条件
        // 比如 年龄大于等于18的是成年人,小于18的是未成年人
        Map<Boolean, List<Person>> adultsAndTeenagers = list.stream().collect(Collectors.partitioningBy(person -> person.getAge() >= 18));
        System.out.println(adultsAndTeenagers);
    }
    
   @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

提一句,collect()方法是最为丰富的,可以搭配Collector玩出很多花样,特别是经过各种嵌套组合。本文末尾留了几道思考题,大家到时可以试着做做。

统计

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    /**
     * 统计
     * @param args
     */
    public static void main(String[] args) {
        // 平均年龄
        Double averageAge = list.stream().collect(Collectors.averagingInt(Person::getAge));
        System.out.println(averageAge);

        // 平均薪资
        Double averageSalary = list.stream().collect(Collectors.averagingDouble(Person::getSalary));
        System.out.println(averageSalary);
        
        // 其他的不演示了,大家自己看api提示。简而言之,就是返回某个字段在某个纬度的统计结果
        
        // 有个更绝的,针对某项数据,一次性返回多个纬度的统计结果:总和、平均数、最大值、最小值、总数,但一般用的很少
        IntSummaryStatistics allSummaryData = list.stream().collect(Collectors.summarizingInt(Person::getAge));
        long sum = allSummaryData.getSum();
        double average = allSummaryData.getAverage();
        int max = allSummaryData.getMax();
        int min = allSummaryData.getMin();
        long count = allSummaryData.getCount();
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

flatMap

总的来说,就是flatMap就是把多个流合并成一个流:

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9, new ArrayList<>(Arrays.asList("成年人", "学生", "男性"))));
        list.add(new Person("am", 19, "温州", 777.7, new ArrayList<>(Arrays.asList("成年人", "打工人", "宇宙最帅"))));
        list.add(new Person("iron", 21, "杭州", 888.8, new ArrayList<>(Arrays.asList("喜欢打篮球", "学生"))));
        list.add(new Person("man", 17, "宁波", 888.8, new ArrayList<>(Arrays.asList("未成年人", "家里有矿"))));
    }

    public static void main(String[] args) {
        Set<String> allTags = list.stream().flatMap(person -> person.getTags().stream()).collect(Collectors.toSet());
        System.out.println(allTags);
    }
    

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
        // 个人标签
        private List<String> tags;
    }
}

对于每个Person对象中的tags,如果没有flatMap,想要合并去重会比较麻烦。

对比map和flatMap:

flatMap传入的Function要求返回值是Stream<? extends R>而不是R,所以上面写的是:

Function返回的多个Stream<R>最终会被flatMap汇聚成同一个Stream<R>,而map的Function返回R,最终被收集成Stream<R>。

总之,当你遇到List中还有List,然后你又想把第二层的List都拎出来集中处理时,就可以考虑用flatMap(),先把层级打平,再统一处理。

forEach

这个,其实不算高阶操作,算了,就放这吧。简单来说就是遍历:

public class StreamTest {

    private static List<Person> list;

    static {
        list = new ArrayList<>();
        list.add(new Person("i", 18, "杭州", 999.9));
        list.add(new Person("am", 19, "温州", 777.7));
        list.add(new Person("iron", 21, "杭州", 888.8));
        list.add(new Person("man", 17, "宁波", 888.8));
    }

    public static void main(String[] args) {
        // 遍历操作,接收Consumer
        list.stream().forEach(System.out::println);
        // 简化,本质上不算同一个方法的简化
        list.forEach(System.out::println);
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class Person {
        private String name;
        private Integer age;
        private String address;
        private Double salary;
    }
}

peek()

它接受一个Consumer,一般有两种用法:

  • 设置值
  • 观察数据

设置值的用法:

public class StreamTest {
    public static void main(String[] args) {
        list.stream().peek(person -> person.setAge(18)).forEach(System.out::println);
    }
}

也就是把所有人的年龄设置为18岁。

peek这个单词本身就带有“观察”的意思。

简单来说,就是查看数据,一般实际开发很少用,但可以用来观察数据的流转:

public class StreamTest {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3);
        stream.peek(v-> System.out.print(v+",")).map(value -> value + 100).peek(v-> System.out.print(v+",")).forEach(System.out::println);
    }
}

结果

1,101,101

2,102,102

3,103,103

有没有感到好奇?

用图表示的话,就是这样:

通过peek,我们观察到每一个元素都是逐个通过Stream流的。

为了看得更清楚些,重新写个demo:

public static void main(String[] args) {
    Stream.of(1, 2, 3, 4)
            .peek(v -> System.out.print(v + ","))
            .filter(v -> v >= 2)
            .peek(v -> System.out.print(v + ","))
            .filter(v -> v >= 3)
            .forEach(System.out::println);
}

结果

1,2,2,3,3,3

4,4,4

这打印,怎么这么诡异?

其实不诡异,元素确实是逐个通过的:

一个元素如果中途被过滤,就不会继续往下,换下一个元素。最终串起来,就会打印:

1,2,2,3,3,3

4,4,4

大家思考一下,真正的Stream和我们之前山寨的Stream,遍历时有何不同?能画图解释一下吗?

元素一个个通过关卡

元素一起通过一个个关卡

匹配/查找

findFirst

public static void main(String[] args) {
    Optional<Integer> first = Stream.of(1, 2, 3, 4)
            .peek(v -> System.out.print(v + ","))
            .findFirst();
}

结果:

1,

只要第一个,后续元素不会继续遍历。

findAny

public static void main(String[] args) {
    Optional<Integer> any = Stream.of(1, 2, 3, 4)
            .peek(v -> System.out.print(v + ","))
            .findAny();
}

结果:

1,

findFirst和findAny几乎一样,但如果是并行流,结果可能不一致:

并行流颇有种“分而治之”的味道(底层forkjoin线程池),将流拆分并行处理,能较大限度利用计算资源,提高工作效率。但要注意,如果当前操作对顺序有要求,可能并不适合使用parallelStream。比如上图右边,使用并行流后返回的可能是4而不是1。

关于findFirst()和findAny()有没有人觉得这方法很傻逼?其实是我demo举的不对,通常来说应该是一堆filter操作任取一个等场景下使用。比如list.stream().filter(student->student.getAge()>18).findFirst(),即符合条件的任选一个。

allMatch

public static void main(String[] args) {
    boolean b = Stream.of(1, 2, 3, 4)
        .peek(v -> System.out.print(v + ","))
        .allMatch(v -> v > 2);
}

结果

1,

由于是要allMatch,第一个就不符合,那么其他元素也就没必要测试了。这是一个短路操作。

就好比:

if(0>1 && 2>1){
    // 2>1 不会被执行,因为0>1不成立,所以2>1被短路了
}

noneMatch

public static void main(String[] args) {
    boolean b = Stream.of(1, 2, 3, 4)
            .peek(v -> System.out.print(v + ","))
            .noneMatch(v -> v >= 2);
}

结果

1,2,

和allMatch一样,它期望的是没有一个满足,而2>=2已经false,后面元素即使都大于2也不影响最终结果:noneMatch=false,所以也是个短路操作。

anyMatch

reduce

略,实际开发没用过,大家可以在熟悉上面的用法后再去了解,否则可能比较乱。

Stream API的效率问题

public class Test {

    public static void main(String[] args) {

        // 1. 简单数据类型:整数
        testSimpleType();

        // 2. 复杂数据类型:对象
//        testObjectType();

    }

    private static void testSimpleType() {
        Random random = new Random();
        List<Integer> integerList = new ArrayList<>();

        for (int i = 0; i < 10000000; i++) {
            integerList.add(random.nextInt(Integer.MAX_VALUE));
        }

        // 1) stream
        testStream(integerList);
        // 2) parallelStream
        testParallelStream(integerList);
        // 3) 普通for
        testForLoop(integerList);
        // 4) 增强型for
        testStrongForLoop(integerList);
        // 5) 迭代器
        testIterator(integerList);
    }

    private static void testObjectType() {
        Random random = new Random();
        List<Product> productList = new ArrayList<>();

        for (int i = 0; i < 10000000; i++) {
            productList.add(new Product("pro" + i, i, random.nextInt(Integer.MAX_VALUE)));
        }

        // 1) stream
        testProductStream(productList);
        // 2) parallelStream
        testProductParallelStream(productList);
        // 3) 普通for
        testProductForLoop(productList);
        // 4) 增强型for
        testProductStrongForLoop(productList);
        // 5) 迭代器
        testProductIterator(productList);
    }

    // -------- 测试简单类型 --------
    public static void testStream(List<Integer> list) {
        long start = System.currentTimeMillis();

        Optional<Integer> optional = list.stream().max(Integer::compare);
        System.out.println("result=" + optional.orElse(0));

        long end = System.currentTimeMillis();
        System.out.println("testStream耗时:" + (end - start) + "ms");
    }

    public static void testParallelStream(List<Integer> list) {
        long start = System.currentTimeMillis();

        Optional<Integer> optional = list.parallelStream().max(Integer::compare);
        System.out.println("result=" + optional.orElse(0));

        long end = System.currentTimeMillis();
        System.out.println("testParallelStream耗时:" + (end - start) + "ms");
    }

    public static void testForLoop(List<Integer> list) {
        long start = System.currentTimeMillis();

        int max = Integer.MIN_VALUE;
        for (int i = 0; i < list.size(); i++) {
            int current = list.get(i);
            if (current > max) {
                max = current;
            }
        }
        System.out.println("result=" + max);

        long end = System.currentTimeMillis();
        System.out.println("testForLoop耗时:" + (end - start) + "ms");
    }

    public static void testStrongForLoop(List<Integer> list) {
        long start = System.currentTimeMillis();

        int max = Integer.MIN_VALUE;
        for (Integer integer : list) {
            if (integer > max) {
                max = integer;
            }
        }
        System.out.println("result=" + max);

        long end = System.currentTimeMillis();
        System.out.println("testStrongForLoop耗时:" + (end - start) + "ms");
    }

    public static void testIterator(List<Integer> list) {
        long start = System.currentTimeMillis();

        Iterator<Integer> it = list.iterator();
        int max = it.next();

        while (it.hasNext()) {
            int current = it.next();
            if (current > max) {
                max = current;
            }
        }
        System.out.println("result=" + max);

        long end = System.currentTimeMillis();
        System.out.println("testIterator耗时:" + (end - start) + "ms");
    }


    // -------- 测试对象类型 --------
    public static void testProductStream(List<Product> list) {
        long start = System.currentTimeMillis();

        Optional<Product> optional = list.stream().max((p1, p2) -> p1.hot - p2.hot);
        System.out.println(optional.orElseThrow(() -> new RuntimeException("对象不存在")));

        long end = System.currentTimeMillis();
        System.out.println("testProductStream耗时:" + (end - start) + "ms");
    }

    public static void testProductParallelStream(List<Product> list) {
        long start = System.currentTimeMillis();

        Optional<Product> optional = list.parallelStream().max((p1, p2) -> p1.hot - p2.hot);
        System.out.println(optional.orElseThrow(() -> new RuntimeException("对象不存在")));

        long end = System.currentTimeMillis();
        System.out.println("testProductParallelStream耗时:" + (end - start) + "ms");
    }

    public static void testProductForLoop(List<Product> list) {
        long start = System.currentTimeMillis();

        Product maxHot = list.get(0);
        for (int i = 0; i < list.size(); i++) {
            Product current = list.get(i);
            if (current.hot > maxHot.hot) {
                maxHot = current;
            }
        }
        System.out.println(maxHot);

        long end = System.currentTimeMillis();
        System.out.println("testProductForLoop耗时:" + (end - start) + "ms");
    }

    public static void testProductStrongForLoop(List<Product> list) {
        long start = System.currentTimeMillis();

        Product maxHot = list.get(0);
        for (Product product : list) {
            if (product.hot > maxHot.hot) {
                maxHot = product;
            }
        }
        System.out.println(maxHot);

        long end = System.currentTimeMillis();
        System.out.println("testProductStrongForLoop耗时:" + (end - start) + "ms");
    }

    public static void testProductIterator(List<Product> list) {
        long start = System.currentTimeMillis();

        Iterator<Product> it = list.iterator();
        Product maxHot = it.next();

        while (it.hasNext()) {
            Product current = it.next();
            if (current.hot > maxHot.hot) {
                maxHot = current;
            }
        }
        System.out.println(maxHot);

        long end = System.currentTimeMillis();
        System.out.println("testProductIterator耗时:" + (end - start) + "ms");
    }

}

@Data
@AllArgsConstructor
class Product {
    // 名称
    String name;
    // 库存
    Integer stock;
    // 热度
    Integer hot;
}

大家把测试案例拷贝到本地test包下运行看看,相信心中有判断。虽然上面的案例中没有测试list.stream.filter().map.dictinc()等连环操作,但结果应该差不多。

但我想说的是,绝大部分时候代码的可读性应该优先于性能,况且Stream API性能并不差。和传统代码相比,Stream API让程序员专注于实现的步骤而不是细节,大大提高了代码的可读性。

Integer minPrice = itemSkuPriceTOS.stream()
                .sorted(Comparator.comparingLong(ItemSkuPriceTO::getPrice)) // 先正序排列
                .findFirst()                                                // 找到第一个,也就是价格最低的item
                .map(ItemSkuPriceTO::getPrice)                              // 得到item的价格
                .orElse(0);                                                 // 兜底处理

如果你习惯了Stream API,上面的代码在可读性上会比用for循环实现好得多,所以能用Stream API的尽量用Stream API吧。毕竟上面测试用了1000w数据差距也就几十毫秒,更别说实际项目中可能就几十条了。

最后再来看思维导图复习一下吧(有些使用频率很低,本文就略过了):

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值