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
第二個例子調用的是 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; }
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
,可以把它當做處理基本類型的流,同理還有 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()); }
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完整版。
近期教程:
這是一個能給程序員加buff的公衆號 (CoderBuff)