深入理解HashMap和TreeMap的區別
簡介
HashMap和TreeMap是Map家族中非常常用的兩個類,兩個類在使用上和本質上有什麼區別呢?本文將從這兩個方面進行深入的探討,希望能揭露其本質。
HashMap和TreeMap本質區別
先看HashMap的定義:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
再看TreeMap的定義:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
從類的定義來看,HashMap和TreeMap都繼承自AbstractMap,不同的是HashMap實現的是Map接口,而TreeMap實現的是NavigableMap接口。NavigableMap是SortedMap的一種,實現了對Map中key的排序。
這樣兩者的第一個區別就出來了,TreeMap是排序的而HashMap不是。
再看看HashMap和TreeMap的構造函數的區別。
public HashMap(int initialCapacity, float loadFactor)
HashMap除了默認的無參構造函數之外,還可以接受兩個參數initialCapacity和loadFactor。
HashMap的底層結構是Node的數組:
transient Node<K,V>[] table
initialCapacity就是這個table的初始容量。如果大家不傳initialCapacity,HashMap提供了一個默認的值:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
當HashMap中存儲的數據過多的時候,table數組就會被裝滿,這時候就需要擴容,HashMap的擴容是以2的倍數來進行的。而loadFactor就指定了什麼時候需要進行擴容操作。默認的loadFactor是0.75。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
再來看幾個非常有趣的變量:
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
上面的三個變量有什麼用呢?在java 8之前,HashMap解決hashcode衝突的方法是採用鏈表的形式,爲了提升效率,java 8將其轉成了TreeNode。什麼時候會發送這個轉換呢?
這時候就要看這兩個變量TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD。
有的同學可能發現了,TREEIFY_THRESHOLD爲什麼比UNTREEIFY_THRESHOLD大2呢?其實這個問題我也不知道,但是你看源代碼的話,用到UNTREEIFY_THRESHOLD時候,都用的是<=,而用到TREEIFY_THRESHOLD的時候,都用的是>= TREEIFY_THRESHOLD - 1,所以這兩個變量在本質上是一樣的。
MIN_TREEIFY_CAPACITY表示的是如果table轉換TreeNode的最小容量,只有capacity >= MIN_TREEIFY_CAPACITY的時候才允許TreeNode的轉換。
TreeMap和HashMap不同的是,TreeMap的底層是一個Entry:
private transient Entry<K,V> root
他的實現是一個紅黑樹,方便用來遍歷和搜索。
TreeMap的構造函數可以傳入一個Comparator,實現自定義的比較方法。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
如果不提供自定義的比較方法,則使用的是key的natural order。
排序區別
我們講完兩者的本質之後,現在舉例說明,先看下兩者對排序的區別:
@Test
public void withOrder(){
Map<String, String> books = new HashMap<>();
books.put("bob", "books");
books.put("c", "concurrent");
books.put("a", "a lock");
log.info("{}",books);
}
@Test
public void withOrder(){
Map<String, String> books = new TreeMap<>();
books.put("bob", "books");
books.put("c", "concurrent");
books.put("a", "a lock");
log.info("{}",books);
}
同樣的代碼,一個使用了HashMap,一個使用了TreeMap,我們會發現TreeMap輸出的結果是排好序的,而HashMap的輸出結果是不定的。
Null值的區別
HashMap可以允許一個null key和多個null value。而TreeMap不允許null key,但是可以允許多個null value。
@Test
public void withNull() {
Map<String, String> hashmap = new HashMap<>();
hashmap.put(null, null);
log.info("{}",hashmap);
}
@Test
public void withNull() {
Map<String, String> hashmap = new TreeMap<>();
hashmap.put(null, null);
log.info("{}",hashmap);
}
HashMap會報出: NullPointerException。
性能區別
HashMap的底層是Array,所以HashMap在添加,查找,刪除等方法上面速度會非常快。而TreeMap的底層是一個Tree結構,所以速度會比較慢。
另外HashMap因爲要保存一個Array,所以會造成空間的浪費,而TreeMap只保存要保持的節點,所以佔用的空間比較小。
HashMap如果出現hash衝突的話,效率會變差,不過在java 8進行TreeNode轉換之後,效率有很大的提升。
TreeMap在添加和刪除節點的時候會進行重排序,會對性能有所影響。
共同點
兩者都不允許duplicate key,兩者都不是線程安全的。
本文的例子https://github.com/ddean2009/learn-java-collections
歡迎關注我的公衆號:程序那些事,更多精彩等着您!
更多內容請訪問 www.flydean.com