付諸實戰
在本節中,我們會將迄今學到的關於流的知識付諸實踐。我們來看一個不同的領域:執行交易的交易員。你的經理讓你為八個查詢找到答案。
- 找出2011年發生的所有交易,並按交易額排序(從低到高)。
- 交易員都在哪些不同的城市工作過?
- 查詢所有來自於劍橋的交易員,並按姓名排序。
- 返回所有交易員的姓名字串,按字母順序排序。
- 有沒有交易員是在米蘭工作的?
- 列印生活在劍橋的交易員的所有交易額。
- 所有交易中,最高的交易額是多少?
- 找到交易額最小的交易。
領域:交易員和交易
以下是我們要處理的領域,一個 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都是沒有副作用的;它們沒有改變任何狀態。
總結
這一章的東西很多,收穫也很多!現在你可以更高效地處理集合了。事實上,流讓你可以簡潔地表達複雜的資料處理查詢。此外,流可以透明地並行化。以下是我們應從本章中學到的關鍵概念。
這一章的讀書筆記中,我們學習和了解到了:
- Streams API可以表達複雜的資料處理查詢。
- 你可以使用 filter 、 distinct 、 skip 和 limit 對流做篩選和切片。
- 你可以使用 map 和 flatMap 提取或轉換流中的元素。
- 你可以使用 findFirst 和 findAny 方法查詢流中的元素。你可以用 allMatch、noneMatch 和 anyMatch 方法讓流匹配給定的謂詞。
- 這些方法都利用了短路:找到結果就立即停止計算;沒有必要處理整個流。
- 你可以利用 reduce 方法將流中所有的元素迭代合併成一個結果,例如求和或查詢最大
元素。 - filter 和 map 等操作是無狀態的,它們並不儲存任何狀態。 reduce 等操作要儲存狀態才
能計算出一個值。 sorted 和 distinct 等操作也要儲存狀態,因為它們需要把流中的所
有元素快取起來才能返回一個新的流。這種操作稱為有狀態操作。 - 流有三種基本的原始型別特化: IntStream 、 DoubleStream 和 LongStream 。它們的操
作也有相應的特化。 - 流不僅可以從集合建立,也可從值、陣列、檔案以及 iterate 與 generate 等特定方法
建立。 - 無限流是沒有固定大小的流。
程式碼
Github: chap5
Gitee: chap5