Java8 Stream sorted排序实战:从Comparator基础到多级排序进阶
1. 从零开始理解Stream sorted排序
第一次接触Java8的Stream sorted方法时,我盯着那段链式调用的代码看了足足十分钟。就像刚拿到新手机的老人,明明按键就在眼前,却不知道从哪下手。后来在实际项目中踩过几次坑才明白,sorted()本质上就是个"智能排序器",而Comparator就是告诉它排序规则的"说明书"。
先看最简单的自然排序。假设我们有个字符串列表List<String> names = Arrays.asList("张三", "李四", "王五");,用names.stream().sorted().forEach(System.out::println);就能按字典序输出。这就像把一堆杂乱的书本按书名首字母排列,但现实中的需求往往更复杂。有次我处理用户数据时,发现直接用sorted()抛出了异常——原来列表里的自定义对象没实现Comparable接口,就像试图给一堆没有条形码的商品排序,系统根本不知道谁该排在前面。
这时候就需要祭出Comparator了。最基础的写法是用lambda表达式:list.stream().sorted((o1, o2) -> o1.getAge() - o2.getAge())。这个减号操作符就像天平,当左边大于右边返回正数就是升序,反过来就是降序。不过实际开发中我更喜欢用Comparator.comparing(User::getAge)这种写法,代码更语义化,就像直接告诉系统:"嘿,按年龄排序"。
2. Comparator的七十二变
Comparator的灵活之处在于它能像乐高积木一样组合。记得有次做电商项目,商品要按销量降序排列,销量相同的再按价格升序排。这用传统写法得嵌套多层if-else,而用Stream sorted只需要:
products.stream() .sorted(Comparator.comparing(Product::getSales).reversed() .thenComparing(Product::getPrice)) .collect(Collectors.toList());这里的reversed()就像把排序规则倒过来看,而thenComparing相当于说"前面的规则分不出胜负时,再用这个新规则"。这种链式调用比俄罗斯套娃式的if-else清爽多了。
更复杂的场景比如处理多语言排序时,可以用Comparator.comparing(String::length, Comparator.reverseOrder())实现先按字符串长度降序,再按字母顺序升序。这就像图书馆先按书厚度分大类,再按书名细分。实测下来,这种写法比传统Collections.sort性能更好,特别是在并行流处理时。
3. 多级排序的实战技巧
真实项目中的排序需求往往像洋葱一样有多层。去年做HR系统时遇到个典型场景:员工要先按部门字母顺序排,同部门再按职级倒序,职级相同的再按入职时间正序。用thenComparing组合起来就像搭积木:
employees.stream() .sorted(Comparator.comparing(Employee::getDepartment) .thenComparing(Employee::getLevel, Comparator.reverseOrder()) .thenComparing(Employee::getHireDate)) .forEach(System.out::println);这里有个容易踩的坑:thenComparing连接的每个Comparator必须独立完整。有次我写成.thenComparing(Employee::getLevel.reversed())直接编译报错,因为reversed()不是字段自带的属性。正确做法是像上面那样用Comparator.reverseOrder(),或者用方法引用加比较器.thenComparing(Employee::getLevel, (a,b)-> b.compareTo(a))。
对于可能为null的字段,可以用Comparator.nullsFirst()或Comparator.nullsLast()。比如处理用户列表时,有些用户可能没填生日:
users.stream() .sorted(Comparator.comparing(User::getBirthday, Comparator.nullsLast(Comparator.naturalOrder())))这相当于把空值当作无穷大或无穷小处理,避免出现NullPointerException。
4. 性能优化与特殊场景
当处理大数据量时,排序可能成为性能瓶颈。有次我处理10万条日志数据,发现sorted()操作耗时占整个流程的70%。后来通过测试发现几个优化点:
- 对于已经实现Comparable的对象,直接使用
sorted()比sorted(Comparator.comparing(...))更快,因为少了一层包装 - 在多级排序中,把过滤条件高的字段放在前面能减少后续比较次数
- 对于固定排序规则,可以预编译Comparator:
private static final Comparator<Employee> EMP_COMPARATOR = Comparator.comparing(Employee::getDepartment) .thenComparingInt(Employee::getLevel);这样每次排序时就不用重复创建比较器实例了。在百万级数据测试中,这种写法能提升约15%的性能。
另一个特殊场景是对中文排序。直接用sorted()会按Unicode编码排,可能不符合预期。可以用Collator实现本地化排序:
Collator zhCollator = Collator.getInstance(Locale.CHINA); users.stream() .sorted(Comparator.comparing(User::getName, zhCollator))处理日期字符串时也要小心,比如"2021-9-1"和"2021-10-1"按字符串排序会出错,应该先转成LocalDate再比较:
Comparator.comparing(s -> LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy-M-d")))5. 那些年我踩过的排序坑
在实际项目中遇到过几个值得分享的案例。有次做订单导出功能,要求按订单状态分组后,每组内部按金额降序排。第一版代码写成:
orders.stream() .sorted(Comparator.comparing(Order::getStatus) .thenComparing(Order::getAmount).reversed())结果发现金额全是升序——原来reversed()作用的是整个Comparator链。正确写法应该是:
.thenComparing(Order::getAmount, Comparator.reverseOrder())还有个内存溢出的坑。有次对数据库查询结果直接做sorted().limit(100),结果当数据量很大时,sorted()会先把所有数据加载到内存。后来改用数据库分页查询+内存排序结合的方式解决。
对于自定义复杂排序规则,比如"VIP用户优先,然后按活跃度,但黑名单用户永远在最后",可以这样写:
Comparator<User> vipFirst = (u1, u2) -> { if(u1.isBlacklist() != u2.isBlacklist()) { return u1.isBlacklist() ? 1 : -1; } if(u1.isVip() != u2.isVip()) { return u1.isVip() ? -1 : 1; } return Integer.compare(u1.getActivity(), u2.getActivity()); };这种写法虽然不如链式调用优雅,但在复杂业务规则下更灵活可控。
