目录
我们知道,集合列表 list 可以按如下顺序进行排序
Collections.sort(list);
如果 List 包含的是 String 类型元素,那么它将按字母的顺序进行排序。如果包含的是 Date 类型的元素,则按时间顺序排序。这些又是怎么发生的呢? 原因就是 String 和 Date 类都实现了Comparable接口,并实现了 compareTo() 方法,String 类的实现如下所示:
// string 对 compareTo 的实现
public int compareTo(String anotherString) {
int len1 = value.length; // 先获取长度
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2); // 获取最小长度的字符串
char v1[] = value; // 获取值
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) { // 一个一个字符进行比较
return c1 - c2; // 返回不等处的比较值
}
k++;
}
return len1 - len2; // 如果最小部分都相等,计算长度差值
}
实现 Comparable
接口,为实现类提供了一种自然的排序方式,它允许对该类的对象进行自动排序。下表总结了 Java 中实现了 Comparable 接口的一些比较重要的类:
Class | Natural Ordering |
---|---|
Byte | Signed numerical |
Character | Unsigned numerical |
Long | Signed numerical |
Integer | Signed numerical |
Short | Signed numerical |
Double | Signed numerical |
Float | Signed numerical |
BigInteger | Signed numerical |
BigDecimal | Signed numerical |
Boolean | Boolean.FALSE < Boolean.TRUE |
File | System-dependent lexicographic on path name |
String | Lexicographic |
Date | Chronological |
CollationKey | Locale-specific lexicographic |
如果列表中的元素没有实现 Comparable 接口,将不能使用 Collections.sort(list) 进行比较,否则会抛出 ClassCastException 异常。同样,如果使用比较器 Comparator 试图对一个不能相互比较的元素列表进行排序,Collections.sort(list, comparator)
也会抛出ClassCastException 异常。
1、Comparable
自定义可比较的类型
Comparable接口由以下方法组成
public interface Comparable<T> {
public int compareTo(T o);
}
compareTo 方法将接收对象与指定对象进行比较,并根据接收对象是小于、等于还是大于指定对象返回负整数、0或正整数。
示例代码,如 Name.class 实现了Comparable 接口,定义了自己类的 compareTo() 逻辑
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Name implements Comparable<Name> {
/**
* 名称对象是不可变的。
* 在一些情况下,需要类型是不可变的,比如作为set集合中的元素,或者在map中用作键的对象。
*/
private final String firstName, lastName;
public Name(String firstName, String lastName) {
// 在构造函数中检查参数是否为空。确保使用Name对象的方法不会抛出NullPointerException。
if (firstName == null || lastName == null) {
throw new NullPointerException();
}
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
public boolean equals(Object o) {
// 如果指定的对象为空或类型不合适,equals方法返回false。
if (Objects.isNull(o) || !(o instanceof Name)) {
return false;
}
Name n = (Name) o;
return n.firstName.equals(firstName) && n.lastName.equals(lastName);
}
/**
* hashCode方法被重新定义
*/
public int hashCode() {
return 31 * firstName.hashCode() + lastName.hashCode();
}
/**
* toString方法重新定义,以可读的形式打印Name。
*/
public String toString() {
return firstName + " " + lastName;
}
/**
* 实现了Comparable排序接口,姓优先于名进行比较。
*/
public int compareTo(Name n) {
int lastCmp = lastName.compareTo(n.lastName);
return (lastCmp != 0 ? lastCmp : firstName.compareTo(n.firstName));
}
public static void main(String[] args) {
Name nameArray[] = {
new Name("John", "Smith"),
new Name("Karl", "Ng"),
new Name("Jeff", "Smith"),
new Name("Tom", "Rich")
};
List<Name> names = Arrays.asList(nameArray);
Collections.sort(names);
System.out.println(names);
}
}
在示例代码中,使用 Collections.sort() 对集合进行排序,执行 main 函数的结果如下:
[Karl Ng, Tom Rich, Jeff Smith, John Smith] // 排序结果,先比较 lastName
Comparable 使用的注意事项:
- compareTo方法内必须做非空判断(规范问题),基本类型除外。
- 子类需要实现Comparable<T>接口
- Collection.sort(list) 不会自动调用 compareTo,如果不显示调用,集合不会自动进行排序
- 如果是数组则用专用的 Arrays.sort(a) 方法
2、Comparator 自定义比较规则
如果希望以不同于其自然顺序的顺序对某些对象进行排序,又有什么办法呢?或者,想对一些没有实现 Comparable 接口的对象进行排序呢?
要实现上边这两点,需要借助 Comparator (比较器:一个封装排序的对象)。与Comparable接口类似,Comparator接口也由单个方法组成。
public interface Comparator<T> {
int compare(T o1, T o2);
}
compare方法比较它的两个参数,根据对比第一个参数小于、等于或大于第二个参数,返回一个负整数、0或正整数。如果其中一个参数的类型不适合进行比较,compare 就会抛出ClassCastException 异常。
编写 compare() 与编写 compareTo() 类似,不同的是 compare() 获取两个传入的参数对象。而且,Comparator 也必须对所比较的对象给出一个总的顺序。// 相等情况下的顺序再比较
测试案例
Employee 对象的定义如下,如果要求按照雇佣日期对 Employee 对象进行排序,应该如何做?
import java.time.LocalDate;
import lombok.Data;
@Data
public class Employee {
private Name name;
private int EmployeeNo;
private LocalDate hireDate;
public Employee(Name name, int employeeNo, LocalDate hireDate) {
this.name = name;
EmployeeNo = employeeNo;
this.hireDate = hireDate;
}
}
因为 Employee 对象并没有实现Comparable接口,所以需要定义一个比较器,在 EmpSort 类中命名为 SENIORITY_ORDER。然后实现 compare() 方法,首先根据雇佣日期 HireDate 进行排序,如果雇佣日期相同,那么就必须要再进行相等情况下的顺序比较。因为,如果把集合中的数据放入Set类等不允许重复元素的集合中时,雇佣日期相等的 Employee 将会被视为重复对象,从而造成数据丢失,这明显不符合现实中的应用场景。
因此,如果 Employee 对象的雇佣日期相同,还需要再比较 employeeNo,一般来说 employeeNo 是唯一的,能够始终定义出两个Employee 对象的顺序。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class EmpSort {
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
int dateCmp = e2.getHireDate().compareTo(e1.getHireDate());
if (dateCmp != 0) {
return dateCmp; // 调整正负,实现正序或倒序排序
}
// 必须处理相等的情况以应对 treeSet 等非重复读元素的集合
// return e1.number() - e2.number();
// 注意:如果i是一个大的正整数而j是一个大的负整数,i - j将溢出并返回一个负整数
return (e1.getEmployeeNo() < e2.getEmployeeNo() ? -1 :
(e1.getEmployeeNo() == e2.getEmployeeNo() ? 0 : 1));
}
};
// 测试:Employee database
// static final Collection<Employee> employees = null;
public static List<Employee> employees() {
List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Employee employee = new Employee(new Name("ZHOU" + i, "XIAO" + i), 1001 + i,
LocalDate.parse("2021-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd")).plusDays(i));
employees.add(employee);
}
return employees;
}
public static void main(String[] args) {
List<Employee> employees = employees();
// 排序 -> 传入排序集合和比较器
Collections.sort(employees, SENIORITY_ORDER);
System.out.println(employees);
}
}
参考文章:https://docs.oracle.com/javase/tutorial/collections/interfaces/order.html