关注公众号(CoderBuff)回复“stream”获取《Java8 Stream编码实战》PDF完整版。html
《Java8 Stream编码实战》的代码所有在https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/stream-coding,必定要配合源码阅读,而且不断加以实践,才能更好的掌握Stream。java
对于初学者,必需要声明一点的是,Java8中的Stream尽管被称做为“流”,但它和文件流、字符流、字节流彻底没有任何关系。Stream流使程序员得以站在更高的抽象层次上对集合进行操做[1]。也就是说Java8中新引入的Stream流是针对集合的操做。git
咱们在使用集合时,最经常使用的就是迭代。程序员
public int calcSum(List<Integer> list) { int sum = 0; for (int i = 0; i < list.size(); i++) { sum += list.get(i); } return sum; }
例如,咱们可能会对集合中的元素累加并返回结果。这段代码因为for循环的样板代码并不能很清晰的传达程序员的意图。也就是说,实际上除了方法名叫“计算总和”,程序员必须阅读整个循环体才能理解。你可能以为一眼就能理解上述代码的意图,但若是碰上下面的代码,你还能一眼理解吗?github
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后会从新改变对它的见解。编程
要想使用Stream,首先要建立一个流,建立流最经常使用的方式是直接调用集合的stream
方法。数组
/** * 经过集合构造流 */ private void createByCollection() { List<Integer> list = new ArrayList<>(); Stream<Integer> stream = list.stream(); }
也能经过数组构造一个流。数据结构
/** * 经过数组构造流 */ private void createByArrays() { Integer[] intArrays = {1, 2, 3}; Stream<Integer> stream = Stream.of(intArrays); Stream<Integer> stream1 = Arrays.stream(intArrays); }
对于Stream流操做共分为两个大类:惰性求值、及时求值。app
所谓惰性求值,指的是操做最终不会产生新的集合。及时求值,指的是操做会产生新的集合。举如下示例加以说明:
/** * 经过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; }
经过for循环过滤元素返回新的集合,这里的“过滤”表示排除不符合条件的元素。咱们使用Stream流过滤并返回新的集合:
/** * 经过Stream流过滤元素返回新的集合 * @param list 待过滤的集合 * @return 新的集合 */ private List<Integer> filterByStream(List<Integer> list) { return list.stream() .filter(number -> number > 1) .collect(Collectors.toList()); }
Stream操做时,先调用了filter
方法传入了一个Lambda表达式表明过滤规则,后调用了collect
方法表示将流转换为List集合。
按照常理来想,一个方法调用完后,接着又调用了一个方法,看起来好像作了两次循环,把问题搞得更复杂了。但实际上,这里的filter
操做是惰性求值,它并不会返回新的集合,这就是Stream流设计精妙的地方。既能在保证可读性的同时,也能保证性能不会受太大影响。
因此使用Stream流的理想方式就是,造成一个惰性求值的链,最后用一个及早求值的操做返回想要的结果。
咱们不须要去记哪些方法是惰性求值,若是方法的返回值是Stream那么它表明的就是惰性求值。若是返回另一个值或空,那么它表明的就是及早求值。
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; }
这是只借助JDK的“传统”方式。若是使用Stream则能够直接经过map
操做来获取只包含学生学号的集合。
/** * 经过Stream map提取学生学号集合 * @param list 学生对象集合 * @return 学生学号集合 */ public List<Long> fetchStudentNumbersByStreamMap(List<Student> list) { return list.stream() .map(Student::getStudentNumber) .collect(Collectors.toList()); }
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()); }
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; }
这是咱们一般的实现方式,经过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()); }
排序,也是平常最经常使用的操做之一。咱们经常会把数据按照修改或者建立时间的倒序、升序排列,这步操做一般会放到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; }
关于
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; }
在使用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()); }
简洁的后果就是,代码变得不那么好读,其实并非代码的可读性下降了,而只是代码不是按照你的习惯去写的。而大部分人刚好只习惯墨守成规,而不肯意接受新鲜事物。
上面的排序是按照从小到大排序,若是想要从大到小应该如何修改呢?
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
仍是比较晦涩,若是是简单最大值、最小值、求和计算,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(); }
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); }
第二个例子调用的是reduce
的T 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; }
要按前文的说法,“这样的代码充斥了样板代码,除了方法名,代码并不能直观的反应程序员的意图,程序员须要读完整个循环体才能理解”,但凡事不是绝对的,若是换作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); }
这样的代码,已经不是样板代码的问题了,是大部分程序员即便读十遍可能也不知道要表达什么含义。可是为了学习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个参数,与第1、二个重载方法不一样的是,第1、第二个重载方法参数和返回类型都是泛型“T”,意思是入参和返回都是同一种数据类型。但在第三个例子中,入参是Student
对象,返回倒是Double
,显然不能使用第1、二个重载方法。
第三个重载方法的第一个参数类型是泛型“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(); }
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(); }
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(); }
和min
的用法相同,含义相反取最大值。这里再也不举例。
求和操做也是经常使用的操做,利用reduce
会让代码晦涩难懂,特别是复杂的对象类型。
好在Streaam提供了求和计算的简便方法——summaryStatistics
,这个方法并非Stream对象提供,而是 IntStream
,能够把它当作处理基本类型的流,同理还有LongStream
、DoubleStream
。
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()); }
返回的summaryStatistics
包含了咱们想要的全部结果,不须要咱们单独计算。mapToDouble
方法将Stream流按“成绩”字段组合成新的DoubleStream
流,summaryStatistics
方法返回的DoubleSummaryStatistics
对象为咱们提供了经常使用的计算。
灵活运用好summaryStatistics
,必定能给你带来更少的bug和更高效的编码。
前面的大部分操做都是以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()); }
/** * 将学生类型的集合转换为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)); }
/** * 按学生的学号对学生集合进行分组返回Map,key=学生学号,value=学生集合 * @param students 学生集合 * @return 按学号分组的Map */ private Map<Long, List<Student>> studentGroupByStudentNumber(List<Student> students) { return students.stream() .collect(Collectors.groupingBy(Student::getStudentNumber)); }
关注公众号(CoderBuff)回复“stream”抢先获取PDF完整版。
近期教程:
《Java 8函数式编程》 ↩︎