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

雷俠發表於2019-02-17

付諸實戰

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

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

領域:交易員和交易

以下是我們要處理的領域,一個 Traders 和 Transactions 的列表:

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類的定義:

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中。

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

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

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

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

請注意,此解決方案效率不高(所有字串都被反覆連線,每次迭代的時候都要建立一個新
的 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

相關文章