在Java开发中,对数据进行分组是极高频的操作,Java集合框架提供的TreeMap和List组合可以很好地满足大多数分组场景的需求,既能够利用TreeMap的排序特性得到有序的分组结果,又能通过List存储同组内的多个数据元素。

核心实现思路
分组的核心逻辑是先确定分组的维度作为键,然后将对应维度的数据放入List作为值,TreeMap负责维护键的有序性,List负责存储同组的所有元素。整体流程可以分为三步:
- 初始化TreeMap实例,可根据需要传入自定义的比较器
- 遍历原始数据,计算每个数据对应的分组键
- 从TreeMap中获取对应键的List,若不存在则新建List放入,再将当前数据加入List
基础分组示例
假设我们有一个用户类,需要按照用户的年龄对用户进行分组,分组结果按照年龄升序排列。
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
// 用户实体类
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class GroupDemo {
public static void main(String[] args) {
// 准备测试数据
List<User> userList = new ArrayList<>();
userList.add(new User("张三", 20));
userList.add(new User("李四", 22));
userList.add(new User("王五", 20));
userList.add(new User("赵六", 25));
userList.add(new User("孙七", 22));
// 初始化TreeMap,键为年龄,值为对应用户列表
TreeMap<Integer, List<User>> ageGroupMap = new TreeMap<>();
// 遍历用户数据进行分组
for (User user : userList) {
int ageKey = user.getAge();
// 获取当前年龄对应的用户列表,不存在则新建
List<User> groupUsers = ageGroupMap.get(ageKey);
if (groupUsers == null) {
groupUsers = new ArrayList<>();
ageGroupMap.put(ageKey, groupUsers);
}
groupUsers.add(user);
}
// 输出分组结果
for (Integer age : ageGroupMap.keySet()) {
System.out.println("年龄:" + age + ",用户列表:");
for (User user : ageGroupMap.get(age)) {
System.out.println(" 姓名:" + user.getName());
}
}
}
}
上述代码运行后,分组结果会按照年龄从小到大输出,符合TreeMap的默认排序特性。
自定义排序的分组场景
如果需要按照自定义的规则对分组键排序,比如按照字符串长度升序排列,只需要给TreeMap传入自定义的比较器即可。
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
public class CustomSortGroupDemo {
public static void main(String[] args) {
// 原始字符串数据
List<String> dataList = new ArrayList<>();
dataList.add("apple");
dataList.add("banana");
dataList.add("cat");
dataList.add("dog");
dataList.add("elephant");
dataList.add("fox");
// 按照字符串长度分组,分组键按照长度升序排列
TreeMap<Integer, List<String>> lengthGroupMap = new TreeMap<>((a, b) -> a - b);
for (String str : dataList) {
int lengthKey = str.length();
List<String> groupStrs = lengthGroupMap.get(lengthKey);
if (groupStrs == null) {
groupStrs = new ArrayList<>();
lengthGroupMap.put(lengthKey, groupStrs);
}
groupStrs.add(str);
}
// 输出分组结果
for (Integer length : lengthGroupMap.keySet()) {
System.out.println("字符串长度:" + length + ",对应字符串:");
for (String str : lengthGroupMap.get(length)) {
System.out.println(" " + str);
}
}
}
}
实践注意事项
1. 避免重复创建List
上述示例中通过判断get()返回值是否为null来决定是否新建List,也可以使用computeIfAbsent方法简化代码,减少重复判断逻辑。
// 简化分组逻辑,computeIfAbsent会在键不存在时自动创建List
for (User user : userList) {
int ageKey = user.getAge();
ageGroupMap.computeIfAbsent(ageKey, k -> new ArrayList<>()).add(user);
}
2. 线程安全问题
TreeMap和ArrayList都不是线程安全的,如果在多线程环境下进行分组操作,需要额外加锁,或者使用Collections.synchronizedSortedMap包装TreeMap,同时保证List的线程安全。
3. 分组键的选择
分组键需要保证hashCode和equals方法的正确实现,否则TreeMap可能无法正确识别相同的分组键,导致分组结果异常。如果是自定义对象作为分组键,还需要确保比较器的逻辑和对象的相等性判断一致。
4. 性能考量
TreeMap的插入和查询时间复杂度是O(log n),如果分组键不需要有序性,优先考虑使用HashMap,性能会更高。只有在确实需要有序分组结果时,才选择TreeMap配合List的方案。