(一)Map方法概述
首先先看一下官方對Map接口的解釋,《Java Platform SE 8》:
An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.
Map是一個通過鍵值對保存的對象,一個map只能由一個key,但是一個key可以有多個value。
1.1 Map的幾個常用方法
public class MapTest {
public static void main(String[] args) {
Map map=new HashMap();
//添加 put(key,value)
map.put("a1",1);
map.put("a2",1);
map.put(null,1);
System.out.println(map);
//刪除 remove(key)
map.remove("a2");
System.out.println(map);
//是否包含 key value
//containsKey(key) containsValue(value)
System.out.println(map.containsKey("a1"));
System.out.println(map.containsValue("1"));
//獲取數據 get(key)
System.out.println(map.get("a1"));
//獲取大小 size()
System.out.println(map.size());
//是否爲空 isEmpty()
System.out.println(map.isEmpty());
//獲取所有的關係 entrySet()
System.out.println(map.entrySet());
//獲取所有的key keySet()
System.out.println(map.keySet());
//獲取所有的value values()
System.out.println(map.values());
}
}
(二)HashMap的特點
HashMap底層是一個哈希表,以數組加鏈表的形式存儲值。HashMap具有以下特點:
4.在jdk7.0中,底層是數組加鏈表;在jdk8.0中,底層是數組加鏈表加紅黑樹(這一點在後面會重點講一下)
(三)HashMap的源碼分析
通過代碼斷點的方法逐個添加元素,單步觀察代碼執行步驟,首先進入HashMap的構造方法:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
該構造方法把負載因子設置爲0.75,負載因子的意思是當存入的數據大於總容量的0.75倍時,就擴容。構造方法結束後進入put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法直接返回putVal()方法,putVal方法的第一個參數是根據key計算的一個哈希值,可以看一下這個hash方法:通過hash運算和異或操作得到hash值並返回
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//查看此時table的容量(即哈希表數組部分的長度),如果爲空(第一次進入),則進入resize()方法
//resize()是個初始化或擴容方法,初始化成16或擴容2倍
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根據此時數組的長度n和計算的hash值算出索引
//計算出的索引一定在0~n-1之間
//如果該索引位置沒有元素,則直接將元素添加進入
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;
//判斷容量是否超過臨界值,如果超過了就2倍擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
源碼分析:
HashMap中維護了Node類型的數組table,當HashMap創建對象時,設置負載因子爲0.75,table還是null。
當第一次添加元素時,將table的容量設置爲16,臨界值設置爲12
1.將key的hash值和table容量-1進行與運算,得到索引值
2.判斷該存放位置上是否有元素,如若沒有元素則直接放上去;如果該索引位置已存在元素,則繼續判斷
3.如果該位置的元素和添加元素相等,則直接覆蓋,如果不相等,則繼續判斷是鏈表結構還是樹狀結構,按照相對應的方式添加。
如果添加的數量大於臨界值,執行resize方法對容量雙倍擴容。並打亂順序重新排列。
(四)HashMap在JDK7和JDK8中的區別
前面一直提到樹狀結構和紅黑樹,這是HashMap在JDK7和JDK8之間最大的區別。數組+鏈表的結構下,如果一個索引後跟着的鏈表數量很多時,會很影響查找效率,因此在JDK8中,HashMap當滿足某種條件(鏈表長度大於8,table容量大於64)時,會將鏈表轉化爲紅黑樹結構,提高效率。
截取一段源碼:當鏈表長度大於等於(TREEIFY_THRESHOLD - 1)時,這個值是7,進入treeifyBin方法。鏈表長度大於等於7,再加上數組上的一個元素,一共是8個元素。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果進入treeifyBin但是table的容量小於64,則執行resize擴容並重新打亂
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//鏈表長度大於8,table容量大於64,轉化成紅黑樹
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
如果進入treeifyBin但是table的容量小於64,則執行resize擴容並重新打亂。所以並非容量大於臨界容量纔會擴容。
1.初始化對象時,JDK7直接初始化對象容量爲16,JDK8僅僅初始化負載因子爲0.75
2.table類型:JDK7是Entry(映射key和value),JDK8是Node類型(爲了紅黑樹)
3.底層結構:JDK7數組+鏈表,JDK8數組+鏈表+紅黑樹(鏈表長度大於8,table容量大於64)
(四)HashMap和HashTable的對比
HashMap和HashTable的處境有點像Vector和ArrayList,HashTable現在很少使用,就用一個表格來總結它和HashMap的區別
|
底層結構 |
版本 |
線程安全(同步) |
允許null |
HashMap |
哈希表 |
1.2 |
不安全 |
允許鍵值爲null |
HashTable |
哈希表 |
1.0 |
安全 |
不允許鍵值null |
(五)TreeMap的介紹
A Red-Black tree based NavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.
根據官方文檔的介紹,TreeMap底層是一個紅黑樹,map是根據keys進行自然排序或者定製排序。
使用自然排序:需要在類中繼承Comparable接口,並重寫compareTo方法。
public class Book implements Comparable{
private String name;
private float price;
public Book(String name, float price){
this.name=name;
this.price=price;
}
//.........
@Override
public int compareTo(Object o) {
Book book= (Book) o;
return Double.compare(book.price,this.price);
}
}
使用定製排序:需要在創建TreeMap對象時傳入一個Comparator接口,並實現裏面的compare方法。
TreeMap map=new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
Book book1= (Book) o1;
Book book2= (Book) o2;
return Double.compare(book1.getPrice(),book2.getPrice());
}
});
(六)總結
HashMap絕對是Map中的重點,也是據我所知面試中問到最多的集合知識。因此有條件的話打開源碼自己單步調試一遍。HashTable、TreeMap就算沒有看過代碼但是也要了解各自的特點。