一、前言
前文说到,Stream API中的collect方法,可以通过参数Collector 将流按照相应的规则转换为对应的集合类型,而Collector的帮助类Collectors提供了许多现成的静态方法来减少我们的操作。本文,我们就来了解下这些常用的方法。
二、Collectors静态方法
1. toCollection
toCollection方法可以指定转换集合的类型:
// 将Stream转换为HashSet集合
HashSet<Integer> hashSet = Stream.of(1, 2, 4, 5).collect(Collectors.toCollection(HashSet::new));
2. toSet/toList方法
这两个方法比较简单,就是转为对应的集合对象。其中toList返回的是ArrayList,而toSet返回的是HashSet:
List<Integer> list = Stream.of(1, 2, 4, 5).collect(Collectors.toList());
Set<Integer> set = Stream.of(1, 2, 4, 5).collect(Collectors.toSet());
3. joining方法
将Stream流中的数据通过某个分隔符号,拼接成字符串。该方法共有三个重载方法,并且该方法要求Stream流中的对象是字符串类型:
// 默认直接拼接
public static Collector<CharSequence, ?, String> joining()
// 添加分隔符进行拼接
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
// 添加分隔符进行拼接,并且指定前后缀
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix)
方法比较简单,直接通过例子来看:
// 没有分隔符,默认直接拼接,打印出 1234
String join = Stream.of("1", "2", "3", "4").collect(Collectors.joining());
// 使用逗号分隔符,打印出 1,2,3,4
join = Stream.of("1", "2", "3", "4").collect(Collectors.joining(","));
// 使用逗号分隔符,并且给最终拼接的字符串添加前后缀, 打印 [1,2,3,4]
join = Stream.of("1", "2", "3", "4").collect(Collectors.joining(",", "[","]"));
4. groupingBy方法
4.1 方法简介
该方法用于对Stream流中某个属性进行分组,返回的类型是Map,同样,该方法也是有多个重载方法:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
这是最基础的groupingBy方法,传递单个对应的表达式即可,可以看到,这里调用了两个参数的groupingBy方法,并且返回的List默认调用的是toList方法,也就是返回的是ArrayList,再来看两个参数的方法:
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);
}
可以看到最终执行的还是三个参数的groupingBy方法,不过这里可以看到,groupBy返回的map默认是HashMap类型。
4.2 例子
接下来我们来看例子,我们新建Person对象:
class Person {
private String city;
private String surname;
private Integer sum;
// get set,构造方法,toString省略
}
然后初始化数据:
public static List<Person> initPerson() {
return Arrays.asList(new Person("shenzhen", "张", 1),
new Person("beijing", "王", 2),
new Person("shanghai", "李", 3),
new Person("beijing", "赵", 4));
}
然后,我们通过属性city进行分组:
List<Person> list = initPerson();
Map<String, List<Person>> map = list.stream().collect(Collectors.groupingBy(Person::getCity));
System.out.println(map);
分组生成的Map的key是我们分组的条件,这里是city属性,而Map的value默认是key相同的对象组成的List集合,然后看一下打印的结果:
{shanghai=[Person{city='shanghai', surname='李', sum=3}],
shenzhen=[Person{city='shenzhen', surname='张', sum=1}],
beijing=[Person{city='beijing', surname='王', sum=2}, Person{city='beijing', surname='赵', sum=4}]}
然后,如果我们分组后要统计map中各项value的某一项的总和,这里我们统计每个city中的城市人数,这时候我们就可以使用:
Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity,
Collectors.summingInt(Person::getSum)));
System.out.println(map);
打印结果:
{shanghai=3, shenzhen=1, beijing=6}
紧接着,在此基础上,如果我们想指定返回的Map类型,比如说根据city进行排序的TreeMap,那么我们可以接着调用groupingBy方法的另一个重载方法:
Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity,
TreeMap::new, Collectors.summingInt(Person::getSum)));
System.out.println(map);
最终打印结果:
{beijing=6, shanghai=3, shenzhen=1}
5. groupingByConcurrent方法
由于groupingBy是非线程安全的,而该方法则是groupingBy方法的线程安全版本,默认情况下,返回的Map类型是ConcurrentHashMap,用法和groupingBy方法是类似的。简单看下方法声明:
public static <T, K> Collector<T, ?, ConcurrentMap<K, List<T>>>
groupingByConcurrent(Function<? super T, ? extends K> classifier) {
return groupingByConcurrent(classifier, ConcurrentHashMap::new, toList());
}
public static <T, K, A, D>Collector<T, ?, ConcurrentMap<K, D>>
groupingByConcurrent(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingByConcurrent(classifier, ConcurrentHashMap::new, downstream);
}
public static <T, K, A, D, M extends ConcurrentMap<K, D>>
Collector<T, ?, M> groupingByConcurrent(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)
再来简单看下使用:
list.stream().collect(Collectors.groupingByConcurrent(Person::getCity));
list.stream().collect(Collectors.groupingByConcurrent(Person::getCity,
ConcurrentSkipListMap::new, Collectors.summingInt(Person::getSum)));
6. toMap方法
将Stream流转换为Map对象。同样,该方法有三个重载方法,我们先来看最简单的方法:
public static <T, K, U> Collector<T, ?, Map<K,U>>
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
该方法传递两个参数,分别是Map中key和value对应的函数式接口参数,我们来简单看一下例子,还借用上文的Person对象:
public static List<Person> initPerson() {
return Arrays.asList(new Person("shenzhen", "张", 1),
new Person("beijing", "王", 2),
new Person("shanghai", "李", 3));
}
然后,使用toMap方法:
List<Person> list = initPerson();
// 打印结果 {shanghai=3, shenzhen=1, beijing=2}
Map<String, Integer> map = list.stream().collect(Collectors.toMap(Person::getCity, Person::getSum));
当然,该方法是不允许Stream中对应的key有重复值的,如果有重复值,将直接抛出异常。而针对key中有重复值的情况,我们可以调用另外一个重载方法:
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
通过第三个函数式参数来指定key重复的时候,是使用新值还是旧值,或者指定额外的操作(比如新值与旧值的和):
public static List<Person> initPerson() {
return Arrays.asList(new Person("shenzhen", "张", 1),
new Person("beijing", "王", 2),
new Person("shanghai", "李", 3),
new Person("beijing", "张", 4));
}
我们在例子中添加了一个重复的key,现在如果我们来使用上面这个方法来处理下key重复的问题:
List<Person> list = initPerson();
Map<String, Integer> map = list.stream().collect(
Collectors.toMap(Person::getCity, Person::getSum, (f, s) -> f + s));
System.out.println(map);
可以看到,我们在第三个参数里通过lambda表达式指定了针对key重复的情况下要返回的值,lambda表达式的第一个代表旧值,第二个代表新值:
// 使用旧的值,本例子中结果打印:{shanghai=3, shenzhen=1, beijing=2}
(f, s) -> f
// 使用新的值替换旧的值,打印:{shanghai=3, shenzhen=1, beijing=4}
(f, s) -> s
// 对新值和旧值进行处理,比如返回新值与旧值的和:{shanghai=3, shenzhen=1, beijing=6}
(f, s) -> f + s
如果我们不想使用默认的返回类型HashMap,可以通过toMap方法的最后一个参数来进行自定义返回类型:
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
来看下实现:
List<Person> list = initPerson();
Map<String, Integer> map = list.stream().collect(
Collectors.toMap(Person::getCity, Person::getSum, (f, s) -> s + f, TreeMap::new));
System.out.println(map);
使用该重载方法,我们最终返回的对象定义为了有顺序的Map类型。
6. toConcurrentMap方法
这个方法就不多说了,toMap方法的线程安全版本。
7. summarizingInt/summarizingLong/summarizingDouble方法
这个方法比较简单,用于对Stream中的数值对象生成统计信息,返回值类型比如针对int的IntSummaryStatistics,我们前文已经了解过,包含了常用的操作:count,sum,max,min,average等操作:
List<Person> list = initPerson();
IntSummaryStatistics intSummaryStatistics = list.stream().collect(
Collectors.summarizingInt(input -> input.getSum()));
System.out.println(intSummaryStatistics.getSum());
System.out.println(intSummaryStatistics.getMax());
这其中要求对应的参数是数值类型,而针对long的LongSummaryStatistics和针对double类型的DoubleSummaryStatistics是类似的。
8.summingInt/summingLong/summingDouble方法
这三个方法更简单了,表示返回Stream中数值对象的总和,相当于上面三个方法返回值中的sum属性:
Integer sum = list.stream().collect(Collectors.summingInt(Person::getSum));
并且该方法也相当于IntStream的sum方法:
Integer sum = list.stream().mapToInt(Person::getSum).sum();
9. averagingInt/averagingLong/averagingDouble方法
见名知义,这几个方法是为了获取平均值的:
Double sum = list.stream().collect(Collectors.averagingInt(Person::getSum));
这几个方法返回值都是Double类型。
10. partitioningBy方法
该方法也是用于分组,不过是根据某一条件进行分组,最终分成满足条件的true和不满足条件的false两个分组,返回类型是Map<Boolean, Object>,看下面的例子,比如我们根据Person中sum是否大于2来进行分组:
Map<Boolean, List<Person>> map = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2));
System.out.println(map);
最终打印:
{false=[Person{city='shenzhen', surname='张', sum=1}],
true=[Person{city='beijing', surname='王', sum=2}, Person{city='shanghai', surname='李', sum=3}, Person{city='beijing', surname='张', sum=4}]}
该方法还有一个重载方法,包含两个参数,我们可以通过第二个参数在上面分组基础上再进一步做处理:
Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T, A, D> downstream)
比如我们再判断sum是否大于2的基础上,再判断city是否是beijing的:
Map<Boolean, Map<Boolean, List<Person>>> map = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2,
Collectors.partitioningBy(input -> "beijing".equals(input.getCity()))));
{false={false=[Person{city='shenzhen', surname='张', sum=1}], true=[]},
true={false=[Person{city='shanghai', surname='李', sum=3}], true=[Person{city='beijing', surname='王', sum=2}, Person{city='beijing', surname='张', sum=4}]}}
再比如说,我们在判断sum是否大于2的基础上,计算对应的个数:
Map<Boolean, Long> map = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.counting()));
{false=1, true=3}
该方法是非线程安全的。
11. reducing方法
该方法和Stream中的reduce方法功能类似,用来执行一些二目运算的操作,比如数值类型的求和,求最大值,求最小值,字符串连接,集合类型的交集并集等操作。同样,该方法也有多个重载方法,不过和Stream中的reduce方法差不多:
Stream.of(1, 2, 4, 5).collect(Collectors.reducing(0, (x, y) -> x + y));
// 等同于
Stream.of(1, 2, 4, 5).reduce(0, (x, y) -> x + y);
然后看下结合groupingBy方法使用:
// 分组,保留组内key相同的最后一个
Map<String, Optional<Person>> maxMap = list.stream().collect(Collectors.groupingBy(Person::getCity,
Collectors.reducing((f,s) -> s)));
System.out.println(maxMap);
结合partitioningBy方法使用:
// 根据条件过滤分组后,保留最大的
Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
Map<Boolean, Optional<Person>> maxBoolMap = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2,
Collectors.reducing(BinaryOperator.maxBy(sumComparator))));
System.out.println(maxBoolMap);
最终的打印结果:
{shanghai=Optional[Person{city='shanghai', surname='李', sum=3}],
shenzhen=Optional[Person{city='shenzhen', surname='张', sum=1}],
beijing=Optional[Person{city='beijing', surname='张', sum=4}]}
{false=Optional[Person{city='shenzhen', surname='张', sum=1}],
true=Optional[Person{city='beijing', surname='张', sum=4}]}
其他重载方法和reduce重载的方法类似,不多说了。
12. maxBy/minBy方法
这两个方法就比较简单了,就是通过给定的比较器获取最小,最大元素:
public static <T> Collector<T, ?, Optional<T>>
minBy(Comparator<? super T> comparator) {
return reducing(BinaryOperator.minBy(comparator));
}
public static <T> Collector<T, ?, Optional<T>>
maxBy(Comparator<? super T> comparator) {
return reducing(BinaryOperator.maxBy(comparator));
}
从源代码可以看出,其实这两个方法都是借助于reducing方法来实现的,那么上面reducing的例子我们也可以表示为:
// 根据条件过滤分组后,保留最大的
Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
Map<Boolean, Optional<Person>> maxBoolMap = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2,
Collectors.minBy(sumComparator)));
13. counting方法
用于获取Stream流中满足条件的元素数量,我们来看一个前面已经接触过的例子:
Long count = Stream.of(1, 2, 4, 5).collect(Collectors.counting());
// 等价于
Long count = Stream.of(1, 2, 4, 5).count();
// 获取满足条件的元素的数量
Map<Boolean, Long> maxBoolMap = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.counting()));
{false=1, true=3}
14. collectingAndThen方法
该方法接收两个参数,表示在第一个参数执行基础上,再执行第二个参数对应的函数表达式,我们来看几个例子:
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Double result = list.stream().collect(Collectors.collectingAndThen(Collectors.averagingInt(v -> v),
s -> s * s));
// output: 6.25
System.out.println(result);
该例子表示先对Stream的元素计算平均值,然后将平均值的平方返回,注意下返回值类型。再看一个例子:
// 根据条件过滤分组后,获取最小的
Comparator<Person> sumComparator = Comparator.comparing(Person::getSum);
Map<Boolean, Person> maxBoolMap = list.stream().collect(
Collectors.partitioningBy(input -> input.getSum() >= 2,
Collectors.collectingAndThen(Collectors.minBy(sumComparator), Optional::get)));
System.out.println(maxBoolMap);
最后看下官网的例子:
// 生成不可变List
List<Person> unmodifiableList = list.stream().collect(
Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
15. mapping方法
mapping方法用于对Stream中元素的某个具体属性做进一步的映射处理,一般是和其他方法一起组合使用。我们来简单看一下例子:
// 根据条件过滤分组后,获取组内元素的surname,并用逗号分割
Map<String, String> nameByCity
= list.stream().collect(Collectors.groupingBy(Person::getCity,
Collectors.mapping(Person::getSurname, Collectors.joining(","))));
// output:{shanghai=李, shenzhen=张, beijing=王,张}
System.out.println(nameByCity);
再看另一个例子:
// 根据条件过滤分组后,获取组内元素的surname,并用逗号分割
Map<String, Set<String>> nameByCity
= list.stream().collect(Collectors.groupingBy(Person::getCity,
Collectors.mapping(Person::getSurname, Collectors.toSet())));
// output:{shanghai=[李], shenzhen=[张], beijing=[张, 王]}
System.out.println(nameByCity);
三、总结
到这里,Collectors的静态方法基本上都学习过了,从上面这些例子可以看出,这些方法在某些条件下是可以互换,或者说实现方式不止一种。平时在工作中我们可以多尝试去使用,以加深对这些方法的了解。
本文主要参考自官网API及源代码:https://docs.oracle.com/javase/8/docs/api/
java.util.stream 库简介
网友评论