Dating Java8系列之Java8中的流操作

翎野君/文

 

圖片

 

本次我們會使用到很多的流操作,如篩選、切片、映射、查找、匹配和歸約,這些操作可以讓我們能快速完成複雜的數據查詢。

 

圖片

篩選和切片

 

用謂詞篩選

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/

 

本篇文章如有幫助到您,請給「翎野君」點個贊,感謝您的支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章