JDK1.8-Collectors方法介绍

2018-06-17  本文已影响24人  骑着乌龟去看海

一、前言

  前文说到,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 库简介

上一篇下一篇

猜你喜欢

热点阅读