在上一篇文章中,我們介紹了Stream可以像運算元據庫一樣來操作集合,但是我們沒有介紹flatMap和collect操作。這兩種操作對實現複雜的查詢是非常有用的。比如你可以結果flatMap
和collect
計算stream中的單詞的字元數,像下面程式碼那樣。
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.*;
Stream<String> words = Stream.of("Java", "Magazine", "is", "the", "best");
Map<String, Long> letterToCount =words.map(w -> w.split(""))
.flatMap(Arrays::stream)
.collect(groupingBy(identity(), counting()));
複製程式碼
上述程式碼的執行結果是:
[a:4, b:1, e:3, g:1, h:1, i:2, ..]
複製程式碼
這篇文章將會介紹flatMap和collect這兩種操作的更多細節。
flatMap操作
假設你在一個文章中查詢一個單詞,你會怎麼做?
我們可以使用Files.lines()
方法,因為它可以返回一個文章一行一行資訊組成的stream。我們可以使用map()
把文章的每行分割是很多單詞,最後,使用`distinct()``移除重複的。我們將想法轉化為程式碼:
Files.lines(Paths.get("stuff.txt"))
.map(line -> line.split("\\s+"))
.distinct() // Stream<String[]>
.forEach(System.out::println);
複製程式碼
很不幸,這樣並不正確。如果你執行得到這樣的結果:
[Ljava.lang.String;@7cca494b
[Ljava.lang.String;@7ba4f24f
…
複製程式碼
到底發生了什麼事呢?問題出在使用的lambda表示式將會把檔案的每行轉化成一個字串陣列(String[])。這就導致map返回的是一個Stream<String[]>型別的結果,我們實際上需要的是一個Stream型別的結果。
我們需要一串的單詞,而不是一串的陣列。對於陣列可以使用Arrays.stream()
將陣列變成一個stream。看下面的實現:
String[] arrayOfWords = {"Java", "Magazine"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
複製程式碼
如果我們使用下面方式的話其實還有不起作用的,這是因為使用map(Arrays::stream)
後返回的其實是Stream<Stream>型別。
Files.lines(Paths.get("stuff.txt"))
.map(line -> line.split("\\s+")) // Stream<String[]>
.map(Arrays::stream) // Stream<Stream<String>>
.distinct() // Stream<Stream<String>>
.forEach(System.out::println);
複製程式碼
我們可以使用flatMap來解決這種問題,像下面這樣。使用flatMap方法的作用是返回的是stream中的內容而不是一個stream。
Files.lines(Paths.get("stuff.txt"))
.map(line -> line.split("\\s+")) // Stream<String[]>
.flatMap(Arrays::stream) // Stream<String>
.distinct() // Stream<String>
.forEach(System.out::println);
複製程式碼
collect 操作
我們來具體看一下collect操作。上面文章中看到了返回stream的操作(說明該操作是一箇中間操作)和返回一個值、boolean型值、int型值和Optional型值的操作(說明該操作是終結操作) 。
將Stream中的元素轉化到集合中.
使用toSet()你可以把一個stream轉化成一個不包含重複項的集合。下面的程式碼展示了怎麼生成高消費(單筆交易>1000$)城市的集合。
Set<String> cities = transactions.stream()
.filter(t -> t.getValue() > 1000)
.map(Transaction::getCity)
.collect(toSet());
複製程式碼
注意這樣你不能保證返回什麼型別的Set,你可以使用toCollection()來提高可控性。比如你可以像下面程式碼這樣將一個HashSet的構造方法作為引數。
Set<String> cities = transactions.stream()
.filter(t -> t.getValue() > 1000)
.map(Transaction::getCity)
.collect(toCollection(HashSet::new));
複製程式碼
collect操作方法不止這些,上面介紹的只是很小一部分,還可以實現這些功能:
-
通過貨幣型別進行分組,計算各種獲取型別的交易總金額(將會返回一個 Map<Currency, Integer>)
-
將所有交易分類兩組:大金額的和非大金額的(將會返回一個Map<Boolean, List>)
-
建立多級分組,比如先根據城市分組,然後再根據是否為大金額交易分組( 將會返回一個Map<String, Map<Boolean, List>>)
讓我們看一下Stream API和集合器怎麼實現這些查詢,我們先對一個stream中的資料進行計算平均值,最大值和最小值。接下來我們再看如果實現簡單的分組,最後我們我們將多個集合器放在一起實現強大的查詢功能,比如多級分組。
Summarizing
有很多預定義的集合器和是很方便的使用,比如使用counting() 計算個數:
long howManyTransactions = transactions.stream().collect(counting());
複製程式碼
你可以對Double, Int, 或者Long屬性的元素進行 summing Double(), summingInt(), and summingLong() 操作,像下面這樣:
int totalValue = transactions.stream().collect(summingInt(Transaction::getValue));
複製程式碼
類似的你還可以使用averagingDouble(), averagingInt(), and averagingLong() 計算平均值,像下面這樣:
double average = transactions.stream().collect(averagingInt(Transaction::getValue));
複製程式碼
還可以通過使用maxBy()和minBy()計算元素中的最大值和最小值,不過你需要定義一個做比較的 比較器,所以maxBy和minBy需要一個Comparator物件最為引數:
下面的例子中我們使用了靜態方法comparing(),它將根據傳遞進去的引數生成一個Comparator物件。這個方法根據提取stream中元素的可以做比較的key來做判斷。在這個例子中是通過銀行交易的金額大小來做比較的。
Optional<Transaction> highestTransaction = transactions.stream()
.collect(maxBy(comparing(Transaction::getValue)));
複製程式碼
還有一個叫reducing()的集合器,它可以通過重複地對stream中的所有元素進行一種操作指導產生一個結果。它和reduce()有點類似。比如下面的程式碼使用reducing()方法計算交易的總金額。
int totalValue = transactions.stream().collect(reducing(0, Transaction::getValue, Integer::sum));
複製程式碼
reducing() 有三個引數:
- 初始值(如果stream是空也將返回該值):這裡是0
- 一個會被應用到各個元素的方法
- 結合兩個提取出來的值,這是是將兩個值加起來
Grouping
一個常規的資料庫操作就是根據一個屬性對資料進行分組。比如根據貨幣對交易進行分組,如果使用迭代那簡直太複雜了:
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for(Transaction transaction : transactions) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
複製程式碼
Java 8 中有一個叫groupingBy()
的集合器,我們可以像這樣做查詢:
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream().collect(groupingBy(Transaction::getCurrency));
複製程式碼
groupingBy() 方法有一個提取分類key的函式做引數,我們可以叫它為分類函式。在這個例子中我們使用的是Transaction::getCurrency來實現根據貨幣分組。
Partitioning
還有一個叫做partitioningBy()的函式,這個可以看做是groupingBy()的特例。它需要一個predicate(返回一個boolean的函式)作為引數,將會對stream中的元素根據是否滿足predicate進行分類。partitioning可以將stream變成一個 Map<Boolean, List>。使用程式碼如下:
Map<Boolean, List<Transaction>> partitionedTransactions =transactions.stream().collect(partitioningBy( t -> t.getValue() > 1000));
複製程式碼
如果要要對不同貨幣的金額進行求和操作在SQL中可以結合使用SUM和GROUP BY。那我們使用Stream API也能這麼做嗎?當然可以了,像下面這樣使用:
Map<String, Integer> cityToSum = transactions.stream()
.collect(groupingBy(Transaction::getCity, summingInt(Transaction::getValue)));
複製程式碼
之前使用的groupingBy (Transaction::getCity)其實是groupingBy (Transaction::getCity, toList())的速寫方式。
再看一個例子,如果你要統計每個城市的交易最大值,可以做這樣實現:
Map<String, Optional<Transaction>> cityToHighestTransaction =
transactions.stream().collect(groupingBy(
Transaction::getCity, maxBy(comparing(Transaction::getValue))));
複製程式碼
再看一個更加複雜的例子,在剛才的例子中我們給groupingBy傳遞了另外一個集合器作為引數來進一步對元素進行分組。由於groupingBy本身是一個集合器,我們可以通過傳遞其他groupingBy集合器來建立多級分組,被傳遞進來的這個groupingBy定義了一個二級標準可以對stream中的元素進行再分組。
下面程式碼中我們先對城市進行分組,然後我們再根據每個城市的交易不同貨幣的平均值進行分組
Map<String, Map<Currency, Double>> cityByCurrencyToAverage =
transactions.stream().collect(groupingBy(Transaction::getCity,groupingBy(Transaction::getCurrency, averagingInt(Transaction::getValue))));
複製程式碼
自定義集合器
我們看到的這些集合器都實現了java.util.stream .Collector
介面。這就意味著你可以自定義集合器。
總結
這篇文章中,我們探索了兩個Stream API的高階操作:flatMap和colelct。通過這兩個操作你可以建立更加複雜的資料處理查詢。
我們還通過collect方法實現了summarizing, grouping, 和 partitioning 操作。這些操作還可以被結合起來建立更加複雜的查詢。
最後
感謝閱讀,有興趣可以關注微信公眾賬號獲取最新推送文章。