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

付諸實戰

在本節中,咱們會將迄今學到的關於流的知識付諸實踐。咱們來看一個不一樣的領域:執行交易的交易員。你的經理讓你爲八個查詢找到答案。java

  1. 找出2011年發生的全部交易,並按交易額排序(從低到高)。
  2. 交易員都在哪些不一樣的城市工做過?
  3. 查找全部來自於劍橋的交易員,並按姓名排序。
  4. 返回全部交易員的姓名字符串,按字母順序排序。
  5. 有沒有交易員是在米蘭工做的?
  6. 打印生活在劍橋的交易員的全部交易額。
  7. 全部交易中,最高的交易額是多少?
  8. 找到交易額最小的交易。

領域:交易員和交易

如下是咱們要處理的領域,一個 Traders 和 Transactions 的列表:git

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");

List<Transaction> transactions = Arrays.asList(
        new Transaction(brian, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
);
複製代碼

Trader和Transaction類的定義:github

public class Trader {
    private String name;
    private String city;

    public Trader(String n, String c){
        this.name = n;
        this.city = c;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "Trader{" +
                "name='" + name + '\'' + ", city='" + city + '\'' + '}'; } } 複製代碼

Transaction類:數組

public class Transaction {
    private Trader trader;
    private Integer year;
    private Integer value;

    public Transaction(Trader trader, Integer year, Integer value) {
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public void setTrader(Trader trader) {
        this.trader = trader;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer year) {
        this.year = year;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Transaction{" +
                "trader=" + trader +
                ", year=" + year +
                ", value=" + value +
                '}';
    }
}
複製代碼
首先,咱們來看第一個問題:找出2011年發生的全部交易,並按交易額排序(從低到高)。
List<Transaction> tr2011 = transactions.stream()
                // 篩選出2011年發生的全部交易
                .filter(transaction -> transaction.getYear() == 2011)
                // 按照交易額從低到高排序
                .sorted(Comparator.comparing(Transaction::getValue))
                // 轉爲集合
                .collect(Collectors.toList());
複製代碼

太棒了,第一個問題咱們很輕鬆的就解決了!首先,將transactions集合轉爲流,而後給filter傳遞一個謂詞來選擇2011年的交易,接着按照交易額從低到高進行排序,最後將Stream中的全部元素收集到一個List集合中。緩存

第二個問題:交易員都在哪些不一樣的城市工做過?
List<String> cities = transactions.stream()
                // 提取出交易員所工做的城市
                .map(transaction -> transaction.getTrader().getCity())
                // 去除已有的城市
                .distinct()
                // 將Stream中全部的元素轉爲一個List集合
                .collect(Collectors.toList());
複製代碼

是的,咱們很簡單的完成了第二個問題。首先,將transactions集合轉爲流,而後使用map提取出與交易員相關的每位交易員所在的城市,接着使用distinct去除重複的城市(固然,咱們也能夠去掉distinct,在最後咱們就要使用collect,將Stream中的元素轉爲一個Set集合。collect(Collectors.toSet())),咱們只須要不一樣的城市,最後將Stream中的全部元素收集到一個List中。bash

第三個問題:查找全部來自於劍橋的交易員,並按姓名排序。
List<Trader> traders = transactions.stream()
                // 從交易中提取全部的交易員
                .map(Transaction::getTrader)
                // 進選擇位於劍橋的交易員
                .filter(trader -> "Cambridge".equals(trader.getCity()))
                // 確保沒有重複
                .distinct()
                // 對生成的交易員流按照姓名進行排序
                .sorted(Comparator.comparing(Trader::getName))
                .collect(Collectors.toList());
複製代碼

第三個問題,從交易中提取全部的交易員,而後進選擇位於劍橋的交易員確保沒有重複,接着對生成的交易員流按照姓名進行排序。dom

第四個問題:返回全部交易員的姓名字符串,按字母順序排序。
String traderStr =
                transactions.stream()
                        // 提取全部交易員姓名,生成一個 Strings 構成的 Stream
                        .map(transaction -> transaction.getTrader().getName())
                        // 只選擇不相同的姓名
                        .distinct()
                        // 對姓名按字母順序排序
                        .sorted()
                        // 逐個拼接每一個名字,獲得一個將全部名字鏈接起來的 String
                        .reduce("", (n1, n2) -> n1 + " " + n2);
複製代碼

這些問題,咱們都很輕鬆的就完成!首先,提取全部交易員姓名,生成一個 Strings 構成的 Stream而且只選擇不相同的姓名,而後對姓名按字母順序排序,最後使用reduce將名字拼接起來!ide

請注意,此解決方案效率不高(全部字符串都被反覆鏈接,每次迭代的時候都要創建一個新 的 String 對象)。下一章中,你將看到一個更爲高效的解決方案,它像下面這樣使用 joining (其 內部會用到 StringBuilder ):函數

String traderStr =
                transactions.stream()
                            .map(transaction -> transaction.getTrader().getName())
                            .distinct()
                            .sorted()
                            .collect(joining());
複製代碼
第五個問題:有沒有交易員是在米蘭工做的?
boolean milanBased =
                transactions.stream()
                        // 把一個謂詞傳遞給 anyMatch ,檢查是否有交易員在米蘭工做
                        .anyMatch(transaction -> "Milan".equals(transaction.getTrader()
                                .getCity()));
複製代碼

第五個問題,依舊很簡單把一個謂詞傳遞給 anyMatch ,檢查是否有交易員在米蘭工做。學習

第六個問題:打印生活在劍橋的交易員的全部交易額。
transactions.stream()
                // 選擇住在劍橋的交易員所進行的交易
                .filter(t -> "Cambridge".equals(t.getTrader().getCity()))
                // 提取這些交易的交易額
                .map(Transaction::getValue)
                // 打印每一個值
                .forEach(System.out::println);
複製代碼

第六個問題,首先選擇住在劍橋的交易員所進行的交易,接着提取這些交易的交易額,而後就打印出每一個值。

第七個問題:全部交易中,最高的交易額是多少?
Optional<Integer> highestValue =
                transactions.stream()
                        // 提取每項交易的交易額
                        .map(Transaction::getValue)
                        // 計算生成的流中的最大值
                        .reduce(Integer::max);
複製代碼

第七個問題,首先提取每項交易的交易額,而後使用reduce計算生成的流中的最大值。

第八個問題:找到交易額最小的交易。
Optional<Transaction> smallestTransaction =
                transactions.stream()
                        // 經過反覆比較每一個交易的交易額,找出最小的交易
                        .reduce((t1, t2) ->
                                t1.getValue() < t2.getValue() ? t1 : t2);
複製代碼

是的,第八個問題很簡單,可是還有更好的作法!流支持 min 和 max 方法,它們能夠接受一個 Comparator 做爲參數,指定 計算最小或最大值時要比較哪一個鍵值:

Optional<Transaction> smallestTransaction = transactions.stream()
                                         .min(comparing(Transaction::getValue));
複製代碼

上面的八個問題,咱們經過Stream很輕鬆的就完成了,真是太棒了!

數值流

咱們在前面看到了可使用 reduce 方法計算流中元素的總和。例如,你能夠像下面這樣計 算菜單的熱量:

int calories = menu.stream()
                    .map(Dish::getCalories)
                    .reduce(0, Integer::sum);
複製代碼

這段代碼的問題是,它有一個暗含的裝箱成本。每一個 Integer 都必須拆箱成一個原始類型, 再進行求和。要是能夠直接像下面這樣調用 sum 方法,豈不是更好?

int calories = menu.stream()
                    .map(Dish::getCalories)
                    .sum();
複製代碼

但這是不可能的。問題在於 map 方法會生成一個 Stream 。雖然流中的元素是 Integer 類 型,但 Streams 接口沒有定義 sum 方法。爲何沒有呢?比方說,你只有一個像 menu 那樣的Stream ,把各類菜加起來是沒有任何意義的。但不要擔憂,Stream API還提供了原始類型流特化,專門支持處理數值流的方法。

原始類型流特化

Java 8引入了三個原始類型特化流接口來解決這個問題: IntStream 、 DoubleStream 和 LongStream ,分別將流中的元素特化爲 int 、 long 和 double ,從而避免了暗含的裝箱成本。每一個接口都帶來了進行經常使用數值歸約的新方法,好比對數值流求和的 sum ,找到最大元素的max。此外還有在必要時再把它們轉換回對象流的方法。要記住的是,這些特化的緣由並不在於流的複雜性,而是裝箱形成的複雜性——即相似 int 和 Integer 之間的效率差別。

1.映射到數值流

將流轉換爲特化版本的經常使用方法是 mapToInt 、 mapToDouble 和 mapToLong 。這些方法和前 面說的 map 方法的工做方式同樣,只是它們返回的是一個特化流,而不是 Stream 。例如,咱們能夠像下面這樣用 mapToInt 對 menu 中的卡路里求和:

int calories = menu.stream()
        // 返回一個IntStream
        .mapToInt(Dish::getCalories)
        .sum();
複製代碼

這裏, mapToInt 會從每道菜中提取熱量(用一個 Integer 表示),並返回一個 IntStream (而不是一個 Stream )。而後你就能夠調用 IntStream 接口中定義的 sum 方法,對卡 路里求和了!請注意,若是流是空的, sum 默認返回 0 。 IntStream 還支持其餘的方便方法,如 max 、 min 、 average 等。

2.轉換回對象流

一樣,一旦有了數值流,你可能會想把它轉換回非特化流。例如, IntStream 上的操做只能 產生原始整數: IntStream 的 map 操做接受的Lambda必須接受 int 並返回 int (一個 IntUnaryOperator )。可是你可能想要生成另外一類值,好比 Dish 。爲此,你須要訪問 Stream 接口中定義的那些更廣義的操做。要把原始流轉換成通常流(每一個 int 都會裝箱成一個 Integer ),可使用 boxed 方法,以下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
複製代碼

3.默認值 OptionalInt

求和的那個例子很容易,由於它有一個默認值: 0 。可是,若是你要計算 IntStream 中的最 大元素,就得換個法子了,由於 0 是錯誤的結果。如何區分沒有元素的流和最大值真的是 0 的流呢? 前面咱們介紹了 Optional 類,這是一個能夠表示值存在或不存在的容器。 Optional 能夠用 Integer 、 String 等參考類型來參數化。對於三種原始流特化,也分別有一個 Optional 原始類 型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。

例如,要找到 IntStream 中的最大元素,能夠調用 max 方法,它會返回一個 OptionalInt :

OptionalInt maxCalories = menu.stream()
                .mapToInt(Dish::getCalories)
                .max();
複製代碼

如今,若是沒有最大值的話,你就能夠顯式處理 OptionalInt 去定義一個默認值了:

int max = maxCalories.orElse(1);
複製代碼

數值範圍

和數字打交道時,有一個經常使用的東西就是數值範圍。好比,假設你想要生成1和100之間的全部數字。Java 8引入了兩個能夠用於 IntStream 和 LongStream 的靜態方法,幫助生成這種範圍: range 和 rangeClosed 。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但 range 是不包含結束值的,而 rangeClosed 則包含結束值。讓咱們來看一個例子:

// 一個從1到100的偶數流 包含結束值
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
        .filter(n -> n % 2 == 0);
// 從1到100共有50個偶數
System.out.println(evenNumbers.count());
複製代碼

這裏咱們用了 rangeClosed 方法來生成1到100之間的全部數字。它會產生一個流,而後你 能夠連接 filter 方法,只選出偶數。到目前爲止尚未進行任何計算。最後,你對生成的流調 用 count 。由於 count 是一個終端操做,因此它會處理流,並返回結果 50 ,這正是1到100(包括 兩端)中全部偶數的個數。請注意,比較一下,若是改用 IntStream.range(1, 100) ,則結果 將會是 49 個偶數,由於 range 是不包含結束值的。

構建流

但願到如今,咱們已經讓你相信,流對於表達數據處理查詢是很是強大而有用的。到目前爲 止,你已經可以使用 stream 方法從集合生成流了。此外,咱們還介紹瞭如何根據數值範圍建立 數值流。但建立流的方法還有許多!本節將介紹如何從值序列、數組、文件來建立流,甚至由生成函數來建立無限流!

由值建立流

你可使用靜態方法 Stream.of ,經過顯式值建立一個流。它能夠接受任意數量的參數。例 如,如下代碼直接使用 Stream.of 建立了一個字符串流。而後,你能夠將字符串轉換爲大寫,再 一個個打印出來:

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
複製代碼

你可使用 empty 獲得一個空流,以下所示:

Stream<String> emptyStream = Stream.empty();
複製代碼

由數組建立流

咱們可使用靜態方法 Arrays.stream 從數組建立一個流。它接受一個數組做爲參數。例如, 咱們能夠將一個原始類型 int 的數組轉換成一個 IntStream ,以下所示:

int[] numbers = {2, 3, 5, 7, 11, 13};
// 總和41
int sum = Arrays.stream(numbers).sum();
複製代碼
由文件生成流

Java中用於處理文件等I/O操做的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files 中的不少靜態方法都會返回一個流。例如,一個頗有用的方法是 Files.lines ,它會返回一個由指定文件中的各行構成的字符串流。使用咱們迄今所學的內容,咱們能夠用這個方法看看一個文件中有多少各不相同的詞:

long uniqueWords;
try (Stream<String> lines = Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()),
        Charset.defaultCharset())) {
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
            .distinct()
            .count();
    System.out.println("uniqueWords:" + uniqueWords);
} catch (IOException e) {
    e.fillInStackTrace();
} catch (URISyntaxException e) {
    e.printStackTrace();
}
複製代碼

你可使用 Files.lines 獲得一個流,其中的每一個元素都是給定文件中的一行。而後,你 能夠對 line 調用 split 方法將行拆分紅單詞。應該注意的是,你該如何使用 flatMap 產生一個扁平的單詞流,而不是給每一行生成一個單詞流。最後,把 distinct 和 count 方法連接起來,數數流中有多少各不相同的單詞。

由函數生成流:建立無限流

Stream API提供了兩個靜態方法來從函數生成流: Stream.iterate 和 Stream.generate 。 這兩個操做能夠建立所謂的無限流:不像從固定集合建立的流那樣有固定大小的流。由 iterate和 generate 產生的流會用給定的函數按需建立值,所以能夠無窮無盡地計算下去!通常來講,應該使用 limit(n) 來對這種流加以限制,以免打印無窮多個值。

1.迭代

咱們先來看一個 iterate 的簡單例子,而後再解釋:

Stream.iterate(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println);
複製代碼

iterate 方法接受一個初始值(在這裏是 0 ),還有一個依次應用在每一個產生的新值上的 Lambda( UnaryOperator 類型)。這裏,咱們使用Lambda n -> n + 2 ,返回的是前一個元素加上2。所以,iterate方法生成了一個全部正偶數的流:流的第一個元素是初始值 0 。而後加上 2 來生成新的值 2 ,再加上 2 來獲得新的值 4 ,以此類推。這種 iterate 操做基本上是順序的,由於結果取決於前一次應用。請注意,此操做將生成一個無限流——這個流沒有結尾,由於值是按需計算的,能夠永遠計算下去。咱們說這個流是無界的。正如咱們前面所討論的,這是流和集合之間的一個關鍵區別。咱們使用limit方法來顯式限制流的大小。這裏只選擇了前10個偶數。而後能夠調用 forEach 終端操做來消費流,並分別打印每一個元素。

2.生成

與 iterate 方法相似, generate 方法也可以讓你按需生成一個無限流。但 generate 不是依次 對每一個新生成的值應用函數的。它接受一個 Supplier 類型的Lambda提供新的值。咱們先來 看一個簡單的用法:

Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
複製代碼

這段代碼將生成一個流,其中有五個0到1之間的隨機雙精度數。例如,運行一次獲得了下面 的結果:

0.8404010101858976
0.03607897810804739
0.025199243727344833
0.8368092999566692
0.14685668895309267
複製代碼

Math.Random 靜態方法被用做新值生成器。一樣,你能夠用 limit 方法顯式限制流的大小, 不然流將會無限長。

你可能想知道, generate 方法還有什麼用途。咱們使用的供應源(指向 Math.random 的方 法引用)是無狀態的:它不會在任何地方記錄任何值,以備之後計算使用。但供應源不必定是無狀態的。你能夠建立存儲狀態的供應源,它能夠修改狀態,並在爲流生成下一個值時使用。

咱們在這個例子中會使用 IntStream 說明避免裝箱操做的代碼。 IntStream 的 generate 方 法會接受一個 IntSupplier ,而不是 Supplier 。例如,能夠這樣來生成一個全是1的無限流:

IntStream ones = IntStream.generate(() -> 1);
複製代碼

還記得第三章的筆記中,Lambda容許你建立函數式接口的實例,只要直接內聯提供方法的實 現就能夠。你也能夠像下面這樣,經過實現 IntSupplier 接口中定義的 getAsInt 方法顯式傳遞一個對象(雖然這看起來是平白無故地繞圈子,也請你耐心看):

IntStream twos = IntStream.generate(new IntSupplier(){
            @Override
            public int getAsInt(){
                return 2;
            }
        });
複製代碼

generate 方法將使用給定的供應源,並反覆調用 getAsInt 方法,而這個方法老是返回 2 。 但這裏使用的匿名類和Lambda的區別在於,匿名類能夠經過字段定義狀態,而狀態又能夠用 getAsInt 方法來修改。這是一個反作用的例子。咱們迄今見過的全部Lambda都是沒有反作用的;它們沒有改變任何狀態。

總結

這一章的東西不少,收穫也不少!如今你能夠更高效地處理集合了。事實上,流讓你能夠簡潔地表達複雜的數據處理查詢。此外,流能夠透明地並行化。如下是咱們應從本章中學到的關鍵概念。 這一章的讀書筆記中,咱們學習和了解到了:

  1. Streams API能夠表達複雜的數據處理查詢。
  2. 你可使用 filter 、 distinct 、 skip 和 limit 對流作篩選和切片。
  3. 你可使用 map 和 flatMap 提取或轉換流中的元素。
  4. 你可使用 findFirst 和 findAny 方法查找流中的元素。你能夠用 allMatch、noneMatch 和 anyMatch 方法讓流匹配給定的謂詞。
  5. 這些方法都利用了短路:找到結果就當即中止計算;沒有必要處理整個流。
  6. 你能夠利用 reduce 方法將流中全部的元素迭代合併成一個結果,例如求和或查找最大 元素。
  7. filter 和 map 等操做是無狀態的,它們並不存儲任何狀態。 reduce 等操做要存儲狀態才 能計算出一個值。 sorted 和 distinct 等操做也要存儲狀態,由於它們須要把流中的所 有元素緩存起來才能返回一個新的流。這種操做稱爲有狀態操做。
  8. 流有三種基本的原始類型特化: IntStream 、 DoubleStream 和 LongStream 。它們的操 做也有相應的特化。
  9. 流不只能夠從集合建立,也可從值、數組、文件以及 iterate 與 generate 等特定方法 建立。
  10. 無限流是沒有固定大小的流。

代碼

Github: chap5

Gitee: chap5

相關文章
相關標籤/搜索