JDK1.8-Collectors方法介绍
一、引言
JDK1.8提供了许多现成的静态方法来减少我们的操作,本文,我们就来了解下这些常用的方法,并且让你的代码更加优雅,为什么要说JDK1.8,因为是个质的飞越,颠覆了JAVA7很多繁琐的操作。
二、Collectors静态方法
1. toCollection
toCollection方法可以指定转换集合的类型:
1 2 | // 将Stream转换为HashSet集合 HashSet<Integer> hashSet = Stream.of(1, 2, 4, 5).collect(Collectors.toCollection(HashSet:: new )); |
2. toSet/toList方法
这两个方法比较简单,就是转为对应的集合对象。其中toList返回的是ArrayList,而toSet返回的是HashSet:
1 2 | 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流中的对象是字符串类型:
1 2 3 4 5 6 7 8 | // 默认直接拼接 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) |
方法比较简单,直接通过例子来看:
1 2 3 4 5 6 | // 没有分隔符,默认直接拼接,打印出 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,同样,该方法也是有多个重载方法:
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()); } |
这是最基础的groupingBy方法,传递单个对应的表达式即可,可以看到,这里调用了两个参数的groupingBy方法,并且返回的List默认调用的是toList方法,也就是返回的是ArrayList,再来看两个参数的方法:
1 2 3 4 5 | 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对象:
1 2 3 4 5 6 | class Person { private String city; private String surname; private Integer sum; // get set,构造方法,toString省略 } |
1 2 3 4 5 6 | 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进行分组:
1 2 3 | 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集合,然后看一下打印的结果:
1 2 3 | {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中的城市人数,这时候我们就可以使用:
1 2 3 | Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity, Collectors.summingInt(Person::getSum))); System. out .println(map); |
打印结果:
1 | {shanghai=3, shenzhen=1, beijing=6} |
紧接着,在此基础上,如果我们想指定返回的Map类型,比如说根据city进行排序的TreeMap,那么我们可以接着调用groupingBy方法的另一个重载方法:
1 2 3 | Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getCity, TreeMap:: new , Collectors.summingInt(Person::getSum))); System. out .println(map); |
最终打印结果:
1 | {beijing=6, shanghai=3, shenzhen=1} |
5. groupingByConcurrent方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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) |
再来简单看下使用:
1 2 3 | list.stream().collect(Collectors.groupingByConcurrent(Person::getCity)); list.stream().collect(Collectors.groupingByConcurrent(Person::getCity, ConcurrentSkipListMap:: new , Collectors.summingInt(Person::getSum))); |
6. toMap方法
将Stream流转换为Map对象。同样,该方法有三个重载方法,我们先来看最简单的方法:1 2 3 | 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对象:
1 2 3 4 5 | public static List<Person> initPerson() { return Arrays.asList( new Person( "shenzhen" , "张" , 1), new Person( "beijing" , "王" , 2), new Person( "shanghai" , "李" , 3)); } |
然后,使用toMap方法:
1 2 3 | 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中有重复值的情况,我们可以调用另外一个重载方法:
1 2 3 4 5 6 | 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重复的时候,是使用新值还是旧值,或者指定额外的操作(比如新值与旧值的和):
1 2 3 4 5 6 | 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重复的问题:
1 2 3 4 | 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表达式的第一个代表旧值,第二个代表新值:
1 2 3 4 5 6 | // 使用旧的值,本例子中结果打印:{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方法的最后一个参数来进行自定义返回类型:
1 2 3 4 5 | 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) { |
来看下实现:
1 2 3 4 | 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等操作:
1 2 3 4 5 | 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属性:
1 | Integer sum = list.stream().collect(Collectors.summingInt(Person::getSum)); |
并且该方法也相当于IntStream的sum方法:
1 | Integer sum = list.stream().mapToInt(Person::getSum).sum(); |
9. averagingInt/averagingLong/averagingDouble方法
见名知义,这几个方法是为了获取平均值的:
1 | Double sum = list.stream().collect(Collectors.averagingInt(Person::getSum)); |
这几个方法返回值都是Double类型。
10. partitioningBy方法
1 2 3 | Map<Boolean, List<Person>> map = list.stream().collect( Collectors.partitioningBy(input -> input.getSum() >= 2)); System. out .println(map); |
最终打印:
1 2 | { 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}]} |
该方法还有一个重载方法,包含两个参数,我们可以通过第二个参数在上面分组基础上再进一步做处理:
1 2 | Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream) |
比如我们再判断sum是否大于2的基础上,再判断city是否是beijing的:
1 2 3 | Map<Boolean, Map<Boolean, List<Person>>> map = list.stream().collect( Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.partitioningBy(input -> "beijing" . equals (input.getCity())))); |
结果:
1 2 | { 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的基础上,计算对应的个数:
1 2 | Map<Boolean, Long> map = list.stream().collect( Collectors.partitioningBy(input -> input.getSum() >= 2, Collectors.counting())); |
结果:
1 | { false =1, true =3} |
该方法是非线程安全的。
11. reducing方法1 2 3 | 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方法使用:
1 2 3 4 | // 分组,保留组内key相同的最后一个 Map<String, Optional<Person>> maxMap = list.stream().collect(Collectors.groupingBy(Person::getCity, Collectors.reducing((f,s) -> s))); System. out .println(maxMap); |
结合partitioningBy方法使用:
1 2 3 4 5 6 | // 根据条件过滤分组后,保留最大的 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); |
最终的打印结果:
1 2 3 4 5 6 | {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方法1 2 3 4 5 6 7 8 9 | 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的例子我们也可以表示为:
1 2 3 4 5 | // 根据条件过滤分组后,保留最大的 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流中满足条件的元素数量,我们来看一个前面已经接触过的例子:
1 2 3 4 5 6 7 | 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())); |
结果:
1 | { false =1, true =3} |
14. collectingAndThen方法
该方法接收两个参数,表示在第一个参数执行基础上,再执行第二个参数对应的函数表达式,我们来看几个例子:
1 2 3 4 5 | 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的元素计算平均值,然后将平均值的平方返回,注意下返回值类型。再看一个例子:
1 2 3 4 5 6 | // 根据条件过滤分组后,获取最小的 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); |
最后看下官网的例子:
1 2 3 | // 生成不可变List List<Person> unmodifiableList = list.stream().collect( Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); |
15. mapping方法
mapping方法用于对Stream中元素的某个具体属性做进一步的映射处理,一般是和其他方法一起组合使用。我们来简单看一下例子:
1 2 3 4 5 6 7 | // 根据条件过滤分组后,获取组内元素的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); |
再看另一个例子:
1 2 3 4 5 6 7 | // 根据条件过滤分组后,获取组内元素的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); |
去重:
List<Order> orders = Lists.newArrayList(); // 按照订单编号去重 orders = orders.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Order::getNum))), ArrayList::new)); // 按照订单编号和类型去重 orders = orders.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getNum() + ";" + o.getType()))), ArrayList::new));
三、总结
到这里,Collectors的静态方法基本上都学习过了,从上面这些例子可以看出,这些方法在某些条件下是可以互换,或者说实现方式不止一种。平时在工作中我们可以多尝试去使用,以加深对这些方法的了解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具