深入Java集合学习系列:HashMap的实现原理

1.    HashMap概述:

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  

2.    HashMap的数据结构:

   在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。


   从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

源码如下:


<span style="font-family:KaiTi_GB2312;font-size:14px;">static final Entry<?,?>[] EMPTY_TABLE = {};    
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  
/**
* The number of key-value mappings contained in this map.
*/ 
transient int size; 
int threshold;  // 临界值 它等于HashMap的容量乘以负载因子 
final float loadFactor;// 负载因子 
public V put(K key, V value) { 
   // 如果table为空,则使其不为空 
        if (table == EMPTY_TABLE) { 
            inflateTable(threshold); 
        } 
       // 如果key为null,调用putForNullKey处理 
        if (key == null) 
            return putForNullKey(value); 
        int hash = hash(key); 
     // 搜索指定hash值对应的索引 
        int i = indexFor(hash, table.length); 
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
            Object k; 
  // 如果hash值相同,并且equals比较返回true,则覆盖,然后返回被覆盖的 
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
                V oldValue = e.value; 
                e.value = value; 
                e.recordAccess(this); 
                return oldValue; 
            } 
        } 
        // 如果i索引处的entry为null,表明此处还没有entry 
        modCount++; 
        addEntry(hash, key, value, i); 
        return null; 
} 
// 添加entry 
void addEntry(int hash, K key, V value, int bucketIndex) { 
        if ((size >= threshold) && (null != table[bucketIndex])) { 
            resize(2 * table.length);//原来长度的2倍 
            hash = (null != key) ? hash(key) : 0; 
            bucketIndex = indexFor(hash, table.length); 
        } 
    
        createEntry(hash, key, value, bucketIndex); 
} 
void createEntry(int hash, K key, V value, int bucketIndex) { 
        Entry<K,V> e = table[bucketIndex]; 
  // 头插法建立链 
        table[bucketIndex] = new Entry<>(hash, key, value, e); 
        size++; 
 } </span>
</pre><pre name="code" class="java">
<p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;">  当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;">   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;">
</span></p><span style="font-family:KaiTi_GB2312;font-size:14px;">void resize(int newCapacity) { 
        Entry[] oldTable = table;//先记录下来table 
        int oldCapacity = oldTable.length; 
        if (oldCapacity == MAXIMUM_CAPACITY) { 
            threshold = Integer.MAX_VALUE;  //static final int MAXIMUM_CAPACITY = 1 << 30; 
            return; 
        } 
    
        Entry[] newTable = new Entry[newCapacity];//这个是原来长度的2倍 
        transfer(newTable, initHashSeedAsNeeded(newCapacity)); 
        table = newTable; 
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 
    } 
    /**
     * Transfers all entries from current table to newTable.
     */ 
    void transfer(Entry[] newTable, boolean rehash) {// rehash 是否重新hash 
        int newCapacity = newTable.length; 
        for (Entry<K,V> e : table) { 
            while(null != e) { 
                Entry<K,V> next = e.next; 
                if (rehash) { 
                    e.hash = null == e.key ? 0 : hash(e.key); 
                } 
                int i = indexFor(e.hash, newCapacity); 
                e.next = newTable[i]; 
                newTable[i] = e; 
                e = next; 
            } 
        } 
    } 
 /**
     * Initialize the hashing mask value. We defer(延迟) initialization until we
     * really need it.
     */ 
    final boolean initHashSeedAsNeeded(int capacity) { 
        boolean currentAltHashing = hashSeed != 0; 
        boolean useAltHashing = sun.misc.VM.isBooted() && 
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 
        boolean switching = currentAltHashing ^ useAltHashing; 
        if (switching) { 
            hashSeed = useAltHashing 
                ? sun.misc.Hashing.randomHashSeed(this) 
                : 0; 
        } 
        return switching; 
    } 
// 内部类 entry  
static class Entry<K,V> implements Map.Entry<K,V> { 
        final K key; 
        V value; 
        Entry<K,V> next;// 指向下一个entry 
        int hash; 
    
        /**
         * Creates new entry.
         */ 
        Entry(int h, K k, V v, Entry<K,V> n) { 
            value = v; 
            next = n; 
            key = k; 
            hash = h; 
        }</span>


   可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。


  3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

 

.    HashMap的resize(rehash):

   当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

.    HashMap的性能参数:

   HashMap 包含如下几个构造器:

   HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

   HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。

   initialCapacity:HashMap的最大容量,即为底层数组的长度。

   loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

   HashMap的实现中,通过threshold字段来判断HashMap的最大容量:


  1. threshold = (int)(capacity * loadFactor);  

   结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

 


  1. if (size++ >= threshold)     
  2.     resize(2 * table.length);    

 

.    Fail-Fast机制:

   我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

   这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。


  1. HashIterator() {  
  2.     expectedModCount = modCount;  
  3.     if (size > 0) { // advance to first entry  
  4.     Entry[] t = table;  
  5.     while (index < t.length && (next = t[index++]) == null)  
  6.         ;  
  7.     }  
  8. }  

 

   在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:

   注意到modCount声明为volatile,保证线程之间修改的可见性。


  1. final Entry<K,V> nextEntry() {     
  2.     if (modCount != expectedModCount)     
  3.         throw new ConcurrentModificationException();  

 

   在HashMap的API中指出:

   由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

   注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。



自定义Map ,便于更好的l理解Map底层数据结构原理:

<span style="font-family:KaiTi_GB2312;font-size:14px;">public class CustomHashMap<K,V>{

	
	@SuppressWarnings("unchecked")
	private LinkedList<Entry<K,V>>[]  arr  = new LinkedList[9]; //Map的底层结构就是:数组+链表!
	
	private int size;
	
	public void put(K key,V value){
		Entry<K, V> entry=new Entry<K, V>(key, value);
		//Hash key
		int hashcode=key.hashCode();
		hashcode=hashcode<0?-hashcode:hashcode;
		int index=hashcode%arr.length;
		LinkedList<Entry<K,V>> object=arr[index];
		if(object==null){
			LinkedList<Entry<K,V>> list=new LinkedList<>();
			list.add(entry);
			arr[index]=list;
		}else{
			for (int i = 0; i < object.size(); i++) {
				Entry<K,V> e=object.get(i);
				if(e.getKey().equals(key)){
					e.setValue(value);
					return;  //键值重复直接覆盖!
				}
			}
			object.add(entry);
		}
		size++;
	}
	
	public V get(K key){
		int index=key.hashCode()%arr.length;
		LinkedList<Entry<K,V>> object=arr[index];
		if(object!=null){
			for (int i = 0; i < object.size(); i++) {
				Entry<K,V> e=object.get(i);
				if(e.getKey().equals(key)){
					return e.getValue();
				}
			}
		}
		return null;
	}
	
	public Entry<K,V> remove(K key){
		if (size == 0) {
	       return null;
	    }
		 
		int index=key.hashCode()%arr.length;
		LinkedList<Entry<K,V>> array=arr[index];
		Entry<K,V> entry=null;
		if(array!=null){
			if(array.size()==1){ //说明只有一个值
				Entry<K,V> e=array.get(0);
				if(e.getKey().equals(key)){
					entry=e;
					array=null;
					arr[index]=null;
					size--;
				}
			}else{
				for (Iterator<Entry<K, V>> iterator = array.iterator(); iterator.hasNext();) {
					Entry<K, V> e = (Entry<K, V>) iterator.next();
					if(e.getKey().equals(key)){
						entry=e;
						iterator.remove();
						break;
					}
				}
			}
		}
		return entry;
	}
	
	
	public boolean containsKey(K key){
		int index=key.hashCode()%arr.length;
		LinkedList<Entry<K,V>> array=arr[index];
		for(int i=0;i<array.size();i++){
			Entry<K,V> e=array.get(i);
			if(e.getKey().equals(key)){
				return true;
			}
		}
		return false;
	}
	
	public boolean containsValue(V value){
		for (int i = 0; i < size; i++) {
			LinkedList<Entry<K,V>> array=arr[i];
			if(array!=null){
				for (int j = 0; j < array.size(); j++) {
					Entry<K,V> entry=array.get(j);
					if(entry!=null){
						if (value.equals(entry.value))
		                    return true; 
					}
				}
			}
		}
		return false;
	}
	
	
	
	class Entry<K,V>{
		
		
		private K key;
		
		private V value;

		public K getKey() {
			return key;
		}

		public void setKey(K key) {
			this.key = key;
		}

		public V getValue() {
			return value;
		}

		public void setValue(V value) {
			this.value = value;
		}

		public Entry(K key, V value) {
			super();
			this.key = key;
			this.value = value;
		}

		@Override
		public int hashCode() {
			  return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Entry other = (Entry) obj;
			if (key == null) {
				if (other.key != null)
					return false;
			} else if (!key.equals(other.key))
				return false;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}
		
	}
	
	public static void main(String[] args) {
		CustomHashMap<String,String> m = new CustomHashMap<String,String>();
		m.put("测试", "谢霆锋");
		m.put("测试", "刘德华");
		m.put("测试2", "周润发");
		System.out.println(m.size);
		String w = m.get("测试");
		m.remove("测试");
	    w = m.get("测试");
		System.out.println(w); 
	}
	
	
}
</span>


已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页