转载

Java8 Stream流

第三章 Stream流

《Java8 Stream编码实战》的代码全部在 https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/stream-coding ,一定要配合源码阅读,并且不断加以实践,才能更好的掌握Stream。

对于初学者,必须要声明一点的是,Java8中的Stream尽管被称作为“流”,但它和文件流、字符流、字节流 完全没有任何关系 。Stream流使程序员得以站在更高的抽象层次上对集合进行操作。也就是说Java8中新引入的Stream流是针对集合的操作。

3.1 迭代

我们在使用集合时,最常用的就是迭代。

public int calcSum(List<Integer> list) {
    int sum = 0;
    for (int i = 0; i < list.size(); i++) {
        sum += list.get(i);
    }
    return sum;
}

com.coderbuff.chapter3_stream.chapter3_1.ForDemo#calcSum

例如,我们可能会对集合中的元素累加并返回结果。这段代码由于for循环的样板代码并不能很清晰的传达程序员的意图。也就是说,实际上除了方法名叫“计算总和”,程序员必须阅读整个循环体才能理解。你可能觉得一眼就能理解上述代码的意图,但如果碰上下面的代码,你还能一眼理解吗?

public Map<Long, List<Student>> useFor(List<Student> students) {
    Map<Long, List<Student>> map = new HashMap<>();
    for (Student student : students) {
        List<Student> list = map.get(student.getStudentNumber());
        if (list == null) {
            list = new ArrayList<>();
            map.put(student.getStudentNumber(), list);
        }
        list.add(student);
    }
    return map;
}

阅读完这个循环体以及包含的if判断条件,大概可以知道这是想使用“studentNumber”对“Student”对象分组。这段代码在Stream进行重构后,将会变得非常简洁和 易读

public Map<Long, List<Student>> useStreamByGroup(List<Student> students) {
    Map<Long, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getStudentNumber));
    return map;
}

当第一次看到这样的写法时,可能会认为这样的代码可读性不高,不容易测试。我相信,当你在学习掌握Stream后会重新改变对它的看法。

3.2 Stream

3.2.1 创建

要想使用Stream,首先要创建一个流,创建流最常用的方式是直接调用集合的 stream 方法。

/**
 * 通过集合构造流
 */
private void createByCollection() {
    List<Integer> list = new ArrayList<>();
    Stream<Integer> stream = list.stream();
}

com.coderbuff.chapter3_stream.chapter3_2.StreamCreator#createByCollection

也能通过数组构造一个流。

/**
 * 通过数组构造流
 */
private void createByArrays() {
    Integer[] intArrays = {1, 2, 3};
    Stream<Integer> stream = Stream.of(intArrays);
    Stream<Integer> stream1 = Arrays.stream(intArrays);
}

com.coderbuff.chapter3_stream.chapter3_2.StreamCreator#createByArrays

学习Stream流,掌握集合创建流就足够了。

3.2.2 使用

对于Stream流操作共分为两个大类: 惰性求值及时求值

所谓惰性求值,指的是操作最终不会产生新的集合。及时求值,指的是操作会产生新的集合。举以下示例加以说明:

/**
 * 通过for循环过滤元素返回新的集合
 * @param list 待过滤的集合
 * @return 过滤后的集合
 */
private List<Integer> filterByFor(List<Integer> list) {
    List<Integer> filterList = new ArrayList<>();

    for (Integer number : list) {
        if (number > 1) {
            filterList.add(number);
        }
    }
    return filterList;
}

com.coderbuff.chapter3_stream.chapter3_3.Example#filterByFor

通过for循环过滤元素返回新的集合,这里的“过滤”表示排除不符合条件的元素。我们使用Stream流过滤并返回新的集合:

/**
 * 通过Stream流过滤元素返回新的集合
 * @param list 待过滤的集合
 * @return 新的集合
 */
private List<Integer> filterByStream(List<Integer> list) {
    return list.stream()
            .filter(number -> number > 1)
            .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_3.Example#filterByStream

Stream操作时,先调用了 filter 方法传入了一个Lambda表达式代表过滤规则,后调用了 collect 方法表示将流转换为List集合。

按照常理来想,一个方法调用完后,接着又调用了一个方法,看起来好像做了两次循环,把问题搞得更复杂了。但实际上,这里的 filter 操作是 惰性求值 ,它并不会返回新的集合,这就是Stream流设计精妙的地方。既能在保证可读性的同时,也能保证性能不会受太大影响。

所以使用Stream流的理想方式就是, 形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。

我们不需要去记哪些方法是惰性求值,如果方法的返回值是Stream那么它代表的就是惰性求值。如果返回另外一个值或空,那么它代表的就是及早求值。

3.2.3 常用的Stream操作

map

map操作不好理解,它很容易让人以为这是一个转换为Map数据结构的操作。实际上他是将集合中的元素类型,转换为另外一种数据类型。

例如,你想将“学生”类型的集合转换为只有“学号”类型的集合,应该怎么做?

/**
 * 通过for循环提取学生学号集合
 * @param list 学生对象集合
 * @return 学生学号集合
 */
public List<Long> fetchStudentNumbersByFor(List<Student> list) {
    List<Long> numbers = new ArrayList<>();
    for (Student student : list) {
        numbers.add(student.getStudentNumber());
    }
    return numbers;
}

com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#fetchStudentNumbersByFor

这是只借助JDK的“传统”方式。如果使用Stream则可以直接通过 map 操作来获取只包含学生学号的集合。

/**
 * 通过Stream map提取学生学号集合
 * @param list 学生对象集合
 * @return 学生学号集合
 */
public List<Long> fetchStudentNumbersByStreamMap(List<Student> list) {
    return list.stream()
               .map(Student::getStudentNumber)
               .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#fetchStudentNumbersByStreamMap

map 传入的是一个方法,同样可以理解为传入的是一个“行为”,在这里我们传入方法“getStudentNumber”表示将通过这个方法进行转换分类。

“Student::getStudentNumber”叫 方法引用 ,它是“student -> student.getStudentNumber()”的简写。表示 直接引用已有Java类或对象的方法或构造器 。在这里我们是需要传入“getStudentNumber”方法,在有的地方,你可能会看到这样的代码“Student::new”,new调用的就是构造方法,表示创建一个对象。方法引用,可以将我们的代码变得更加紧凑简洁。

我们再举一个例子,将小写的字符串集合转换为大写字符串集合。

/**
 * 通过Stream map操作将小写的字符串集合转换为大写
 * @param list 小写字符串集合
 * @return 大写字符串集合
 */
public List<String> toUpperByStreamMap(List<String> list) {
    return list.stream()
               .map(String::toUpperCase)
               .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamMapDemo#toUpperByStreamMap

filter

filter ,过滤。这里的过滤含义是“排除不符合某个条件的元素”,也就是返回true的时候保留,返回false排除。

我们仍然以“学生”对象为例,要排除掉分数低于60分的学生。

/**
 * 通过for循环筛选出分数大于60分的学生集合
 * @param students 待过滤的学生集合
 * @return 分数大于60分的学生集合
 */
public List<Student> fetchPassedStudentsByFor(List<Student> students) {
    List<Student> passedStudents = new ArrayList<>();
    for (Student student : students) {
        if (student.getScore().compareTo(60.0) >= 0) {
            passedStudents.add(student);
        }
    }
    return passedStudents;
}

com.coderbuff.chapter3_stream.chapter3_4.StreamFilterDemo#fetchPassedStudentsByFor

这是我们通常的实现方式,通过for循环能解决“一切”问题,如果使用Stream filter一行就搞定。

/**
 * 通过Stream filter筛选出分数大于60分的学生集合
 * @param students 待过滤的学生集合
 * @return 分数大于60分的学生集合
 */
public List<Student> fetchPassedStudentsByStreamFilter(List<Student> students) {
    return students.stream()
            .filter(student -> student.getScore().compareTo(60.0) >= 0)
            .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamFilterDemo#fetchPassedStudentsByStreamFilter

sorted

排序,也是日常最常用的操作之一。我们常常会把数据按照修改或者创建时间的倒序、升序排列,这步操作通常会放到SQL语句中。但如果实在是遇到要对集合进行排序时,我们通常也会使用 Comparator.sort 静态方法进行排序,如果是复杂的对象排序,还需要实现 Comparator 接口。

/**
 * 通过Collections.sort静态方法 + Comparator匿名内部类对学生成绩进行排序
 * @param students 待排序学生集合
 * @return 排好序的学生集合
 */
private List<Student> sortedByComparator(List<Student> students) {
    Collections.sort(students, new Comparator<Student>() {
        @Override
        public int compare(Student student1, Student student2) {
            return student1.getScore().compareTo(student2.getScore());
        }
    });
    return students;
}

com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByComparator

关于 Comparator 可以查看这篇文章《 似懂非懂的Comparable与Comparator 》。简单来讲,我们需要实现 Compartor 接口的 compare 方法,这个方法有两个参数用于比较,返回1代表前者大于后者,返回0代表前者等于后者,返回-1代表前者小于后者。

当然我们也可以手动实现冒泡算法对学生成绩进行排序,不过这样的代码大多出现在课堂教学中。

/**
 * 使用冒泡排序算法对学生成绩进行排序
 * @param students 待排序学生集合
 * @return 排好序的学生集合
 */
private List<Student> sortedByFor(List<Student> students) {
    for (int i = 0; i < students.size() - 1; i++) {
        for (int j = 0; j < students.size() - 1 - i; j++) {
            if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) > 0) {
                Student temp = students.get(j);
                students.set(j, students.get(j + 1));
                students.set(j + 1, temp);
            }
        }
    }
    return students;
}

com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByFor

在使用Stream sorted后,你会发现代码将变得无比简洁。

/**
 * 通过Stream sorted对学生成绩进行排序
 * @param students 待排序学生集合
 * @return 排好序的学生集合
 */
private List<Student> sortedByStreamSorted(List<Student> students) {
    return students.stream()
                   .sorted(Comparator.comparing(Student::getScore))
                   .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamSortedDemo#sortedByStreamSorted

简洁的后果就是,代码变得不那么好读,其实并不是代码的可读性降低了,而只是代码不是按照你的习惯去写的。而大部分人恰好只习惯墨守成规,而不愿意接受新鲜事物。

上面的排序是按照从小到大排序,如果想要从大到小应该如何修改呢?

Compartor.sort 方法和for循环调换if参数的位置即可。

return student1.getScore().compareTo(student2.getScore()); 
修改为
return student2.getScore().compareTo(student1.getScore());
if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) > 0)
修改为
if (students.get(j).getScore().compareTo(students.get(j + 1).getScore()) < 0)

这改动看起来很简单,但如果这是一段 没有注释并且不是你本人写的代码 ,你能一眼知道是按降序还是升序排列吗?你还能说这是可读性强的代码吗?

如果是Stream操作。

return students.stream()
               .sorted(Comparator.comparing(Student::getScore))
               .collect(Collectors.toList());
修改为
return students.stream()
               .sorted(Comparator.comparing(Student::getScore).reversed())
               .collect(Collectors.toList());

这就是 声明式编程 ,你只管叫它做什么,而不像 命令式编程 叫它如何做。

reduce

reduce 是将传入一组值,根据计算模型输出一个值。例如求一组值的最大值、最小值、和等等。

不过使用和读懂 reduce 还是比较晦涩,如果是简单最大值、最小值、求和计算,Stream已经为我们提供了更简单的方法。如果是复杂的计算,可能为了代码的可读性和维护性还是建议用传统的方式表达。

我们来看几个使用 reduce 进行累加例子。

/**
 * Optional<T> reduce(BinaryOperator<T> accumulator);
 * 使用没有初始值对集合中的元素进行累加
 * @param numbers 集合元素
 * @return 累加结果
 */
private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce((total, number) -> total + number).get();
}

com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotal

reduce 有3个重载方法,

第一个例子调用的是 Optional<T> reduce(BinaryOperator<T> accumulator); 它只有 BinaryOperator 一个参数,这个接口是一个 函数接口 ,代表它可以接收一个Lambda表达式,它继承自 BiFunction 函数接口,在 BiFunction 接口中,只有一个方法:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

这个方法有两个参数。也就是说,传入 reduce 的Lambda表达式需要“实现”这个方法。如果不理解这是什么意思,我们可以抛开Lambda表达式,从纯粹传统的接口角度去理解。

首先, Optional<T> reduce(BinaryOperator<T> accumulator); 方法接收 BinaryOperator 类型的对象,而 BinaryOperator 是一个接口并且继承自 BiFunction 接口,而在 BiFunction 中只有一个方法定义 R apply(T t, U u) ,也就是说我们需要实现 apply 方法。

其次,接口需要被实现,我们不妨传入一个匿名内部类,并且实现 apply 方法。

private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce(new BinaryOperator<Integer>() {
                @Override
                public Integer apply(Integer integer, Integer integer2) {
                    return integer + integer2;
                }
            }).get();
}

最后,我们在将匿名内部类改写为Lambda风格的代码,箭头左边是参数,右边是函数主体。

private Integer calcTotal(List<Integer> numbers) {
    return numbers.stream()
            .reduce((total, number) -> total + number).get();
}

至于为什么两个参数相加最后就是不断累加的结果,这就是 reduce 的内部实现了。

接着看第二个例子:

/**
 * T reduce(T identity, BinaryOperator<T> accumulator);
 * 赋初始值为1,对集合中的元素进行累加
 * @param numbers 集合元素
 * @return 累加结果
 */
private Integer calcTotal2(List<Integer> numbers) {
    return numbers.stream()
            .reduce(1, (total, number) -> total + number);
}

com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotal2

第二个例子调用的是 reduceT reduce(T identity, BinaryOperator<T> accumulator); 重载方法,相比于第一个例子,它多了一个参数“identity”,这是进行后续计算的初始值, BinaryOperator 和第一个例子一样。

第三个例子稍微复杂一点,前面两个例子集合中的元素都是基本类型,而现实情况是,集合中的参数往往是一个 对象 我们常常需要对对象中的某个字段做累加计算,比如计算学生对象的总成绩。

我们先来看for循环怎么做的:

/**
 * 通过for循环对集合中的学生成绩字段进行累加
 * @param students 学生集合
 * @return 分数总和
 */
private Double calcTotalScoreByFor(List<Student> students) {
    double total = 0;
    for (Student student : students) {
        total += student.getScore();
    }
    return total;
}

com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByFor

要按前文的说法,“这样的代码充斥了样板代码,除了方法名,代码并不能直观的反应程序员的意图,程序员需要读完整个循环体才能理解”,但凡事不是绝对的,如果换做 reduce 操作:

/**
 * <U> U reduce(U identity,
 *                  BiFunction<U, ? super T, U> accumulator,
 *                  BinaryOperator<U> combiner);
 * 集合中的元素是"学生"对象,对学生的"score"分数字段进行累加
 * @param students 学生集合
 * @return 分数总和
 */
private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0),
                    (total, student) -> total + student.getScore(),
                    (aDouble, aDouble2) -> aDouble + aDouble2);
}

com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByStreamReduce

这样的代码,已经不是样板代码的问题了,是大部分程序员即使读十遍可能也不知道要表达什么含义。但是为了学习Stream我们还是要硬着头皮去理解它。

Lambda表达式不好理解,过于简洁的语法,也代表更少的信息量,我们还是先将Lambda表达式还原成匿名内部类。

private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0), new BiFunction<Double, Student, Double>() {
                @Override
                public Double apply(Double total, Student student) {
                    return total + student.getScore();
                }
            }, new BinaryOperator<Double>() {
                @Override
                public Double apply(Double aDouble, Double aDouble2) {
                    return aDouble + aDouble2;
                }
            });
}

reduce 的第三个重载方法 <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner); 一共有3个参数,与第一、二个重载方法不同的是,第一、第二个重载方法参数和返回类型都是泛型“T”,意思是入参和返回都是同一种数据类型。但在第三个例子中,入参是 Student 对象,返回却是 Double ,显然不能使用第一、二个重载方法。

第三个重载方法的第一个参数类型是泛型“U”,它的返回类型也是泛型“U”,所以第一个参数类型,代表了返回的数据类型,我们必须将第一个类型定义为 Double 例子中的入参是 Double.valueOf(0) 表示了累加的初始值为0,且返回值是 Double 类型 。第二个参数可以简单理解为“应该如何计算,累加还是累乘”的计算模型。最难理解的是第三个参数,因为前两个参数类型看起来已经能满足我们的需求,为什么还有第三个参数呢?

当我在第三个参数中加上一句输出时,发现它确实没有用。

private Double calcTotalScoreByStreamReduce(List<Student> students) {
    return students.stream()
            .reduce(Double.valueOf(0), new BiFunction<Double, Student, Double>() {
                @Override
                public Double apply(Double total, Student student) {
                    return total + student.getScore();
                }
            }, new BinaryOperator<Double>() {
                @Override
                public Double apply(Double aDouble, Double aDouble2) {
                    System.out.println("第三个参数的作用");
                    return aDouble + aDouble2;
                }
            });
}

控制台没有输出“第三个参数的作用”,改变它的返回值最终结果也没有任何改变,这的确表示它 真的没有用

第三个参数在这里的确没有用,这是因为我们目前所使用的Stream流是串行操作,它在 并行Stream流 中发挥的是 多路合并 的作用,在下一章会继续介绍并行Stream流,这里就不再多做介绍。

对于 reduce 操作,我的个人看法是, 不建议在现实中使用 。如果你有累加、求最大值、最小值的需求,Stream封装了更简单的方法。如果是特殊的计算,不如直接按for循环实现,如果一定要使用Stream对学生成绩求和也不妨换一个思路。

前面提到 map 方法可以将集合中的元素类型转换为另一种类型,那我们就能把学生的集合转换为分数的集合,再调用 reduce 的第一个重载方法计算总和:

/**
 * 先使用map将学生集合转换为分数的集合
 * 再使用reduce调用第一个重载方法计算总和
 * @param students 学生集合
 * @return 分数总和
 */
private Double calcTotalScoreByStreamMapReduce(List<Student> students) {
    return students.stream()
            .map(Student::getScore)
            .reduce((total, score) -> total + score).get();
}

com.coderbuff.chapter3_stream.chapter3_4.StreamReduceDemo#calcTotalScoreByStreamMapReduce

min

min 方法能返回集合中的最小值。它接收一个 Comparator 对象,Java8对 Comparator 接口提供了新的静态方法 comparing ,这个方法返回 Comparator 对象,以前我们需要手动实现 compare 比较,现在我们只需要调用 Comparator.comparing 静态方法即可。

/**
 * 通过Stream min计算集合中的最小值
 * @param numbers 集合
 * @return 最小值
 */
private Integer minByStreamMin(List<Integer> numbers) {
    return numbers.stream()
                  .min(Comparator.comparingInt(Integer::intValue)).get();
}

com.coderbuff.chapter3_stream.chapter3_4.StreamMinDemo#minByStreamMin

Comparator.comparingInt 用于比较int类型数据。因为集合中的元素是Integer类型,所以我们传入Integer类型的iniValue方法。如果集合中是对象类型,我们直接调用 Comparator.comparing 即可。

/**
 * 通过Stream min计算学生集合中的最低成绩
 * @param students 学生集合
 * @return 最低成绩
 */
private Double minScoreByStreamMin(List<Student> students) {
    Student minScoreStudent = students.stream()
            .min(Comparator.comparing(Student::getScore)).get();
    return minScoreStudent.getScore();
}

com.coderbuff.chapter3_stream.chapter3_4.StreamMinDemo#minScoreByStreamMin

max

min 的用法相同,含义相反取最大值。这里不再举例。

summaryStatistics

求和操作也是常用的操作,利用 reduce 会让代码晦涩难懂,特别是复杂的对象类型。

好在Streaam提供了求和计算的简便方法—— summaryStatistics ,这个方法并不是Stream对象提供,而是 IntStream ,可以把它当做处理基本类型的流,同理还有 LongStreamDoubleStream

summaryStatistics 方法也不光是只能求和,它还能求最小值、最大值。

例如我们求学生成绩的平均分、总分、最高分、最低分。

/**
 * 学生类型的集合常用计算
 * @param students 学生
 */
private void calc(List<Student> students) {
    DoubleSummaryStatistics summaryStatistics = students.stream()
            .mapToDouble(Student::getScore)
            .summaryStatistics();
    System.out.println("平均分:" + summaryStatistics.getAverage());
    System.out.println("总分:" + summaryStatistics.getSum());
    System.out.println("最高分:" + summaryStatistics.getMax());
    System.out.println("最低分:" + summaryStatistics.getMin());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamSummaryStatisticsDemo#calc

返回的 summaryStatistics 包含了我们想要的所有结果,不需要我们单独计算。 mapToDouble 方法将Stream流按“成绩”字段组合成新的 DoubleStream 流, summaryStatistics 方法返回的 DoubleSummaryStatistics 对象为我们提供了常用的计算。

灵活运用好 summaryStatistics ,一定能给你带来更少的bug和更高效的编码。

3.3 Collectors

前面的大部分操作都是以 collect(Collectors.toList()) 结尾,看多了自然也大概猜得到它是将流转换为集合对象。最大的功劳当属Java8新提供的类—— Collectors 收集器。

Collectors 不但有 toList 方法能将流转换为集合,还包括 toMap 转换为Map数据类型,还能 分组

/**
 * 将学生类型的集合转换为只包含名字的集合
 * @param students 学生集合
 * @return 学生姓名集合
 */
private List<String> translateNames(List<Student> students) {

    return students.stream()
                   .map(Student::getStudentName)
                   .collect(Collectors.toList());
}

com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#translateNames

/**
 * 将学生类型的集合转换为Map类型,key=学号,value=学生
 * @param students 学生集合
 * @return 学生Map
 */
private Map<Long, Student> translateStudentMap(List<Student> students) {
    return students.stream()
            .collect(Collectors.toMap(Student::getStudentNumber, student -> student));
}

com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#translateStudentMap

/**
 * 按学生的学号对学生集合进行分组返回Map,key=学生学号,value=学生集合
 * @param students 学生集合
 * @return 按学号分组的Map
 */
private Map<Long, List<Student>> studentGroupByStudentNumber(List<Student> students) {
    return students.stream()
            .collect(Collectors.groupingBy(Student::getStudentNumber));
}

com.coderbuff.chapter3_stream.chapter3_4.StreamCollectorsDemo#studentGroupByStudentNumber

关注公众号( CoderBuff )回复“ stream ”抢先获取PDF完整版。

近期教程:

《ElasticSearch6.x实战教程》

《Redis5.x入门教程》

《Java8 编码实战》

这是一个能给程序员加buff的公众号 (CoderBuff)

Java8 Stream流

原文  http://www.cnblogs.com/yulinfeng/p/12561664.html
正文到此结束
Loading...