《Java8實戰》-第五章讀書筆記(使用流Stream-01)

使用流

在上一篇的讀書筆記中,咱們已經看到了流讓你從外部迭代轉向內部迭代。這樣,你就用不着寫下面這樣的代碼來顯式地管理數據集合的迭代(外部迭代)了:java

/**
 * 菜單
 */
public static final List<Dish> MENU =
        Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 400, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));
複製代碼
List<Dish> menu = Dish.MENU;
List<Dish> vegetarianDishes = new ArrayList<>();
for(Dish d: menu){
    if(d.isVegetarian()){
        vegetarianDishes.add(d);
    }
}
複製代碼

咱們可使用支持 filter 和 collect 操做的Stream API(內部迭代)管理對集合數據的迭代。 你只須要將篩選行爲做爲參數傳遞給 filter 方法就好了。git

List<Dish> vegetarianDishes =
                menu.stream()
                        .filter(Dish::isVegetarian)
                        .collect(toList());
複製代碼

這種處理數據的方式頗有用,由於你讓StreamAPI管理如何處理數據。這樣StreamAPI就能夠在背後進行多種優化。此外,使用內部迭代的話,StreamAPI能夠決定並行運行你的代碼。這要是用外部迭代的話就辦不到了,由於你只能用單一線程挨個迭代。接下來,你將會看到StreamAPI支持的許多操做。這些操做能讓你快速完成複雜的數據查詢,如篩選、切片、映射、查找、匹配和歸約。github

切片和篩選

咱們來看看如何選擇流中的元素:用謂詞篩選,篩選出各不相同的元素,忽略流中的頭幾個元素,或將流截短至指定長度。編程

用謂詞篩選

Streams 接口支持 filter方法(你如今應該很熟悉了)。該操做會接受一個謂詞(一個返回boolean 的函數)做爲參數,並返回一個包括全部符合謂詞的元素的流。數組

List<Dish> vegetarianDishes =
                menu.stream()
                        // 方法引用檢查菜餚是否適合素食者
                        .filter(Dish::isVegetarian)
                        .collect(toList());
複製代碼

image

篩選各異的元素

流還支持一個叫做 distinct 的方法,它會返回一個元素各異(根據流所生成元素的hashCode 和 equals 方法實現)的流。例如,如下代碼會篩選出列表中全部的偶數,並確保沒有重複。bash

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 個元素。好比,你能夠創建一個 List ,選出熱量超過300卡路里的頭三道菜:函數式編程

List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                .limit(3)
                .collect(toList());
// pork beef chicken
dishes.forEach(dish -> System.out.println(dish.getName()));
複製代碼

上面的代碼展現了filter和limit的組合。咱們能夠看到,該方法之篩選出來了符合謂詞的頭三個元素,而後就當即返回告終果。請注意limit也能夠放在無序流上好比源是一個 Set 。這種狀況下, limit 的結果不會以任何順序排列。函數

跳過元素

流還支持 skip(n) 方法,返回一個扔掉了前n個元素的流。若是流中元素不足n個,則返回一個空流。請注意,limit(n)和skip(n)是互補的!例如,下面的代碼將跳過超過300卡路里的頭兩道菜,並返回剩下的。工具

List<Dish> dishes = menu.stream()
                .filter(d -> d.getCalories() > 300)
                // 跳過前兩個
                .skip(2)
                .collect(toList());
// chicken french fries rice pizza prawns salmon
dishes.forEach(dish -> System.out.println(dish.getName()));
複製代碼

映射

一個很是常見的數據處理套路就是從某些對象中選擇信息。好比在SQL裏,你能夠從表中選擇一列。Stream API也經過 map 和 flatMap 方法提供了相似的工具。

對流中每個元素應用函數

流支持 map 方法,它會接受一個函數做爲參數。這個函數會被應用到每一個元素上,並將其映 射成一個新的元素(使用映射一詞,是由於它和轉換相似,但其中的細微差異在於它是「建立一 個新版本」而不是去「修改」)。例如,下面的代碼把方法引用 Dish::getName 傳給了 map 方法,來提取流中菜餚的名稱:

List<String> dishNames = menu.stream()
                .map(Dish::getName)
                .collect(toList());
// [pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
System.out.println(dishNames);
複製代碼

getName方法返回的是一個String,因此map方法輸出的流類型就是Stream。固然,咱們也能夠獲取經過map獲取其餘的屬性。好比:我須要知道這個菜單的名字有多長,那麼咱們能夠這樣作:

List<Integer> len = menu.stream()
                .map(dish -> dish.getName().length())
                .collect(toList());
// [4, 4, 7, 12, 4, 12, 5, 6, 6]
System.out.println(len);
複製代碼

是的,就是這麼簡單,當咱們只須要獲取某個對象中的某個屬性時,經過map就能夠實現了。

流的扁平化

你已經看到如何使用 map方法返回列表中每一個菜單名稱的長度了。讓咱們拓展一下:對於一張單詞 表 , 如 何 返 回 一 張 列 表 , 列 出 裏 面 各 不 相 同 的 字 符 呢 ? 例 如 , 給 定 單 詞 列 表["Hello","World"] ,你想要返回列表 ["H","e","l", "o","W","r","d"] 。

你可能立刻會想到,將每一個單詞映射成一張字符表,而後調用distance 來過濾重複的字符。

List<String> words = Arrays.asList("Hello", "World");
List<String[]> wordList = words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());
wordList.forEach(wordArray -> {
    for (String s : wordArray) {
        System.out.print(s);
    }
    System.out.println();
});
複製代碼

執行結果:

Hello
World
複製代碼

執行完後一看,不對呀。仔細想想:咱們把["Hello", "World"]這兩個單詞把它們分割稱爲了字符數組,["H", "e", "l", "l", "o"],["W", "o", "r", "l", "d"]。而後將這個字符數組去判斷是否重複,不是一個字符是否重複,而是這一個字符數組是否有重複。因此,打印出來就是Hello World。

幸虧能夠用flatMap來解決這個問題!讓咱們一步步地來解決它。

  1. 嘗試使用 map 和 Arrays.stream()
首先,咱們須要一個字符流,而不是數組流。有一個叫做Arrays.stream()的方法能夠接受
一個數組併產生一個流,例如:
String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
按照剛剛上面的作法,使用map和Arrays.stream(),顯然是不行的。
這是由於,你如今獲得的是一個流的列表(更準確地說是Stream<String>)!的確,
你先是把每一個單詞轉換成一個字母數組,而後把每一個數組變成了一個獨立的流。
複製代碼
  1. 使用 flatMap
咱們能夠像下面這樣使用flatMap來解決這個問題:
String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
List<String> uniqueCharacters = streamOfwords
        // 將每一個單詞轉換爲由其字母構成的數組
        .map(w -> w.split(""))
        // 將各個生成流扁平化爲單個流
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());
// HeloWrd
uniqueCharacters.forEach(System.out::print);
複製代碼

太棒了,實現了咱們想要的效果!使用flatMap方法的效果是,各個數組並非分別映射成爲一個流,而是映射成流的內容。全部使用map(s -> split(""))時生成的單個流都被合併起來,即扁平化爲一個流。一言以蔽之, flatMap 方法讓你把一個流中的每一個值都換成另外一個流,而後把全部的流鏈接起來成爲一個流。

查找和匹配

另外一個常見的數據處理套路是看看數據集中的某些元素是否匹配一個給定的屬性。Stream API經過 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法提供了這樣的工具。

檢查謂詞是否至少匹配一個元素

anyMatch 方法能夠回答「流中是否有一個元素能匹配給定的謂詞」。好比,你能夠用它來看 看菜單裏面是否有素食可選擇:

if(menu.stream().anyMatch(Dish::isVegetarian)){
    System.out.println("有素菜,不用擔憂!");
}
複製代碼

anyMatch 方法返回一個 boolean ,所以是一個終端操做。

檢查謂詞是否匹配全部元素

allMatch 方法的工做原理和 anyMatch 相似,但它會看看流中的元素是否都能匹配給定的謂詞。好比,你能夠用它來看看菜品是否有利健康(即全部菜的熱量都低於1000卡路里):

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
複製代碼

** noneMatch ** 和 allMatch 相對的是 noneMatch 。它能夠確保流中沒有任何元素與給定的謂詞匹配。好比, 你能夠用 noneMatch 重寫前面的例子:

boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
複製代碼

anyMatch 、 allMatch 和 noneMatch 這三個操做都用到了咱們所謂的短路,這就是你們熟悉 的Java中 && 和 || 運算符短路在流中的版本。

查找元素

findAny方法返回當前流中的任意元素。它能夠與其餘流結合操做使用。好比,你可能想找到一道素食菜餚。咱們可使用filter和findAny來實現:

Optional<Dish> dish = menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();
複製代碼

OK,這樣就完成咱們想要的了。可是,你會發現它返回的是一個Optional。Optional類(java.util.Optional)是一個容器類,表明一個值存在或者不存在。在上面的代碼中,findAny可能什麼都沒找到。。Java 8的庫設計人員引入了 Optional ,這 樣就不用返回衆所周知容易出問題的 null 了。很好的解決了「十億美圓的錯誤」!不過咱們如今不討論它,之後再去詳細的瞭解它是如何的使用。

查找第一個元素

有些流有一個出現順序(encounter order)來指定流中項目出現的邏輯順序(好比由 List 或 排序好的數據列生成的流)。對於這種流,你可能想要找到第一個元素。爲此有一個 findFirst 方法,它的工做方式相似於 findany 。例如,給定一個數字列表,下面的代碼能找出第一個平方 能被3整除的數:

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> firstSquareDivisibleByThree =
        someNumbers.stream()
                .map(x -> x * x)
                .filter(x -> x % 3 == 0)
                // 9
                .findFirst();
複製代碼

是的,經過鏈式調用,就完成了咱們想要的功能,比起之前來講好太多了。你可能有一個疑問,findAny和findFrist在何時使用比較好或者說兩個都存在怎麼辦。findAny和findFrist是並行的。找到第一個元素在並行上限制的更多。若是,你不關心放回元素是哪個,請使用findAny,由於它在使用並行流時限制比較少。

歸約

到目前爲止,咱們見到過的終端操做都是返回一個 boolean ( allMatch 之類的)、 void ( forEach )或 Optional 對象( findAny 等)。你也見過了使用 collect 來將流中的全部元素組合成一個 List 。接下來,咱們將會看到如何把一個流中的元素組合起來,使用reduce操做來表達更復雜的查詢,好比「計算菜單中的總卡路里」或者「菜單中卡路里最高的菜是哪個」。此類查詢須要將流中的全部元素反覆結合起來,獲得一個值,好比一個Integer。這樣的查詢能夠被歸類爲歸約操做(將流歸約成一個值)。用函數式編程語言的術語來講,這稱爲摺疊(fold),由於你能夠將這個操做當作把一張長長的紙(你的流)反覆摺疊成一個小方塊,而這就是摺疊操做的結果。

元素求和

在沒有reduce以前,咱們先用foreach循環來對數字列表中的元素求和:

int sum = 0;
for (int x : numbers) {
    sum += x;
}
複製代碼

numbers 中的每一個元素都用加法運算符反覆迭代來獲得結果。經過反覆使用加法,你把一個 數字列表歸約成了一個數字。

要是還能把全部的數字相乘,而沒必要去複製粘貼這段代碼,豈不是很好?這正是 reduce 操 做的用武之地,它對這種重複應用的模式作了抽象。你能夠像下面這樣對流中全部的元素求和:

List<Integer> numbers = Arrays.asList(3, 4, 5, 1, 2);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 15
System.out.println(sum);
複製代碼

咱們很簡單的就完成了元素與元素相加最後獲得的結果。若是是元素與元素相乘,也很簡單:

numbers.stream().reduce(1, (a, b) -> a * b);
複製代碼

是的,就是這麼簡單!咱們還可使用方法引用來簡化求和的代碼,讓它看起來更加簡潔:

int sum2 = numbers.stream().reduce(0, Integer::sum);
複製代碼

** 無初始值 ** reduce 還有一個重載的變體,它不接受初始值,可是會返回一個 Optional 對象:

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
複製代碼

爲何它返回一個 Optional 呢?考慮流中沒有任何元素的狀況。 reduce 操做無 法返回其和,由於它沒有初始值。這就是爲何結果被包裹在一個 Optional 對象裏,以代表和 可能不存在。如今看看用 reduce 還能作什麼。

最大值和最小值

原來,只要用歸約就能夠計算最大值和最小值了!讓咱們來看看如何利用剛剛學到的 reduce 來計算流中最大或最小的元素。

Optional<Integer> max = numbers.stream().reduce(Integer::max);
複製代碼

reduce 操做會考慮新值和流中下一個元素,併產生一個新的最大值,直到整個流消耗完!就像這樣:

3 - 4 - 5 - 1 - 2
↓
3 → 4
    ↓
    4 → 5
        ↓
        5 → 1
            ↓
            5 → 2
                ↓
                5
複製代碼

經過這樣的形式去比較哪一個數值是最大的!若是,你獲取最小的數值,也很簡單隻須要這樣:

Optional<Integer> min = numbers.stream().reduce(Integer::min);
複製代碼

好了,關於流的使用就想講到這了,在下一節中咱們將會付諸實戰,而不是看完了以後不去使用它,相信過不了多久咱們就會忘記的!

小結

這一章的讀書筆記中,咱們學習和了解到了:

  1. Streams API能夠表達複雜的數據處理查詢。
  2. 你可使用 filter 、 distinct 、 skip 和 limit 對流作篩選和切片。
  3. 你可使用 map 和 flatMap 提取或轉換流中的元素。
  4. 你可使用 findFirst 和 findAny 方法查找流中的元素。你能夠用 allMatch、noneMatch 和 anyMatch 方法讓流匹配給定的謂詞。
  5. 這些方法都利用了短路:找到結果就當即中止計算;沒有必要處理整個流。
  6. 你能夠利用 reduce 方法將流中全部的元素迭代合併成一個結果,例如求和或查找最大 元素。

代碼

Github: chap5

Gitee: chap5

相關文章
相關標籤/搜索