Java8——Stream流式编程、Optional容器的详细介绍与使用案例【两万字】

·  阅读 706

基于Java8详细介绍了Stream流的含义和大部分API方法的使用,以及Optional容器、并行流等Java8的新特性!

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

1 Stream的概述

public interface Stream< T > extends BaseStream<T,Stream< T >>

Java8开始提供了Stream API,这些API方法是用函数式编程方式在对一批数据集合进行复杂操作的工具,简称“流”。流的特点如下:

  1. Stream将不同的操作使用不同的方法来描述,比如filter、map、reduce、find、match、sort,它的数据处理类似于数据库的操作。我们只需要编写核心的代码逻辑,这样可以让我们在一定程度上摆脱循环遍历、if判断等控制语句的编写,相当于对集合的操作做了更高层次的抽象。
  2. 相比于集合的操作,很多流操作本身会返回一个流。对于filter过滤、sorted排序、map收集等等操作,它们都是一个个的动作,因此我们还可以将这些动作链接起来,来表达复杂的数据处理流水线,比如先过滤,然后排序,最后收集输出……
  3. 就像集合必须存放数据一样,流也需要一个提供数据的源,这个源可以是集合、数组、或者一个函数!如果数据源是有序的,那么生成的流也是有序的!
  4. 流操作可以单线程顺序执行,也可多线程并行执行。
  5. 获得流之后,流里面的数据就不能在增加或者删除了,所有的操作都是基于流中现有的数据的操作。和迭代器类似,一个流只能被使用(遍历)一次,如果我们还需要其他按要求,此时只能从数据源那么再一次获取一个新的流。
  6. 使用普通集合操作元素,需要我们写迭代的代码,这被称为外部迭代,比如for、while、foreach,而流则帮助我们进行了内部数据迭代,这就有了并行迭代优化速度的可能,而我们无需关心它是怎么做的!
  7. 流元素按需计算,比如要找出大于10 的第一个数字,那么并不需要和所有元素去做比较,只要找出第一个匹配的元素就够了。
  8. Streams有一个BaseStream.close()方法和实现AutoCloseable,但几乎所有的流实例实际上不需要在使用后关闭。 一般来说,只有来源为IO通道的流(如Files.lines(Path, Charset)返回的流 )才需要关闭。 大多数流都由集合,数组或生成函数支持,这些函数不需要特殊的资源管理。 (如果流确实需要关闭,则可以在try -with-resources语句中将其声明为资源。)
  9. Stream API通常是接收一个函数式接口为参数,因此对lambda的支持非常好,Stream API配合lambda表达式,我们可以写出非常漂亮且简练的链式编程代码!可以说,要想把流用得好,那么lambda表达式必须先掌握,关于lambda表达式可以看这篇文章:Java8—一万字的lambda表达式的详细介绍与应用案例

先看看一个常见的普通集合操作和流操作的代码对比,体验流式编程的连续性以及简单性:

public class StreamFrist {
    public static void main(String[] args) {
        //学生集合,学生有age-年龄 ,name-名字,score-分数,三个属性
        List<Student> students = new ArrayList<>();
        students.add(new Student(10, 55, "小花"));
        students.add(new Student(13, 100, "小华"));
        students.add(new Student(9, 85, "晓华"));
        students.add(new Student(8, 70, "肖华"));


        //我们需要筛选出成绩大于等于60的学生名字

        //使用普通集合操作
        ArrayList<String> nameList1 = new ArrayList<>();
        for (Student student : students) {
            if (student.getScore() >= 60) {
                nameList1.add(student.getName());
            }
        }

        //使用流,不同的操作都是链式的,非常适合人类的思维,而不需要考虑迭代、判断操作
        List<String> nameList2 = students.stream()
                //筛选出成绩大于等于60的学生
                .filter(student -> student.getScore() >= 60)
                //收集学生名字
                .map(Student::getName)
                //返回结果
                .collect(toList());
    }
}
复制代码

2 流的操作

Stream API定义了许多流的操作,它们大概可以分为两类:

  1. 中间操作
    1. 中间操作的一个显著特征是会返回另一个Stream流,比如filter、sorted、linit等等,这样的好处是我们可以进行链式编程,形成一个操作流水线!
    2. 中间操作的另一个隐藏特征是延迟执行,或者称为惰性求值!因为他只是描述了流水线要进行的操作,而并没有真正的执行这条流水线,它需要一个“触发操作”,这就是终端操作!
  2. 终端操作
    1. 终端操作的一个显著特征是不会返回另一个Stream流,比如count、collect,相当于从流水线中获取结果,只有存在终端操作,中间操作才会执行。
    2. 终端操作的另一个隐藏特征是它会终结这个流,此后这个流不能被重复使用,也被称为及早求值!

案例演示:

@Test
public void test() {
    //学生集合,学生有age-年龄 ,name-名字,score-分数,三个属性
    List<Student> students = new ArrayList<>();
    students.add(new Student(10, 55, "小花"));
    students.add(new Student(13, 100, "小华"));
    students.add(new Student(9, 85, "晓华"));
    students.add(new Student(8, 70, "肖华"));


    //没有终端操作的流,不会执行中间操作
    students.stream()
            //筛选出成绩大于等于60的学生
            .filter(student -> {
                System.out.println("中间操作" + student.getScore());
                return student.getScore() >= 60;
            })
            //收集学生名字
            .map(Student::getName);


    //有终端操作的流,才会执行
    //没有终端操作的流,不会执行中间操作
    students.stream()
            //筛选出成绩大于等于60的学生
            .filter(student -> {
                System.out.println("终端操作" + student.getScore());
                return student.getScore() >= 60;
            })
            //收集学生名字
            .map(Student::getName)
            //collect是一个终端操作
            .collect(toList());

}
复制代码

3 流的使用

要想使用流,需要做三件事:

  1. 数据来源,比如集合、数组、函数,用于生成流,这是必须的
  2. 一系列中间流水线操作,用于对数据进行筛选、过滤、整合等等,中间操作不是必须的;
  3. 一个终端操作,用于触发流水线中的中间操作的执行,消耗流并且获取结果;

我们可以发现流的使用和Java构建者模式类似,构建者模式使用一系列操作设置属性和配置,最后调用一个build方法时,对象才被真正创建。而流操作同样使用一系列中间操作,最后调用一个终端操作方法,该方法触发中间操作的执行并获取最终的结果!

Stream提供了非常多的API方法,这些方法可以根据不同的作用进行分类讲解!

3.1 获取流

有以下常见方式获取流:

  1. 从集合
    1. Java8开始Collection超级接口中,提供了一个stream()默认方法,这个方法可以从调用集合中获取全部集合元素的流,因此全部的Collection体系下面的集合都可以调用stream()方法获取流,流元素就是单个集合元素。
    2. 注意,Map集合中没有直接获取流的方法!
  2. 从数组
    1. 数组的Arrays.stream静态方法接收一个数组,返回一个具有数组全部元素的流,流元素就是单个数组元素。
  3. 从文件
    1. Files类中具有读取文件并且生成流的方法,最重要的就是lines方法,用于获取文件的所有行的流。
  4. 从函数
    1. Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。从函数生成的流被称为无限流,通常我们只需要截取一部分即可!
    2. static < T > Stream< T > iterate(T seed, UnaryOperator< T > f),iterate第一个参数是一个初始值,第二个参数是一个依次应用在每个产生的新值上的UnaryOperator,即一元操作器,这个 将上一次生成的值当作参数,生成下一个值。
    3. static < T > Stream< T > generate(Supplier< T > s),generate方法没有指定初始值,后面的值也不是依据前一个值计算出来的,generate接受一个Supplier,它的值就是通过这个生产者返回的!因此,如果我们需要获取随机数,使用generate就很方便!
  5. 指定元素
    1. static < T > Stream< T > of(T... values),返回其元素是指定值的顺序排序流。
    2. static < T > Stream< T > of(T t),返回包含单个元素的顺序 Stream 。

获取流的案例:

/**
 * @author lx
 */
public class CreateTest {

    /**
     * @author lx
     */
    class Filter {
        private int x;

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public Filter(int x) {
            this.x = x;
        }

        public Filter() {
        }

        @Override
        public String toString() {
            return "Filter{" +
                    "x=" + x +
                    '}';
        }
    }


    /**
     * 从集合获取流
     */
    @Test
    public void test() {
        List<Filter> filters = new ArrayList<>();
        filters.add(new Filter(0));
        filters.add(new Filter(3));
        filters.add(new Filter(9));
        filters.add(new Filter(8));
        //从集合
        Stream<Filter> stream = filters.stream();
        stream.forEach(System.out::println);
    }

    /**
     * 从数组获取流
     */
    @Test
    public void test1() {
        Filter[] filArr = new Filter[]{new Filter(1),
                new Filter(3),
                new Filter(9),
                new Filter(8)};

        //从数组
        Stream<Filter> stream = Arrays.stream(filArr);
        stream.forEach(System.out::println);
    }


    /**
     * 从文件获取流
     */
    @Test
    public void test2() {
        //读取文件的所有行的流
        try (Stream<String> lines = Files.lines(Paths.get("target/classes/lines.txt"))) {
            lines.forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * iterate获取10个偶数
     */
    @Test
    public void iterate() {
        Stream.iterate(0, n -> n + 2)
                //取前十个数据(后面会讲,这叫做“筛选”)
                .limit(10)
                .forEach(System.out::println);


    }

    /**
     * iterate获取下标
     * 采用Stream通过下标遍历集合
     */
    @Test
    public void iterate2() {
        ArrayList<Object> objects = new ArrayList<>();
        objects.add(1);
        objects.add(3);
        objects.add(2);
        objects.add(4);
        Stream.iterate(0, i -> i + 1)
                //截取前集合长度的数据
                .limit(objects.size())
                .forEach(i -> System.out.println(i + "-->" + objects.get(i)));

    }

    /**
     * generate获取10个随机数
     */
    @Test
    public void generate() {
        Stream.generate(Math::random)
                .limit(10)
                .forEach(System.out::println);
    }


    //复杂数据生成

    /**
     * iterate获取10个斐波那契数
     */
    @Test
    public void iterateFibonacci() {
        //斐波那契数列的规律:F(0)=0,F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
        //我们初始化数组new int[]{0, 1}
        //后续的数组的第1个元素是前一个数组的第2个元素,后续的数组的第2个元素是前一个数组的第1、2个元素的和
        //这样实际上生成的数组如下:
        //new int[]{0, 1}
        //new int[]{1, 1}
        //new int[]{1, 2}
        //new int[]{2, 3}
        //new int[]{3, 5}
        //new int[]{5, 8}
        //new int[]{8, 13}
        //new int[]{13, 21}
        //new int[]{21, 34}
        //取每个数组的第一个元素就是斐波那契数

        Stream.iterate(new int[]{0, 1},
                t -> new int[]{t[1], t[0] + t[1]})
                //生成10个数组
                .limit(10)
                //获取每个数组的第一个元素(后面会讲,这叫做“映射”)
                .map(t -> t[0])
                .forEach(System.out::println);
    }


    /**
     * Stream.of
     */
    @Test
    public void of() {
        Stream.of(1, 2, 3, "11").forEach(System.out::println);
    }
}
复制代码

3.2 筛选操作

Stream API提供了对流元素的筛选操作,既有普通的条件筛选filter,也有特殊的筛选,比如distinct去重、limit和skip截取!

筛选操作是一个中间操作!

Stream< T > filter(Predicate< ? super T > predicate)

filter是使用最广泛的筛选操作,它接受一个断言,返回由与此给定断言匹配的此流的元素组成的流。

Stream< T > distinct()

返回一个由该流的不同元素(根据元素的equals方法)组成的流。对于有序流,选择不同的元素是稳定的(对于重复的元素,首先在遇到顺序中出现的元素被保留。)对于无序流,不能保证稳定性。

Stream< T > limit(long maxSize)

返回由该流的前maxSize元素组成的流,limit会最多截取流元素的前maxSize个。

Stream< T > skip(long n)

在丢弃流的前n个元素后,返回由该流的前n元素之后的元素组成的流。 如果此流包含少于n元素,那么将返回一个空流。

使用案例:

/**
 * @author lx
 */
public class FilterTest {


    List<Student> students = new ArrayList<>();

    @Before
    public void test() {
        students.add(new Student(10, 55, "小花"));
        students.add(new Student(13, 100, "小华"));
        students.add(new Student(9, 85, "晓华"));
        students.add(new Student(8, 70, "肖华"));
        students.add(new Student(8, 70, "肖华"));
    }

    @Test
    public void filter() {
        System.out.println("filter筛选成绩大于等于70的学生");
        //filter筛选成绩大于等于70的学生
        students.stream().filter(student -> student.getScore() >= 70).forEach(System.out::println);

        System.out.println("filter+distinct筛选成绩大于等于70的学生,且去除重复数据");
        //filter+distinct筛选成绩大于等于70的学生,且去除重复数据
        students.stream().filter(student -> student.getScore() >= 70).distinct().forEach(System.out::println);

        System.out.println("limit最多截取前2个数据");
        //limit最多截取前2个数据
        students.stream().filter(student -> student.getScore() >= 70).limit(2).forEach(System.out::println);

        System.out.println("skip丢弃前2个数据");
        //skip丢弃前2个数据
        students.stream().filter(student -> student.getScore() >= 70).skip(2).forEach(System.out::println);

        System.out.println("skip丢弃前1个数据,limit最多截取前1个数据");
        students.stream().filter(student -> student.getScore() >= 70).skip(1).limit(1).forEach(System.out::println);
    }

    static class Student {
        private int age;
        private int score;
        private String name;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int age, int score, String name) {
            this.age = age;
            this.score = score;
            this.name = name;
        }

        public Student(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + age +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getAge() != student.getAge()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getAge();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }

}
复制代码

3.3 排序操作

集合支持排序,Stream API也提供了对流元素进行排序操作的方法!

排序操作是一个中间操作!

Stream< T > sorted()

返回由此流的元素组成的流,根据自然顺序排序如果流元素不是Comparable类型 ,则执行终端操作时抛出ClassCastException。

Stream< T > sorted(Comparator< ? super T > comparator)

返回由该流的元素组成的流,根据提供的Comparator进行排序。对于有序流,排序稳定。 对于无序的流,不能保证稳定性。

使用案例:

/**
 * @author lx
 */
public class SortedTest {
    /**
     * 自然排序
     */
    @Test
    public void sorted() {
        Stream.of(2, 0, 3, 7, 5).sorted().forEach(System.out::println);
    }


    /**
     * 指定比较规则
     */
    @Test
    public void sortedCom() {
        students.stream()
                //首先根据score比较排序,相等时再根据id比较排序
                .sorted(Comparator.comparingInt(Student::getScore).thenComparing(Student::getId))
                .forEach(System.out::println);
    }


    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(2, 100, "小花"));
        students.add(new Student(1, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
    }


    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.4 映射操作

Stream API支持对每一个流元素应用相同的函数,同时将函数计算结果生成一个新的流,类似于映射操作。这种操作在实际开发中非常的有用,比如流元素是一批对象,现在我们需要获取全部对象的id集合,那么我们就可以使用Stream提供的map和flatMap这两个映射方法来完成。

映射操作是一个中间操作!

3.4.1 map映射

< R > Stream< R > map(Function< ? super T,? extends R > mapper)

返回由给定函数应用于此流的全部元素的结果组成的流。返回的流的元素数据类型就是函数返回的结果的数据类型。

使用案例:

/**
 * @author lx
 */
public class MapTest {
    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
    }

    @Test
    public void test() {
        students.stream()
                //获取每一个学生对象的学生id集合
                .map(Student::getId)
                //这是一个终端消费操作,后面会讲
                .forEach(System.out::println);


        List<Integer> collect = students.stream()
                //获取每一个学生对象的学生id集合
                .map(Student::getId)
                //这是一个终端收集操作,后面会讲
                .collect(toList());
        System.out.println(collect);


        /*将小写字母转换为大写*/
        List<String> collected = Stream.of("a", "b", "C")
                .map(String::toUpperCase)
                .collect(toList());
        System.out.println(collected);
    }

    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.4.2 flatMap扁平化

< R > Stream< R > flatMap(Function< ? super T,? extends Stream<? extends R >> mapper)

返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。每个映射的流在其内容被放入此流之后是closed 。(如果映射的流是null则使用空的流)。

简单的说,flatMap函数的返回值必须是Stream< XXX >流类型,随后返回的结果流会被合并成一个流,那么最终的流元素类型就是XXX。而前面的map函数的返回值不一定是Stream< XXX >流类型,一般来说应该返回一个实体类型XXX,那么最终的流元素类型就是XXX;如果map返回Stream< XXX >流类型,那么最终的流元素类型就是Stream< XXX >。

即map返回的结果将使用一个流来收集,最后返回这个流,而flatMap一定是返回一个流,随后将所有返回的流和并成一个流返回,里面的元素自然也到一块儿去了,这里也就是“扁平化”的由来!

因此flatMap常被用在来将不同批次的数据拆分成单个元素随后合并的操作!

使用案例:

/**
 * @author lx
 */
public class FlatMapTest {

    List<String> words = new ArrayList<>();

    @Before
    public void before() {
        words.add("hello");
        words.add("word");
    }


    /**
     * 将words数组中的元素再按照字符拆分,然后字符去重,最终达到["h", "e", "l", "o", "w", "r", "d"]
     */
    @Test
    public void test2() {

        //如果用map,可以看到流就成了Stream<Stream<String>>类型,那么最后收集的元素就是Stream<String>类型
        List<Stream<String>> mapList = words.stream()
                .map(word -> Arrays.stream(word.split("")))
                .distinct()
                .collect(toList());
        System.out.println(mapList);


        //如果使用flatMap,那么就可以达到想要的结果
        List<String> flatMapList = words.stream()
                .flatMap(word -> Arrays.stream(word.split("")))
                .distinct()
                .collect(Collectors.toList());
        System.out.println(flatMapList);
    }

    /**
     * 原因
     */
    @Test
    public void test() {
        //可以看到Arrays.stream方法返回的就是Stream<String>类型
        Stream<String> stream = Arrays.stream("word".split(""));
        //如果使用map,那么返回的流就是一个Stream<Stream<String>>类型,流元素也是一个流……
        //因为map将Arrays.stream方法返回的Stream<String>作为流元素使用一个流进行收集,随后返回这个流
        Stream<Stream<String>> streamStream = words.stream().map(word -> Arrays.stream(word.split("")));
        //如果使用flatMap,那么返回的流就是一个Stream<String>类型,这才是正常的类型
        //因为flatMap将Arrays.stream方法返回的Stream<String>流进行了合并,随后返回合并之后的大流
        Stream<String> stringStream = words.stream().flatMap(word -> Arrays.stream(word.split("")));
    }
}
复制代码

3.5 查看操作

Stream< T > peek(Consumer< ? super T > action)

返回由该流的元素组成的流,另外在从生成的流中消耗元素时对每个元素执行提供的消费操作。该方法可以用来查看流水线中间某个点的元素,当前也可以修改一些元素的属性,但是要自己保证线程安全!

查看操作是一个中间操作!

使用案例:

/**
 * @author lx
 */
public class PeekTest {
    @Test
    public void test(){
        System.out.println(Stream.of(1, 2, 3, 4, 5)
                .peek(System.out::println)
                .map(i -> i + 1)
                .collect(Collectors.toList()));

        System.out.println(Stream.of("one", "two", "three", "four")
                .filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList()));
    }
}
复制代码

3.6 匹配操作

在集合、数组操作中,我们常常需要判断集合是否包含或者不包含具有某些规则的元素。Stream API同样提供了allMatch、anyMatch、noneMatch方法来完成规则匹配操作。

匹配操作是一个终端操作!

boolean allMatch(Predicate< ? super T > predicate)

如果流的所有元素都匹配提供的断言,那么返回true,如果流是空的或者有任何不匹配的元素就返回false。

boolean noneMatch(Predicate< ? super T > predicate)

如果流的所有元素都不匹配提供的断言或者流是空的,那么返回true,如果有任何至少一个匹配的元素就返回false。

boolean anyMatch(Predicate< ? super T > predicate)

如果流存在至少一个元素匹配提供的断言,那么返回true,如果或者流是空的或者任何元素都不匹配就返回false。

注意:流是否为空是最先判断的条件。空集合、空数组、空文件等等来源都将构建一个空流。

使用案例:

/**
 * @author lx
 */
public class MatchTest {

    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
    }


    /**
     * 空流测试
     */
    @Test
    public void testNull() {
        System.out.println(Stream.of().anyMatch(i -> true));
        System.out.println(Stream.of().anyMatch(i -> false));
        System.out.println(Stream.of().noneMatch(i -> true));
        System.out.println(Stream.of().noneMatch(i -> false));
        System.out.println(Stream.of().anyMatch(i -> true));
        System.out.println(Stream.of().anyMatch(i -> false));
    }

    /**
     * match测试
     */
    @Test
    public void testMatch() {
        //集合中是否有姓名为 小华 的学生
        System.out.println(students.stream().anyMatch(student -> "小华".equals(student.getName())));
        //集合中是否有姓名为 小华1 的学生
        System.out.println(students.stream().anyMatch(student -> "小华1".equals(student.getName())));

        //集合中是否所有的学生分数都大于55
        System.out.println(students.stream().allMatch(student -> student.getScore() > 55));
        //集合中是否所有的学生分数都大于等于55
        System.out.println(students.stream().allMatch(student -> student.getScore() >= 55));

        //集合中是否所有的学生分数都不小于55
        System.out.println(students.stream().noneMatch(student -> student.getScore() < 55));
        //集合中是否所有的学生分数都不小于等于55
        System.out.println(students.stream().noneMatch(student -> student.getScore() <= 55));
    }


    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.7 查找操作

Stream提供了findFirst查找第一个流元素和findAny查找任意一个流元素的两个方法,但是没有提供其他的查找操作!

查找操作是一个终端操作!

Optional< T > findFirst()

返回此流的第一个元素的Optional。如果流为空,则返回一个空的Optional,如果源数据没有顺序,则可能会返回任何元素。

Optional< T > findAny()

返回此流的任意一个元素的Optional。如果流为空,则返回一个空的Optional,如果源数据没有顺序,则可能会返回任何元素。这个方法主要是为了并行流的效率考虑的,如果是单线程下也是返回第一个流元素!

使用案例:

@Test
public void find() {
    Optional<String> first = Stream.of("xx", "ccc", "eee").findFirst();
    Optional<String> any = Stream.of("xx", "ccc", "eee").findAny();

    //通常我们这样使用
    //如果first存在
    if (first.isPresent()) {
        String s = first.get();
    }
    //如果any存在
    if (any.isPresent()) {
        String s = any.get();
    }
    
    //或者这样用
    first.ifPresent(System.out::println);
}
复制代码

find方法没啥好说的,但是我们注意它的返回的结果是一个Optional类型的对象,Optional是Java8新增的类,算作Java8的新特性,这个类实际上是希望帮助程序员避免空指针异常而添加的!

3.7.1 Optional容器对象

Optional< T >类(java.util.Optional)是一个容器类,代表一个值存在或不存在。

在上面的代码中,findAny可能什么元素都没找到,如果此时返回一个原始类型的null,那么粗心的程序员可能就忘记null校验而导致程序异常了,因此返回一个一定不为null的Optional对象,这个对象将真正的返回值包装起来,通过一系列方法获取值或者进行其他操作!

方法 说明
public boolean isPresent() 如果存在值,则返回 true ,否则为 false 。
public T get() 如果 Optional中存在值,则返回值,否则抛出 NoSuchElementException 。
public void ifPresent(Consumer consumer) 如果存在值,则使用该值作为参数调用指定的消费者,否则不执行任何操作。如果值存在且consumer为空,那么抛出NullPointerException。
public T orElse(T other) 返回值(如果存在),否则返回一个默认值。
public T orElseGet(Supplier other) 返回值(如果存在),否则调用 other并返回该调用的结果。
public T orElseThrow(Supplier exceptionSupplier) 返回包含的值(如果存在),否则抛出由Supplier参数返回的异常。如果没有值,则抛出指定异常;如果没有值且exceptionSupplier为空,则抛出NullPointerException。
public static < T > Optional< T > empty() 返回一个空的Optional实例。
public static < T > Optional< T > of(T value) 返回具有非空值的Optional。如果value为null,那么抛出NullPointerException。
public static < T > Optional< T > ofNullable(T value) 返回具有指定值的Optional。value可以为null。
public Optional< T > filter(Predicate predicate) 如果一个值存在,并且该值给定的断言相匹配时,返回一个 包含此值的Optional,否则返回一个空的 Optional 。如果断言为null,那么抛出NullPointerException。
public < U > Optional< U > map(Function mapper) 如果存在值,则应用提供的映射函数,如果结果不为空,则返回一个包含此结果的Optional。否则返回一个空的Optional 。如果函数为null,那么抛出NullPointerException。
public < U > Optional< U > flatMap(Function> mapper) 如果一个值存在,则应用提供的映射函数,返回的结果一定是一个Optional,这个Optional包含了真实结果,如果真实结果为null,则返回一个空的Optional。如果函数为null或者返回null结果,那么抛出NullPointerException。

使用案例:

@Test
public void optional() {
    System.out.println("filter");
    System.out.println(Optional.of(3).filter(integer -> integer > 2));
    System.out.println(Optional.of(3).filter(integer -> integer > 3));

    System.out.println("map");
    System.out.println(Optional.of(3).map(integer -> integer + 2));
    System.out.println(Optional.of(2).map(integer -> integer + 2));

    System.out.println("flatMap");
    System.out.println(Optional.of(3).flatMap(integer -> Optional.of(integer + 2)));
    System.out.println(Optional.of(2).flatMap(integer -> Optional.of(integer + 2)));
    //map对比,可以看到和Stream的flatMap与map方法差不多
    System.out.println(Optional.of(2).map(integer -> Optional.of(integer + 2)));
}
复制代码

3.8 归纳操作

在集合、数组操作中,我们有时候需要对集合的元素进行一个整体的统计,特别是求和、求差、求最值、计数等等计算操作,这些操作需要使用每一个元素,最后返回唯一的一个结果,这样的操作称为“归纳”,Stream API提供了reduce、max、min等方法来完成归纳操作!

归纳操作是一个终端操作!

Optional< T > reduce(BinaryOperator< T > accumulator)

没有初始值,返回一个Optional对象,以表明结果可能不存在。

accumulator:二元操作器,操作两个元素得到一个新元素。第一次计算时两个参数分别取前两个元素,计算的值将会作为下一次计算的第一个参数,后面的元素作为第二个参数,以此类推,最终会返回计算结果。

T reduce(T identity, BinaryOperator< T > accumulator)

  1. identity:一个初始值,最终会返回同类型的值。
  2. accumulator:二元操作器,操作两个元素得到一个新元素。串行模式下:第一次计算时第一个参数取初始值,第二个参数取第一个元素,计算的值将会作为下一次计算的第一个参数,后面的元素作为第二个参数,以此类推,最终会返回计算结果。并行模式下:对初始值和每一个元素应用一次accumulator函数(初始值为第一个参数,流元素为第二个参数),会得到对应个数的结果,最后通过accumulator再一次整合,即对第一轮accumulator获得的结果在应用一轮accumulator!

< U > U reduce(U identity,BiFunction< U,? super T,U > accumulator,BinaryOperator< U > combiner)

  1. identity:一个初始值,最终会返回同类型的值。
  2. accumulator: 一个二元函数,操作两个元素得到一个新元素。串行模式下:第一次计算时第一个参数取初始值,第二个参数取第一个元素,计算的值将会作为下一次计算的第一个参数,后面的元素作为第二个参数,以此类推,最终会返回计算结果。并行模式下:对初始值和每一个元素应用一次accumulator函数(初始值为第一个参数,流元素为第二个参数),会得到对应元素个数的结果,最后通过combiner整合!
  3. combiner:二元操作器,并行模式下第三个参数才会生效。在并行模式下初始值会和每一个流元素进行一次accumulator计算(初始值为第一个参数,流元素为第二个参数),随后会通过combiner对这些计算结果进行整合,最后返回整合后的结果!

使用案例(在“并行流”章节部分,有更加详细的测试):

/**
 * @author lx
 */
public class ReduceTest {


    /**
     * 求差
     * 验证二元操作器的参数关系
     */
    @Test
    public void subtract() {
        //Optional< T > reduce(BinaryOperator< T > accumulator);
        //求差  前一个计算的值减去后一个元素
        Optional<Integer> subtractReduce1 = Stream.of(1, 2, 3, 4, 7)
                .reduce((i, j) -> i - j);
        subtractReduce1.ifPresent(System.out::println);

        //Optional< T > reduce(BinaryOperator< T > accumulator);
        //求差  后一个元素减去前一个计算的值
        Optional<Integer> subtractReduce2 = Stream.of(1, 2, 3, 4, 7)
                .reduce((i, j) -> j - i);
        subtractReduce2.ifPresent(System.out::println);
    }


    @Test
    public void test() {
        //Optional< T > reduce(BinaryOperator< T > accumulator);
        //求和
        Optional<Integer> sumReduce = Stream.of(1, 2, 3, 4, 7)
                //使用方法引用Integer::sum来表示求和的意图
                .reduce(Integer::sum);
        sumReduce.ifPresent(System.out::println);


        //Optional< T > reduce(BinaryOperator< T > accumulator);
        //求最值
        Optional<Integer> maxReduce = Stream.of(1, 2, 3, 4, 7)
                //使用方法引用Integer::max来表示求最值的意图
                .reduce(Integer::max);
        maxReduce.ifPresent(System.out::println);


        //T reduce(T identity, BinaryOperator< T > accumulator);
        //求最值,初始值10
        System.out.println(Stream.of(1, 2, 3, 4, 7)
                //使用方法引用Integer::max来表示求最值的意图
                .reduce(10, Integer::max));

        //T reduce(T identity, BinaryOperator< T > accumulator);
        //计数
        System.out.println(Stream.of(1, 2, 3, 4, 7)
                .map(d -> 1)
                .reduce(0, Integer::sum));
        //更简单的方法,内部使用的数值流,内部实际上是特性化的流操作,后面会讲
        System.out.println(Stream.of(1, 2, 3, 4, 7).count());
    }


    /**
     * < U > U reduce(U identity,BiFunction<U,? super T,U> accumulator,BinaryOperator< U > combiner)
     * 复杂计算
     */
    @Test
    public void test2() {

        //串行模式下无效
        System.out.println(Stream.of(1, 2, 3, 4, 7).reduce(2, Integer::sum, Integer::sum));
        System.out.println(Stream.of(1, 2, 3, 4, 7).reduce(2, Integer::sum, Integer::max));

        //并行模式下有效
        //初始值为2,首先对每一个元素应用一个计算accumulator,得到结果:3,4,5,6,9,最后combiner整合这些值:得到27
        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(2, Integer::sum, Integer::sum));
        //初始值为2,首先对每一个元素应用一个计算accumulator,得到结果:3,4,5,6,9,最后combiner整合这些值:得到9
        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(2, Integer::sum, Integer::max));
        //初始值为2,首先对每一个元素应用一个计算accumulator,得到结果:1,2,3,4,7,最后combiner整合这些值:得到1
        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(0, Integer::sum, Integer::min));

        //初始值为2,首先对每一个元素应用一个计算accumulator,得到结果:-1,0,1,2,5,最后combiner整合这些值:得到7
        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(2, (i, j) -> j - i, Integer::sum));
        //初始值为2,首先对每一个元素应用一个计算accumulator,得到结果:1,0,-1,-2,-5,最后combiner整合这些值:得到-7
        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(2, (i, j) -> i - j, Integer::sum));


        System.out.println(Stream.of(1, 2, 3, 4, 7).parallel().reduce(2, (i, j) -> {
                    //查看线程,多线程
                    System.out.println(Thread.currentThread().getName());
                    return j - i;
                }
                , Integer::sum));

        System.out.println(Stream.of(1, 2, 3, 4, 7).reduce(2, (i, j) -> {
                    //查看线程,单线程
                    System.out.println(Thread.currentThread().getName());
                    return j - i;
                }
                , Integer::sum));
    }
}
复制代码

3.9 特性化操作

集合中只能存储对象,对于基本类型也会涉及到拆装箱操作。而在上面的归纳操作中,比如对于数值的求和、求差等操作,实际上也涉及到基本类型的自动拆箱和装箱的操作,因为Stream内部也是以对象类型存储数据的。另外对于一些简单的操作求和求最值操作Stream并没有提供现成的API,主要是因为Stream面向全部类型,如果是普通对象类型那肯定不能求和或者求最值的。

Java8有什么改进吗?当然有,Java8为我们提供了基于Stream的三个特性化流IntStream、LongStream、DoubleStream,它们只接受指定类型的数据,底层基于基本类型,直接操作基本类型,避免了拆装箱操作的性能开销,并且提供了sum、min等快捷归纳的方法!实际上Stream的count方法内部就是先将对象流转换为LongStream数值流,然后调用sum方法。

Stream流转换特性化流的方法如下,这些方法都是中间操作:

IntStream mapToInt(ToIntFunction<? super T> mapper)

返回一个IntStream ,其中包含将给定函数应用于此流的元素的结果。

LongStream mapToLong(ToLongFunction<? super T> mapper)

返回一个LongStream ,其中包含将给定函数应用于此流的元素的结果。

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

返回一个DoubleStream ,其中包含将给定函数应用于此流的元素的结果。

特性化流除了包含Strean的大部分方法之外,特有的一些常用方法如下,以IntStream为例子:

< U > Stream< U > mapToObj(IntFunction<? extends U> mapper)

返回一个对象值Stream ,其中包含将给定函数应用于此流的元素的结果。该方法将特性化流转换为对象流。

Stream< Integer > boxed()

对每个元素进行装箱操作,返回一个Stream< Integer >。该方法将特性化流转换为对象流。

OptionalDouble average()

返回此流的元素的算术平均值的OptionalDouble,如果此流为空,则返回一个空的OptionalDouble。

OptionalInt max()

返回此流的最大元素的OptionalInt,如果此流为空,则返回一个空的OptionalInt。

OptionalInt min()

返回此流的最小元素的OptionalInt,如果此流为空,则返回一个空的OptionalInt。

long count()

返回此流中的元素数。

static IntStream range(int startInclusive,int endExclusive)

返回有序的IntStream,范围是[startInclusive endExclusive)。

static IntStream rangeClosed(int startInclusive,int endInclusive)

返回有序的IntStream,范围是[startInclusive endInclusive]。

相应的,Optional也有三个对应的特性化类OptionalInt、OptionalLong、 OptionalDouble,同样避免了拆装箱操作,他们的方法和Optional都差不多,但是不接受null元素。

使用案例:

/**
 * @author lx
 */
public class Specialtest {
    @Test
    public void test() {
        //求和
        System.out.println(Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).sum());
        //求最大值
        Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).max().ifPresent(System.out::println);
        //求最小值
        Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).min().ifPresent(System.out::println);
        //求算术平均数
        Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).average().ifPresent(System.out::println);
    }
}
复制代码

3.10 收集操作

在流水线处理完毕之后,我们总想将此时的流元素使用一个数据结构收集起来,或者得到一个汇总的结果。此时,我们就可以利用Stream中非常强大的collect收集操作了,通过collect操作,我们可以得到自己想要的数据结构或者一个结果。

实际上,只需要将一个通用的Collector收集器传递给collect方法,收集器定义了如何收集元素以及返回什么样的结果,我们几乎可以自己实现前面已经学习的所有终端操作,比如reduce、max、count……!而Java8经帮我们预定义了很多收集器,他们都可以通过Collectors工厂类的静态方法获取,通常我们都会直接将Collectors的所有静态方法都导入进来。当然我们也可以创建属于自己的收集器,完成更加特殊的功能!

3.10.1 归纳

collect方法几乎支持reduce方法的所有归纳操作,比如计数、求和、最值等等,并且Collectos已经为这些归纳操作预定义好了收集器,我们直接使用即可!

3.10.1.1 计数

Collectos的静态方法中定义了计数的收集器!

public static < T > Collector<T,?,Long> counting()

返回一个Collector,用于计算输入元素的数量,如果没有元素,则结果为0。相当于:reducing(0L, e -> 1L, Long::sum)操作。

使用案例:

/**
 * 计数操作
 */
@Test
public void count() {
    //前面我们讲过三种计数操作
    //Stream的count方法
    System.out.println(Stream.of(1, 2, 3, 4, 7).count());
    //特性化流的count方法
    System.out.println(Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).count());
    //map+reduce实现计数
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .map(d -> 1)
            .reduce(0, Integer::sum));


    //现在,collect操作也提供计数功能,并且已经提供了预定于的收集器
    //counting()静态方法就返回一个用于计数的收集器
    System.out.println(Stream.of(1, 2, 3, 4, 7).collect(counting()));
}
复制代码

3.10.1.2 最值

Collectos的静态方法中定义了求最值的收集器!

public static < T > Collector<T,?,Optional< T >> minBy(Comparator<? super T> comparator)

返回一个Collector,它根据给定的Comparator产生最小元素,返回Optional< T > 。

public static < T > Collector<T,?,Optional< T >> maxBy(Comparator<? super T> comparator)

返回一个Collector,它根据给定的Comparator产生最大元素,返回Optional< T > 。

使用案例:

/**
 * 最值
 */
@Test
public void max_min() {
    //前面我们讲过二种最值操作

    //特性化流的方法
    Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).max().ifPresent(System.out::println);
    //reduce方法
    Optional<Integer> maxReduce = Stream.of(1, 2, 3, 4, 7)
            //使用方法引用Integer::max来表示求最值的意图
            .reduce(Integer::max);
    maxReduce.ifPresent(System.out::println);


    //现在,collect操作也提供求最值功能,并且已经提供了预定于的收集器

    //minBy(Comparator) 静态方法就返回一个用于求最小值的收集器
    Stream.of(1, 2, 3, 4, 7).collect(minBy(Integer::compareTo)).ifPresent(System.out::println);
    //maxBy(Comparator) 静态方法就返回一个用于求最大值的收集器
    Stream.of(1, 2, 3, 4, 7).collect(maxBy(Integer::compareTo)).ifPresent(System.out::println);
    Stream.of(1, 2, 3, 4, 7).collect(maxBy(Comparator.comparingInt(x -> x))).ifPresent(System.out::println);
}
复制代码

3.10.1.3 汇总

Collectors的静态方法中预定义了汇总操作的收集器,比如求和、求平均数,还有一个能够返回所有参数的收集器!

public static < T > Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper)

返回一个Collector,它产生应用于输入元素的int值函数的和。如果没有元素,结果为0。

public static < T > Collector<T,?,Long> summingLong(ToLongFunction<? super T> mapper)

返回一个Collector,它产生应用于输入元素的long值函数的和。如果没有元素,结果为0。

public static < T > Collector<T,?,Double> summingDouble(ToDoubleFunction<? super T> mapper)

返回一个Collector,它产生应用于输入元素的double值函数的和。如果没有元素,结果为0。

public static < T > Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper)

返回一个Collector ,它产生应用于输入元素的int值函数的算术平均值。 如果没有元素,结果为0。

public static < T > Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper)

返回一个Collector ,它产生应用于输入元素的long值函数的算术平均值。 如果没有元素,结果为0。

public static < T > Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper)

返回一个Collector ,它产生应用于输入元素的double值函数的算术平均值。 如果没有元素,结果为0。

public static < T > Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)

返回一个Collector ,返回int数据的总数,总和、平均值、最大值和最小值的汇总。

public static < T > Collector<T,?,LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)

返回一个Collector ,返回long数据的总数,总和、平均值、最大值和最小值的汇总。

public static < T > Collector<T,?,DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)

返回一个Collector ,返回double数据的总数,总和、平均值、最大值和最小值的汇总。

使用案例:

/**
 * 汇总
 */
@Test
public void sum() {
    //前面我们讲过二种求和操作

    //特性化流的方法
    System.out.println(Stream.of(1, 2, 3, 4, 7).mapToInt(x -> x).sum());
    //reduce方法
    Stream.of(1, 2, 3, 4, 7)
            //使用方法引用Integer::sum来表示求和的意图
            .reduce(Integer::sum)
            .ifPresent(System.out::println);


    //现在,collect操作也提供求和功能,并且已经提供了预定于的收集器

    //summingInt(ToIntFunction) 静态方法就返回一个用于求int数据和的收集器,返回int类型的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summingInt(x -> x)));

    //summingLong(ToLongFunction) 静态方法就返回一个用于求long数据和的收集器,返回long类型的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summingLong(x -> x)));

    //summingDouble(ToDoubleFunction) 静态方法就返回一个用于求double数据和的收集器,返回double类型的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summingDouble(x -> x)));

    //现在,count操作还提供求平均数功能,并且已经提供了预定于的收集器

    //averagingInt(ToIntFunction) 静态方法就返回一个用于求int数据算术平均数的收集器
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(averagingInt(x -> x)));

    //averagingLong(ToLongFunction) 静态方法就返回一个用于求long数据算术平均数的收集器
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(averagingLong(x -> x)));

    //averagingDouble(ToDoubleFunction) 静态方法就返回一个用于求double数据算术平均数的收集器
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(averagingDouble(x -> x)));


    //summarizingInt(ToIntFunction) 静态方法就返回一个用于求int数据的总和、平均值、最大值和最小值的收集器
    //返回IntSummaryStatistics对象,内部收集了所有的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summarizingInt(x -> x)));

    //summarizingLong(ToLongFunction) 静态方法就返回一个用于求long数据的总和、平均值、最大值和最小值的收集器
    //返回LongSummaryStatistics对象,内部收集了所有的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summarizingLong(x -> x)));

    //summarizingDouble(ToDoubleFunction) 静态方法就返回一个用于求double数据的总和、平均值、最大值和最小值的收集器
    //返回DoubleSummaryStatistics对象,内部收集了所有的值
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(summarizingDouble(x -> x)));
}
复制代码

3.10.1.4 连接

Collectors预定义了用于连接所有字符串元素的收集器!

public static Collector<CharSequence,?,String> joining()

返回一个Collector ,将输入元素按照顺序连接成为一个String。

public static Collector<CharSequence,?,String> joining(CharSequence delimiter)

返回一个Collector ,将输入元素按照顺序连接成为一个String,每一个元素的字符串使用delimiter分隔。

public static Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

返回一个Collector ,将输入元素按照顺序连接成为一个String,每一个元素的字符串使用delimiter分隔。在开始连接之前加上prefix,在连接完成之后加上suffix。

当然,前两个方法也可以直接使用Java8在String API中新增的join方法替代。使用案例:

/**
 * 连接字符串
 */
@Test
public void string() {

    //join()
    System.out.println(Stream.of("校花","小花","晓华","笑话").collect(joining()));
    System.out.println(String.join("", "校花","小花","晓华","笑话"));

    //joining(CharSequence delimiter)
    System.out.println(Stream.of("校花","小花","晓华","笑话").collect(joining("——")));
    System.out.println(String.join("——", "校花","小花","晓华","笑话"));

    //joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
    System.out.println(Stream.of("校花","小花","晓华","笑话").collect(joining("——","开始:","。结束")));
}
复制代码

3.10.1.5 自定义

实际上,上面的预定义归纳操作内部都是调用的reducing工厂方法,或者说是reducing归纳操作的一些特殊情况,当上面的预定于定义方法不能满足我们的要求,此时我们可以使用reducing定义一个自己的归纳操作!

public static < T > Collector<T,?,Optional< T >> reducing(BinaryOperator< T > op)

op:二元操作器,操作两个元素得到一个新元素。第一次计算时两个参数分别取前两个元素,计算的值将会作为下一次计算的第一个参数,后面的元素作为第二个参数。

返回一个Collector。没有初始值,返回一个Optional对象,以表明结果可能不存在。

public static < T > Collector<T,?,T> reducing(T identity, BinaryOperator< T > op)

  1. identity:一个初始值,最终会返回同类型的值。
  2. op:二元操作器,操作两个元素得到一个新元素。第一次计算时第一个参数取初始值,第二个参数取第一个元素,计算的值将会作为下一次计算的第一个参数,后面的元素作为第二个参数。

public static <T,U> Collector<T,?,U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator< U > op)

  1. identity:一个初始值,最终会返回同类型的值。
  2. mapper: 一个一元函数,操作一个流元素得到一个结果。
  3. op:二元操作器,操作两个元素得到一个新元素。第一次计算时第一个参数取初始值,第二个参数取第一个元素应用mapper的结果,计算的值将会作为下一次计算的第一个参数,后面元素应用mapper的结果作为第二个参数。

这样看起来,collect方法和reduce的方法有很多归纳功能都是重合的,但是它们仍然有显著的区别,最重要的就是reduce本身被作为不可变的归纳,每一次操作都应该由两个值生成新的值而不是改变原值,但是collect则作为可变归纳,支持并行的操作!

使用案例:

/**
 * 自定义
 * 看起来和reduce的功能差不多
 */
@Test
public void custom() {

    //自定义求和操作
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(reducing(0, Integer::sum)));

    //自定义计数操作
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            .collect(reducing(0, x -> 1, Integer::sum)));

    //求最值,初始值10
    System.out.println(Stream.of(1, 2, 3, 4, 7)
            //使用方法引用Integer::max来表示求最值的意图
            .collect(reducing(10, Integer::max)));

    //求最值
    Stream.of(1, 2, 3, 4, 7)
            //使用方法引用Integer::max来表示求最值的意图
            .collect(reducing(Integer::max))
            .ifPresent(System.out::println);

}
复制代码

3.10.2 集合

collect最重要的收集操作之一就是将流元素转换为集合,当然Collectos的静态方法中也定义了将流元素输出到集合的收集器!有了这些方法,我们可以方便快捷的实现集合转换!

3.10.2.1 收集到Collection

public static < T > Collector<T,?,List< T >> toList()

返回一个Collector,将所有流元素按顺序收集到一个List中。实际类型是一个ArrayList,不安全!

public static < T > Collector<T,?,Set< T >> toSet()

返回一个Collector,将所有流元素按顺序收集到一个Set中。实际类型是一个HashSet,不安全!

public static <T,C extends Collection< T >> Collector<T,?,C> toCollection(Supplier< C > collectionFactory)

collectionFactory:一个生产者,用于指定集合实际类型,只要是Collection体系中的集合!

返回一个Collector,将所有流元素按顺序收集到一个Collection中。实际类型通过传入的参数自己指定,只要是Collection体系中的集合!

3.10.2.2 收集到Map

public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. 返回一个Collector ,它将元素收集到一个Map ,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型是一个HashMap,不安全!
  4. 注意:如果有重复的key,那么将抛出IllegalStateException异常!

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)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. mergeFunction:一个二元操作器,处理遇到key冲突的情况!第一个参数是前一个冲突的key的value,第二个参数是后一个冲突的key的value。返回一个结果,作为value。
  4. 返回一个Collector,它将元素收集到一个Map,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型是一个HashMap,不安全!指定冲突解决策略!

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)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. mergeFunction:一个二元操作器,处理遇到key冲突的情况!第一个参数是前一个冲突的key的value,第二个参数是后一个冲突的key的value。返回一个结果,作为value。
  4. mapSupplier:一个生产者,用于指定集合实际类型,只要是Map体系中的集合!
  5. 返回一个Collector,它将元素收集到一个Map,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型通过传入的参数自己指定,只要是Map体系中的集合!指定冲突解决策略!

3.10.2.3 收集到ConcurrentMap

public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. 返回一个Collector ,它将元素收集到一个ConcurrentMap,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型是一个ConcurrentHashMap,线程安全!
  4. 注意:如果有重复的key,那么将抛出IllegalStateException异常!

public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator< U > mergeFunction)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. mergeFunction:一个二元操作器,处理遇到key冲突的情况!第一个参数是前一个冲突的key的value,第二个参数是后一个冲突的key的value。返回一个结果,作为value。
  4. 返回一个Collector ,它将元素收集到一个ConcurrentMap,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型是一个ConcurrentHashMap,线程安全!指定冲突解决策略!

public static <T,K,U,M extends ConcurrentMap<K,U>> Collector<T,?,M> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator< U > mergeFunction, Supplier< M > mapSupplier)

  1. keyMapper:map的key的函数,参数就是每一个流元素。
  2. valueMapper:map的value的函数,参数就是每一个流元素。
  3. mergeFunction:一个二元操作器,处理遇到key冲突的情况!第一个参数是前一个冲突的key的value,第二个参数是后一个冲突的key的value。返回一个结果,作为value。
  4. mapSupplier:一个生产者,用于指定集合实际类型,只要是ConcurrentMap体系中的集合!
  5. 返回一个Collector,它将元素收集到一个ConcurrentMap,其键和值是将所提供的映射函数应用于输入元素(流元素)的结果。实际类型通过传入的参数自己指定,只要是ConcurrentMap体系中的集合!指定冲突解决策略!

3.10.2.4 使用案例

/**
 * @author lx
 */
public class CollectCollection {


    /**
     * collection
     */
    @Test
    public void collection() {
        //收集全部学生分数ArrayList集合
        List<Integer> scoreArrayList = students.stream().map(Student::getScore).collect(toList());
        //收集全部学生分数HashSet集合
        Set<Integer> scoreHashSet = students.stream().map(Student::getScore).collect(toSet());
        //收集全部学生分数LinkedHashSet集合
        Set<Integer> scoreLinkedHashSet = students.stream().map(Student::getScore).collect(toCollection(LinkedHashSet::new));

        System.out.println(scoreArrayList);
        System.out.println(scoreHashSet);
        System.out.println(scoreHashSet);
    }

    /**
     * map
     */
    @Test
    public void map() {

        //public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)
        //注意:如果有重复的key,那么将抛出IllegalStateException异常

        //收集全部学生分数 名字-分数 的HashMap,将会抛出异常
        //Map<String, Integer> nameStoreHashMap = students.stream().collect(toMap(Student::getName, Student::getScore));

        //收集全部学生分数 id-分数 的HashMap,不会抛出异常
        Map<Integer, Integer> idStoreHashMap = students.stream().collect(toMap(Student::getId, Student::getScore));

        System.out.println(idStoreHashMap);


        //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)
        //指定key冲突解决策略

        //收集全部学生分数 名字-分数 的HashMap,取前一个冲突的value
        Map<String, Integer> nameStoreHashMap1 = students.stream().collect(toMap(Student::getName, Student::getScore, (x
                , y) -> {
            System.out.println(x);
            System.out.println(y);
            return x;
        }));
        //收集全部学生分数 名字-分数 的HashMap,取后一个冲突的value
        Map<String, Integer> nameStoreHashMap2 = students.stream().collect(toMap(Student::getName, Student::getScore, (x
                , y) -> {
            System.out.println(x);
            System.out.println(y);
            return y;
        }));

        System.out.println(nameStoreHashMap1);
        System.out.println(nameStoreHashMap2);



        //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)
        //指定key冲突解决策略以及指定Map类型LinkedHashMap
        LinkedHashMap<Integer, String> scoreNameLinkedHashMap = students.stream()
                .collect(toMap(Student::getScore, Student::getName, (x, y) -> y, LinkedHashMap::new));
        //指定key冲突解决策略以及指定Map类型,TreeMap
        TreeMap<Integer, String> scoreNameTreeMap = students.stream()
                .collect(toMap(Student::getScore, Student::getName, (x, y) -> y, TreeMap::new));

        System.out.println(scoreNameLinkedHashMap);
        System.out.println(scoreNameTreeMap);
    }


    /**
     * ConcurrentMap
     */
    @Test
    public void concurrentMap() {

        //public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper)
        //注意:如果有重复的key,那么将抛出IllegalStateException异常

        //收集全部学生分数 名字-分数 的HashMap,将会抛出异常
        //ConcurrentMap<String, Integer> collect = students.stream().collect(toConcurrentMap(Student::getName,Student::getScore));

        //收集全部学生分数 id-分数 的ConcurrentHashMap,不会抛出异常
        ConcurrentMap<Integer, Integer> idStoreHashMap = students.stream().collect(toConcurrentMap(Student::getId, Student::getScore));

        System.out.println(idStoreHashMap);


        //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)
        //指定key冲突解决策略

        //收集全部学生分数 名字-分数 的ConcurrentHashMap,取前一个冲突的value
        ConcurrentMap<String, Integer> nameStoreHashMap1 = students.stream().collect(toConcurrentMap(Student::getName, Student::getScore, (x
                , y) -> {
            System.out.println(x);
            System.out.println(y);
            return x;
        }));
        //收集全部学生分数 名字-分数 的HashMap,取后一个冲突的value
        ConcurrentMap<String, Integer> nameStoreHashMap2 = students.stream().collect(toConcurrentMap(Student::getName, Student::getScore, (x
                , y) -> {
            System.out.println(x);
            System.out.println(y);
            return y;
        }));

        System.out.println(nameStoreHashMap1);
        System.out.println(nameStoreHashMap2);



        //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)
        //指定key冲突解决策略以及指定Map类型ConcurrentHashMap
        ConcurrentMap<Integer, String> scoreNameConcurrentHashMap = students.stream()
                .collect(toConcurrentMap(Student::getScore, Student::getName, (x, y) -> y, ConcurrentHashMap::new));
        //指定key冲突解决策略以及指定Map类型,ConcurrentSkipListMap
        ConcurrentMap<Integer, String> scoreNameConcurrentSkipListMap = students.stream()
                .collect(toConcurrentMap(Student::getScore, Student::getName, (x, y) -> y, ConcurrentSkipListMap::new));

        System.out.println(scoreNameConcurrentHashMap);
        System.out.println(scoreNameConcurrentSkipListMap);
    }


    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
        students.add(new Student(5, 70, "小小"));
        students.add(new Student(6, 66, "小小"));
        students.add(new Student(7, 60, "小夏"));
        students.add(new Student(8, 77, "花花"));
    }


    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.10.3 转换

3.10.3.1 转换收集

public static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream)

  1. mapper:对每一个流元素都应用的一元函数,返回的值将被作为downstream收集器的初始流元素!
  2. downstream:一个收集器,对转换之后的流元素进行收集操作。
  3. 返回一个Collector。mapping方法首先将元素流元素分别应用一元函数转换为相应的数据,然后再使用一个收集器对这些数据进行收集。类似于Stream的map+collect方法的结合。

在上面的集合操作中可能用不到该方法,因为我们可以先直接调用Srteam的map方法转换之后再收集。但是在后面的分组和分区方法中,却是非常的有用,后面会讲到!

3.10.3.2 收集转换

public static <T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher)

  1. downstream:一个收集器,对流元素进行收集操作。
  2. finisher:对每一个收集结果都应用的一元函数,返回的值将被作为最终返回值!
  3. 返回一个Collector。collectingAndThen方法首先对流元素进行收集,然后再使用一个一元函数对这些收集结果进行运算,最后返回运算后的结果。

可以看到,mapping和collectingAndThen方法是相反的,前一个先转换然后收集,后一个先收集然后转换!

3.10.3.3 使用案例

/**
 * @author lx
 */
public class MappingTest {
    /**
     * mapping
     * 类似于 map + collect
     */
    @Test
    public void mappingTest() {
        //收集全部学生分数ArrayList集合
        System.out.println(students.stream().collect(mapping(Student::getScore, toList())));
        //收集全部学生分数HashSet集合
        System.out.println(students.stream().collect(mapping(Student::getScore, toSet())));
        //收集全部学生分数LinkedHashSet集合
        LinkedHashSet<Integer> collect = students.stream().collect(mapping(Student::getScore,
                toCollection(LinkedHashSet::new)));
        System.out.println(collect);

        //收集全部学生分数的总和
        System.out.println(students.stream().collect(mapping(Student::getScore, reducing(0, Integer::sum))));
    }


    /**
     * collectingAndThen
     */
    @Test
    public void collectingAndThenTest() {
        //收集全部学生总数
        Integer collect = students.stream().collect(collectingAndThen(toList(), List::size));
        System.out.println(collect);

        //收集全部学生分数的总和
        Integer collect2 = students.stream().collect(collectingAndThen(mapping(Student::getScore, reducing(Integer::sum)), Optional::get));
        System.out.println(collect2);
    }


    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
        students.add(new Student(5, 70, "小小"));
        students.add(new Student(6, 66, "小小"));
        students.add(new Student(7, 60, "小夏"));
        students.add(new Student(8, 77, "花花"));
    }


    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.10.4 分组

前面的集合操作中,我们可以通过toMap等方法将每一个元素通过指定的函数映射成为map的key和value,但有时候我们需要对一批数据根据某些属性进行一个总体的分组,而不是简单对每一个元素都进行操作,此时我们可以使用分组操作!

Collectos的groupingBy静态方法定义了将流元素进行分组的收集器!有了这些方法,我们可以方便快捷的实现集合分组!这个名字看起来就像sql分组操作group by一样,Stream中分组操作和sql中的分组的意义都差不多!

3.10.4.1 分组到Map

public static <T,K> Collector<T,?,Map<K,List< T >>> groupingBy(Function<? super T,? extends K> classifier)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素List集合。实际Map类型是一个HashMap,线程不安全;实际value类型是一个ArrayList,线程不安全。

public static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. downstream:一个收集器,用于指定将返回相同的key的流元素收起到一起的规则,比如指定使用什么集合作为value、甚至使用一个归纳操作等等!这里就可以使用mapping方法对原始流数据进行进一步处理之后再收集,完成更加细致的工作!
  3. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素应用downstream收集器返回的结果。实际Map类型是一个HashMap,线程不安全;实际value类型根据downstream收集器的类型来确定。

public static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier< M > mapFactory, Collector<? super T,A,D> downstream)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. mapFactory:一个生产者,用于指定集合实际类型,只要是Map体系中的集合!
  3. downstream:一个收集器,用于指定将返回相同的key的流元素收起到一起的规则,比如指定使用什么集合作为value、甚至使用一个归纳操作等等!这里就可以使用mapping方法对原始流数据进行进一步处理之后再收集,完成更加细致的工作!
  4. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素应用downstream收集器返回的结果。实际Map类型是自己指定的,要求是Map体系中的集合;实际value类型根据downstream收集器的类型来确定。

3.10.4.2 分组到ConcurrentMap

public static <T,K> Collector<T,?,ConcurrentMap<K,List< T >>> groupingByConcurrent(Function<? super T,? extends K> classifier)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素List集合。实际Map类型是一个ConcurrentHashMap,线程安全;实际value类型是一个ArrayList,线程不安全。

public static <T,K,A,D> Collector<T,?,ConcurrentMap<K,D>> groupingByConcurrent(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. downstream:一个收集器,用于指定将返回相同的key的流元素收起到一起的规则,比如指定使用什么集合作为value、甚至使用一个归纳操作等等!这里就可以使用mapping方法对原始流数据进行进一步处理之后再收集,完成更加细致的工作!
  3. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素应用downstream收集器返回的结果。实际Map类型是一个ConcurrentHashMap,线程安全;实际value类型根据downstream收集器的类型来确定。

public static <T,K,A,D,M extends ConcurrentMap<K,D>> Collector<T,?,M> groupingByConcurrent(Function<? super T,? extends K> classifier, Supplier< M > mapFactory, Collector<? super T,A,D> downstream)

  1. classifier:一个分组函数。参数是每一个流元素,返回的值作为生成的map的key,流中具有相同返回值(key)的所有元素组成的List列表作为value。
  2. mapFactory:一个生产者,用于指定集合实际类型,只要是ConcurrentMap体系中的集合!
  3. downstream:一个收集器,用于指定将返回相同的key的流元素收起到一起的规则,比如指定使用什么集合作为value、甚至使用一个归纳操作等等!这里就可以使用mapping方法对原始流数据进行进一步处理之后再收集,完成更加细致的工作!
  4. 返回一个Collector,它将元素分组收集到一个Map,key是通过分组函数对流元素进行计算返回的结果,value是具备相同返回结果(key)的流元素应用downstream收集器返回的结果。实际Map类型是自己指定的,要求是ConcurrentMap体系中的集合,线程安全;实际value类型根据downstream收集器的类型来确定。

3.10.4.3 使用案例

/**
 * @author lx
 */
public class CollectGrouping {

    /**
     * groupingBy 一个参数
     */
    @Test
    public void groupingByOne() {

        //对不同等级的学生进行收集分组
        Map<Integer, List<Student>> gradeMap = students.stream().collect(groupingBy(Student::getGrade));
        System.out.println(gradeMap);


        //对不同分数的学生进行收集分组,自定义分组规则
        Map<Integer, List<Student>> collect = students.stream().collect(groupingBy(
                //通过函数自定义分组规则
                student -> {
                    int score = student.getScore();
                    if (score >= 90) {
                        return 1;
                    } else if (score >= 70) {
                        return 2;
                    } else {
                        return 3;
                    }
                }));
        System.out.println(collect);
    }

    /**
     * groupingBy 两个参数
     * 第二个Collector参数可以实现各种收集逻辑
     */
    @Test
    public void groupingByTwo() {
        //对不同等级的学生进行收集分组,使用List收集value元素,实际上就是上面单参数groupingBy内部调用的方法
        Map<Integer, List<Student>> collectList = students.stream().collect(groupingBy(Student::getGrade,
                toList()));
        System.out.println(collectList);
        System.out.println("=============");

        //对不同等级的学生进行收集分组,使用ArrayList收集元素
        Map<Integer, ArrayList<Student>> collectArrayList = students.stream().collect(groupingBy(Student::getGrade,
                toCollection(ArrayList::new)));
        System.out.println(collectArrayList);
        System.out.println("=============");

        //对不同等级的学生进行收集分组,使用HashSet收集元素
        Map<Integer, HashSet<Student>> collectSet = students.stream().collect(groupingBy(Student::getGrade,
                toCollection(HashSet::new)));
        System.out.println(collectSet);

        System.out.println("=============");

        //对不同等级的学生进行收集分组,使用Map收集同组学生的id-name
        Map<Integer, Map<Integer, String>> collect = students.stream().collect(groupingBy(Student::getGrade,
                toMap(Student::getId, Student::getName)));
        System.out.println(collect);
        System.out.println("=============");


        //对不同等级的学生进行收集分组,使用Integer收集同组学生的人数
        Map<Integer, Long> collectCounting = students.stream().collect(groupingBy(Student::getGrade,
                counting()));
        System.out.println(collectCounting);
        System.out.println("=============");
        //对不同等级的学生进行收集分组,使用TreeMap收集同组学生的id-name并根据id大小倒序排序
        Map<Integer, TreeMap<Integer, String>> collect1 = students.stream().collect(groupingBy(Student::getGrade,
                toMap(Student::getId, Student::getName, (x, y) -> x,
                        () -> new TreeMap<>(Comparator.comparingInt(o -> (int) o).reversed()))));
        System.out.println(collect1);
    }

    /**
     * groupingBy 三个参数
     * 多了一个可以指定外层Map类型的参数
     */
    @Test
    public void groupingByThr() {
        //对不同等级的学生进行收集分组,外层使用TreeMap排序
        TreeMap<Integer, List<Student>> collectTreeMap = students.stream().collect(groupingBy(Student::getGrade,
                TreeMap::new,
                toList()));
        System.out.println(collectTreeMap);

    }

    /**
     * groupingByConcurrent 一个参数,和groupingBy差不多,区别就是外层Map是安全的
     */
    @Test
    public void groupingByConcurrentByOne() {
        //对不同等级的学生进行收集分组
        ConcurrentMap<Integer, List<Student>> gradeMap = students.stream().collect(groupingByConcurrent(Student::getGrade));
        System.out.println(gradeMap);


        //对不同分数的学生进行收集分组,自定义分组规则
        ConcurrentMap<Integer, List<Student>> collect = students.stream().collect(groupingByConcurrent(
                //通过函数自定义分组规则
                student -> {
                    int score = student.getScore();
                    if (score >= 90) {
                        return 1;
                    } else if (score >= 70) {
                        return 2;
                    } else {
                        return 3;
                    }
                }));
        System.out.println(collect);
    }


    /**
     * groupingByConcurrent 两个参数,和groupingBy差不多,区别就是外层Map是安全的
     */
    @Test
    public void groupingByConcurrentByTwo() {
        //对不同等级的学生进行收集分组
        ConcurrentMap<Integer, List<Student>> gradeMap = students.stream().collect(groupingByConcurrent(Student::getGrade));
        System.out.println(gradeMap);


        //对不同分数的学生进行收集分组,自定义分组规则
        ConcurrentMap<Integer, List<Student>> collect = students.stream().collect(groupingByConcurrent(
                //通过函数自定义分组规则
                student -> {
                    int score = student.getScore();
                    if (score >= 90) {
                        return 1;
                    } else if (score >= 70) {
                        return 2;
                    } else {
                        return 3;
                    }
                }));
        System.out.println(collect);
    }

    /**
     * grouping或者groupingByConcurrent结合mapping使用,可以完成各种强大的,几乎满足所有业务需求的功能
     */
    @Test
    public void groupMapping() {

        //对不同等级的学生进行收集分组,使用ArrayList收集同组学生的name
        Map<Integer, List<Integer>> collectName = students.stream().collect(groupingBy(Student::getGrade,
                mapping(Student::getId, toList())));
        System.out.println(collectName);


        //对不同等级的学生进行收集分组,使用Integer收集同组学生的分数的和
        Map<Integer, Integer> collectSum = students.stream().collect(groupingBy(Student::getGrade,
                mapping(Student::getId, reducing(0, Integer::sum))));
        System.out.println(collectSum);

        System.out.println(collectSum);
    }


    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花", 4));
        students.add(new Student(2, 100, "小华", 1));
        students.add(new Student(3, 85, "晓华", 2));
        students.add(new Student(4, 70, "肖华", 2));
        students.add(new Student(5, 70, "小小", 2));
        students.add(new Student(6, 66, "小小", 3));
        students.add(new Student(7, 60, "小夏", 3));
        students.add(new Student(8, 77, "花花", 3));
    }


    static class Student {
        private int id;
        private int score;
        private String name;
        private int grade;

        public Integer getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getGrade() {
            return grade;
        }

        public void setGrade(int grade) {
            this.grade = grade;
        }

        public Student(int id, int score, String name, int grade) {
            this.id = id;
            this.score = score;
            this.name = name;
            this.grade = grade;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    ", grade=" + grade +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            if (getGrade() != student.getGrade()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            result = 31 * result + getGrade();
            return result;
        }
    }
}
复制代码

3.10.5 分区

分区可以看作分组的特例,相比于分组操作可以将流元素分成多组,分区操作最多将流元素分成两个区域。某些时候我们需要将流元素根据是否满足某个或者某些条件来将元素分成两部分,这时就可以使用分区操作了。

分区操作结束后的map只有两条键值对,一条是满足条件的数据,key为true,另一条就是不满足条件的数据,key为false。

public static < T > Collector<T,?,Map<Boolean,List< T >>> partitioningBy(Predicate<? super T> predicate)

  1. predicate:分区断言,根据元素是否满足断言条件来对元素进行分区。
  2. 返回一个Collector。它根据断言对输入元素进行判断,并将符合或者不符合条件的元素收集到一个list集合中,最终返回Map<Boolean, List< T >>类型的数据。实际Map类型是一个HashMap,线程不安全;实际value类型是一个ArrayList,线程不安全。

public static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream)

  1. predicate:分区断言,根据元素是否满足断言条件来对元素进行分区。
  2. downstream:一个收集器,对分区之后的两部分流元素进行其他收集操作。
  3. 返回一个Collector。它根据断言对输入元素进行判断,并对符合或者不符合条件的元素继续应用第二个收集器,最终value的类型根据第二个收集器的返回值决定!实际Map类型是一个HashMap,线程不安全;实际value类型根据downstream收集器的类型来确定。

使用案例:

/**
 * @author lx
 */
public class CollectPartitioning {


    @Test
    public void partitioning() {
        //根据学生成绩是否大于等于80进行分区
        Map<Boolean, List<Student>> collect = students.stream()
                .collect(partitioningBy(student -> student.getScore() >= 80));
        System.out.println(collect);


        //根据学生成绩是否大于等于80进行分区,输出学生名字
        Map<Boolean, List<String>> collect1 = students.stream()
                .collect(partitioningBy(student -> student.getScore() >= 80, mapping(Student::getName, toList())));
        System.out.println(collect1);


        //根据学生成绩是否大于等于80进行分区,输出学生id-名字map
        Map<Boolean, Map<Integer, String>> collect2 = students.stream()
                .collect(partitioningBy(student -> student.getScore() >= 80, toMap(Student::getId, Student::getName)));
        System.out.println(collect2);
    }


    List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
        students.add(new Student(5, 70, "小小"));
        students.add(new Student(6, 66, "小小"));
        students.add(new Student(7, 60, "小夏"));
        students.add(new Student(8, 77, "花花"));
    }


    static class Student {
        private int id;
        private int score;
        private String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "age=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

3.10.6 自定义收集器

前面我们讲的收集器都是Collectors工厂类为我们定义好的,包含了大部分的可能用到的场景,因此我们一般只使用工厂方法即可。但是实际上我们也可以实现自己的收集器,实现自己的逻辑。

3.10.6.1 Collector接口

实现自定义收集器首先需要实现Collector接口,下面来看看Collector的各项参数和方法的含义:

/**
 * 收集器接口,实现该接口可以定义自己的收集器
 *
 * @param < T > 待收集的流元素类型
 * @param <A> 累加器的类型,累加器用于收集流元素
 * @param <R> 收集器最终返回的结果类型,结果类型可以就是收集器类型,也可以是其他类型
 */
public interface Collector<T, A, R> {
    /**
     * @return 返回一个生产者。通过这个生产者的get方法可以获取一个累加器,是一个容器,也可以是对象
     */
    Supplier<A> supplier();

    /**
     * @return 返回一个二元消费者。第一个参数是获取的累加器,第二个参数就是流元素,
     * 这里需要将流元素应用到累加器中进行各种自定义操作,比如添加、求和等等,最终还是返回一个累加器
     * 这个返回的累加器作为下一次计算的第一个参数
     */
    BiConsumer<A, T> accumulator();

    /**
     * 并行模式的时候会调用该方法,因为并行模式下可能会将流拆分为多个子流分别计算,可能有多个累加器
     * 这里需要将累加器的结果进行两两合并等自定义操作,最终会只返回一个累加器
     *
     * @return 返回一个二元操作器。将两个累加器合并,返回一个累加器,最终只会返回一个累加器。
     */
    BinaryOperator<A> combiner();

    /**
     * @return 返回一个函数。从累加器中获取要返回的最终数据
     * 如果累加器就是要返回的数据,那么就不用转换
     */
    Function<A, R> finisher();

    /**
     * 每个收集器实例都有自己的特征,特征是一组描述收集器的对象,框架可以根据特征对收集器的计算进行适当优化
     *
     * @return 一组不可变的收集器特征集合,集合元素从内部的Characteristics枚举中选取
     */
    Set<Characteristics> characteristics();

    /**
     * 收集器的特征是一个枚举,描述了流是否可以并行归纳,以及可以使用的优化。
     */
    enum Characteristics {
        /**
         * 指示此收集器是多线程的,即accumulator支持多线程调用
         * 这意味着结果容器可以支持与来自多个线程的相同结果容器同时调用的累加器函数
         * <p>
         * 如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归纳。
         */
        CONCURRENT,

        /**
         * 指示集合操作不承诺保留输入元素的顺序。
         */
        UNORDERED,

        /**
         * 这表明finisher方法返回的函数是一个恒等函数,即输入什么返回什么。
         * 这种情况下,累加器对象将会直接用作归纳过程的最终结果。
         */
        IDENTITY_FINISH
    }


    /**
     * 返回一个新的收集器实例,默认具有IDENTITY_FINISH的特征
     *
     * @param supplier        新收集器的supplier函数
     * @param accumulator     新收集器的accumulator函数
     * @param combiner        新收集器的combiner函数
     * @param characteristics 新收集器的特征集合
     * @param < T >             待收集的流元素类型
     * @param <R>             累加器的类型,以及收集器最终返回的结果类型,即是同一个类型
     * @return 新的累加器实例
     * @throws NullPointerException 如果任意参数为null
     */
    public static <T, R> Collector<T, R, R> of(Supplier<R> supplier,
                                               BiConsumer<R, T> accumulator,
                                               BinaryOperator<R> combiner,
                                               Characteristics... characteristics) {
        Objects.requireNonNull(supplier);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(combiner);
        Objects.requireNonNull(characteristics);
        //设置特征集合
        Set<Characteristics> cs = (characteristics.length == 0)
                ? Collectors.CH_ID
                : Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH,
                characteristics));
        //返回一个Collectors内部实现的CollectorImpl收集器实例
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, cs);
    }

    /**
     * 返回一个新的收集器实例
     *
     * @param supplier        新收集器的supplier函数
     * @param accumulator     新收集器的accumulator函数
     * @param combiner        新收集器的combiner函数
     * @param finisher        新收集器的finisher函数
     * @param characteristics 新收集器的特征集合
     * @param < T >             待收集的流元素类型
     * @param <A>             累加器的类型
     * @param <R>             以及收集器最终返回的结果类型
     * @return the new {@code Collector}
     * @throws NullPointerException if any argument is null
     */
    public static <T, A, R> Collector<T, A, R> of(Supplier<A> supplier,
                                                  BiConsumer<A, T> accumulator,
                                                  BinaryOperator<A> combiner,
                                                  Function<A, R> finisher,
                                                  Characteristics... characteristics) {
        //null校验
        Objects.requireNonNull(supplier);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(combiner);
        Objects.requireNonNull(finisher);
        Objects.requireNonNull(characteristics);
        //设置特征集合
        Set<Characteristics> cs = Collectors.CH_NOID;
        if (characteristics.length > 0) {
            cs = EnumSet.noneOf(Characteristics.class);
            Collections.addAll(cs, characteristics);
            cs = Collections.unmodifiableSet(cs);
        }
        //返回一个Collectors内部实现的CollectorImpl收集器实例
        return new Collectors.CollectorImpl<>(supplier, accumulator, combiner, finisher, cs);
    }
}


/**
 * 收集器的简单实现类,作为Collectors的内部类
 *
 * @param < T > 待收集的流元素类型
 * @param <A> 累加器的类型,累加器用于收集流元素
 * @param <R> 收集器最终返回的结果类型,结果类型可以就是收集器类型,也可以是其他类型
 */
static class CollectorImpl<T, A, R> implements java.util.stream.Collector<T, A, R> {
    //保存了传递的函数
    private final Supplier<A> supplier;
    private final BiConsumer<A, T> accumulator;
    private final BinaryOperator<A> combiner;
    private final Function<A, R> finisher;
    private final Set<Characteristics> characteristics;

    //两个构造器,接收各种函数

    CollectorImpl(Supplier<A> supplier,
                  BiConsumer<A, T> accumulator,
                  BinaryOperator<A> combiner,
                  Function<A, R> finisher,
                  Set<Characteristics> characteristics) {
        this.supplier = supplier;
        this.accumulator = accumulator;
        this.combiner = combiner;
        this.finisher = finisher;
        this.characteristics = characteristics;
    }

    CollectorImpl(Supplier<A> supplier,
                  BiConsumer<A, T> accumulator,
                  BinaryOperator<A> combiner,
                  Set<Characteristics> characteristics) {
        this(supplier, accumulator, combiner, castingIdentity(), characteristics);
    }

    //下面这些方法被调用的时候,直接返回我们自定义的函数就行了

    @Override
    public BiConsumer<A, T> accumulator() {
        return accumulator;
    }

    @Override
    public Supplier<A> supplier() {
        return supplier;
    }

    @Override
    public BinaryOperator<A> combiner() {
        return combiner;
    }

    @Override
    public Function<A, R> finisher() {
        return finisher;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return characteristics;
    }
}

/**
 * Collectors的的静态方法
 * 较少参数的of方法的finisher函数就是这个函数,可以看到就是直接转换类型
 *
 * @return 返回自身
 */
@SuppressWarnings("unchecked")
private static <I, R> Function<I, R> castingIdentity() {
    return i -> (R) i;
}
复制代码

可以看到,实际上收集器就是通过内部的五个方法进行收集操作的。并且还提供了of方法,这个of方法实际上就是用于返回一个自定义的收集器,因此我们不必再去写一个收集器的实现类,通过of方法,把这几个函数传递进取即可获得一个自定义的收集器实例,这个收集器同样是Collectors在内部帮我们实现的,类型为CollectorImpl!

3.10.6.2 使用案例

/**
 * @author lx
 */
public class CollectCustom {
    @Test
    public void test() {
        //使用toList
        List<Student> collect1 = students.stream().collect(Collectors.toList());
        System.out.println(collect1);

        //自定义收集器完成toList的功能
        List<Student> collect = students.stream().collect(Collector.of((Supplier<List<Student>>) ArrayList::new, List::add, (x, y) -> {
            x.addAll(y);
            return x;
        }));
        System.out.println(collect);

        //实际上toList方法的源码和此自定义收集器差不多:
        //new CollectorImpl<>((Supplier<List< T >>) ArrayList::new, List::add,(left, right) -> { left.addAll(right); return left; },CH_ID);


        //通常Collects提供的预定义收集器都能满足业务
        //自定义收集器一般用于完成一些比较独特的业务,比如在收集学生的时候对学生进行评分
        Function<Student, Integer> function = o -> {
            int score = o.getScore();
            if (score >= 90) {
                return 1;
            } else if (score >= 70) {
                return 2;
            } else {
                return 3;
            }
        };
        List<Student> collect2 = students.stream().collect(Collector.of((Supplier<List<Student>>) ArrayList::new, (x, y) -> {
            y.setGrade(function.apply(y));
            x.add(y);
        }, (x, y) -> {
            x.addAll(y);
            return x;
        }));
        System.out.println(collect2);


    }

    static List<Student> students = new ArrayList<>();

    @Before
    public void before() {
        students.add(new Student(1, 55, "小花"));
        students.add(new Student(2, 100, "小华"));
        students.add(new Student(3, 85, "晓华"));
        students.add(new Student(4, 70, "肖华"));
        students.add(new Student(5, 70, "小小"));
        students.add(new Student(6, 66, "小小"));
        students.add(new Student(7, 60, "小夏"));
        students.add(new Student(8, 77, "花花"));
    }


    static class Student {
        private int id;
        private int score;
        private String name;
        private Integer grade;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public int getScore() {
            return score;
        }

        public void setScore(int score) {
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getGrade() {
            return grade;
        }

        public void setGrade(int grade) {
            this.grade = grade;
        }

        public Student(int id, int score, String name) {
            this.id = id;
            this.score = score;
            this.name = name;
        }

        public Student(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", score=" + score +
                    ", name='" + name + '\'' +
                    ", grade=" + grade +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;

            Student student = (Student) o;

            if (getId() != student.getId()) return false;
            if (getScore() != student.getScore()) return false;
            return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
        }

        @Override
        public int hashCode() {
            int result = getId();
            result = 31 * result + getScore();
            result = 31 * result + (getName() != null ? getName().hashCode() : 0);
            return result;
        }
    }
}
复制代码

4 并行流

4.1 概述

随着计算机的发展,市面上很多服务器或者个人电脑都是多核多线程的CPU,在一个多核多线程的CPU中,程序可以做到真正的并行执行,如果我们编写了合理的多线程并行代码,那么对于那种处理大量数据的程序的性能的提升是非常可观的!

在Java7之前,如果我们想要编写并行处理数据的代码,一般来说首先我们需要明确手动将数据分为多个部分,然后对每一部分使用一个线程进行计算,最后将计算的结果进行汇总,在此期间我们必须自己实现线程安全,代码实现起来非常麻烦。

还好在Java7中新增了一个ForkJoinPool,又被称为分支/合并的框架,这个而框架是线程池框架的扩展,可以让我们更加轻松的编写并行数据处理代码。

而在Java8中,新增的Stream流也支持并行的执行任务,并且使用起来比ForkJoinPool更加简单,并行流的底层实际上也是调用的ForkJoinPool,但是它已经帮我们做好了任务分割、结果汇总等操作,我们只需要某些方法启动即可!关于ForkJoinPool的原理我们在前面的文章已经讲解了,类似于分治算法,在此不再赘述!

并行化操作流只需改变一个方法调用。如果已经有一个Stream对象,调用它的parallel方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parallelStream 就能立即获得一个拥有并行能力的流。另外sequential则是指定使用串行流。一个流只能应用一种模式,如果同时调用了parallel和sequential 方法,最后调用的那个方法起效。

我们使用串行流和并行流对reduce方法进行测试:

/**
 * @author lx
 */
public class ParallelTest {


    @Test
    public void test1() {
        System.out.println("串行流");
        //串行流
        System.out.println(Stream.of("s", "qq", ";;", ".", ".", ".", ".").reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));

    }

    @Test
    public void test2() {
        System.out.println("parallel并行流");
        //添加parallel方法,变成并行流
        System.out.println(Stream.of("s", "qq", ";;", ".", ".", ".", ".").parallel().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));

    }


    @Test
    public void test7() {
        System.out.println("串行流");
        Optional<Integer> reduce = Arrays.asList(1, 2, 3, 4, 5, 6, 7).stream().reduce((x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        });
        reduce.ifPresent(System.out::println);
    }


    @Test
    public void test8() {
        System.out.println("parallelStream并行流");
        Optional<Integer> reduce = Arrays.asList(1, 2, 3, 4, 5, 6, 7).parallelStream().reduce((x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        });
        reduce.ifPresent(System.out::println);
    }


    @Test
    public void test3() {
        System.out.println("串行流");

        //串行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").stream().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));
    }


    @Test
    public void test4() {
        System.out.println("parallelStream并行流");
        //并行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").parallelStream().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));
    }


    @Test
    public void test30() {
        System.out.println("串行流");

        //串行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").stream().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }, (x, y) -> x + y));
    }

    @Test
    public void test31() {
        System.out.println("并行流");

        //串行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").parallelStream().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }, (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y + x;
        }));
    }


    @Test
    public void test5() {
        System.out.println("sequential串行流");
        //串行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").parallelStream().sequential().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));
    }

    @Test
    public void test6() {
        System.out.println("sequential串行流");

        //串行流
        System.out.println(Arrays.asList("s", "qq", ";;", ".", ".", ".", ".").stream().parallel().sequential().reduce("-", (x, y) -> {
            System.out.println(Thread.currentThread().getName());
            return x + y;
        }));
    }
}
复制代码

4.2 正确使用

并行虽好,但是一定要注意正确使用,特别是对于我们自己传入的函数,并行模式下不会保证函数的线程安全,因此这要求我们的函数具有天然的线程安全性。

如果使用的算法改变了某些共享对象的属性状态,并且我们自己没有保证并行安全,那么就可能造成数据异常!另外,在reduce方法中,并行流操作需要保证初始值与其他之操作之后的值不会改变,并且运算需要满足结合律。

下面是两个复杂案例:

/**
 1. @author lx
 */
public class ParallelErr {

    /**
     * 求总和,long普通变量在串行模式下会丢失数据
     */
    @Test
    public void test() {
        Accumulator accumulator = new Accumulator();

        IntStream.rangeClosed(1, 10000).forEach(accumulator::add);
        System.out.println(accumulator);
    }

    /**
     * 求总和,long普通变量在并行模式下会丢失数据
     */
    @Test
    public void test1() {
        Accumulator accumulator = new Accumulator();
        IntStream.rangeClosed(1, 10000).parallel().forEach(accumulator::add);
        System.out.println(accumulator);
    }

    /**
     * 求总和,使用LongAdder累加器,这是一个线程安全的累加器
     */
    @Test
    public void test2() {
        LongAdder longAdder = new LongAdder();
        IntStream.rangeClosed(1, 10000).parallel().forEach(longAdder::add);
        System.out.println(longAdder);
    }


    public static class Accumulator {
        public static int total = 0;

        public void add(int value) {
            total += value;
        }

        @Override
        public String toString() {
            return "" + total;
        }
    }

    /**
     * 要求:计算n的阶乘,然后将结果乘以5后返回
     */
    @Test
    public void test4() {
        System.out.println("安全的串行");

        //5的阶乘再乘以5,然后返回
        //串行模式下使用reduce没问题
        System.out.println(IntStream.rangeClosed(1, 5).reduce(5, (x, y) -> x * y));


        System.out.println("错误的并行");

        //如果仅仅改成并行操作,那么就会出问题,最终计算结果为375000
        //因为并行操作首先会将[1,5]之间的数拆分成为五组数据分别与5相乘,得到:5、10、15、20、25
        //然后将结果继续汇总相乘:5*10=50、20*25=500、15 ,然后继续汇总相乘 15*500=7500、50,最后一次汇总:50*7500=375000
        System.out.println(IntStream.rangeClosed(1, 5).parallel().reduce(5, (x, y) -> {
            System.out.println("x:" + x + " y: " + y + " -> " + (x * y));
            return x * y;
        }));

        System.out.println("collectingAndThen改进");

        //改成并行时,首先我们要保证初始值与其他之操作之后的值不会改变,因此这里的初始值只能是1,然后拆分相乘之后我们可以得到:1、2、3、4、5
        //随后会将结果继续汇总相乘:1*2=2、4*5=20、3 ,然后继续汇总相乘 30*3=60、2,最后一次汇总:60*2=120
        //这样我们就首先把5的阶乘求得了,最后是一个乘以5的操作,这一步我们必须保证不能并行
        //因此使用collectingAndThen方法在最后调用乘以5即可,collectingAndThen可以保证最后一步是串行的
        Integer collect = IntStream.rangeClosed(1, 5).parallel().boxed().collect(Collectors.collectingAndThen(Collectors.reducing(1, (x, y) -> x * y), x -> x * 5));
        System.out.println(collect);


        System.out.println("自定义收集器改进");
        //当前我们也可以自定义收集器,同样满足我们的要求,而且性能更好

        //完整版如下
        System.out.println(IntStream.rangeClosed(1, 5).parallel().boxed().collect(Collector.of(new Supplier<List<Integer>>() {

            /**
             * 返回累加器函数
             */
            @Override
            public List<Integer> get() {
                return new ArrayList<>();
            }
        }, new BiConsumer<List<Integer>, Integer>() {
            /**
             * 处理元素函数,添加到集合中
             */
            @Override
            public void accept(List<Integer> integers, Integer integer) {
                integers.add(integer);
            }
        }, new BinaryOperator<List<Integer>>() {
            /**
             * 结果并行汇总函数
             * 我们将两个集合的第一个元素相乘,然后替换第一个元素,最终结果就是1*2*3*4*5=120
             */
            @Override
            public List<Integer> apply(List<Integer> integers, List<Integer> integers2) {
                Integer integer = integers.get(0);
                Integer integer1 = integers2.get(0);
                integers.add(0, integer * integer1);
                return integers;
            }
        }, new Function<List<Integer>, Integer>() {
            /**
             * 返回最终结果函数
             * 最后乘以5返回即可
             */
            @Override
            public Integer apply(List<Integer> integers) {
                return integers.get(0) * 5;
            }
        })));


        //自定义收集器简化之后,这里也能看出来自定义收集器功能的强大
        System.out.println(IntStream.rangeClosed(1, 5).parallel().boxed().collect(Collector.of(ArrayList::new, List::add, (integers, integers2) -> {
            integers.add(0, integers.get(0) * integers2.get(0));
            return integers;
        }, (Function<List<Integer>, Integer>) integers -> integers.get(0) * 5)));
    }
}
复制代码

除了我们自己要保证安全之外,并行流而并不一定适用与所有操作,通常在生产环境使用并行流之前,我们需要进行性能测试。

一般来说,影响并行化性能的有这么几个因素:

  1. 源数据数量:只有在数据足够多的时候,并行化效果才会更好,因为并行操作涉及到新的线程的启动、任务划分等并行化操作,会消耗额外的时间。
  2. 源数据结构:原数据的结构对数据的拆分操作有非常重要的影响。支持随机存取的结构比如ArrayList、数组或IntStream.range等结构拆分非常容易。HashSet、TreeSet涉及到复杂树结构,不容易被分解,但是可以通过分解树形结构勉强分解。而LinkedList、Streams.iterate 、BufferedReader.lines等数据结构,难以选择分解的点位,不知道数据量,可能需要花费O(N)时间统计数量然后分解,性能极差。
  3. 装箱和拆箱:如果处理基本类型的原数据,那么尽量使用特性化的流,因为没有中间的拆装箱操作,大量数据情况下能够节省很多时间。
  4. 单个元素流水线操作耗时:一个元素在流水线上处理时间越长,并行化带来的收益越高。
  5. 合并操作耗时:并行化的最后步骤会将结果合并,如果合并操作(比如Collector中的combiner方法)是一个非常耗时的操作,那么可能造成性能反而不如串行流。
  6. 处理器线程数量:如果是单核单线程,那么没必要并行化,因为一定不能并行,只能是并发,带来的收益一般不大,处理器可用线程数量越多,并行化收益越高!

5 总结

Java8新增的Stream 将一批要处理的数据当作源数据,中间的数据一系列处理看作一个流水线,这一批数据顺序的通过流水线上的每一个节点,一个节点就是一个个的中间处理操作, 比如筛选, 排序,聚合等,流水线上的元素经过每一个节点处理完毕之后最终会通过一个终端操作,获取处理结果!整个流程看起来就像流动的生产线一样,或者像水流在管道中的流动一样,这也是“流”的名字的得来!

Stream API使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和程序结构表达的高阶抽象,比如groupingBy表示分组、max表示求最大值、summing表示求和、forEach表示循环……这种声明式的编程方式,对于传统Java代码结构进行了彻底的改变,比如将外循环变成内循环,Optional容器等等新特性让我们的代码变成更加高效、精简和健壮,极大提高Java程序员的生产力!总之,Java8的Stream值得我们学习!

相关文章:

lambda:Java8—一万字的lambda表达式的详细介绍与应用案例

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改