自定义 Java 类型的 TreeMapTreeSet

由于 TreeMaps 和 TreeSets 根据其自然顺序维护键/元素。因此,TreeMap 键和 TreeSet 元素必须相互比较。

假设我们有一个自定义 Person 类:

public class Person  {

    private int id;
    private String firstName, lastName;
    private Date birthday;

    //... Constuctors, getters, setters and various methods
}

如果我们将它原样存储在 TreeSet(或 TreeMap 中的 Key)中:

TreeSet<Person2> set = ...      
set.add(new Person(1,"first","last",Date.from(Instant.now())));

然后我们会遇到像这样的异常:

Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to java.lang.Comparable
    at java.util.TreeMap.compare(TreeMap.java:1294)
    at java.util.TreeMap.put(TreeMap.java:538)
    at java.util.TreeSet.add(TreeSet.java:255)

为了解决这个问题,我们假设我们想要根据其 id(private int id)的顺序来命令 Person 实例。我们可以通过以下两种方式之一来实现:

  1. 一种解决方案是修改 Person,以便实现 Comparable 接口

    public class Person implements Comparable<Person> {
        private int id;
        private String firstName, lastName;
        private Date birthday;
    
        //... Constuctors, getters, setters and various methods
    
        @Override
        public int compareTo(Person o) {
            return Integer.compare(this.id, o.id); //Compare by id
        }
    }
    
  2. 另一个解决方案是为 TreeSet 提供一个比较器

Version >= Java SE 8

TreeSet<Person> treeSet = new TreeSet<>((personA, personB) -> Integer.compare(personA.getId(), personB.getId()));
TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>(){
    @Override
    public int compare(Person personA, Person personB) {
        return Integer.compare(personA.getId(), personB.getId());
    }
});

但是,这两种方法都有两点需要注意:

  1. 将实例插入 TreeSet / TreeMap 后,不要修改用于排序的任何字段,这一点非常重要。在上面的示例中,如果我们更改已插入集合的人员的 id,我们可能会遇到意外行为。

  2. 正确和一致地实施比较非常重要。根据 Javadoc

实现者必须确保所有 x 和 y 的 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。 (这意味着如果 y.compareTo(x) 抛出异常,x.compareTo(y) 必须抛出异常。)

实现者还必须确保关系是可传递的:(x.compareTo(y)>0 && y.compareTo(z)>0) 暗示了 x.compareTo(z)>0

最后,实现者必须确保 x.compareTo(y)==0 暗示 sgn(x.compareTo(z)) == sgn(y.compareTo(z)),对于所有 z。