Java中TreeSet與HashSet的對比

我們知道TreeSet與HashSet都實現了Set<E>,Set的特性就是不允許重複的元素。《thinking in Java》中說set必須定義equals方法確保對象的唯一性,但是經過測試貌似不是這樣,不知道是不是java1.5以後版本改變後造成的。下面就從唯一性展開問題,先看例子:

先來看HashSet:

package testsortset;

import java.util.HashSet;
import java.util.TreeSet;

public class TestHashSet {
	public static void main(String[] args) {
		HashSet<B> setsB = new HashSet<B>();
		setsB.add(new B(1));
		setsB.add(new B(2));
		setsB.add(new B(3));
		setsB.add(new B(3));
		B b4 = new B(4);
		B b4_ = new B(4);
		setsB.add(b4);
		setsB.add(b4_);
		System.out.println(b4.equals(b4_));
		
		for (B b : setsB) {
			System.out.println(b.i);
		}
	}
}

class B {
	public int i;
	
	public B(int n) {
		this.i = n;
	}
	
//	@Override
//	public boolean equals(Object obj) {
//		return (obj instanceof B) && (this.i == ((B) obj).i);
//	}
//	
//	@Override
//	public int hashCode() {
//		return i;
//	}
}
輸出:

false
3
4
1
2
3
4
第一個false,很好理解,因爲我們沒有覆蓋equals方法,類B繼承自Object的equals方法,比較的是類內存地址的hashcode。

這裏我估計註釋了equals和hashcode方法,確實,hashset中包含了重複的元素。接下來打開註釋(先只打開equals方法):

package testsortset;

import java.util.HashSet;
import java.util.TreeSet;

public class TestHashSet {
	public static void main(String[] args) {
		HashSet<B> setsB = new HashSet<B>();
		setsB.add(new B(1));
		setsB.add(new B(2));
		setsB.add(new B(3));
		setsB.add(new B(3));
		B b4 = new B(4);
		B b4_ = new B(4);
		setsB.add(b4);
		setsB.add(b4_);
		System.out.println(b4.equals(b4_));
		
		for (B b : setsB) {
			System.out.println(b.i);
		}
	}
}

class B {
	public int i;
	
	public B(int n) {
		this.i = n;
	}
	
	@Override
	public boolean equals(Object obj) {
		return (obj instanceof B) && (this.i == ((B) obj).i);
	}
	
//	@Override
//	public int hashCode() {
//		return i;
//	}
}
輸出:

true
3
4
1
2
3
4
第一個true很好理解,因爲我們覆蓋了equals方法。但是爲什麼還是含有重複的元素呢?!爲什麼和書上說的不一樣,確定唯一性由equals方法確定呢?先不着急,再打開hashcode方法的註釋看看:

package testsortset;

import java.util.HashSet;
import java.util.TreeSet;

public class TestHashSet {
	public static void main(String[] args) {
		HashSet<B> setsB = new HashSet<B>();
		setsB.add(new B(1));
		setsB.add(new B(2));
		setsB.add(new B(3));
		setsB.add(new B(3));
		B b4 = new B(4);
		B b4_ = new B(4);
		setsB.add(b4);
		setsB.add(b4_);
		System.out.println(b4.equals(b4_));
		
		for (B b : setsB) {
			System.out.println(b.i);
		}
	}
}

class B {
	public int i;
	
	public B(int n) {
		this.i = n;
	}
	
	@Override
	public boolean equals(Object obj) {
		return (obj instanceof B) && (this.i == ((B) obj).i);
	}
	
	@Override
	public int hashCode() {
		return i;
	}
}
輸出:

true
1
2
3
4
是的,現在你看到了你想要的結果。確定了唯一性。我們可以知道hashset確定唯一性由equals和hashcode兩個方法同時確定。當然,另一種情況註釋equals,留下hashcode的情況可以試一下也是一樣的包含重複的元素,這裏就不貼代碼了。

爲什麼呢?

原因:我們知道hashset底層是由hashmap實現的,也就是說是否包含重複元素由hashmap確定,我們可以看一下hashmap的put方法:(hashmap 1.8的實現是數組+鏈表+紅黑樹)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
從源碼可以看出,當hashmap在put一個元素時,確定該元素是否已經存在hashcode和equals兩個方法確定的。

接下來看TreeSet:

package testsortset;

import java.util.TreeSet;

public class TestSortedSet {
	public static void main(String[] args) {
		TreeSet<A> sets = new TreeSet<A>();
		sets.add(new A(1));
		sets.add(new A(2));
		sets.add(new A(3));
		sets.add(new A(3));
		A a4 = new A(4);
		A a4_ = new A(4);
		sets.add(a4);
		sets.add(a4_);
		System.out.println(a4.equals(a4_));
		
		for (A a : sets) {
			System.out.println(a.i);
		}
	}
}

class A implements Comparable<A> {
	public int i;

	public A(int n) {
		this.i = n;
		//System.out.println(i);
	}

	@Override
	public int compareTo(A o) {
//		if (o.i > this.i) {
//			return -1;
//		} else if (o.i < this.i) {
//			return 1;
//		} else {
//			return 0;
//		}
		return 0;
	}

	@Override
	public boolean equals(Object obj) {
//		return (obj instanceof A) && (this.i == ((A) obj).i);
		return false;
	}
	
}

輸出:

false
1

第一個false很好理解,equals不管怎樣都返回false。爲什麼只有一個元素呢?不是由equals方法確定嗎?應該每個元素都不相等,應該是多個元素的啊。我們改一改(我們知道sortedSet中的元素要實現comparable接口或者使用comparator):

package testsortset;

import java.util.TreeSet;

public class TestSortedSet {
	public static void main(String[] args) {
		TreeSet<A> sets = new TreeSet<A>();
		sets.add(new A(1));
		sets.add(new A(2));
		sets.add(new A(3));
		sets.add(new A(3));
		A a4 = new A(4);
		A a4_ = new A(4);
		sets.add(a4);
		sets.add(a4_);
		System.out.println(a4.equals(a4_));
		
		for (A a : sets) {
			System.out.println(a.i);
		}
	}
}

class A implements Comparable<A> {
	public int i;

	public A(int n) {
		this.i = n;
	}

	@Override
	public int compareTo(A o) {
		if (o.i > this.i) {
			return -1;
		} else if (o.i < this.i) {
			return 1;
		} else {
			return 0;
		}
	}

	@Override
	public boolean equals(Object obj) {
		return (obj instanceof A) && (this.i == ((A) obj).i);
	}
	
}
輸出:

true
1
2
3
4

發現這纔是你預期的效果,是的。猜想,是不是由compareTo方法來確定是否唯一的?我們再做一個測試:

package testsortset;

import java.util.TreeSet;

public class TestSortedSet {
	public static void main(String[] args) {
		TreeSet<A> sets = new TreeSet<A>();
		sets.add(new A(1));
		sets.add(new A(2));
		sets.add(new A(3));
		sets.add(new A(3));
		A a4 = new A(4);
		A a4_ = new A(4);
		sets.add(a4);
		sets.add(a4_);
		System.out.println(a4.equals(a4_));
		
		for (A a : sets) {
			System.out.println(a.i);
		}
	}
}

class A implements Comparable<A> {
	public int i;

	public A(int n) {
		this.i = n;
	}

	@Override
	public int compareTo(A o) {
		if (o.i > this.i) {
			return -1;
		} else if (o.i < this.i) {
			return 1;
		} else {
			return 0;
		}
	}

	@Override
	public boolean equals(Object obj) {
//		return (obj instanceof A) && (this.i == ((A) obj).i);
		return true;
	}
	
}
輸出:

true
1
2
3
4

第一個true很好理解,後邊的元素不是一個就說明跟equals沒有關係,而是跟compareTo方法有關係,也就是說treeset的唯一性由compareTo方法確定。爲什麼(treeset的底層由treeMap實現)?那麼看看treeMap的put方法:

<span style="font-size:14px;">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) {//是否有comparator
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);//根據comparator比較的結果確定插入樹中的左子樹,右子樹還是設值
              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);////根據compareTo比較的結果確定插入樹中的左子樹,右子樹還是設值
                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;
    }</span>

從源碼中我們可以知道,確定TreeSet唯一性的是Comparable或者Comparator,跟equals方法沒有關係。

總結:set集合具有唯一元素的特性,而不同具體的set的集合有差別。

存入HashSet的元素必須定義hashcode和equals方法,共同確定唯一性。並且它是沒有順序的。

存入TreeSet的元素必須實現接口Comparable或者傳入比較實現Comparator,equals根據具體情況覆蓋,跟唯一性沒有關係。由於TreeSet有紅黑樹實現,所以是有序的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章