关于 Collectors.groupingBy 按照某个字段进行分组并排序的问题


最近,在工作中遇到一个问题,是关于Collectors.groupingBy 按照某个字段进行分组并排序的问题。特此记录。

众所周知,对一个 List<Map<String, Object>> 类型的集合,如果需要按照元素中的某个字段进行分组,通常使用 lambda 表达式对集合进行流处理。然后使用 Collectors.groupingBy 函数进行分组,此方法源码如下:

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

该方法需要一个 Function 函数。返回方法如下:

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);
}

从此方法中看出,除了一个 Function 函数外,还需要一个 默认的 HashMap,但是 HashMap 是一个无序集合,它存储键值对(key-value)但是不保证顺序。也就是说,当你遍历 HashMap 时,元素的顺序可能可插入的顺序无关。这是 HashMap 的特性,不像 LinkedHashMap 或者 TreeMap 那样可以保证顺序。所以,如果要保证按照某个字段进行分组后,还能同时保证排序,groupingBy 方法中需要将 HashMap 改为 LinkedHashMap 或者 TreeMap 即可。

示例如下:

1、使用 HashMap 接收数据时:

package com.example.demo.test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Test {
public static void main(String[] args) {
List<Map<String, Object>> list = getDate();
Map<Object, List<Map<String, Object>>> dateMap = list.stream().
collect(Collectors.groupingBy(e -> e.get("date")));
System.out.println(dateMap);
}
public static List<Map<String, Object>> getDate(){
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> map1 = new HashMap<>();
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> map3 = new HashMap<>();
map1.put("date", "20240830");
map1.put("value", "张三1");
map2.put("date", "20240926");
map2.put("value", "张三2");
map3.put("date", "20241028");
map3.put("value", "张三3");
list.add(map1);
list.add(map2);
list.add(map3);
return list;
}
}

执行 main 方法,返回的 dateMap 顺序应该和 list 中的添加顺序一致。打印结果如下:

{20240926=[{date=20240926, value=张三2}], 20241028=[{date=20241028, value=张三3}], 20240830=[{date=20240830, value=张三1}]}

结果并非如此。

2、使用 LinkedHashMap/TreeMap 接收数据

package com.example.demo.test;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Classname Test
* @Description TODO
* @Date 2025/1/4 14:59
* @Created by wangrui
*/
public class Test {
public static void main(String[] args) {
List<Map<String, Object>> list = getDate();
Map<Object, List<Map<String, Object>>> linkedHashMap = list.stream()
.collect(Collectors.groupingBy(e -> e.get("date"), LinkedHashMap::new, Collectors.toList()));
Map<Object, List<Map<String, Object>>> treeMap = list.stream()
.collect(Collectors.groupingBy(e -> e.get("date"), TreeMap::new, Collectors.toList()));
System.out.println("linkedHashMap接收数据:" + linkedHashMap);
System.out.println("treeMap接收数据:" + treeMap);
}
public static List<Map<String, Object>> getDate(){
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> map1 = new HashMap<>();
Map<String, Object> map2 = new HashMap<>();
Map<String, Object> map3 = new HashMap<>();
map1.put("date", "20240830");
map1.put("value", "张三1");
map2.put("date", "20240926");
map2.put("value", "张三2");
map3.put("date", "20241028");
map3.put("value", "张三3");
list.add(map3);
list.add(map1);
list.add(map2);
return list;
}
}

执行 main 方法,返回的 dateMap 顺序应该和 list 中的添加顺序一致。打印结果如下:

linkedHashMap接收数据:{20241028=[{date=20241028, value=张三3}], 20240830=[{date=20240830, value=张三1}], 20240926=[{date=20240926, value=张三2}]}
treeMap接收数据:{20240830=[{date=20240830, value=张三1}], 20240926=[{date=20240926, value=张三2}], 20241028=[{date=20241028, value=张三3}]}

此时发现,linkedHashMap 接收数据是按照 list 插入元素的顺序进行排序的,但是 TreeMap 并非如此。这是因为 linkedHashMap 和 TreeMap 两者之间的差别。

1、LinkedHashMap:

  • LinkedHashMap 保留了插入元素的顺序。这意味着, list 中的 date 元素(对应Map元素)出现的顺序会决定 LinkedHashMap 的顺序。

  • 因此,linkedHashMap 中的键值对是按照 List 中的顺序排列的。

2、TreeMap:

  • TreeMap 是一个基于红黑树的有序 Map 实现。它会对键进行自然排序(如果键实现了 Comparable 接口) 或者使用指定的 Comparator 进行排序。
  • 由于 e.get("date") 此时的数据是一个String 类型,String 实现了 Comparable 接口,此时会按照自然顺序进行排序。这就是以上代码中, 使用 TreeMap 进行接收数据时,打印的结果是按照自然排序排列的原因。
  • 因此,treeMap 中的键值对顺序可能与 list 的顺序不同,但一定是按键值的排序顺序排列的。