最近在使用TreeSet的过程中遇到一个问题,发现对元素覆写compareTo方法后部分元素被吞掉了。于是查了相关资料并看了一下源码,在此记录一下使用过程中的注意事项

原始需求是对一系列规则根据优先级进行排序,从而构成规则链,规则的信息如下:


/**
 * @author 刘晓东
 */
public class RuleInfo implements Comparable {
    private long id;
    private int priority;//规则优先级
    ......
     public boolean equals(Object obj) {
        if (obj instanceof RuleInfo) {
            RuleInfo ruleInfo = (RuleInfo) obj;
            return this.id == ruleInfo.id;
        }
        return false;
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof RuleInfo) {
            RuleInfo ruleInfo = (RuleInfo) o;
            return this.getPriority() - ruleInfo.getPriority();
        }
        return 0;
    }


}

在插入几条具有相同优先级的规则后,发现treeset中只会保留一条规则,其他相同优先级的规则都被吞掉了。然后看了一下TreeSet的源码,发现注释中有这么几句话。

* <p>Note that the ordering maintained by a set (whether or not an explicit
* comparator is provided) must be <i>consistent with equals</i> if it is to
* correctly implement the {@code Set} interface.  (See {@code Comparable}
* or {@code Comparator} for a precise definition of <i>consistent with
* equals</i>.)  This is so because the {@code Set} interface is defined in
* terms of the {@code equals} operation, but a {@code TreeSet} instance
* performs all element comparisons using its {@code compareTo} (or
* {@code compare}) method, so two elements that are deemed equal by this method
* are, from the standpoint of the set, equal.  The behavior of a set
* <i>is</i> well-defined even if its ordering is inconsistent with equals; it
* just fails to obey the general contract of the {@code Set} interface.

翻译过来也就是:Set中各个元素是以equal方法来判断两个元素是否一样,而在TreeSet中是以compareTo方法来进行排序并判断两个元素是否一样的,覆写equal方法在TreeSet中不起作用。因此上面的compareTo方法可以改写为:

@Override
   public int compareTo(Object o) {
       if (o instanceof RuleInfo) {
           RuleInfo ruleInfo = (RuleInfo) o;
           if (this.getId() == ruleInfo.getId()) {
               return 0;
           }
           int ret = this.getPriority() - ruleInfo.getPriority();
           if (ret == 0) {
               return (int) (this.id - ruleInfo.id);
           } else {
               return ret;
           }
       }
       return 0;
   }

接下来看一下TreeSet的add方法是如何实现的。
TreeSet是用TreeMap实现的,其中的add方法直接使用的TreeMap的put方法

public boolean add(E e) {
      return m.put(e, PRESENT)==null;
  }

TreeMap的put方法为:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

可以看到如果元素自定义了比较器,那么当返回比较值为0时,直接将值覆盖了,从而相同优先级的元素只会保留一个。所以使用TreeSet自定义比较器时一定要注意这一点。