Java 8 推出了 Stream API 后,一下子把集合操作拉到了一个新的层次,特别是排序操作,写法一下子优雅了不少。但老实说吧,我刚开始用的时候,真没觉得比Collections.sort()高级到哪去,甚至还踩过不少坑……直到我在几个实际项目里真刀真枪地跑了一遍,才真切体会到 Stream 排序那股“用起来爽”的感觉。
先说个简单的,最常见的排序需求:按某个字段升序排。传统写法你要写个Comparator,然后丢给Collections.sort()。用 Stream 就是:
List<Entity> sorted = list.stream()
.sorted(Comparator.comparing(Entity::getId))
.collect(Collectors.toList());
一行搞定,代码可读性也好了不少。对新同事更友好,看个方法引用就知道排序逻辑了。
不过事情到了反向排序就稍微有点不同了,不少人开始出问题了。很多人以为.sorted((a, b) -> b.getId() - a.getId())这么写就行,结果一不小心Integer溢出踩坑。保险写法其实是:
.sorted(Comparator.comparing(Entity::getId).reversed())
简单又不出错,反正我已经懒得手写那些“容易翻车”的比较器了。
说到翻车,就不得不提空值排序。你别看 Java 是强类型语言,空指针照样是祖传问题。你要是排序字段可能为 null,不小心就NullPointerException。那怎么破?得靠Comparator.nullsFirst()或nullsLast():
Comparator<Entity> cmp = Comparator.comparing(
Entity::getId,
Comparator.nullsFirst(Comparator.naturalOrder())
);
别小看这个技巧,我项目里排序LocalDateTime时就中招过,调试半天才发现是字段为 null。
那更复杂一点,如果你要按多个字段排序,比如先按部门排,再按 ID 排,这种组合排序 Stream 也是可以轻松应对的:
.sorted(Comparator.comparing(Entity::getDepartment)
.thenComparing(Entity::getId))
这种链式写法极度舒服,不再像以前一样写几个嵌套的 if else 了。
那问题来了,Stream 这么优雅,是不是性能也更好?讲真,不一定。Stream 是懒加载模型,适合一次性计算处理。但如果你的数据量非常大,比如几十万条,建议考虑parallelStream()并行处理:
List<Entity> sortedList = originalList.parallelStream()
.sorted(Comparator.comparing(Entity::getId))
.collect(Collectors.toList());
这在我做一个电商活动统计分析的时候确实救过我一命,原来顺序处理得跑 6 秒,用上并行后直接压到 1 秒多,还是挺香的。当然,也别滥用,小数据量上反而可能更慢,还占线程资源。
顺带说一下,如果你只是想在原集合上排序,那别走 Stream 了,直接原地排最合适:
originalList.sort(Comparator.comparing(Entity::getId));
性能好,不造新对象,不折腾 GC,这在移动端或者高频调用场景尤其重要。
还有些小技巧,比如你想保持原集合不变,那就要做个防御性拷贝:
List<Entity> copy = new ArrayList<>(originalList);
copy.sort(Comparator.comparing(Entity::getId));
我平时处理第三方库传来的 List 就会这么干,防止改了别人的数据出 bug。
不过也不是所有场景都适合方法引用那种写法。比如你要搞一些非常定制的比较逻辑,就得用 Lambda:
.sorted((e1, e2) -> {
if (e1.getId() == null && e2.getId() == null) return 0;
if (e1.getId() == null) return -1;
if (e2.getId() == null) return 1;
return e1.getId().compareTo(e2.getId());
})
这种写法虽然啰嗦,但逻辑更灵活,有些业务场景必须靠它。
哦对了,提醒一点,用Collectors.toList()虽然方便,但返回的 List 其实不保证是可变的,尤其是某些实现下返回的是Arrays$ArrayList,结果你后续add就会抛异常。所以如果你后面还要改,最好显式指定:
.collect(Collectors.toCollection(ArrayList::new))
稳妥很多,少些莫名其妙的问题。
当然也不能只顾写代码不看场景,Stream 再好,也不是银弹。你数据都在数据库里了,非得拉出来用 Java 排?还不如写个ORDER BY交给数据库引擎搞定。别让 CPU 干 IO 干的活。
最后说个我很喜欢的技巧:多条件混排,比如先按名称倒序,再按 ID 正序:
.sorted(Comparator.comparing(Entity::getName).reversed()
.thenComparing(Entity::getId))
很符合业务常见排序逻辑,用得多得很。我之前做一个搜索推荐模块,就是按照“权重优先、时间次之”这么排的。
到这你可能会想,Stream 排序到底和老牌的Collections.sort()有啥本质区别?从我经验看,Stream 更适合函数式编程风格,代码更简洁易读;而传统 sort 更适合直接原地排、需要手动控制流程的时候。各有优势,不要死磕一种写法。
你要问我,什么时候该用哪种?我会说:你能用 Stream 表达得清楚的,优先用 Stream;你需要极致性能、或者特别复杂的逻辑,那就老老实实用传统方式。
毕竟,写代码不是为了炫技,是为了让同事能看懂、项目能跑得稳,你说是不是?
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
也可以看我写的这篇文章《DeepSeek满血复活,直接起飞!》来进行本地搭建。
东哥作为一名超级老码农,整理了全网最全《Java高级架构师资料合集》。