← 返回题目列表

Comparator.comparing、thenComparing、reversed 怎么用?函数式比较器怎么优雅地做多字段排序?

简单 第 17 / 24 题 更新于 2026/07/28
Comparatorcomparing排序函数式

简化版

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)AgeInteger)每次比较会装箱/拆箱,数据量大时用 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 单独 reversedthenComparing(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——自然顺序。配合 Streamsorted(Comparator)max/min(Comparator)Collectors.maxBy记忆comparing 定主字段、thenComparing 加次字段、reversed 反转,链式组合出任意排序规则。一句话「Comparator 函数式:comparing(字段)定主排序、thenComparing(字段)多字段排序(一行替代一堆 if)、reversed()反转(★反转前面整个比较器不是某字段,混合方向要对具体字段单独 reversed)、comparingInt 避免装箱、nullsLast 处理 null;配合 sorted/max/min 用」。