在一起来学Java8(七)——Stream(上)
中我们了解到了Stream对象的常用方法以及用法。现在一起来深入了解下Stream.collect()
方法的使用
collect基本用法
collect意思为收集,它是对Stream中的元素进行收集和归纳,返回一个新的集合对象。先来看一个简单例子:
public class CollectTest {
@Data
@AllArgsConstructor
static class Goods {
private String goodsName;
private int price;
}
public static void main(String[] args) {
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("redmek20", 2999)
);
List<String> nameList = list.stream()
.map(Goods::getGoodsName)
.collect(Collectors.toList());
}
}
在这个例子中,通过map方法返回商品名称,然后把所有的商品名称放到了List对象中。
查看源码发现,collect方法由两个重载方法组成。
- 方法1:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
- 方法2:
<R, A> R collect(Collector<? super T, A, R> collector);
其中用的最多的是方法2,这个方法可以看做是方法1的快捷方式,因为Collector中同样提供了Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner
这三个参数,不难猜测其底层还是要用到方法1对应的实现。
我们可以先从collect(Collector<? super T, A, R> collector)
开始入手,通过这个再去慢慢了解方法1的用法。
Collectors
Stream.collect(Collector<? super T, A, R> collector)
方法的参数Collector对象主要由Collectors
类提供。Collectors类里面包含了一系列的静态方法,用来返回Collector对象,常用的方法如下列表所示:
averagingXX | 求平均数 |
counting | 求集合中元素个数 |
groupingBy | 对集合进行分组 |
joining | 对集合元素进行拼接 |
mapping | 可在分组的过程中再次进行值的映射 |
maxBy | 求最大值 |
minBy | 求最小值 |
partitioningBy | 对元素进行分区 |
reducing | 归纳 |
summarizingXX | 汇总 |
toCollection | 转换成集合对象 |
toConcurrentMap | 转换成ConcurrentMap |
toList | 转换成List |
toMap | 转换成Map |
toSet | 转换成Set |
下面依次来讲解下每个方法的用处。
averagingXX
averagingXX包括averagingDouble,averagingInt,averagingLong。它们表示求平均值。
double averagingInt = Stream.of(1, 2, 3)
.collect(Collectors.averagingInt(val -> val));
System.out.println("averagingInt:" + averagingInt);
double averagingLong = Stream.of(10L, 21L, 30L)
.collect(Collectors.averagingLong(val -> val));
System.out.println("averagingLong:" + averagingLong);
double averagingDouble = Stream.of(0.1, 0.2, 0.3)
.collect(Collectors.averagingDouble(val -> val));
System.out.println("averagingDouble:" + averagingDouble);
它们的参数是一个函数式接口,可以使用Lambda表达式编写,其中Lambda表达式中的参数为Stream中的元素,返回的是待求平均的数值。下面这则列子是求商品的平均值:
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("redmek20", 2999)
);
double avgPrice = list.stream()
.collect(Collectors.averagingInt(goods -> goods.getPrice()));
System.out.println("商品的平均价格:" + avgPrice);
summingXX
与averagingXX类似,summingXX方法用来求集合中的元素值的总和。
double summingInt = Stream.of(1, 2, 3)
.collect(Collectors.summingInt(val -> val));
System.out.println("summingInt:" + summingInt);
double summingLong = Stream.of(10L, 21L, 30L)
.collect(Collectors.summingLong(val -> val));
System.out.println("summingLong:" + summingLong);
double summingDouble = Stream.of(0.1, 0.2, 0.3)
.collect(Collectors.summingDouble(val -> val));
System.out.println("summingDouble:" + summingDouble);
打印:
summingInt:6.0
summingLong:61.0
summingDouble:0.6
counting()
counting()返回集合中元素个数。
long count = Stream.of(1,2,3,4,5)
.collect(Collectors.counting());
System.out.println("count:" + count); // 5
summarizingXX
上面讲到了averagingXX(求平均)、summingXX(求和)、counting(求总数),如果我要同时获取这三个数该怎么办呢,可以用summarizingXX。
IntSummaryStatistics summarizingInt = Stream.of(1, 2, 3)
.collect(Collectors.summarizingInt(val -> val));
System.out.println("平均值:" + summarizingInt.getAverage());
System.out.println("总个数:" + summarizingInt.getCount());
System.out.println("总和:" + summarizingInt.getSum());
System.out.println("最大值:" + summarizingInt.getMax());
System.out.println("最小值:" + summarizingInt.getMin());
打印:
平均值:2.0
总个数:3
总和:6
最大值:3
最小值:1
summarizingInt将统计结果放到了一个IntSummaryStatistics对象里面,在对象中可以获取不同的统计信息。
groupingBy()
groupingBy()是对集合中的元素进行分组,由三个重载方法组成
- 重载1: groupingBy(Function)
- 重载2: groupingBy(Function, Collector)
- 重载3: groupingBy(Function, Supplier, Collector)
其中重载1调用了重载2,重载2调用重载3,因此最终都会执行到重载3中来。
首先看下重载1groupingBy(Function)
的用法,这个方法默认分组到新的List中,下面这个例子对商品类型进行分组,同样的类型的商品放到一个List中。
@Data
@AllArgsConstructor
static class Goods {
private String goodsName;
// 类型,1:手机,2:电脑
private int type;
@Override
public String toString() {
return goodsName;
}
}
public static void main(String[] args) {
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 1)
, new Goods("mate30 pro", 1)
, new Goods("thinkpad T400", 2)
, new Goods("macbook pro", 2)
);
Map<Integer, List<Goods>> goodsListMap = list.stream()
.collect(Collectors.groupingBy(Goods::getType));
goodsListMap.forEach((key, value) -> {
System.out.println("类型" + key + ":" + value);
});
}
打印:
类型1:[iphoneX, mate30 pro]
类型2:[thinkpad T400, macbook pro]
上面说到了groupingBy(Function)
实际上是调用了groupingBy(Function, Collector)
,其中第二个参数Collector
决定了转换到哪里,默认是toList()
,参见groupingBy(Function)
的源码:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
因此我们可以调用groupingBy(Function, Collector)
手动指定Collector,假设我们要把转换后的元素放到Set当中,可以这样写:
Map<Integer, Set<Goods>> goodsListMap = list.stream()
.collect(Collectors.groupingBy(Goods::getType, Collectors.toSet()));
查看重载2方法源码,发现其调用了重载3:
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
其中Goods::getType
对应classifier,Collectors.toSet()
对应downstream。中间那个参数HashMap::new
意思很明显了,即返回的Map的具体实现类是哪个,如果要改成LinkedHashMap,可以这样写:
LinkedHashMap<Integer, Set<Goods>> goodsListMap = list.stream()
.collect(Collectors.groupingBy(Goods::getType, LinkedHashMap::new, Collectors.toSet()));
这正是重载3的使用方式。
Collectors中的groupingByConcurrent方法正是基于重载3而来,中间的代码改成了ConcurrentHashMap::new
而已。
public static <T, K>
Collector<T, ?, ConcurrentMap<K, List<T>>>
groupingByConcurrent(Function<? super T, ? extends K> classifier) {
return groupingByConcurrent(classifier, ConcurrentHashMap::new, toList());
}
groupingBy方法中的Collector参数不仅仅只可以toList(),toSet(),它还有更加灵活的用法,之前我们转换的都是Map<Integer, List<Goods>>
形式,value中存放的是集合对象,如果不想要那么多属性,只想要对象里面的商品名称,,也就是说我们想得到Map<Integer, List<String>>
,其中key为商品类型,value为商品名称集合。
这个时候Collectors.mapping()
就派上用场了,我们使用groupingBy(Function, Collector)
方法,第二参数传Collectors.mapping()
Map<Integer, List<String>> goodsListMap =
list.stream()
.collect(
Collectors.groupingBy(
Goods::getType,
Collectors.mapping(Goods::getGoodsName, Collectors.toList())
)
);
mapping()方法有两个参数,第一参数指定返回的属性,第二个参数指定返回哪种集合。
joining
joining方法可以把Stream中的元素拼接起来。
List<String> list = Arrays.asList("hello", "world");
String str = list.stream().collect(Collectors.joining());
System.out.println(str); // 打印:helloworld
还可以指定分隔符:
List<String> list = Arrays.asList("hello", "world");
String str = list.stream().collect(Collectors.joining(","));
System.out.println(str); // 打印:hello,world
除此之外,String
类提供了一个join方法,功能是一样的
String str2 = String.join(",", list);
System.out.println(str2);
maxBy&minBy
- maxBy:找出Stream中最大的元素
@Data
@AllArgsConstructor
static class Goods {
private String goodsName;
private int price;
}
public static void main(String[] args) {
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("redmek20", 2999)
);
Goods maxPriceGoods = list.stream()
.collect(
Collectors.maxBy(
Comparator.comparing(Goods::getPrice)
)
)
.orElse(null);
System.out.println("最贵的商品:" + maxPriceGoods);
}
上面的例子演示了查找最贵的商品,Collectors.maxBy()方法需要传入一个比较器,需要根据商品的价格来比较。
同理,找到最便宜的商品只需把maxBy
替换成minBy
即可。
partitioningBy
partitioningBy方法表示分区,它将根据条件将Stream中的元素分成两部分,并分别放入到一个Map当中,Map的key为Boolean类型,key为true部分存放满足条件的元素,key为false存放不满足条件的元素。
{
true -> 符合条件的元素
false -> 不符合条件的元素
}
partitioningBy方法由两个重载方法组成
- 重载1:partitioningBy(Predicate)
- 重载2:partitioningBy(Predicate, Collector)
其中重载1会调用重载2,因此最终还是调用了重载2方法,我们先看下重载1方法。
下面这个例子根据商品类型,将商品划分为手机类商品和非手机类商品。
@Data
@AllArgsConstructor
static class Goods {
private String goodsName;
// 类型,1:手机,2:电脑
private int type;
@Override
public String toString() {
return goodsName;
}
}
public static void main(String[] args) {
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 1)
, new Goods("mate30 pro", 1)
, new Goods("thinkpad T400", 2)
, new Goods("macbook pro", 2)
);
// 手机归为一类,非手机商品归为一类
// true -> 手机类商品
// false -> 非手机类商品
Map<Boolean, List<Goods>> goodsMap = list.stream()
.collect(
Collectors.partitioningBy(goods -> goods.getType() == 1)
);
// 获取手机类商品
List<Goods> mobileGoods = goodsMap.get(true);
System.out.println(mobileGoods);
}
partitioningBy(Predicate, Collector)方法的第二个参数可以用来指定集合元素,默认使用的List存放,如果要使用Set存放,可以这样写:
Map<Boolean, Set<Goods>> goodsMap = list.stream()
.collect(
Collectors.partitioningBy(
goods -> goods.getType() == 1
// 指定收集类型
, Collectors.toSet())
);
toList & toSet & toCollection
toList和toSet可以将Stream中的元素转换成List、Set集合,这是用的比较多的两个方法。
Stream<Goods> stream = Stream.of(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("redmek20", 2999)
);
List<Goods> list = stream.collect(Collectors.toList());
Set<Goods> set = stream.collect(Collectors.toSet());
默认情况下,toList返回的是ArrayList,toSet返回的是HashSet,如果要返回其它类型的集合比如LinkedList,可以使用toCollection
,它可以让开发者自己指定需要哪种集合。
LinkedList<Goods> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));
toConcurrentMap
toConcurrentMap方法是将Stream转换成ConcurrentMap,它由三个重载方法组成
- 重载1:
toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
- 重载2:
toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
- 重载3:
toConcurrentMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
其中重载1调用重载2,重载2调用重载3,最终都会执行到重载3方法上来。
先看重载1,提供了两个参数
- keyMapper:指定ConcurrentMap中的key值
- valueMapper:指定key对应的value
下面这个例子是将商品的名称作为key,价格作为value
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("redmek20", 2999)
);
ConcurrentMap<String, Integer> goodsMap = list.stream()
.collect(
Collectors.toConcurrentMap(Goods::getGoodsName, Goods::getPrice)
);
System.out.println(goodsMap);
打印:
{mate30 pro=5999, iphoneX=4000, redmek20=2999}
注意:这个方法要求key不能重复,如果有重复的key,会抛IllegalStateException异常,如果有key重复,需要使用toConcurrentMap(Function, Function, BinaryOperator)
,即重载2
再来看下重载2:toConcurrentMap(Function, Function, BinaryOperator)
,这个方法前两个参数跟重载1一样,第三个参数用来处理key冲突的情况,让开发者选择一个value值返回。
List<Goods> list = Arrays.asList(
new Goods("iphoneX", 4000)
, new Goods("mate30 pro", 5999)
, new Goods("mate30 pro", 6000) // 这里有两个冲突了
, new Goods("redmek20", 2999)
);
ConcurrentMap<String, Integer> goodsMap = list.stream()
.collect(
Collectors.toConcurrentMap(Goods::getGoodsName, Goods::getPrice, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer price1, Integer price2) {
// 选择价格贵的返回
return Math.max(price1, price2);
}
})
);
System.out.println(goodsMap);
打印:{mate30 pro=6000, iphoneX=4000, redmek20=2999}
这个例子中mate30 pro作为key重复了,在BinaryOperator
中,我们选择价格高的那一条数据返回。
最后看下重载3,相比于重载2,又多了一个参数Supplier
,它可以让开发者指定返回一种ConcurrentMap
重载2调用重载3,默认使用的是ConcurrentMap::new
。
注意:第四个参数必须是ConcurrentMap或ConcurrentMap的子类
小节
本篇主要讲解了Stream.collect
的用法,以及Collectors
类中静态方法的使用,在下一篇文章中,我们将详细讲解关于reduce
的相关用法。