目录

  • 1. 自然排序:java.lang.Comparable
  • 2. 定制排序:java.util.Compartor
  • 3. 比较方法的返回值正负与升序、降序的关系
  • 4. Comparable接口和Comparator接口的区别

在Java中经常会涉及到多个对象的排序问题,那么就涉及到对象之间的比较。

Java实现对象排序的方式有两种:

自然排序:java.lang.Comparable

定制排序:ava.util.Comparator

1. 自然排序:java.lang.Comparable

  • Comparable 接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。 Comparable 接口中只有一个抽象方法:int compareTo(Object o);
  • 实现 Comparable 的类必须实现 compareTo(Object o) 方法,两个对象即通过 compareTo(Object o) 方法的返回值来比较大小。
  • 如果当前对象 this 大于形参对象 o ,则返回正整数,
  • 如果当前对象 this 小于形参对象 o ,则返回负整数,
  • 如果当前对象 this 等于形参对象 o ,则返回零。
  • 实现 Comparable 接口的类的对象数组(和有序集合)可以通过 Arrays.sort(和 Collections.sort )进行自动排序。
  • Comparable的典型实现:(默认都是从小到大排序
    String、包装类等实现了Comparable接口,重写了compareTo(obj)方法

String:按照字符串中字符的Unicode值进行比较
数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
Character:按照字符的Unicode值来进行比较
Boolean:true 对应的包装类实例大于 false 对应的包装类实例
Date、Time等:后面的日期时间比前面的日期时间大

  • 代码示例:
String[] arr = new String[]{"AA","cc","ac","dd","aa","FF","ff"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [AA, FF, aa, ac, cc, dd, ff]
  • 对于自定义类来说,如果需要排序,我们可以让自定义类实现 Comparable 接口,重写 compareTo(Object o) 方法。在 compareTo(Object o) 方法中指明如何排序。
    定义学生类:
public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试类:

public class ComparableTest {
    public static void main(String[] args) {
        Student[] students = new Student[5];
        students[0] = new Student("rose", 16);
        students[1] = new Student("jack", 18);
        students[2] = new Student("mark", 16);
        students[3] = new Student("john", 16);
        students[4] = new Student("lily", 17);

        Arrays.sort(students);

        for (int i = 0; i < students.length; i++) {
            System.out.println(students[i]);
        }
    }
}

发现程序出现了类型转换异常:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggMQjfNv-1598369086597)(img/Comparable.png)]

原因:对于自定义类来说,如果需要排序,自定义类必须实现 Comparable 接口,重写 compareTo(Object o) 方法。在 compareTo(Object o) 方法中指明如何排序。

修改Student类,实现 Comparable 接口,指定按学生年龄升序排序:

public class Student implements Comparable {
    ...
    
    @Override
    public int compareTo(Object o) {
 		Student student = (Student) o;
        if (this.age < student.age) {
            return -1;
        } 
        if (this.age > student.age) {
            return 1;
        }
        return 0;
    }
}

运行结果:

Student{name='mark', age=15}
Student{name='rose', age=16}
Student{name='john', age=16}
Student{name='lily', age=17}
Student{name='jack', age=18}

如果要按学生年龄降序排序,则修改compareTo方法:当前对象的年龄小于参数对象时,返回正整数;当前对象大于参数对象时,返回负整数:

@Override
public int compareTo(Object o) {
    Student student = (Student) o;
    if (this.age < student.age) {
        return 1;
    } 
    if (this.age > student.age) {
        return -1;
    }
    return 0;
}

2. 定制排序:java.util.Compartor

  • 当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 接口的实现类来排序。
  • Comparator 接口中只有两个抽象方法 int compare(Object o1, Object o2);boolean equals(Object obj);Comparator 接口实现类默认继承了 Object 类的 equals 方法,即间接实现了 equals 方法,因此只需实现 int compare(Object o1, Object o2) 即可。
  • 可以将 Comparator 接口实现类传递给 sort 方法(如 Arrays.sort 或 Collections.sort),从而允许在排序顺序上实现精确控制。
  • 重写 int compare(Object o1, Object o2) 方法,比较o1和o2的大小:

如果要按照升序排序,
则 o1小于o2返回负整数,o1与o2相等返回0,01大于02返回正整数

如果要按照降序排序
则 o1小于o2返回正整数,o1与o2相等返回0,01大于02返回负整数

  • 代码演示:
    定义Student类,无需实现Comparable接口:
public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试类:向sort方法中传入待排序的数组对象和Comparator接口实现类,指定按学生年龄升序排序:

public class ComparableTest {
    public static void main(String[] args) {
        Student[] students = new Student[5];
        students[0] = new Student("rose", 16);
        students[1] = new Student("jack", 18);
        students[2] = new Student("mark", 15);
        students[3] = new Student("john", 16);
        students[4] = new Student("lily", 17);

        Arrays.sort(students, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                Student s1 = (Student) o1;
                Student s2 = (Student) o2;
                //下面的代码可简化为 return s1.getAge() - s2.getAge();
                if (s1.getAge() < s2.getAge()) {
                    return -1;
                }
                if (s1.getAge() > s2.getAge()) {
                    return 1;
                }
                return 0;
            }
        });

        for (int i = 0; i < students.length; i++) {
            System.out.println(students[i]);
        }
    }

}

运行结果:

Student{name='mark', age=15}
Student{name='rose', age=16}
Student{name='john', age=16}
Student{name='lily', age=17}
Student{name='jack', age=18}

如果要按学生年龄降序排序,则修改compare方法:当前对象的年龄小于参数对象时,返回正整数;当前对象大于参数对象时,返回负整数:

@Override
public int compare(Object o1, Object o2) {
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;
    //下面的代码可简化为 return s2.getAge() - s1.getAge();
    if (s1.getAge() < s2.getAge()) {
        return 1;
    }
    if (s1.getAge() > s2.getAge()) {
        return -1;
    }
    return 0;
}

按年龄降序排序,年龄相同时按姓名首字母升序排序:

@Override
public int compare(Object o1, Object o2) {
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;
    //第一条排序规则:年龄降序
    int result = s2.getAge() - s1.getAge();
    //第二条排序规则:年龄相同时,姓名首字母升序
    if (result == 0) {
        result = s1.getName().charAt(0) - s2.getName().charAt(0);
    }
    return result;
}

运行结果:

Student{name='jack', age=18}
Student{name='lily', age=17}
Student{name='john', age=16}
Student{name='rose', age=16}
Student

3. 比较方法的返回值正负与升序、降序的关系

从前面已经知道:如果要按照升序排序,则 o1小于o2返回负整数,o1与o2相等返回0,01大于02返回正整数

如果要按照降序排序,则 o1小于o2返回正整数,o1与o2相等返回0,01大于02返回负整数

那么,对元素升序、降序排序时调整元素位置与接口中返回值的正负有什么关系呢?

测试环境:JDK8

原数组元素顺序:

Student{name='rose', age=16}
Student{name='jack', age=18}
Student{name='mark', age=15}
Student{name='john', age=16}
Student{name='lily', age=17}
  • 在compare方法中,不管o1、o2对象的内容,统一返回正整数:
@Override
public int compare(Object o1, Object o2) {
    return 1;
}

运行结果发现,数组元素顺序未改变。

  • 在compare方法中,不管o1、o2对象的内容,统一放回负整数,运行结果发现,数组元素倒置。
  • 在compare方法中,不管o1、o2对象的内容,统一放回0,运行结果发现,数组元素顺序未改变。

在comparable接口中及集合中的运行结果一致。

从而得出结论:在调用 compare(Object o1, Object o2) 方法时,会把两个元素中索引较小的元素赋值给o2,索引较大的元素赋值给o1。Comparable接口中compareTo(Object o) 方法的调用者是两个元素中索引较大的元素,参数对象 o 则是索引较小的元素。(JDK版本不同,对应关系可能不同)。通过debug调试或查看源码也能得出这个结论。

比如,对于数组{5, 10, 20, 15},比较前两个元素调用 compare(Object o1, Object o2) 方法时,会把5赋值给o2,把10赋值给o1。按升序排序时,若o1 > o2,则返回正整数,不调整元素位置;若o1 == o2,则返回0,不调整元素位置;若o1 < o2,则返回负整数,调整元素位置。按降序排序时,若o1 > o2,则返回负整数,需要调整元素位置;若o1 == o2,则返回0,不调整元素位置;若o1 < o2,则返回正整数,不调整元素位置。因此,不管是升序还是降序,只有在返回负整数时,才会调整元素的位置。

综上,排序时是否需要调整元素位置是由对象o1、o2与数组中元素的对应关系 和 比较方法返回值的正负共同决定的。

4. Comparable接口和Comparator接口的区别

只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。

用Comparator 的好处是不需要修改源代码, 而是在待比较对象的类的外部实现一个比较器, 当某个自定义的对象需要作比较的时候,把待比较对象和比较器一起传递过去就可以实现排序功能。

此外,像String类、包装类等JDK内置类实现了Comparable接口默认是升序排序,如果要降序排序或指定其他排序规则只能使用Comparator接口。