- 如果兩個對象通過equals()方法判斷出是相等的,那么他們hashCode也應該相等;
- 如果兩個對象通過equals()方法判斷不等,那么他們hashCode可以相等,也可以不等。
如果沒有實現以上兩條(特別是第1條),在使用Java某些集合時(特別是HashSet和HashMap),你將得不到想要的結果,甚至造成嚴重的內存泄漏問題。要搞明白里面的原由,我們還得從HashMap的內部實現開始說起。
HashMap在存儲鍵值對(Key-Value Pair)時,首先調用Key對象的hashCode()方法得到一個數字,這個數字對應了該鍵值對在HashMap中的存放位置,每個位置上不僅存放了Value,還存放了Key,即鍵值對(如下圖所示)。我們將存放鍵值對的位置叫做一個Bucket(中文名為“桶”,即用來裝東西的容器,很形象哈),Bucket中維護了一個LinkedList(下圖中的Entries),該LinkedList用於存放實際的鍵值對本身。因此,要通過Key來獲取到相應的Value,Java只需要再次調用Key的hashCode()方法得到該鍵值對在HashMap中的位置,便可以准確快速地獲取到Key對應的Value。因此,即便用於存放的Key和用於獲取的Key是相等的,如果他們的hashCode不等,那么在獲取時便得不到先前存放的准確位置,進而得不到正確的結果。另外,如果Key對象的類沒有實現equals()方法,那么默認情況下Java將使用對象的引用地址來判斷兩個對象的相等性, 而之后我們又很難再次創建出一個和先前引用地址相同的對象,因此便可能出現永遠也獲取不到Value的情況,從而導致內存泄漏。進而我們得到另一個結論:如果一個類的對象將被用於HashMap的Key或者被直接放入Set集合中,那么這個類應該實現equals()方法和hashCode()方法。

注意到上圖中的152號Bucket了嗎?我們發現同時有John Smith和Sandra Dee兩個Key都指向了這個相同的Bucket,即這兩個對象的hashCode相等。這便是equals()和hashCode()契約關系的第2點。Java采用了LinkedList來存放所有Key的hashCode相等的鍵值對。此時在LinkedList中,前一個鍵值對維護了一個指針指向了下一個鍵值對。當之后通過John Smith來獲取Value時,Java首先發現其對應的Bucket中存在着兩個鍵值對,然后Java分別將各個鍵值對中Key對象與所傳來的Key對象相比,如果相等則返回該鍵值對所對應的Value。由此,我們也知道HashMap中為什么需要同時存Key和Value而不是只存Value的原因;同時也知道了契約第2點的來由。事實上,我們可以讓一個類的所有對象都返回相同的hashCode,只是此時如果該類的對象作為Key時,所有的鍵值對都將存放都相同的Bucket位置,每次在獲取的時候都需要做多次的比較,因此會影響HashMap的獲取速度。
線程安全性
通常來說,在Java中可以通過兩種方式來實現線程安全,一種是通過Java自帶的並發管控手段(比如使用syncronized關鍵字),另一中是通過創建不可變(Immutable)對象。
在很早的時候,Java里面有Vector和HashTable兩個集合對象,他們通過使用syncronized關鍵字實現了線程安全,但是同時也暴露出了很大的性能問題。因此現在基本上沒有人使用了。為了解決Vector和HashTable的性能問題,Java從1.2引入的Collections框架采用了線程不安全的類,比如ArrayList和HashMap都是線程不安全的。當然,為了性能而犧牲了線程安全性也是不可取的,因此Java通過Fail-Fast的Iterator來避免多個線程同時操作集合所帶的線程沖突問題。比如,當一個線程正在遍歷一個集合而另一個線程正在修改該集合時,前者將拋出ConcurrentModificationException。這當然也不是萬全的辦法。
另一方面,Java其實也提供了線程安全的封裝類(Wrapper)來實現集合的線程安全性,我們可以通過:
Collections.synchronizedXXX(collection)
來創建線程安全的集合,這里的XXX可以是Collection、List、Map和Set等。對於一個常規的colleciton對象,調用(synchronizedXXX)方法將得到封裝后的線程安全性。
集合封裝類同樣采用了syncronized關鍵字來達到線程安全性,並且是在整個集合類上上鎖,這樣也會帶來嚴重的性能問題。為了解決這樣的問題,從Java 5開始引入了並發集合(Concurrent Collections),他們要么采用Immutable集合,要么采用更加精細的鎖控制來達到線程安全的目的,同時又能保證很高的性能。
並發集合主要包含三類,一是Copy-On-Write集合,二是Compare-And-Swap集合,三是采用特殊鎖的並發集合。Copy-On-Write集合底層維護的是一個不變的(Immutable)的數組,通過在寫(Write)入集合時重新復制(Copy)一份新的集合來達到線程安全,進而得名Copy-On-Write。Coppy-On-Write集合包括有CopyOnWriteArrayList和CopyOnWriteArraySet等。Compare-And-Swap集合在進行更新的時候,首先維護一個本地拷貝,當執行更新時,比較本地拷貝與原值,如果值相等,則證明在這段時間內還沒有其他線程修改原值,此時立即更新;如果不相等,則重新拷貝原值,再計算,再更新,這樣也到了線程安全的目的。Compare-And-Swap集合包括ConcurrentLinkedQueue和ConcurrentSkipListMap等。第三類是使用特殊鎖的集合,這種集合類並不在整個集合類上上鎖,而是通過在Bucket級別上上鎖,從而達到了對並發的更精細的控制,減少了線程的等待時間,從而提高了並發性能。
除了提供並發控制機制外,Java還提供了不可修改的(unmodifiable)集合來保證線程安全性,可以通過:
Collections.unmodifiableXXX(collection)
來創建不同unmodifiable集合。這樣的集合其實也是一個封裝類,對於傳入的正常集合collection,通過對add()等方法拋出UnsupportedOperationException異常來達到不可修改的目的。但是,這樣的集合其實並不達到不可修改目的,因為被其包裝的collection本身依然是可以修改的。
為了實現更好的集合不變性,Guava類庫提供了很多Immutable的集合,這些集合是真正不變的。
