刚开始工作时用过TreeSet+Comparator实现按添加顺序排序的情况(当然现在用LinkHashSet)。比较器的定义是直接return 1 :
public static void testTreeSet(){
Set<String> set = new TreeSet<>(new Comparator<String>() {
/**
* 虽然该方法不符合规定,但能按添加顺序排列元素。(具体规范请参见JDK文档)
*/
@Override
public int compare(String o1, String o2) {
return 1;
}
});;
set.add("hahahah");
set.add("aaaaa");
set.add("xxxxxx");
set.add("bbbbb");
System.out.println(set); //print [hahahah, aaaaa, xxxxxx, bbbbb],排序成功
}
由于Set底层的结构和Map是一样的:
/*
* TreeSet中m为NavigableMap(TreeMap的父接口),HashSet中m为HashMap。
*/
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
于是乎同样用TreeMap试了试,结果发现一个小问题,这个问题是由TreeMap底层的二叉树结构引起的。测试代码:
public static void main(String[] args) {
Map<String,String> map = new TreeMap<String,String>(new Comparator<String>() {
/**
* 同样不符合规定,但这时产生了问题
*/
@Override
public int compare(String o1, String o2) {
//if(o1.equals(o2)) return 0; //这一行先注释
return 1;
}
});
map.put("ss", "abc1");
map.put("s2", "abc2");
map.put("hahah2", "abc3");
map.put("abcdefg", "abc4");
System.out.println(map.keySet()+"-----" + map.values()); //print:[ss, s2, hahah2, abcdefg]-----[abc1, abc2, abc3, abc42]
System.out.println(map.get("ss")); //print:null
<span style="white-space:pre"> </span>System.out.println(map.get("hahah2")); //print:null, 但打开注释后print:abc3
}
这里的问题就是 为什么明明添加成功了但map.get("ss")却返回null?事实上这种情况下(不打开注释)获取任何一个key的值都会返回null。原因就在于当get(key)时这里会调用我们自定义的比较器比较key(return 0时表示存在),由于compare总返回1, 所以永远都找不到对应结点,则返回null。
打开注释后发现 其它元素却已经可以获取了,为什么map.get("ss")仍然返回null?这是因为TreeMap底层采用的是二叉树结构,其put方法与get方法如下:
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;
}
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
//......
//......
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key); //调用自定义比较器的比较方法
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
//......
//......
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
while (p != null) {
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p; //等于0时才返回对应的节点(找到相同的key)
}
}
return null;<span style="white-space:pre"> </span>//找不到返回null
}
通过跟踪代码中的map发现其根结点为s2,而不是第一个被添加的元素ss,由于“ss”.equals("s2")为false, 所以get("ss")时的比较器永远返回1(代表着只在根结点的右边进行检索),所以get("ss")返回null,而其它key可以正常返回。 map结构图类似如下:
总结:TreeSet可以使用这种方式实现按添加顺序排序(虽然Set底层也是Map结构,但它的元素是底层map中的key,与value无关,不需要get(key)。而TreeMap使用这种方式虽然也能实现按添加顺序排序,但不能这么干。因为其取值时的检索方式不同,很可能导致无法获取value。这种方式是不规范的,应使用LinkHashSet或LinkHashMap。