在上一篇文章中,咱们介绍了Stream能够像操做数据库同样来操做集合,可是咱们没有介绍flatMap和collect操做。这两种操做对实现复杂的查询是很是有用的。好比你能够结果flatMap
和collect
计算stream中的单词的字符数,像下面代码那样。java
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这两种操做的更多细节。数组
假设你在一个文章中查找一个单词,你会怎么作?微信
咱们可使用Files.lines()
方法,由于它能够返回一个文章一行一行信息组成的stream。咱们可使用map()
把文章的每行分割是不少单词,最后,使用`distinct()``移除重复的。咱们将想法转化为代码:ide
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类型的结果。post
咱们须要一串的单词,而不是一串的数组。对于数组可使用Arrays.stream()
将数组变成一个stream。看下面的实现:spa
String[] arrayOfWords = {"Java", "Magazine"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
复制代码
若是咱们使用下面方式的话其实还有不起做用的,这是由于使用map(Arrays::stream)
后返回的实际上是Stream<Stream>类型。code
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。cdn
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操做。上面文章中看到了返回stream的操做(说明该操做是一个中间操做)和返回一个值、boolean型值、int型值和Optional型值的操做(说明该操做是终结操做) 。
使用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中的数据进行计算平均值,最大值和最小值。接下来咱们再看若是实现简单的分组,最后咱们咱们将多个集合器放在一块儿实现强大的查询功能,好比多级分组。
有不少预约义的集合器和是很方便的使用,好比使用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() 有三个参数:
一个常规的数据库操做就是根据一个属性对数据进行分组。好比根据货币对交易进行分组,若是使用迭代那简直太复杂了:
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来实现根据货币分组。
还有一个叫作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 操做。这些操做还能够被结合起来建立更加复杂的查询。
感谢阅读,有兴趣能够关注微信公众帐号获取最新推送文章。