翎野君/文
本次我們會使用到很多的流操作,如篩選、切片、映射、查找、匹配和歸約,這些操作可以讓我們能快速完成複雜的數據查詢。
篩選和切片
用謂詞篩選
Streams接口支持filter方法。該操作會接受一個謂詞(一個返回 boolean的函數)作爲參數,並返回一個包括所有符合謂詞的元素的流。
List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
篩選各異的元素
流支持一個叫作distinct的方法,它會返回一個元素各異的流。例如,以下代碼會篩選出列表中所有的偶數,並確保沒有重複。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
截短流
流支持limit(n)方法,該方法會返回一個不超過給定長度的流。所需的長度作爲參數傳遞給limit。如果流是有序的,則最多會返回前n個元素。
請注意limit也可以用在無序流上,比如源是一個Set。這種情況下,limit的結果不會以任何順序排列。
比如,你可以建立一個List,選出熱量超過300卡路里的頭三道 :
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
圖中展示了filter和limit的組合。你可以看到,該方法只選出了符合謂詞的頭三個元素,然後就立即返回了結果。
跳過元素
流還支持skip(n)方法,返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一個空流。請注意,limit(n)和skip(n)是互斥的!例如,下面的代碼將跳過超過300卡路里的頭兩道菜,並返回剩下的。
List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
映射
映射:對流中每一個元素應用函數
流支持map方法,它會接受一個函數作爲參數。這個函數會被應用到每個元素上,並將其映射成一個新的元素。
例如,下面的代碼把方法引用Dish::getName傳給了map方法, 來提取流中菜品的名稱:
List<String> dishNames = menu.stream().map(Dish::getName).collect(toList());
兩個題目
給定一個單詞列表,你想要返回另一個列表,顯示每個單詞中有幾個字母。怎麼做呢?
List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream().map(String::length).collect(toList());
現在讓我們回到提取菜名的例子。如果你要找出每道菜的名稱有多長,怎麼做?你可以像下面這樣,再鏈接上一個map:
List<Integer> dishNameLengths = menu.stream().map(Dish::getName).map(String::length).collect(toList());
流的扁平化
讓我們拓展一下:對於一張單詞表,如何返回一張列表,列出裏面各不相同的字符呢?例如,給定單詞列表["Hello","World"],你想要返回列表["H","e","l", "o","W","r","d"]。你可能會認爲這很容易,你可以把每個單詞映射成一張字符表,然後調用distinct來過過濾重複的字符。
你可能會這樣寫:
words.stream().map(word -> word.split("")).distinct().collect(toList());
這個方法的問題在於,傳遞給map方法的Lambda爲每個單詞返回了一個String[](String 列表)。因此,map返回的流實際上是Stream<String[]>類型的。你真正想要的是用 Stream<String>來表示一個字符流。
用map和Arrays.stream(),首先,你需要一個字符流,而不是數組流。有一個叫作Arrays.stream()的方法可以接受一個數組併產生一個流,例如:
String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
把它用在前面的那個流水線裏,看看會發生什麼:
words.stream().map(word -> word.split(“")).map(Arrays::stream).distinct().collect(toList());
當前的解決方案仍然搞不定!這是因爲,你現在得到的是一個流的列表(更準確地說是 Stream<String>)先是把每個單詞轉換成一個字母數組,然後把每個數組變成了一個獨立的流。
用flatMap 你可以像下面這樣使用flatMap來解決這個問題:
List<String> uniqueCharacters =words.stream().map(w -> w.split("")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());
使用flatMap方法的效果是,各個數組並不是分別映射成一個流,而是映射成流的內容。所有使用map(Arrays::stream)時生成的單個流都被合併起來,即扁平化爲一個流。
即,flatmap方法讓你把一個流中的每個值都換成另一個流,然後把所有的流連接起來成爲一個流。
查找和匹配
查看數據集中的某些元素是否匹配一個給定的屬性。
Stream API通過allMatch、anyMatch、noneMatch、findFirst和findAny方法來完成這些工作。
檢查謂詞是否至少匹配一個元素
anyMatch方法可以回答“流中是否有一個元素能匹配給定的謂詞”。比如,你可以用它來看看菜單裏面是否有素菜可選擇:
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!”);
}
anyMatch方法返回一個boolean,因此是一個終端操作。
檢查謂詞是否匹配所有元素
allMatch方法的工作原理和anyMatch類似,但它會看看流中的元素是否都能匹配給定的謂詞。
比如,你可以用它來看是否所有菜品的熱量都低於1000卡路里):
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
沒有任何元素與給定的謂詞匹配
和allMatch相對的是noneMatch。它可以確保流中沒有任何元素與給定的謂詞匹配。
比如, 你可以用noneMatch重寫前面的例子:
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
返回當前流中的任意元素
findAny方法將返回當前流中的任意元素。它可以與其他流操作結合使用。比如,你可能想找到一道素菜。
你可以結合使用filter和findAny方法來實現這個查詢:
Optional<Dish> dish =menu.stream().filter(Dish::isVegetarian)
.findAny();
有些流有一個出現順序(encounter order)來指定流中項目出現的邏輯順序(比如由List或排序好的數據列生成的流)。對於這種流,你可能想要找到第一個元素。爲此有一個findFirst 方法,它的工作方式類似於findany。例如,給定一個數字列表,下面的代碼能找出第一個平方 能被3整除的數:
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst();
歸約
有一些查詢操作需要將流中所有元素反覆結合起來,得到一個值,比如一個Integer。
這樣的查詢可以被歸類爲歸約操作(將流歸約成一個值)。
用函數式編程語言的術語來說,這稱爲摺疊(fold),因爲你可以將這個操作看成把一張長長的值(你的流)反覆摺疊成一個小方塊,而這就是摺疊操作的結果。
元素求和
在我們研究如何使用reduce方法之前,先來看看如何使用for-each循環來對數字列表中的元素求和。
numbers中的每個元素都用加法運算符反覆迭代來得到結果。通過反覆使用加法,你把一個數字列表歸約成了一個數字。
int sum = 0;
for (int x : numbers){
sum += x;
}
這段代碼中有兩個參數:
-
總和變量的初始值,在這裏是0;
-
將列表中所有元素結合在一起的操作,在這裏是+。
要是還能把所有的數字相乘,而不必去複製粘貼這段代碼,這豈不是很好?這正是reduce操作的用武之地,它對這種重複應用的模式做了抽象。
你可以像下面這樣對流中所有的元素求和:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce接受兩個參數:
-
一個初始值,這裏是0;
-
一個BinaryOperator<T>來將兩個元素結合起來產生一個新值,這裏我們用的是lambda (a, b) -> a + b。
你也很容易把所有的元素相乘,只需要將另一個Lambda:(a, b) -> a * b傳遞給reduce操作就可以了:
int product = numbers.stream().reduce(1, (a, b) -> a * b);
Lambda反覆結合每個元素,直到流被歸約成一個值。
最大值和最小值
來看一下如何利用剛纔學到的reduce 來計算流中最大或最小的元素。正如你前面看到的,reduce接受兩個參數:
-
一個初始值
-
一個Lambda來把兩個流元素結合起來併產生一個新值
Lambda是一步步用加法運算符應用到流中每個元素上的。因此,你需要一個給定兩個元素能夠返回最大值的Lambda。
reduce操作會考慮新值和流中下一個元素,併產生一個新的最大值,直到整個流消耗完!
你可以像下面這樣使用reduce來計算流中的最大值。
Optional<Integer> max = numbers.stream().reduce(Integer::max);
總結
-
Streams API可以表達複雜的數據處理查詢。
-
可以使用filter、distinct、skip和limit對流做篩選和切片。
-
可以使用map和flatMap提取或轉換流中的元素。
-
可以使用findFirst和findAny方法查找流中的元素。你可以用allMatch、noneMatch和anyMatch方法讓流匹配給定的謂詞。
-
可以利用reduce方法將流中所有的元素迭代合併成一個結果,例如求和或查找最大元素。
作者:翎野君
博客:https://www.cnblogs.com/lingyejun/
本篇文章如有幫助到您,請給「翎野君」點個贊,感謝您的支持。