Java 高级排序(Comparator 和 Comparable)

Java 高级排序

在“列表排序”章节中,您学习了如何按字母顺序和数字顺序对列表进行排序,但如果列表中包含对象呢?

为了对对象进行排序,您需要指定一个规则来决定对象的排序方式。例如,如果您有一个汽车列表,你可能想按年份排序,规则可以是年份较早的汽车排在前面。

ComparatorComparable 接口允许你指定用于排序对象的规则。

能够指定排序规则还允许您改变字符串和数字的排序方式。

Comparator(比较器)

实现了 Comparator 接口的对象被称为比较器。

Comparator 接口允许你创建一个包含 compare() 方法的类,该方法比较两个对象以决定哪个在列表中应排在前面。

compare() 方法应返回一个数字,该数字:

  • 为负数时,表示第一个对象应在列表中排在前面。
  • 为正数时,表示第二个对象应在列表中排在前面。
  • 为零时,表示顺序无关紧要。

实现 Comparator 接口的类可能如下所示:

// 按年份排序 Car 对象
class SortByYear implements Comparator {
  public int compare(Object obj1, Object obj2) {
    // 确保对象是 Car 对象
    Car a = (Car) obj1;
    Car b = (Car) obj2;
    
    // 比较对象
    if (a.year < b.year) return -1; // 第一辆车的年份更小
    if (a.year > b.year) return 1;  // 第一辆车的年份更大
    return 0; // 两辆车的年份相同
  }
}

要使用比较器,将其作为参数传递给排序方法:

// 使用比较器对汽车进行排序
Comparator myComparator = new SortByYear();
Collections.sort(myCars, myComparator);

下面是一个使用比较器按年份排序汽车列表的完整示例:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

// 定义一个 Car 类
class Car {
  public String brand; // 品牌
  public String model; // 型号
  public int year; // 年份
  
  // 构造函数
  public Car(String b, String m, int y) {
    brand = b;
    model = m;
    year = y;
  }
}

// 创建一个比较器
class SortByYear implements Comparator {
  // 实现 compare 方法
  public int compare(Object obj1, Object obj2) {
    // 确保传入的对象是 Car 类型
    Car a = (Car) obj1;
    Car b = (Car) obj2;
    
    // 比较两辆车的年份
    if (a.year < b.year) return -1; // 第一辆车年份更小
    if (a.year > b.year) return 1;  // 第一辆车年份更大
    return 0; // 两辆车年份相同
  }
}

public class Main { 
  public static void main(String[] args) { 
    // 创建一个汽车列表
    ArrayList<Car> myCars = new ArrayList<Car>();    
    myCars.add(new Car("BMW", "X5", 1999));
    myCars.add(new Car("Honda", "Accord", 2006));
    myCars.add(new Car("Ford", "Mustang", 1970));

    // 使用比较器对汽车进行排序
    Comparator myComparator = new SortByYear();
    Collections.sort(myCars, myComparator);

    // 显示排序后的汽车
    for (Car c : myCars) {
      System.out.println(c.brand + " " + c.model + " " + c.year);
    }
  } 
}

亲自试一试

使用 Lambda 表达式

为了简化代码,比较器可以被替换为具有与 compare() 方法相同参数和返回值的 Lambda 表达式:

Collections.sort(myCars, (obj1, obj2) -> {
  Car a = (Car) obj1;
  Car b = (Car) obj2;
  if (a.year < b.year) return -1;
  if (a.year > b.year) return 1;
  return 0;
});

亲自试一试

特殊排序规则

比较器还可以用于为字符串和数字制定特殊的排序规则。在此例中,我们使用比较器将所有偶数列在奇数之前:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

// 定义一个比较器类 SortEvenFirst,实现 Comparator 接口
class SortEvenFirst implements Comparator {
  // 实现 compare 方法,用于比较两个对象
  public int compare(Object obj1, Object obj2) {
    // 确保传入的对象是 Integer 类型
    Integer a = (Integer) obj1;
    Integer b = (Integer) obj2;
    
    // 检查每个数字是否为偶数
    // 一个数字如果除以 2 的余数为 0,则为偶数
    boolean aIsEven = (a % 2) == 0;
    boolean bIsEven = (b % 2) == 0;
    
    // 如果两个数字同为偶数或同为奇数,则按正常排序规则比较
    if (aIsEven == bIsEven) {
      if (a < b) return -1; // a小于b,返回-1
      if (a > b) return 1;  // a大于b,返回1
      return 0;             // a等于b,返回0
    } else {
      // 如果 a 是偶数,则 a 排在前面,否则 b 排在前面
      if (aIsEven) {
        return -1; // a 是偶数,返回 -1,表示 a 排在前面
      } else {
        return 1;  // b 是偶数,返回 1,表示 b 排在前面
      }
    }
  }
}

public class Main {
  public static void main(String[] args) {
    // 创建一个 Integer 类型的 ArrayList
    ArrayList<Integer> myNumbers = new ArrayList<Integer>();
    // 向列表中添加一些整数
    myNumbers.add(33);
    myNumbers.add(15);
    myNumbers.add(20);
    myNumbers.add(34);
    myNumbers.add(8);
    myNumbers.add(12);

    // 创建一个 SortEvenFirst 比较器实例
    Comparator myComparator = new SortEvenFirst();
    // 使用该比较器对列表进行排序
    Collections.sort(myNumbers, myComparator);

    // 遍历并打印排序后的列表
    for (int i : myNumbers) {
      System.out.println(i);
    }
  }
}

亲自试一试

Comparable 接口

Comparable 接口允许对象通过 compareTo() 方法指定其自身的排序规则。

compareTo() 方法接受一个对象作为参数,并将可比较对象与该参数进行比较,以决定哪个在列表中应排在前面。

与比较器类似,compareTo() 方法返回一个数字,该数字:

  • 为负数时,表示可比较对象应在列表中排在前面。
  • 为正数时,表示另一个对象应在列表中排在前面。
  • 为零时,表示顺序无关紧要。

许多原生 Java 类(如 StringInteger)都实现了 Comparable 接口。

这就是为什么字符串和数字不需要比较器就可以进行排序的原因。

实现了 Comparable 接口的对象可能如下所示:

class Car implements Comparable {
  public String brand; // 品牌
  public String model; // 型号
  public int year;     // 年份
  
  // 定义此对象如何与其他对象进行比较
  public int compareTo(Object obj) {
    Car other = (Car) obj; // 将传入的对象强制转换为Car类型
    if (year < other.year) return -1; // 如果此对象的年份小于另一个对象的年份,则返回 -1,表示此对象较小
    if (year > other.year) return 1;  // 如果此对象的年份大于另一个对象的年份,则返回 1,表示此对象较大
    return 0; // 如果两个对象的年份相同,则返回 0,表示两个对象相等(在年份上)
  }
}

下面是与之前相同的示例,但使用 Comparable 接口而不是比较器:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

// 定义一个可比较的 Car 类
class Car implements Comparable<Car> {
  public String brand; // 品牌
  public String model; // 型号
  public int year;     // 年份
  
  // 构造方法,用于初始化 Car 对象
  public Car(String b, String m, int y) {
    brand = b;
    model = m;
    year = y;
  }
  
  // 实现 compareTo 方法,决定此对象如何与其他 Car 对象进行比较
  @Override
  public int compareTo(Car other) {
    if (year < other.year) return -1; // 如果此车的年份小于另一辆车的年份,则返回 -1,表示此车较小
    if (year > other.year) return 1;  // 如果此车的年份大于另一辆车的年份,则返回 1,表示此车较大
    return 0; // 如果两车的年份相同,则返回 0,表示两车相等(在年份上)
  }
}

public class Main { 
  public static void main(String[] args) { 
    // 创建一个汽车列表
    ArrayList<Car> myCars = new ArrayList<Car>();    
    myCars.add(new Car("BMW", "X5", 1999));
    myCars.add(new Car("Honda", "Accord", 2006));
    myCars.add(new Car("Ford", "Mustang", 1970));

    // 对汽车进行排序
    Collections.sort(myCars);

    // 显示汽车列表
    for (Car c : myCars) {
      System.out.println(c.brand + " " + c.model + " " + c.year);
    }
  } 
}

亲自试一试

常见的排序技巧

最直观的按自然顺序排序两个数字的方法是编写如下代码:

if(a.year < b.year) return -1; // a 小于 b
if(a.year > b.year) return 1; // a 大于 b
return 0; // a is equal to b

但实际上,这可以用一行代码完成:

return a.year - b.year;

这个技巧还可以用于轻松实现反向排序:

return b.year - a.year;

Comparator vs. Comparable

比较器(comparator)是一个具有一个方法的对象,用于比较两个不同的对象。

可比较对象(comparable)是一个可以与其他对象进行比较的对象。

在可能的情况下,使用 Comparable 接口更容易,但 Comparator 接口更强大,因为它允许你对任何类型的对象进行排序,即使你不能更改其代码。