深入解读Collectors集合收集器

x33g5p2x  于2021-12-18 转载在 其他  
字(10.3k)|赞(0)|评价(0)|浏览(530)

Collectors 要和 Stream-API 结合起来才能起到效果

前置文章推荐:建议先掌握 stream api 后再来看

《java8 Stream接口的深入解读,stream接口内部的方法你都熟悉吗?》

Collectors 是java.util.stream下的工具类

因此Collectors的主要用途是收集stream中的元素。
常用的方法有

  1. toList()、toSet()
  2. toMap(Function, Function)
  3. toMap(Function, Function, BinaryOperator)
  4. toMap(Function, Function, BinaryOperator, Supplier)
  5. toConcurrentMap与Map一样有3种
  6. toCollection(Supplier)

不带参数的toList、toSet使用方式,流对象.collect(Collectors.toSet()) 或者 .collect(Collectors.toList())

重点讲一下带参数的几个方法,这几个方法也是常用的收集方式

1、 toMap(Function, Function)

Map的结构由key 和 value 组成,因此这里的两个Function类型的函数,分表用于获得key和value的

import lombok.Builder;
import lombok.Data;

import java.util.*;
import java.util.stream.Collectors;

@Data
@Builder
class User {
    String name;
    String sex;
    int age;
}

public class Demo {

    public static void main(String[] args) {

        // 准备测试数据
        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().name(String.valueOf(i)).sex("男").age(11).build());
        }
        // 得到转换后的Map
        Map<String, Object> map = users.stream()
                .collect(Collectors.toMap(k -> k.name + k.sex , v -> v.age));

        // 遍历map
        map.forEach((k, v) -> {
            System.out.println(k + " " + v);
        });

    }
}
2、toMap(Function, Function, BinaryOperator) 解决重复key抛异常问题

在key,value的基础上多了一个函数式接口,其作用是为了处理重复key值问题,如果上面两个参数的toMap出现了重复的key值,那么就会抛异常,而新增一个函数式接口的作用就是用于手动处理

BinaryOperator 入参两个value,出参一个
写法可以是(x,y)->z 意思是x是第一个value,y是第二个value值,z则是新的值,可以自己随意更改,但是返回值的类型一定和x,y相同,例如x,y都是字符串则返回的z也是字符串。

import lombok.Builder;
import lombok.Data;

import java.util.*;
import java.util.stream.Collectors;

@Data
@Builder
class User {
    String id;
    String sex;
    int age;
}

public class Demo {

    public static void main(String[] args) {

        // 准备测试数据
        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().id(String.valueOf(i)).sex("男").build());
        }
        // 得到转换后的Map
        Map<String, String> map = users.stream().parallel()
                .collect(Collectors.toMap(k -> k.sex, v -> v.id, (x, y) -> {
                    System.out.println("第一个值的value:"+x);
                    System.out.println("第二个值的value:"+y);
                    System.out.println("===========保留:"+y);
                    return y;
                }));

        // 遍历map
        map.forEach((k, v) -> {
            System.out.println("最终结果:" + k + " " + v);
        });

    }
}
第一个值的value:1
第二个值的value:2
===========保留:2
第一个值的value:0
第二个值的value:2
===========保留:2
最终结果:男 2
3、toMap(Function, Function, BinaryOperator, Supplier) 返回自定义的Map实现类,例如HashMap、ConcurrentHashMap等

最后一个参数一般都是
ConcurrentHashMap::new
HashMap::new 这种形式的

4、toCollection(Supplier)

需要传入一个实现了Collection接口的实现类的构造方法引用
例如:ArrayList::newPriorityQueue::new

Collection接口的实现类可以通过IDEA 的快捷键 Ctrl + H 显示器类继承关系

该接口的实现类太多了,重点关注一些常用的。

会发现 List、Set、Queue(这个需要关注,因为阻塞队列的7种都可以传入),此方法就不深究了,能传入的集合类的实现类太多了

操作流的一些常用方法

1、groupingBy 分组根据传入的方法返回值进行分组
import lombok.Builder;
import lombok.Data;

import java.util.*;
import java.util.stream.Collectors;

@Data
@Builder
class User {
    String id;
    String sex;
    int age;
}

public class Demo {

    public static void main(String[] args) {

        // 准备测试数据
        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().id(String.valueOf(i)).sex("男").age(i*2).build());
            users.add(User.builder().id(String.valueOf(i)).sex("女").age(i*2+1).build());
        }

        // 一个参数的分组,按照字段值进行分组
        final Map<String, List<User>> map = users.stream().collect(Collectors.groupingBy(User::getSex));
        System.out.println(map);
        // 使用2个参数的分组
        final Map<String, Set<User>> map2 = users.stream().collect(Collectors.groupingBy(User::getSex,Collectors.toSet()));
        System.out.println(map2);
        // 使用3个参数的分组
        final Map<String, Set<User>> map3 = users.stream().collect(Collectors.groupingBy(User::getSex,TreeMap::new,Collectors.toSet()));
        System.out.println(map3);
        
    }
}
{女=[User(id=0, sex=女, age=1), User(id=1, sex=女, age=3), User(id=2, sex=女, age=5)], 男=[User(id=0, sex=男, age=0), User(id=1, sex=男, age=2), User(id=2, sex=男, age=4)]}
{女=[User(id=2, sex=女, age=5), User(id=1, sex=女, age=3), User(id=0, sex=女, age=1)], 男=[User(id=0, sex=男, age=0), User(id=2, sex=男, age=4), User(id=1, sex=男, age=2)]}
{女=[User(id=2, sex=女, age=5), User(id=1, sex=女, age=3), User(id=0, sex=女, age=1)], 男=[User(id=0, sex=男, age=0), User(id=2, sex=男, age=4), User(id=1, sex=男, age=2)]}
2、groupingByConcurrent并发分组,因并行流下使用groupingBy不会以并行流的方式处理,可以使用这个并发的分组操作

一个参数的groupingByConcurrent底层是ConcurrentHashMap进行存储
二个参数的groupingByConcurrent,第二个参数传入自定义的并发Map的实现,注意存在线程安全问题,所以一样要是线程安全的Map
三个参数的groupingByConcurrent和上面一样,唯一区别groupingBy 进行收集元素的集合必须是线程安全的Map

3、partitioningBy 条件分组,返回值为true的一组,false的另外一组

有2种方法,默认使用ArrayList作为容器分成2组,第二个参数用于改变存储每一组元素的容器

import lombok.Builder;
import lombok.Data;

import java.util.*;
import java.util.stream.Collectors;

@Data
@Builder
class User {
    String id;
    String sex;
    int age;
}

public class Demo {

    public static void main(String[] args) {

        // 准备测试数据
        final ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            users.add(User.builder().id(String.valueOf(i)).sex("男").age(i*2).build());
            users.add(User.builder().id(String.valueOf(i)).sex("女").age(i*2+1).build());
        }

        // 一个参数的分组,按照字段值进行分组
        final Map<Boolean, List<User>> sexMap = users.stream()
        .collect(Collectors.partitioningBy(x -> x.sex.equals("女")));
        System.out.println(sexMap);
        // 使用2个参数的分组,可以传入想要的容器,比如Set
        final Map<Boolean, List<User>> sexMap2 = users.stream()
        .collect(Collectors.partitioningBy(x -> x.sex.equals("女"),Collectors.toList()));
        System.out.println(sexMap2);

    }
}
4、joining 调用元素的toString()方法并且将其连在一起(底层是StringBuilder.append

有三种joining

  1. 无参:没有连接符
  2. 一个参数:传入一个连接符,中间用这个连接符进行连接,例如:传入-[A,B] 会变成 A-B。底层是StringJoiner
  3. 三个参数:在连接符的基础上,多了首尾连接符。底层是StringJoiner,一个参数间接调用了它
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class JoiningDemo {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B", "C");
        final String join1 = list.stream().collect(Collectors.joining());
        final String join2 = list.stream().collect(Collectors.joining("-"));
        final String join3 = list.stream().collect(Collectors.joining("-", "[", "]"));
        System.out.println(join1); // ABC
        System.out.println(join2); // A-B-C
        System.out.println(join3); // [A-B-C]
    }
}
ABC
A-B-C
[A-B-C]
5、mapping

mapping(Function<? super T, ? extends U> , Collector<? super U, A, R> )

传入的第一个函数式接口说明
该方法传入的第一个参数是一个Function接口,并且根据砖石表达式得知,入参T出参返回值U是两个不同的类型。

根据流的操作,入参是流的元素对象,而出参只要不是流元素对象即可,比如出参是字符串,数字等其它类型即可。

也即第一个参数只要是x->y形式即可,x是流中的元素,而y必须不同与x的类型,可以是字符串,x的内部属性,或者数字运算后的结果等其它类型即可

传入的第二个函数式接口说明
第二个参数要求是Collector接口的实现类,根据IDEA中显示的类继承关系(Ctrl + H),可以发现只有一个实现类,CollectorImpl是Collectors的内部类

示例1:

@Data
@Builder
class User {
    int age;
    String sex;
}

public class MappingDemo {

    public static void main(String[] args) {
        final String collect = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.mapping(x -> "年龄:"+x.getAge(),
                        Collectors.joining(",", "[", "]")));
        System.out.println(collect);
    }
}

如下是执行的结果。
这里面我们的想法是利用字符串"年龄:" 与 User的属性age进行拼串,成为新的流元素,于是就有了3个字符串。
然后传入第二个参数,进行后续的操作,例如 joining ,就形成了下面的带[]的最终字符串。

[年龄:1,年龄:2,年龄:3]

第二个参数还可以是其它方式,单一定要是Collector接口的实现类
具体的有

示例2:
第二个参数传toList

public class MappingDemo {

    public static void main(String[] args) {
        final List<String> list = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.mapping(x -> "年龄:" + x.getAge(),
                        Collectors.toList()));
        System.out.println(list);
    }
}

打印的结果和上面一样,只不过这里是一个List,而上面是一个完整的字符串
示例3:
传入Collectors.averagingInt求平均年龄

public class MappingDemo {

    public static void main(String[] args) {
        final Double avg = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.mapping(x -> x.getAge(),
                        Collectors.averagingInt(x -> x)));
        System.out.println("年龄的平均值:"+avg);
    }
}
6、collectingAndThen

收集后,对收集结果进行后续的操作,实际上用处不大。

public class MappingDemo {

    public static void main(String[] args) {
        final Integer sumAge男 = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                // 根据性别分组,然后对分成的两组数据进行操作。注意分组返回的是一个Map,如果使用toList那么意思就是对后续的List进行后续操作
                .collect(Collectors.collectingAndThen(Collectors.groupingBy(User::getSex), x -> {
                    final List<User> 男 = x.get("男");// 得到男性的所有User
                    final List<User> 女 = x.get("女");
                    return 男.stream().collect(Collectors.summingInt(User::getAge));//求男性的年龄总和
                }));

        System.out.println(sumAge男);// 打印男性年龄总和4

    }
}
7、counting、minBy、maxBy

counting:计算流元素个数
最大最小需要元素之间可以比较,故需要传入比较器
minBy:求最小值
maxBy:求最大值
求最大值最小值意义不大,因为Stream本身就提供了max,min。
唯一可能有用到的地方就是对分组后的数据求最值

public class MappingDemo {

    public static void main(String[] args) {
        final Optional<User> collect = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.maxBy((x1, x2) -> x1.getAge() - x2.getAge()));
        // 实际上调用stream的max或者min即可,这种写法反而麻烦。
        // 暂时举不出一个好一点的例子

        System.out.println(collect.get());// 找出年龄最大的

    }
}
8、求和summingInt、summingLong、summingDouble
public class MappingDemo {

    public static void main(String[] args) {
        final IntSummaryStatistics statistics = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.summarizingInt(x -> x.getAge()));
        System.out.println("年龄的总:" + statistics);
    }
}
9、平均数averagingInt、averagingLong、averagingDouble

这个方法在5、示例3中有用到

使用方法很简单,就是流元素的类型要和对应的类型匹配即可。

public class MappingDemo {

    public static void main(String[] args) {
        final Double avg = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.averagingInt(User::getAge));

        System.out.println(avg);

    }
}
10、归约reducing,有合并的含义

例如:A,B,C
reduce操作就相当于 A和B进行操作得到一个结果Z,然后Z和C进行操作得到最终结果。

11、统计summarizingInt、summarizingLong、summarizingDouble

Int、Long、Double主要区别在于平均值可能是小数,以及长度的限制

返回元素个数、总和、最小值、最大值、平均值

public class MappingDemo {

    public static void main(String[] args) {
        final IntSummaryStatistics collect = Arrays.asList(
                User.builder().age(1).sex("男").build(),
                User.builder().age(2).sex("女").build(),
                User.builder().age(3).sex("男").build()
        ).stream()
                .collect(Collectors.summarizingInt(User::getAge));

        System.out.println(collect);
    }
}
IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}

相关文章