深入理解Java中的HashMap的实现原理

HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口。关系图如下所示:


Java中的Map<key, value>接口允许我们将一个对象作为key,也就是可以用一个对象作为key去查找另一个对象。
在我们探讨HashMap的实现原理之前,我们先自己实现了一个SimpleMap类,该类继承自AbstractMap类。具体实现如下:

[java]  view plain  copy
  1. import java.util.*;  
  2.   
  3.   
  4. public class SimpleMap<K,V> extends AbstractMap<K,V> {  
  5.     //keys存储所有的键  
  6.     private List<K> keys = new ArrayList<K>();  
  7.     //values存储所有的值  
  8.     private List<V> values = new ArrayList<V>();  
  9.       
  10.       
  11.     /** 
  12.      * 该方法获取Map中所有的键值对 
  13.      */  
  14.     @Override  
  15.     public Set entrySet() {  
  16.         Set<Map.Entry<K, V>> set = new SimpleSet<Map.Entry<K,V>>();  
  17.           
  18.         //keys的size和values的size应该一直是一样大的  
  19.         Iterator<K> keyIterator = keys.iterator();  
  20.         Iterator<V> valueIterator = values.iterator();  
  21.         while(keyIterator.hasNext() && valueIterator.hasNext()){  
  22.             K key = keyIterator.next();  
  23.             V value = valueIterator.next();  
  24.             SimpleEntry<K,V> entry = new SimpleEntry<K,V>(key, value);  
  25.             set.add(entry);  
  26.         }  
  27.           
  28.         return set;  
  29.     }  
  30.   
  31.     @Override  
  32.     public V put(K key, V value) {  
  33.         V oldValue = null;  
  34.         int index = this.keys.indexOf(key);  
  35.         if(index >= 0){  
  36.             //keys中已经存在键key,更新key对应的value  
  37.             oldValue = this.values.get(index);  
  38.             this.values.set(index, value);  
  39.         }else{  
  40.             //keys中不存在键key,将key和value作为键值对添加进去  
  41.             this.keys.add(key);  
  42.             this.values.add(value);  
  43.         }  
  44.         return oldValue;  
  45.     }  
  46.       
  47.     @Override  
  48.     public V get(Object key) {  
  49.         V value = null;  
  50.         int index = this.keys.indexOf(key);  
  51.         if(index >= 0){  
  52.             value = this.values.get(index);  
  53.         }  
  54.         return value;  
  55.     }  
  56.   
  57.     @Override  
  58.     public V remove(Object key) {  
  59.         V oldValue = null;  
  60.         int index = this.keys.indexOf(key);  
  61.         if(index >= 0){  
  62.             oldValue = this.values.get(index);  
  63.             this.keys.remove(index);  
  64.             this.values.remove(index);  
  65.         }  
  66.         return oldValue;  
  67.     }  
  68.   
  69.     @Override  
  70.     public void clear() {  
  71.         this.keys.clear();  
  72.         this.values.clear();  
  73.     }  
  74.       
  75.     @Override  
  76.     public Set keySet() {  
  77.         Set<K> set = new SimpleSet<K>();  
  78.         Iterator<K> keyIterator = this.keys.iterator();  
  79.         while(keyIterator.hasNext()){  
  80.             set.add(keyIterator.next());  
  81.         }  
  82.         return set;  
  83.     }  
  84.   
  85.     @Override  
  86.     public int size() {  
  87.         return this.keys.size();  
  88.     }  
  89.   
  90.     @Override  
  91.     public boolean containsValue(Object value) {  
  92.         return this.values.contains(value);  
  93.     }  
  94.   
  95.     @Override  
  96.     public boolean containsKey(Object key) {  
  97.         return this.keys.contains(key);  
  98.     }  
  99.   
  100.     @Override  
  101.     public Collection values() {  
  102.         return this.values();  
  103.     }  
  104.   
  105. }  

当子类继承自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方法的实现:
[java]  view plain  copy
  1. public Collection<V> values() {  
  2.         if (values == null) {  
  3.             values = new AbstractCollection<V>() {  
  4.                 public Iterator<V> iterator() {  
  5.                     return new Iterator<V>() {  
  6.                         private Iterator<Entry<K,V>> i = entrySet().iterator();  
  7.   
  8.                         public boolean hasNext() {  
  9.                             return i.hasNext();  
  10.                         }  
  11.   
  12.                         public V next() {  
  13.                             return i.next().getValue();  
  14.                         }  
  15.   
  16.                         public void remove() {  
  17.                             i.remove();  
  18.                         }  
  19.                     };  
  20.                 }  
  21.   
  22.                 public int size() {  
  23.                     return AbstractMap.this.size();  
  24.                 }  
  25.   
  26.                 public boolean isEmpty() {  
  27.                     return AbstractMap.this.isEmpty();  
  28.                 }  
  29.   
  30.                 public void clear() {  
  31.                     AbstractMap.this.clear();  
  32.                 }  
  33.   
  34.                 public boolean contains(Object v) {  
  35.                     return AbstractMap.this.containsValue(v);  
  36.                 }  
  37.             };  
  38.         }  
  39.         return values;  
  40.     }  

大家可以看到,代码不少,基本的思路是先通过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>接口,代码如下:

[java]  view plain  copy
  1. import java.util.Map;  
  2.   
  3. //Map中存储的键值对,键值对需要实现Map.Entry这个接口  
  4. public class SimpleEntry<K,V> implements Map.Entry<K, V>{  
  5.       
  6.     private K key = null;//键  
  7.       
  8.     private V value = null;//值  
  9.       
  10.     public SimpleEntry(K k, V v){  
  11.         this.key = k;  
  12.         this.value = v;  
  13.     }  
  14.   
  15.     @Override  
  16.     public K getKey() {  
  17.         return this.key;  
  18.     }  
  19.   
  20.     @Override  
  21.     public V getValue() {  
  22.         return this.value;  
  23.     }  
  24.   
  25.     @Override  
  26.     public V setValue(V v) {  
  27.         V oldValue = this.value;  
  28.         this.value = v;  
  29.         return oldValue;  
  30.     }  
  31.       
  32. }  

以下是我们自己实现的集合类SimpleSet,继承自抽象类AbstractSet<K,V>,代码如下:

[java]  view plain  copy
  1. import java.util.AbstractSet;  
  2. import java.util.ArrayList;  
  3. import java.util.Iterator;  
  4.   
  5. public class SimpleSet<E> extends AbstractSet<E> {  
  6.       
  7.     private ArrayList<E> list = new ArrayList<E>();  
  8.   
  9.     @Override  
  10.     public Iterator<E> iterator() {  
  11.         return this.list.iterator();  
  12.     }  
  13.   
  14.     @Override  
  15.     public int size() {  
  16.         return this.list.size();  
  17.     }  
  18.   
  19.     @Override  
  20.     public boolean contains(Object o) {  
  21.         return this.list.contains(o);  
  22.     }  
  23.   
  24.     @Override  
  25.     public boolean add(E e) {  
  26.         boolean isChanged = false;  
  27.         if(!this.list.contains(e)){  
  28.             this.list.add(e);  
  29.             isChanged = true;  
  30.         }  
  31.         return isChanged;  
  32.     }  
  33.   
  34.     @Override  
  35.     public boolean remove(Object o) {  
  36.         return this.list.remove(o);  
  37.     }  
  38.   
  39.     @Override  
  40.     public void clear() {  
  41.         this.list.clear();  
  42.     }  
  43.   
  44. }  

我们测试下我们写的SimpleMap这个类,测试包括两部分,一部分是测试我们写的SimpleMap是不是正确,第二部分测试性能如何,测试代码如下:

[java]  view plain  copy
  1. import java.util.HashMap;  
  2. import java.util.HashSet;  
  3. import java.util.Map;  
  4.   
  5.   
  6. public class Test {  
  7.   
  8.     public static void main(String[] args) {  
  9.         //测试SimpleMap的正确性  
  10.         SimpleMap<String, String> map = new SimpleMap<String, String>();  
  11.         map.put("iSpring""27");  
  12.         System.out.println(map);  
  13.         System.out.println(map.get("iSpring"));  
  14.         System.out.println("-----------------------------");  
  15.           
  16.         map.put("iSpring""28");  
  17.         System.out.println(map);  
  18.         System.out.println(map.get("iSpring"));  
  19.         System.out.println("-----------------------------");  
  20.           
  21.         map.remove("iSpring");  
  22.         System.out.println(map);  
  23.         System.out.println(map.get("iSpring"));  
  24.         System.out.println("-----------------------------");  
  25.           
  26.         //测试性能如何  
  27.         testPerformance(map);  
  28.     }  
  29.       
  30.     public static void testPerformance(Map<String, String> map){  
  31.         map.clear();  
  32.           
  33.         for(int i = 0; i < 10000; i++){  
  34.             String key = "key" + i;  
  35.             String value = "value" + i;  
  36.             map.put(key, value);  
  37.         }  
  38.           
  39.         long startTime = System.currentTimeMillis();  
  40.           
  41.         for(int i = 0; i < 10000; i++){  
  42.             String key = "key" + i;  
  43.             map.get(key);  
  44.         }  
  45.           
  46.         long endTime = System.currentTimeMillis();  
  47.           
  48.         long time = endTime - startTime;  
  49.           
  50.         System.out.println("遍历时间:" + time + "毫秒");  
  51.     }  
  52.       
  53. }  

输出结果如下:
{iSpring=27}
27
-----------------------------
{iSpring=28}
28
-----------------------------
{}
null
-----------------------------
遍历时间:956毫秒

从结果里面我们看到输出结果是正确的,也就是我们写的SimpleMap基本实现都是对的。我们往Map中插入了10000个键值对,我们测试的是从Map中取出这10000条键值对的性能开销,也就是测试Map的遍历的性能开销,结果是956毫秒。

没有对比就不知性能强弱,我们测试下HashMap读取这10000条键值对的时间开销,测试方法完全一样,只是我们传入的是HashMap的实例,测试代码如下:

[java]  view plain  copy
  1. //创建HashMap的实例  
  2.         HashMap<String, String> map = new HashMap<String, String>();  
  3.           
  4.         //测试性能如何  
  5.         testPerformance(map);  

测试结果如下:
遍历时间:32毫秒

我去,不比不知道,一比吓一跳啊,HashMap比我们自己实现的SimpleMap快的那不是一点半点啊。为什么我们的SimpleMap性能这么差?而HashMap的性能如此高呢?我们分别研究。
首先分析SimpleMap性能为什么这么差。
我们的SimpleMap是用ArrayList来存储keys和values的,ArrayList本质是用数组实现的,我们的SimpleMap的get方法是这样实现的:

[java]  view plain  copy
  1. @Override  
  2.     public V put(K key, V value) {  
  3.         V oldValue = null;  
  4.         int index = this.keys.indexOf(key);  
  5.         if(index >= 0){  
  6.             //keys中已经存在键key,更新key对应的value  
  7.             oldValue = this.values.get(index);  
  8.             this.values.set(index, value);  
  9.         }else{  
  10.             //keys中不存在键key,将key和value作为键值对添加进去  
  11.             this.keys.add(key);  
  12.             this.values.add(value);  
  13.         }  
  14.         return oldValue;  
  15.     }  

需要性能开销的主要是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方法的源码实现:
[java]  view plain  copy
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }  
  15.   
  16.         modCount++;  
  17.         addEntry(hash, key, value, i);  
  18.         return null;  
  19.     }  

在put方法中,,调用了对象的hashCode方法,该方法返回一个int类型的值,是个初始的哈希值,这个值就相当于车牌号,例如"鲁E.DE829",HashMap中有个hash方法,该hash方法将我们得到的初始的哈希值做进一步处理,得到最终的哈希值,就好比我们将车牌号传入hash方法,然后返回该存放车辆的大桶,即返回"鲁",这样HashMap就把这辆车放到标有“鲁”的大桶里面了。 上面说到的hash方法叫做哈希函数,专门负责根据传入的值返回指定的最终哈希值,具体实现如下:
[java]  view plain  copy
  1. static int hash(int h) {  
  2.         // This function ensures that hashCodes that differ only by  
  3.         // constant multiples at each bit position have a bounded  
  4.         // number of collisions (approximately 8 at default load factor).  
  5.         h ^= (h >>> 20) ^ (h >>> 12);  
  6.         return h ^ (h >>> 7) ^ (h >>> 4);  
  7.     }  

可以看出来,HashMap中主要是通过位操作符实现哈希函数的。这里简单说一下哈希函数,哈希函数有多种实现方式,比如最简单的就是取余法,比如对i%10取余,然后按照余数创建不同的区块或桶。比如有100个数,分别是从1到100,那么分别对10取余,那么就可以把这100个数放到10个桶子里面了,这就是所谓的哈希函数。只不过HashMap中的hash函数看起来比较复杂,进行的是位操作,但是其作用与简单的取余哈希法的作用是等价的,就是把元素分类放置。
具体将键值对放入到HashMap中的方法是addEntry,代码如下:
[java]  view plain  copy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.         Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  

键值对都是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方法的源码实现:

[java]  view plain  copy
  1. public V get(Object key) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         int hash = hash(key.hashCode());  
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.              e != null;  
  7.              e = e.next) {  
  8.             Object k;  
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.                 return e.value;  
  11.         }  
  12.         return null;  
  13.     }  

在get方法中,也是先调用了对象的hashCode方法,就相当于车牌号,然后再将该值让hash函数处理得到最终的哈希值,也就是桶的索引。然后我们再去这个标有“鲁”的桶里面去找我们的键值对,首先先取出桶里面第一个键值对,比对一下是不是我们要找的元素,如果是就直接返回了,如果不是就通过键值对的next顺藤摸瓜通过单向链表继续找下去,直至找到。  如下图所示:

下面我们再写一个Car类,该类有一个字段String类型的字段num,并且我们重写了Car的equals方法,我们认为只要车牌号相等就认为这是同一辆车。代码如下所示:
[java]  view plain  copy
  1. import java.util.HashMap;  
  2.   
  3. public class Car {  
  4.       
  5.     private final String num;//车牌号  
  6.       
  7.     public Car(String n){  
  8.         this.num = n;  
  9.     }  
  10.       
  11.     public String getNum(){  
  12.         return this.num;  
  13.     }  
  14.   
  15.     @Override  
  16.     public boolean equals(Object obj) {  
  17.         if(obj == null){  
  18.             return false;  
  19.         }  
  20.         if(obj instanceof Car){  
  21.             Car car = (Car)obj;  
  22.             return this.num.equals(car.num);  
  23.         }  
  24.         return false;  
  25.     }  
  26.       
  27.   
  28.     public static void main(String[] args){  
  29.         HashMap<Car, String> map = new HashMap<Car, String>();  
  30.         String num = "鲁E.DE829";  
  31.         Car car1 = new Car(num);  
  32.         Car car2 = new Car(num);  
  33.         System.out.println("Car1 hash code: " + car1.hashCode());  
  34.         System.out.println("Car2 hash code: " + car2.hashCode());  
  35.         System.out.println("Car1 equals Car2: " + car1.equals(car2));  
  36.         map.put(car1, new String("Car1"));  
  37.         map.put(car2, new String("Car2"));  
  38.         System.out.println("map.size(): " + map.size());  
  39.     }  
  40.   
  41. }  
我们在main函数中写了一些测试代码,我们创建了一个HashMap,该HashMap的用Car作为键,用字符串作为值。我们用同一个字符串实例化了两个Car,分别为car1和car2,然后将这两个car都放入到HashMap中,输出结果如下:
Car1 hash code: 404267176
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方法即可,代码如下:
[java]  view plain  copy
  1. @Override  
  2.     public int hashCode() {  
  3.         return this.num.hashCode();  
  4.     }  
重新运行main中的测试代码,输出结果如下:
Car1 hash code: 607836628
Car2 hash code: 607836628
Car1 equals Car2: true
map.size(): 1

之前我们说了,相互equals的对象必须返回相同的哈希值,相同哈希值的对象都在一个桶里面,但是反过来,具有相同哈希值的对象(也就是在同一个桶里面的对象)不必相互equals。

总结:
1. HashMap为了提高查找的效率使用了分块查找的原理,对象的hashCode返回的哈希值进行进一步处理,这样就有规律的把不同的元素放到了不同的区块或桶中。下次查找该对象的时候,还是计算其哈希值,根据哈希值确定区块或桶,然后在这个小范围内查找元素,这样就快多了。
2. 如果重写了equals方法,那么必须重写hashCode方法,保证如果两个对象相互equals,那么二者的hashCode的返回值必定相等。
3. 如果两个对象的hashCode返回值相等,这两个对象不必是equals的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值