Comparator.comparing、thenComparing、reversed 怎么用?函数式比较器怎么优雅地做多字段排序?
简化版
Java 8 给 Comparator 加了一批「函数式」的静态和默认方法,让「写比较器」从「实现一个匿名内部类、手写 compare 方法」变成「一行链式调用」。核心:① Comparator.comparing(keyExtractor)——「按某个字段比较」,传一个「取字段的函数」(如 Comparator.comparing(User::getAge) 就是「按年龄比」);② .thenComparing(...)——「上一个字段相等时,再按下一个字段比」,实现多字段排序(先按年龄、年龄相同再按名字);③ .reversed()——「反转」(降序);④ comparingInt/Long/Double——避免装箱的基本类型版本;⑤ nullsFirst/nullsLast——处理 null。最典型的用法是多字段排序:list.sort(Comparator.comparing(User::getDept).thenComparing(User::getAge).reversed())——一行搞定「先按部门、再按年龄」的排序,比以前写一大段 if-else 比较逻辑清爽得多。核心记忆:comparing 定「主排序字段」、thenComparing 加「次排序字段」、reversed 反转,链式组合出任意复杂的排序规则。
详细版
Comparator 的函数式方法:
| 方法 | 作用 |
|---|---|
| comparing(keyFn) | 按 keyFn 提取的字段比较(升序) |
| comparingInt/Long/Double | 基本类型版本(避免装箱) |
| thenComparing(keyFn) | 前面相等时,再按此字段比 |
| reversed() | 反转(降序) |
| naturalOrder()/reverseOrder() | 自然顺序/逆自然顺序 |
| nullsFirst(cmp)/nullsLast(cmp) | null 排最前/最后 |
List<User> users = ...;
// 按年龄升序
users.sort(Comparator.comparing(User::getAge));
// 按年龄降序
users.sort(Comparator.comparing(User::getAge).reversed());
// 多字段:先按部门升序,部门相同再按年龄降序
users.sort(Comparator.comparing(User::getDept)
.thenComparing(Comparator.comparing(User::getAge).reversed()));
// 基本类型版(避免装箱,性能更好)
users.sort(Comparator.comparingInt(User::getAge));
// 处理 null(name 为 null 的排最后)
users.sort(Comparator.comparing(User::getName,
Comparator.nullsLast(Comparator.naturalOrder())));
// 配合 Stream
users.stream()
.sorted(Comparator.comparing(User::getAge).thenComparing(User::getName))
.collect(Collectors.toList());
⚠️ 一个容易搞错的点:
reversed()反转的是「它前面的整个比较器」,不是「某一个字段」。comparing(A).thenComparing(B).reversed()会把「先 A 再 B」这整个规则反转(变成先 B 降、再 A 降),而不是只反转 B。如果你只想「A 升序、B 降序」,要对 B 单独 reversed:comparing(A).thenComparing(comparing(B).reversed()),或者用thenComparing(B, Comparator.reverseOrder())。另一个坑是性能:comparing(User::getAge)(Age是Integer)每次比较会装箱/拆箱,数据量大时用comparingInt(User::getAge)(基本类型版)避免装箱、更快。还有 null 处理——如果字段可能为 null,直接comparing会 NPE,要用nullsFirst/nullsLast包一层。
完整版教学
一、从匿名类到函数式
先看「函数式比较器」相比传统写法的进步:
传统写法(匿名内部类,冗长):
Collections.sort(users, new Comparator<User>() {
public int compare(User a, User b) {
return Integer.compare(a.getAge(), b.getAge());
}
});
→ 一大段样板代码,就为了"按年龄比"
Lambda 稍好:
users.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));
→ 短了,但还是要手写 compare 逻辑
函数式比较器(最简洁):
users.sort(Comparator.comparing(User::getAge));
→ 只需说"按什么字段比",不用手写 compare
→ comparing 帮你生成比较逻辑
进步的本质:
从"手写怎么比",变成"声明按什么字段比"
→ 声明式、简洁、可组合
函数式比较器相比传统写法的进步:传统匿名内部类冗长(一大段样板就为「按年龄比」)、Lambda 稍好但还要手写 compare、函数式比较器最简洁(Comparator.comparing(User::getAge) 只需说「按什么字段比」,不用手写 compare)。进步本质是「从手写怎么比,变成声明按什么字段比」(声明式、简洁、可组合)。理解「函数式比较器从匿名内部类(冗长)到 Lambda 到 comparing(最简洁)、从手写怎么比变成声明按什么字段比」,就理解了它的价值。
二、comparing:按字段比较
Comparator.comparing(keyExtractor) 是「按某字段比较」:
comparing(Function keyExtractor):
传一个"取字段的函数",生成"按这个字段比较"的 Comparator
Comparator.comparing(User::getAge)
→ 等价于 (a, b) -> a.getAge().compareTo(b.getAge())(升序)
工作原理:
comparing 拿到"取字段"的函数(keyExtractor)
比较两个对象时:分别取出它们的字段值,用字段的自然顺序比较
→ 字段类型要 Comparable(Integer、String 等天然是)
基本类型版(避免装箱):
comparingInt(User::getAge) // Age 是 int/Integer,避免装箱
comparingLong/comparingDouble
→ 数据量大时用这些,比 comparing 快(不装箱)
指定字段的比较方式:
comparing(keyExtractor, keyComparator)
→ 第二个参数指定"字段怎么比"(如字段本身也要自定义比较)
comparing(User::getName, String.CASE_INSENSITIVE_ORDER) // 忽略大小写比名字
Comparator.comparing(keyExtractor) 是「按字段比较」——传「取字段的函数」生成「按该字段升序比较」的 Comparator(comparing(User::getAge) 等价于按 age 的自然顺序比)。字段类型要 Comparable。基本类型版 comparingInt/Long/Double 避免装箱(数据量大时更快)。指定字段比较方式:comparing(keyExtractor, keyComparator)(如 comparing(User::getName, CASE_INSENSITIVE_ORDER) 忽略大小写)。理解「comparing 按字段比较(传取字段函数、按字段自然顺序升序)、comparingInt/Long/Double 避免装箱、comparing(keyFn,keyComparator)指定字段比较方式」,就掌握了 comparing。
三、thenComparing:多字段排序
thenComparing 实现「多字段排序」——前面相等时再按下一个比:
thenComparing(keyExtractor):
当前面的比较器"相等"时(比不出先后),再按这个字段比
多字段排序:
comparing(User::getDept) // 先按部门
.thenComparing(User::getAge) // 部门相同再按年龄
.thenComparing(User::getName) // 年龄也相同再按名字
→ 先按部门,部门相同看年龄,年龄相同看名字(多级排序)
工作原理(链式):
比较两个对象时:
先用 comparing(dept) 比 → 不等就返回结果
相等(部门相同)→ 用 thenComparing(age) 比 → 不等返回
还相等 → 用 thenComparing(name) 比
→ 一级级往下,直到分出先后
对比传统多字段排序(手写 if-else,繁琐):
int c = a.getDept().compareTo(b.getDept());
if (c != 0) return c;
c = Integer.compare(a.getAge(), b.getAge());
if (c != 0) return c;
return a.getName().compareTo(b.getName());
→ thenComparing 一行链式替代这一堆 if
thenComparing(keyExtractor) 实现「多字段排序」——前面的比较器相等时(比不出先后)再按这个字段比。链式:comparing(dept).thenComparing(age).thenComparing(name)(先按部门、部门相同看年龄、年龄相同看名字)。工作原理:一级级往下比,直到分出先后。它一行链式替代传统手写的一堆 if-else 多字段比较(先比 dept、不等返回、相等再比 age…)。理解「thenComparing 多字段排序(前面相等时再按此字段比)、链式 comparing(dept).thenComparing(age)、一行替代传统一堆 if-else 多字段比较」,就掌握了多字段排序。
四、reversed:反转的陷阱
reversed() 反转,但有个「反转范围」的陷阱:
reversed():反转比较器(升序 → 降序)
comparing(User::getAge).reversed() // 按年龄降序
★ 陷阱:reversed 反转的是"它前面的整个比较器",不是某个字段
comparing(A).thenComparing(B).reversed()
→ 反转"先 A 再 B"这整个规则
→ 变成"先 B 降序、再 A 降序"(不是你可能想的"A 升 B 降")
想要"A 升序、B 降序":
① 对 B 单独 reversed:
comparing(A).thenComparing(comparing(B).reversed())
② 用 thenComparing 的两参版:
comparing(A).thenComparing(B, Comparator.reverseOrder())
想要"A 降序、B 升序":
comparing(A).reversed().thenComparing(B)
→ A 先 reversed(降序),再 thenComparing(B)(升序)
所以:
reversed 的位置很关键——它反转"到目前为止的整个比较器"
混合升降序时,要精确控制每个字段的 reversed
reversed() 反转(升序→降序),但有**「反转范围」陷阱**:它反转的是「它前面的整个比较器」,不是某个字段。comparing(A).thenComparing(B).reversed() 反转「先 A 再 B」整个规则(变成先 B 降再 A 降,不是 A 升 B 降)。想要「A 升 B 降」要对 B 单独 reversed(thenComparing(comparing(B).reversed()))或用 thenComparing(B, reverseOrder())。想要「A 降 B 升」用 comparing(A).reversed().thenComparing(B)。理解「reversed 反转它前面的整个比较器不是某字段(陷阱)、A 升 B 降要对 B 单独 reversed 或 thenComparing(B,reverseOrder)、混合升降序要精确控制每个字段」,就避开了 reversed 的陷阱。
五、null 处理与自然顺序
处理 null 字段和自然顺序:
null 处理:
如果比较的字段可能为 null,直接 comparing 会 NPE
用 nullsFirst/nullsLast 包一层:
comparing(User::getName,
Comparator.nullsLast(Comparator.naturalOrder()))
→ name 为 null 的排最后,非 null 的按自然顺序
nullsFirst → null 排最前
自然顺序:
naturalOrder():元素本身的自然顺序(元素要 Comparable)
list.sort(Comparator.naturalOrder()) // 直接按自然顺序
reverseOrder():逆自然顺序(降序)
list.sort(Comparator.reverseOrder())
组合示例(多字段 + null + 混合方向):
comparing(User::getDept) // 部门升序
.thenComparing(User::getAge, reverseOrder()) // 年龄降序
.thenComparing(User::getName,
nullsLast(naturalOrder())) // 名字升序,null 最后
所以:
基本字段 → comparing/comparingInt
可能为 null → nullsFirst/nullsLast 包一层
元素本身可比 → naturalOrder/reverseOrder
处理 null 和自然顺序:null 处理——字段可能为 null 直接 comparing 会 NPE,用 nullsFirst/nullsLast 包一层(comparing(User::getName, nullsLast(naturalOrder())) name 为 null 排最后);自然顺序——naturalOrder()(元素本身的自然顺序,要 Comparable)、reverseOrder()(逆自然顺序)。组合可以做「多字段+null+混合方向」。理解「null 处理用 nullsFirst/nullsLast 包一层(否则 NPE)、naturalOrder/reverseOrder 元素自然顺序、可组合多字段+null+混合方向」,就掌握了 null 和自然顺序的处理。
六、实践与配合 Stream
总结函数式比较器的实践和与 Stream 的配合:
配合 Stream 的 sorted:
users.stream()
.sorted(Comparator.comparing(User::getAge).thenComparing(User::getName))
.collect(toList())
→ sorted 接收 Comparator,用函数式比较器很自然
配合 min/max:
users.stream().max(Comparator.comparing(User::getAge))
→ 找年龄最大的(返回 Optional)
配合 Collectors:
groupingBy(User::getDept,
Collectors.maxBy(comparing(User::getSalary)))
→ 每个部门工资最高的
实践建议:
① 单字段排序:comparing(字段)
② 多字段:comparing(主).thenComparing(次)...
③ 降序:reversed() 或 reverseOrder()(注意反转范围)
④ 性能敏感:comparingInt/Long/Double(避免装箱)
⑤ 有 null:nullsFirst/nullsLast 包一层
⑥ 混合升降序:对具体字段单独 reversed,别整体 reversed
一句话:comparing 定主字段、thenComparing 加次字段、reversed 反转,
链式组合出任意排序规则,配合 sorted/min/max 用
函数式比较器配合 Stream:sorted(Comparator)(排序)、min/max(Comparator)(找最值返回 Optional)、Collectors.maxBy(comparing(...))(分组内最值)。实践:单字段 comparing、多字段 comparing().thenComparing()、降序 reversed(注意范围)、性能敏感用 comparingInt、有 null 用 nullsLast、混合升降序对具体字段单独 reversed。理解「配合 Stream:sorted/min/max/maxBy;实践:单字段 comparing、多字段 thenComparing、降序 reversed 注意范围、性能用 comparingInt、null 用 nullsLast、混合方向单独 reversed」,就掌握了函数式比较器的实践。
记忆钩子:「Comparator 函数式方法:①comparing(keyFn)按字段比较(升序,传取字段函数,如 comparing(User::getAge))②thenComparing(keyFn)多字段排序(前面相等再按此字段比,链式 comparing(dept).thenComparing(age)一行替代一堆 if)③reversed()反转★陷阱:反转它前面的整个比较器不是某字段(A 升 B 降要对 B 单独 reversed 或 thenComparing(B,reverseOrder))④comparingInt/Long/Double 避免装箱(性能)⑤nullsFirst/nullsLast 处理 null(否则 NPE)⑥naturalOrder/reverseOrder 自然顺序;配合 sorted/min/max/maxBy;comparing 定主字段、thenComparing 加次字段、reversed 反转链式组合」。
七、常见误区与追问
- 误区:reversed() 只反转最后一个字段。 不是——reversed() 反转「它前面的整个比较器」;comparing(A).thenComparing(B).reversed() 会反转「先 A 再 B」整个规则(变成先 B 降再 A 降);想只反转 B 要对 B 单独 reversed(thenComparing(comparing(B).reversed()))或用 thenComparing(B, reverseOrder())。
- 误区:comparing(User::getAge) 性能最好。 如果 age 是 Integer,comparing 每次比较会装箱/拆箱;数据量大时用 comparingInt(User::getAge)(基本类型版)避免装箱、更快;comparingLong/comparingDouble 同理。
- 误区:字段可能为 null 也能直接 comparing。 会 NPE——如果比较的字段值为 null,comparing 内部调 compareTo 会抛空指针;要用 nullsFirst/nullsLast 包一层:comparing(User::getName, nullsLast(naturalOrder())),让 null 排最前或最后。
- 误区:多字段排序要手写一堆 if-else。 用 thenComparing 链式一行搞定——comparing(dept).thenComparing(age).thenComparing(name),前面字段相等时自动往下一个字段比;比传统「先比 dept、不等返回、相等再比 age…」的一堆 if 清爽得多。
- 追问:怎么实现「先按部门升序、部门相同再按年龄降序」? comparing(User::getDept).thenComparing(User::getAge, Comparator.reverseOrder())——或 comparing(User::getDept).thenComparing(comparing(User::getAge).reversed());关键是对「年龄」这个字段单独 reversed,不能整体 reversed(否则部门也变降序了)。
- 追问:comparingInt 和 comparing 有什么区别? comparingInt(keyExtractor) 的 keyExtractor 返回 int(基本类型),比较时直接用 int 比、不装箱;comparing(keyExtractor) 的 keyExtractor 返回对象(如 Integer),比较时可能有装箱/拆箱开销;数据量大、性能敏感时用 comparingInt/comparingLong/comparingDouble 更快。
- 追问:Comparator.comparing 怎么配合 Stream 用? sorted(Comparator):排序流;max(Comparator)/min(Comparator):找最大/最小元素(返回 Optional);Collectors.maxBy(comparing(…)):作为 groupingBy 的下游收集器求每组最值;如 users.stream().sorted(comparing(User::getAge).thenComparing(User::getName)).collect(toList())。
八、加强记忆
Java 8 给 Comparator 加了一批函数式方法,让「写比较器」从「匿名内部类手写 compare」变成「一行链式调用」。核心:① Comparator.comparing(keyExtractor)——按某字段比较(升序,传「取字段的函数」,如 comparing(User::getAge));② .thenComparing(keyExtractor)——多字段排序(前面相等时再按此字段比,链式 comparing(dept).thenComparing(age) 一行替代传统一堆 if-else);③ .reversed()——反转(陷阱:反转的是「它前面的整个比较器」不是某个字段,想「A 升 B 降」要对 B 单独 reversed 或用 thenComparing(B, reverseOrder()));④ comparingInt/Long/Double——基本类型版避免装箱(性能敏感时用);⑤ nullsFirst/nullsLast——处理 null(字段可能为 null 时包一层,否则 NPE);⑥ naturalOrder/reverseOrder——自然顺序。配合 Stream:sorted(Comparator)、max/min(Comparator)、Collectors.maxBy。记忆:comparing 定主字段、thenComparing 加次字段、reversed 反转,链式组合出任意排序规则。一句话「Comparator 函数式:comparing(字段)定主排序、thenComparing(字段)多字段排序(一行替代一堆 if)、reversed()反转(★反转前面整个比较器不是某字段,混合方向要对具体字段单独 reversed)、comparingInt 避免装箱、nullsLast 处理 null;配合 sorted/max/min 用」。