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::new​​、​​PriorityQueue::new​

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

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

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

深入解读Collectors集合收集器_i++



操作流的一些常用方法

深入解读Collectors集合收集器_测试数据_02

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的内部类

深入解读Collectors集合收集器_函数式接口_03

示例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接口的实现类

具体的有

深入解读Collectors集合收集器_测试数据_04

示例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}