Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

Collectors 操作

Collectors 是 Java 8 加入的操作类,位于 java.util.stream 包下。它会根据不同的策略将元素收集归纳起来,比如最简单常用的是将元素装入Map、Set、List 等可变容器中。特别对于 Java 8 Stream Api 来说非常有用。它提供了collect() 方法来对 Stream 流进行终结操作派生出基于各种策略的结果集。

Collectors 提供了一系列的静态方法供我们使用,通常情况我们静态导入即可使用。接下来我们来看看都提供了哪些方法。示例的list如下:

1
2
3
4
5
6
7
List<String> servers = new ArrayList<>();
servers.add("Felordcn");
servers.add("Tomcat");
servers.add("Jetty");
servers.add("Undertow");
servers.add("Resin");
servers.add("Tomcat");

类型归纳

这是一个系列,作用是将元素分别归纳进可变容器 List、Map、Set、Collection 或者ConcurrentMap 。

1
2
3
4
5
Collectors.toList();
Collectors.toMap();
Collectors.toSet();
Collectors.toCollection();
Collectors.toConcurrentMap();

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//类型归纳
List<String> newList = servers.stream().distinct().collect(Collectors.toList());
Set<String> newSet = servers.stream().collect(Collectors.toSet());
Map<String, Integer> newMap = servers.stream().collect(Collectors.toMap(e -> e, String::length,(a,b)->b));

LinkedList<String> newLinkedList = servers.stream().distinct().collect(Collectors.toCollection(LinkedList::new));

ConcurrentMap<String, Integer> newConcurrentMap = servers.stream().collect(
Collectors.toConcurrentMap(e -> e, String::length, (a, b) -> b));

System.out.println("newList = " + JSON.toJSONString(newList) + "\n"
+ "newSet = " + JSON.toJSONString(newSet) + "\n"
+ "newMap = " + JSON.toJSONString(newMap) + "\n"
+ "newLinkedList = " + JSON.toJSONString(newLinkedList) + "\n"
+ "newConcurrentMap = " + JSON.toJSONString(newConcurrentMap) + "\n"
);

输出

1
2
3
4
5
newList = ["Felordcn","Tomcat","Jetty","Undertow","Resin"]
newSet = ["Tomcat","Resin","Felordcn","Jetty","Undertow"]
newMap = {"Tomcat":6,"Resin":5,"Felordcn":8,"Jetty":5,"Undertow":8}
newLinkedList = ["Felordcn","Tomcat","Jetty","Undertow","Resin"]
newConcurrentMap = {"Tomcat":6,"Resin":5,"Felordcn":8,"Jetty":5,"Undertow":8}

joining

将元素以某种规则连接起来。该方法有三种重载 joining() ,joining(CharSequence delimiter)joining(CharSequence delimiter,CharSequence prefix,CharSequence suffix)

1
2
3
4
5
6
7
8
//   输出 FelordcnTomcatJettyUndertowResin
servers.stream().collect(Collectors.joining());

// 输出 Felordcn,Tomcat,Jetty,Undertow,Resin
servers.stream().collect(Collectors.joining("," ));

// 输出 [Felordcn,Tomcat,Jetty,Undertow,Resin]
servers.stream().collect(Collectors.joining(",", "[", "]"));

用的比较多的是读取 HttpServletRequest 中的 body :

1
HttpServletRequest.getReader().lines().collect(Collectors.joining());

collectingAndThen

该方法先执行了一个归纳操作,然后再对归纳的结果进行 Function 函数处理输出一个新的结果。

1
2
// 比如我们将servers joining 然后转成大写,结果为: FELORDCN,TOMCAT,JETTY,UNDERTOW,RESIN   
servers.stream.collect(Collectors.collectingAndThen(Collectors.joining(","), String::toUpperCase));

groupingBy

按照条件对元素进行分组,和 SQL 中的 group by 用法有异曲同工之妙,通常也建议使用 Java 进行分组处理以减轻数据库压力。groupingBy 也有三个重载方法 我们将 servers 按照长度进行分组:

1
2
// 按照字符串长度进行分组    符合条件的元素将组成一个 List 映射到以条件长度为key 的 Map<Integer, List<String>> 中
servers.stream.collect(Collectors.groupingBy(String::length))

默认group后都是放入一个list中的

1
2
3
4
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}

如果不想Value是list可以使用

1
2
//Map<Integer, Set<String>>
servers.stream.collect(Collectors.groupingBy(String::length, Collectors.toSet()))

同(定义了Map)

1
2
Supplier<Map<Integer,Set<String>>> mapSupplier = HashMap::new;
Map<Integer,Set<String>> collect = servers.stream.collect(Collectors.groupingBy(String::length, mapSupplier, Collectors.toSet()));

如果需要线程同步可以使用

1
2
Supplier<Map<Integer, Set<String>>> mapSupplier = () -> Collections.synchronizedMap(new HashMap<>());
Map<Integer, Set<String>> collect = servers.stream.collect(Collectors.groupingBy(String::length, mapSupplier, Collectors.toSet()));

或者使用Collectors 的另一个方法 groupingByConcurrent 给我们提供了解决方案。用法和 groupingBy 差不多。

partitioningBy

Collectors.partitioningBy()该方法会将流中符合断言的、不符合断言的元素分别归纳到两个 key 分别为 true 和 false 的 Map 中,我们可以归类得到符合和不符合的元素集。

1
2
3
4
public static <T>
Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
return partitioningBy(predicate, toList());
}

可以看出函数的参数一个Predicate接口,那么这个接口的返回值是boolean类型的,也只能是boolean类型,然后他的返回值是Map的key是boolean类型,也就是这个函数的返回值只能将数据分为两组也就是ture和false两组数据。
使用如下

1
2
3
4
5
6
7
Map<Boolean, List<String>> f = servers.stream().collect(Collectors.partitioningBy(s -> !s.startsWith("F")));

List<String> trues = f.get(Boolean.TRUE);
System.out.println("不以 F 开头的: " + trues);

List<String> falses = f.get(Boolean.FALSE);
System.out.println("以 F 开头的: " + falses);

counting

该方法归纳元素的的数量。

1
2
3
//count = 6
Long count = servers.stream().collect(Collectors.counting());
System.out.println("count = " + count);

maxBy/minBy

这两个方法分别提供了查找大小元素的操作,它们基于比较器接口 Comparator 来比较 ,返回的是一个 Optional 对象。 我们来获取 servers 中最小长度的元素:

1
2
 // Jetty  
Optional<String> min = servers.stream().collect(Collectors.minBy(Comparator.comparingInt(String::length)));

这里其实 Resin 长度也是最小,这里遵循了 “先入为主” 的原则 。当然 Stream.min() 可以很方便的获取最小长度的元素。maxBy 同样的道理。

summingInt/Double/Long

用来做累加计算。计算元素某个属性的总和,类似 Mysql 的 sum 函数,比如计算各个项目的盈利总和、计算本月的全部工资总和等等。我们这里就计算一下 servers 中字符串的长度之和:

1
2
// 总长度 38
servers.stream().collect(Collectors.summingInt(s -> s.length()));

summarizingInt/Double/Long

summarizingInt、summarizingDouble、summarizingLong 这三个方法通过对元素某个属性的提取,会返回对元素该属性的统计数据对象,分别对应 IntSummaryStatisticsDoubleSummaryStatisticsLongSummaryStatistics。我们对 servers 中元素的长度进行统计:

1
2
3
DoubleSummaryStatistics doubleSummaryStatistics = servers.stream().collect(Collectors.summarizingDouble(String::length));
// {count=5, sum=32.000000, min=5.000000, average=6.400000, max=8.000000}
System.out.println("doubleSummaryStatistics.toString() = " + doubleSummaryStatistics.toString());

结果 DoubleSummaryStatistics 中包含了 总数,总和,最小值,最大值,平均值 五个指标。

mapping

该方法是先对元素使用 Function 进行再加工操作,然后用另一个Collector 归纳。比如我们先去掉 servers 中元素的首字母,然后将它们装入 List 。

1
2
// [elordcn, omcat, etty, ndertow, esin]
servers.stream().collect(Collectors.mapping(s -> s.substring(1), Collectors.toList()));

有点类似 Stream 先进行了 map 操作再进行 collect :

1
servers.stream().map(s -> s.substring(1)).collect(Collectors.toList());

reducing

了解reducing必须了解其参数 BinaryOperator<T> ,这是一个函数式接口,是给两个相同类型的量,返回一个跟这两个量相同类型的一个结果,伪表达式为 (T,T) -> T。默认给了两个实现 maxByminBy,根据比较器来比较大小并分别返回最大值或者最小值。当然你可以灵活定制。然后 reducing 就很好理解了,元素两两之间进行比较根据策略淘汰一个,随着轮次的进行元素个数就是 reduce 的。那这个有什么用处呢? Java 官方给了一个例子:统计每个城市个子最高的人。

1
2
3
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Map<String, Optional<Person>> tallestByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(BinaryOperator.maxBy(byHeight))));

上面是根据 Height 属性找最高的 Person ,而且如果这个属性没有初始化值或者没有数据,很有可能拿不到结果所以给出的是 Optional<Person>。 如果我们给出了 identity 作一个基准值,那么我们首先会跟这个基准值进行 BinaryOperator 操作。 比如我们给出高于 2 米 的人作为 identity。 我们就可以统计每个城市不低于 2 米 而且最高的那个人,当然如果该城市没有人高于 2 米则返回基准值identity :

1
2
3
4
5
6
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Person identity= new Person();
identity.setHeight(2.);
identity.setName("identity");
Map<String, Person> collect = persons.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(identity, BinaryOperator.maxBy(byHeight))));

这时候就确定一定会返回一个 Person 了,最起码会是基准值identity 不再是 Optional 。
还有些情况,我们想在 reducing 的时候把 Person 的身高先四舍五入一下。这就需要我们做一个映射处理。定义一个 Function<? super T, ? extends U> mapper 来干这个活。那么上面的逻辑就可以变更为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Person identity = new Person();
identity.setHeight(2.0D);
identity.setName("identity");
// 定义映射 处理 四舍五入
Function<Person, Person> mapper = ps -> {
Double height = ps.getHeight();

BigDecimal decimal = new BigDecimal(height);
Double d = decimal.setScale(1, BigDecimal.ROUND_HALF_UP).doubleValue();
ps.setHeight(d);
return ps;
};
Map<String, Person> collect = persons.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(identity, mapper, BinaryOperator.maxBy(byHeight))));