深入理解Java中的HashMap的實現原理
HashMap繼承自抽象類AbstractMap,抽象類AbstractMap實現了Map介面。關係圖如下所示:
Java中的Map<key, value>介面允許我們將一個物件作為key,也就是可以用一個物件作為key去查詢另一個物件。
在我們探討HashMap的實現原理之前,我們先自己實現了一個SimpleMap類,該類繼承自AbstractMap類。具體實現如下:
import java.util.*;
public class SimpleMap<K,V> extends AbstractMap<K,V> {
//keys儲存所有的鍵
private List<K> keys = new ArrayList<K>();
//values儲存所有的值
private List<V> values = new ArrayList<V>();
/**
* 該方法獲取Map中所有的鍵值對
*/
@Override
public Set entrySet() {
Set<Map.Entry<K, V>> set = new SimpleSet<Map.Entry<K,V>>();
//keys的size和values的size應該一直是一樣大的
Iterator<K> keyIterator = keys.iterator();
Iterator<V> valueIterator = values.iterator();
while(keyIterator.hasNext() && valueIterator.hasNext()){
K key = keyIterator.next();
V value = valueIterator.next();
SimpleEntry<K,V> entry = new SimpleEntry<K,V>(key, value);
set.add(entry);
}
return set;
}
@Override
public V put(K key, V value) {
V oldValue = null;
int index = this.keys.indexOf(key);
if(index >= 0){
//keys中已經存在鍵key,更新key對應的value
oldValue = this.values.get(index);
this.values.set(index, value);
}else{
//keys中不存在鍵key,將key和value作為鍵值對新增進去
this.keys.add(key);
this.values.add(value);
}
return oldValue;
}
@Override
public V get(Object key) {
V value = null;
int index = this.keys.indexOf(key);
if(index >= 0){
value = this.values.get(index);
}
return value;
}
@Override
public V remove(Object key) {
V oldValue = null;
int index = this.keys.indexOf(key);
if(index >= 0){
oldValue = this.values.get(index);
this.keys.remove(index);
this.values.remove(index);
}
return oldValue;
}
@Override
public void clear() {
this.keys.clear();
this.values.clear();
}
@Override
public Set keySet() {
Set<K> set = new SimpleSet<K>();
Iterator<K> keyIterator = this.keys.iterator();
while(keyIterator.hasNext()){
set.add(keyIterator.next());
}
return set;
}
@Override
public int size() {
return this.keys.size();
}
@Override
public boolean containsValue(Object value) {
return this.values.contains(value);
}
@Override
public boolean containsKey(Object key) {
return this.keys.contains(key);
}
@Override
public Collection values() {
return this.values();
}
}
當子類繼承自AbstractMap類時,我們只需要實現AbstractMap類中的entrySet方法和put方法即可,entrySet方法是用來返回該Map所有鍵值對的一個Set,put方法是實現將一個鍵值對放入到該Map中。
大家可以看到,我們上面的程式碼不僅除了實現entrySet和put方法外,我們還重寫了get、remove、clear、keySet、values等諸多方法。其實我們只要重寫entrySet和put方法,該類就可以正確執行,那我們為什麼還要重寫剩餘的那些方法呢?AbstractMap這個方法做了很多處理操作,Map中的很多方法在AbstractMap都實現了,而且很多方法都依賴於entrySet方法,舉個例子,Map介面中的values方法是讓我們返回該Map中所有的值的Collection。我們可以看一下AbstractMap中對values方法的實現:
public Collection<V> values() {
if (values == null) {
values = new AbstractCollection<V>() {
public Iterator<V> iterator() {
return new Iterator<V>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public V next() {
return i.next().getValue();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object v) {
return AbstractMap.this.containsValue(v);
}
};
}
return values;
}
大家可以看到,程式碼不少,基本的思路是先通過entrySet生成包含所有鍵值對的Set,然後通過迭代獲取其中的value值。其中生成包含所有鍵值對的Set肯定需要開銷,所以我們在自己的實現裡面重寫了values方法,就一句話,return this.values,直接返回我們的values欄位。所以我們重寫大部分方法的目的都是讓方法的實現更快更簡潔。
大家還需要注意一下,我們在重寫entrySet方法時,需要返回一個包含當前Map所有鍵值對的Set。首先鍵值對時一種型別,所有的鍵值對類都要實現Map.Entry<K,V>這個介面。其次,由於entrySet要讓我們返回一個Set,這裡我們沒有使用Java中已有的Set型別(比如HashSet、TreeSet),有兩方面的原因:
1. Java中HashSet這個類內部其實用HashMap實現的,本部落格的目的就是要研究HashMap,所以我們不用此類;
2. Java中Set的實現也不是很麻煩,自己實現一下AbstractSet,加深一下對Set的理解。
以下是我們自己實現的鍵值對類SimpleEntry,實現了Map.Entry<K,V>介面,程式碼如下:
import java.util.Map;
//Map中儲存的鍵值對,鍵值對需要實現Map.Entry這個介面
public class SimpleEntry<K,V> implements Map.Entry<K, V>{
private K key = null;//鍵
private V value = null;//值
public SimpleEntry(K k, V v){
this.key = k;
this.value = v;
}
@Override
public K getKey() {
return this.key;
}
@Override
public V getValue() {
return this.value;
}
@Override
public V setValue(V v) {
V oldValue = this.value;
this.value = v;
return oldValue;
}
}
以下是我們自己實現的集合類SimpleSet,繼承自抽象類AbstractSet<K,V>,程式碼如下:
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator;
public class SimpleSet<E> extends AbstractSet<E> {
private ArrayList<E> list = new ArrayList<E>();
@Override
public Iterator<E> iterator() {
return this.list.iterator();
}
@Override
public int size() {
return this.list.size();
}
@Override
public boolean contains(Object o) {
return this.list.contains(o);
}
@Override
public boolean add(E e) {
boolean isChanged = false;
if(!this.list.contains(e)){
this.list.add(e);
isChanged = true;
}
return isChanged;
}
@Override
public boolean remove(Object o) {
return this.list.remove(o);
}
@Override
public void clear() {
this.list.clear();
}
}
我們測試下我們寫的SimpleMap這個類,測試包括兩部分,一部分是測試我們寫的SimpleMap是不是正確,第二部分測試效能如何,測試程式碼如下:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class Test {
public static void main(String[] args) {
//測試SimpleMap的正確性
SimpleMap<String, String> map = new SimpleMap<String, String>();
map.put("iSpring", "27");
System.out.println(map);
System.out.println(map.get("iSpring"));
System.out.println("-----------------------------");
map.put("iSpring", "28");
System.out.println(map);
System.out.println(map.get("iSpring"));
System.out.println("-----------------------------");
map.remove("iSpring");
System.out.println(map);
System.out.println(map.get("iSpring"));
System.out.println("-----------------------------");
//測試效能如何
testPerformance(map);
}
public static void testPerformance(Map<String, String> map){
map.clear();
for(int i = 0; i < 10000; i++){
String key = "key" + i;
String value = "value" + i;
map.put(key, value);
}
long startTime = System.currentTimeMillis();
for(int i = 0; i < 10000; i++){
String key = "key" + i;
map.get(key);
}
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
System.out.println("遍歷時間:" + time + "毫秒");
}
}
輸出結果如下:
{iSpring=27}
27
-----------------------------
{iSpring=28}
28
-----------------------------
{}
null
-----------------------------
遍歷時間:956毫秒
從結果裡面我們看到輸出結果是正確的,也就是我們寫的SimpleMap基本實現都是對的。我們往Map中插入了10000個鍵值對,我們測試的是從Map中取出這10000條鍵值對的效能開銷,也就是測試Map的遍歷的效能開銷,結果是956毫秒。
沒有對比就不知效能強弱,我們測試下HashMap讀取這10000條鍵值對的時間開銷,測試方法完全一樣,只是我們傳入的是HashMap的例項,測試程式碼如下:
//建立HashMap的例項
HashMap<String, String> map = new HashMap<String, String>();
//測試效能如何
testPerformance(map);
測試結果如下:
遍歷時間:32毫秒
我去,不比不知道,一比嚇一跳啊,HashMap比我們自己實現的SimpleMap快的那不是一點半點啊。為什麼我們的SimpleMap效能這麼差?而HashMap的效能如此高呢?我們分別研究。
首先分析SimpleMap效能為什麼這麼差。
我們的SimpleMap是用ArrayList來儲存keys和values的,ArrayList本質是用陣列實現的,我們的SimpleMap的get方法是這樣實現的:@Override
public V put(K key, V value) {
V oldValue = null;
int index = this.keys.indexOf(key);
if(index >= 0){
//keys中已經存在鍵key,更新key對應的value
oldValue = this.values.get(index);
this.values.set(index, value);
}else{
//keys中不存在鍵key,將key和value作為鍵值對新增進去
this.keys.add(key);
this.values.add(value);
}
return oldValue;
}
需要效能開銷的主要是this.keys.indexOf(key)這句程式碼,這句程式碼從ArrayList中查詢指定元素的索引,本質就是從陣列開頭走,往後找,直至陣列的末尾。如下圖所示:
這樣從頭開始查詢,並且每次在遍歷元素的時候,都需要呼叫元素的equals方法,所以從頭開始查詢就會導致呼叫很多次equals方法,這就造成了SimpleMap效率低下。比如我們將全國的車輛放入到SimpleMap中時,我們是依次將車輛放到ArrayList的最後面,依次往後插入值,車牌號就相當於key,車輛就好比是value,所以SimpleMap中有兩個長度很長的ArrayList,分別儲存keys和values,如果要在該SimpleMap中查詢一輛車,車牌是"魯E.DE829",那如果用ArrayList查詢的話就要從全國的的所有車輛中去查詢了,這樣太慢。
那麼HashMap為何效率如此高呢?
HashMap比較聰明,大家可以看看HashMash.java的原始碼,HashMap把裡面的元素分類放置了,還拿上面根據車牌號查詢車輛的例子來說,當把我們把車輛往HashMap裡面放的時候,HashMap將它們分類處理了,首先來一輛車的時候,先看其車牌號,比如車牌號是"魯E.DE829",一看是魯,就知道是山東的車輛,那麼HashMap就開闢了一塊空間,專門放山東的車,就把這輛車放到這塊山東專屬的區間了,下次又要向HashMap放入一輛車牌號為“浙A.GX588",HashMap一看是浙江的車,就將這輛車放入到浙江的專屬區間了,依次類推。說的再通俗點,假設我們有一種很大的桶,該桶就是相應的區間,可以裝下很多車,如下圖所示:
當我們從HashMap中根據車牌號查詢指定的車輛時,比如查詢車牌號為為"魯E.DE829"的車,當呼叫HashMap的get方法時,HashMap一看車牌號是魯,那麼HashMap就去標為魯的那個大桶,也就是山東區間去找這輛車了。這樣就沒有必要從全國的車輛中挨個找這輛車了,這就大大縮短了查詢空間,提高了效率。
我們可以看看HashMap.java中具體的原始碼實現,HashMap中用一個名為table的欄位儲存著一個Entry陣列,table儲存著HashMap裡面的所有鍵值對,每個鍵值對都是一個Entry物件。每個Entry物件都儲存著一個key和value,除此之外每個Entry內部還存著一個next欄位,next也是Entry型別。陣列table的預設長度是DEFAULT_INITIAL_CAPACITY,即初始長度為16,當容器需要更多的空間存取Entry時,它會自動擴容。
以下是HashMap的put方法的原始碼實現:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
在put方法中,,呼叫了物件的hashCode方法,該方法返回一個int型別的值,是個初始的雜湊值,這個值就相當於車牌號,例如"魯E.DE829",HashMap中有個hash方法,該hash方法將我們得到的初始的雜湊值做進一步處理,得到最終的雜湊值,就好比我們將車牌號傳入hash方法,然後返回該存放車輛的大桶,即返回"魯",這樣HashMap就把這輛車放到標有“魯”的大桶裡面了。上面說到的hash方法叫做雜湊函式,專門負責根據傳入的值返回指定的最終雜湊值,具體實現如下:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
可以看出來,HashMap中主要是通過位操作符實現雜湊函式的。這裡簡單說一下雜湊函式,雜湊函式有多種實現方式,比如最簡單的就是取餘法,比如對i%10取餘,然後按照餘數建立不同的區塊或桶。比如有100個數,分別是從1到100,那麼分別對10取餘,那麼就可以把這100個數放到10個桶子裡面了,這就是所謂的雜湊函式。只不過HashMap中的hash函式看起來比較複雜,進行的是位操作,但是其作用與簡單的取餘雜湊法的作用是等價的,就是把元素分類放置。
具體將鍵值對放入到HashMap中的方法是addEntry,程式碼如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
鍵值對都是Map.Entry<K,V>物件,並且Map.Entry具有next欄位,也就是桶裡面的元素都是通過單向連結串列的形式將Map.Entry串連起來的,這樣我們就可以從桶上的第一個元素通過next依次遍歷完桶裡面所有的元素。比如桶中有如下鍵值對:
桶-->e1-->e2-->e3-->e4-->e5-->e6-->e7-->e8-->e9-->...
addEntry程式碼首先取出桶裡面的第一個鍵值對e1,然後將新的鍵值對e置於桶中第一個元素的位置,然後將鍵值對e1放置於新鍵值對e後面,放置完之後,桶中新的鍵值對如下:
桶-->e-->e1-->e2-->e3-->e4-->e5-->e6-->e7-->e8-->e9-->...
這樣就把新的鍵值對放到了桶中了,也就將鍵值對放到HashMap中了。
那麼當我們從HashMap中查詢某個鍵值對時,怎麼查詢呢?原理與我們將鍵值對放入HashMap相似,以下是HashMap的get方法的原始碼實現:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
在get方法中,也是先呼叫了物件的hashCode方法,就相當於車牌號,然後再將該值讓hash函式處理得到最終的雜湊值,也就是桶的索引。然後我們再去這個標有“魯”的桶裡面去找我們的鍵值對,首先先取出桶裡面第一個鍵值對,比對一下是不是我們要找的元素,如果是就直接返回了,如果不是就通過鍵值對的next順藤摸瓜通過單向連結串列繼續找下去,直至找到。 如下圖所示:
下面我們再寫一個Car類,該類有一個欄位String型別的欄位num,並且我們重寫了Car的equals方法,我們認為只要車牌號相等就認為這是同一輛車。程式碼如下所示:
import java.util.HashMap;
public class Car {
private final String num;//車牌號
public Car(String n){
this.num = n;
}
public String getNum(){
return this.num;
}
@Override
public boolean equals(Object obj) {
if(obj == null){
return false;
}
if(obj instanceof Car){
Car car = (Car)obj;
return this.num.equals(car.num);
}
return false;
}
public static void main(String[] args){
HashMap<Car, String> map = new HashMap<Car, String>();
String num = "魯E.DE829";
Car car1 = new Car(num);
Car car2 = new Car(num);
System.out.println("Car1 hash code: " + car1.hashCode());
System.out.println("Car2 hash code: " + car2.hashCode());
System.out.println("Car1 equals Car2: " + car1.equals(car2));
map.put(car1, new String("Car1"));
map.put(car2, new String("Car2"));
System.out.println("map.size(): " + map.size());
}
}
我們在main函式中寫了一些測試程式碼,我們建立了一個HashMap,該HashMap的用Car作為鍵,用字串作為值。我們用同一個字串例項化了兩個Car,分別為car1和car2,然後將這兩個car都放入到HashMap中,輸出結果如下:Car1 hash code: 404267176
Car2 hash code: 2027651571
Car1 equals Car2: true
map.size(): 2
Car2 hash code: 2027651571
Car1 equals Car2: true
map.size(): 2
從結果可以看出來,Car1和Car2是相等的,既然二者是相等的,也就是兩者作為鍵來說是相等的鍵,所以HashMap裡面只能放其中一個作為鍵,但是實際結果中map的長度卻是2個,為什麼會這樣呢?關鍵在於Car的hashCode方法,準確的說是Object的hashCode方法,Object的hashCode方法預設情況下返回的是物件記憶體地址,因為記憶體地址是唯一的。
我們沒有重寫Car的hashCode方法,所以car1的hashCode返回的值和car2的hashCode返回的值肯定不同。通過我們前面研究可知,如果是兩個元素相等,那麼這兩個元素應該放到同一個HashMap的桶裡。但是由於我們的car1和car2的hashCode不同,所以HashMap將car1和car2分別放到不同的桶子裡面了,這就出問題了。相等(equals)的兩個元素(car1和car2)如果hashCode返回值不同,那麼這兩個元素就會放到HashMap不同的區間裡面。所以我們寫程式碼的時候要保證相互equals的兩個物件的雜湊值必定要相等,即必須保證hashCode的返回值相等。那如何解決這個問題?我們只需要重寫hashCode方法即可,程式碼如下:
@Override
public int hashCode() {
return this.num.hashCode();
}
重新執行main中的測試程式碼,輸出結果如下:Car1 hash code: 607836628
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1
之前我們說了,相互equals的物件必須返回相同的雜湊值,相同雜湊值的物件都在一個桶裡面,但是反過來,具有相同雜湊值的物件(也就是在同一個桶裡面的物件)不必相互equals。
總結:
1. HashMap為了提高查詢的效率使用了分塊查詢的原理,物件的hashCode返回的雜湊值進行進一步處理,這樣就有規律的把不同的元素放到了不同的區塊或桶中。下次查詢該物件的時候,還是計算其雜湊值,根據雜湊值確定區塊或桶,然後在這個小範圍內查詢元素,這樣就快多了。
2. 如果重寫了equals方法,那麼必須重寫hashCode方法,保證如果兩個物件相互equals,那麼二者的hashCode的返回值必定相等。
3. 如果兩個物件的hashCode返回值相等,這兩個物件不必是equals的。
相關文章
- Java中HashMap的實現原理JavaHashMap
- 深入理解Java中的底層阻塞原理及實現Java
- 深入理解ReentrantLock的實現原理ReentrantLock
- Java集合——深入理解HashMapJavaHashMap
- 深入理解Java的垃圾回收機制(GC)實現原理JavaGC
- 深入淺出 Java 中列舉的實現原理Java
- 深入理解 Taier:MR on Yarn 的實現原理AIYarn
- 深入理解Vue的computed實現原理及其實現方式Vue
- 深入理解Vue的watch實現原理及其實現方式Vue
- 帶你逐步實現簡易HashMap,助力理解Java HashMapHashMapJava
- 深入理解MySQL中事務隔離級別的實現原理MySql
- 深入理解HashMapHashMap
- HashMap的實現原理 HashMap底層實現,hashCode如何對應bucket?HashMap
- 理解https中的安全及其實現原理HTTP
- 深入理解Java中的AQSJavaAQS
- 深入理解Java中的鎖Java
- 深入理解 Java 中的 LambdaJava
- Java集合詳解(三):HashMap原理解析JavaHashMap
- 夯實Java基礎系列8:深入理解Java內部類及其實現原理Java
- 夯實Java基礎系列18:深入理解Java內部類及其實現原理Java
- 深入理解多執行緒(一)——Synchronized的實現原理執行緒synchronized
- 乾貨:HashMap的工作原理解析HashMap
- java中的鎖及實現原理Java
- AOP的實現深入理解
- HashMap底層實現原理HashMap
- 深入理解Java中的逃逸分析Java
- 深入理解Java中的鎖(二)Java
- 深入理解Java中的鎖(一)Java
- 深入理解Java中的Garbage CollectionJava
- 深入理解React:懶載入(lazy)實現原理React
- 【死磕Java併發】-----深入分析synchronized的實現原理Javasynchronized
- 【死磕Java併發】—–深入分析volatile的實現原理Java
- 夯實Java基礎系列13:深入理解Java中的泛型Java泛型
- 深入理解JS中的物件(三):class 的工作原理JS物件
- 深入理解JS中的物件(二):new 的工作原理JS物件
- HashMap的實現原理(看這篇就夠了)HashMap
- 深入理解Java中的不可變物件Java物件
- JAVA中鎖的深入理解與解析Java
- Java HashMap中的載入因子及初始容量深入分析JavaHashMap