深入理解HashMap及底层实现

概述:HashMap是我们常用的一种集合类,数据以键值对的形式存储。我们可以在HashMap中存储指定的key,value键值对;也可以根据key值从HashMap中取出相应的value值;也可以通过keySet方法返回key视图进行迭代。以上是基于HashMap的常见应用,但是光会使用是远远不够的,接下来将我们深入剖析HashMap的实现原理并手写实现一个简单的HashMap。

HashMap的存储结构

如图:
在这里插入图片描述
通过图例我们可以发现,HashMap其实是由数组和链表组合而成的,综合了二者的优点,可以快速定位并且快速修改数据。数组的特点:数组是一种连续的存储结构,查找元素快,时间复杂度为O(1),但增删元素慢,需要移动整个数组,时间复杂度为O(n);链表的特点:链表是由一个个节点组成,节点之间通过引用联系起来,链表查找元素需要从根节点遍历,逐个查找,时间复杂度为O(n),但链表增删数据十分便捷,只要修改节点之间的引用即可实现。HashMap综合了两者的优点,我们都知道HashMap是通过key来定位的,接下来介绍一下中间环节具体是如何实现的:
1、获取hashCode()值:假设key值是一段字符串,我们先通过hashCode方法计算这段字符串的HashCode值,如下:

public int hashCode() { 
   int h = hash; 
   if (h == 0 && value.length > 0) { 
   char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
  }

hashCode()方法是可以改写的,对于不同数据类型我们可以采用不同的hashCode()方法,我们获得的hashCode值是一串数字,不同key值对应的hashCode是不同的,我们一般无法通过一个hashCode值获取到两个相同的key值。
2、通过哈希函数获取在数组中存储的下标位置:我们刚刚只是获取了一串数据来表示key的身份,但不知道它对应与HashMap的数组部分的存储位置,接下来我们还需要一步转化来获取一个范围在数组长度内的数字,那就是哈希函数,哈希函数也不唯一,有很多实现形式,性能也各有不同,但最终目的都是为了获得一个在长度范围内的值作为下标。这里给出一个哈希函数:

	private int hash(K k) {
        int hashCode=k.hashCode();//获取HashCode
        hashCode^=(hashCode>>>12)^(hashCode>>>20);
        return hashCode^(hashCode>>>7)^(hashCode>>>4);
	}

其中“^”表示按位异或,“>>>”表示无符号右移。
3、找到具体的节点位置并插入:数组的每个位置都可能引出一串链表结构,这是为了防止发生哈希碰撞设计的,如果在这个位置原本就为空,那么直接创建一个新的节点(HashMap的键值对存储在节点中,节点内有key,value,下一个节点的引用等数据 )并存入,如果在下标处已经存在一个节点,当我们再次通过索引准备把一个新的节点存入时,就会发生哈希碰撞。对于哈希碰撞我们有如下几种处理方式:

  • 若和之前存储在此处的节点的key值相同,则更新value即可。
  • 若和之前存储在此处的节点的key值不同,只是因为hashCode相同而都安排在了这个地方,那么我们会通过新的键值对生成新的节点,把它放在原先节点的前面。之后在查询的时候,我们需要遍历这两个节点才能找到对应的key值,获取value。
  • 扩容:当添加的数据较多,满足数组扩容条件时(链表扩容条件:当节点总数达到设定的节点数阈值后初次发生发生哈希碰撞,数组就需要扩容),完美就会创建一个长度为原来两倍的数组,把原节点数据迁移到新的数组,再插入新节点,即可解决哈希碰撞问题,一般数组默认的阈值是0.75,数组的长度一般默认16。

小结:通过以上介绍,我们对于HashMap的存储形式和数据结构已经有了基本的了解,概括一下就是:HashMap通过数组的结构可以迅速找到存储的位置,通过链表实现了相同hashCode的数据的存储以及便捷的增删操作,另外通过扩容机制保证了存储的高效和数组长度的自适应。

手写实现一个简单的HashMap

接下来我们手写实现一个简单的HashMap,可以实现键值对存储,以及通过key值获取value。
1、Map及节点接口(节点用内部接口实现)

public interface MyMap<K,V>{  
	public V put(K t,V v);  //存方法
	public V get(K T);    //取方法		
	interface Entry<K,V>  //Entry为Map中的元素,定义一个内部接口
	{
		public K getKey();  //获取Key值
		public V getValue();  //获取Value值
	}	
}

2、MyHashMap类(支持泛型)

public class MyHashMap<K,V> implements MyMap<K, V>
{
	private static final int LENGTH=16; //默认定义数组的初始长度
	private static final float LOAD = 0.75f; //默认定义阈值比例
	private int length; //主动定义数组的长度
	private float load;  //主动定义阈值比例	
	private int entryUseSize;  //记录map中当前Entry数量
	private Entry<K, V>[] table=null;//申明一个数组
	/**
	 * 默认创建HashMap时,指定数组长度为默认值
	 */
    public MyHashMap(){
    	this(LENGTH,LOAD);  //调用构造方法
    }	
    /**
     * 构造方法,在执行时生成对应长度的数组
     * @param length
     * @param load
     */
	public MyHashMap(int length,float load) {
		// TODO Auto-generated constructor stub
		this.length=length;
		this.load = load;
		table = new Entry[this.length];
	}
	/**
	 * 修改数组长度,建立一个新的数组
	 * @param length
	 */
	public void resize(int length)
	{
		Entry<K,V>[] newTable = new Entry[length]; //创建新的数组
		this.length=length;
		this.entryUseSize=0;
		rehash(newTable);		//内容迁移
	}			
    /**
     * 老数组到新数组的数据迁移
     * @param newTable
     */
	public void rehash(Entry<K, V>[] newTable) {
		ArrayList<Entry<K,V>> array = new ArrayList<>();
		for(Entry<K,V> entry:table) //遍历数组table,添加到ArrayList,即取数据,先取出第一个Entry,再在while循环中取出后续的数据
		{
			if(entry!=null)
			{
				do
				{
				   array.add(entry); //添加当前节点
				   entry=entry.next;//获得链表中下一个节点的引用
				}
				while(entry!=null);//若有后续节点,则继续添加到ArrayList
			}
		}		
		table=newTable;  //覆盖旧引用		
		for(Entry<K,V> entry : array)  //遍历ArrayList开始存放到新数组
		{
			put(entry.getKey(),entry.getValue()); //存值
		}		
	}
	/**
	 * 
	 * 节点类,存放数据和指针
	 * @author mayifan
	 *
	 * @param <K>
	 * @param <V>
	 */
    class Entry<K,V> implements MyMap.Entry<K, V>
    {
        private K key; //存放key值
        private V value; //存放value值
        private Entry<K,V> next; //指向下一个相同hash对应的元素,也是它前一个添加的元素                
        /**
         * 构造方法:定义一个空的节点
         */
        public void Entry()
        {        	
        }        
        /**
         * 构造方法
         * @param key
         * @param value
         * @param entry
         */
        public Entry (K key,V value,Entry<K,V> next){
        	this.key=key;
        	this.value=value;
        	this.next = next;
        }        
		/**
		 * 获取节点的key值
		 */
		public K getKey() {
			// TODO Auto-generated method stub
			return key;
		}
		/**
		 * 获取节点的value值
		 */
		public V getValue() {
			// TODO Auto-generated method stub
			return value;
		}    	
    }
	/**
	 * 存放键值对的方法
	 * 添加键值对的时候要考虑是否需要扩容
	 * 扩容条件:当节点总数超过限额后第一次发生hash碰撞,会导致扩容,容量增加一倍,建一个新的数组,原数组的数据迁移到新的数组上,再加上新的节点,删除原数组
	 */
	public V put(K k, V v) {
		V oldValue=null;
		if(entryUseSize>=length*load)  //满足条件,则扩容
		{
              resize(2*length);
		}
		int i = hash(k)&(length-1);  //得到Hash值,计算在数组中的位置。length是2的幂次,(length-1)可以得到全为1的二进制数。
		if(table[i]==null)  //该位置为空
		{
			table[i]=new Entry<K,V>(k,v,null);
			entryUseSize++;
		}
		else   //不为空,则逐个比对判断有没有重复,如果重复返回源节点
		{	
			Entry<K,V> entry = table[i]; //获取第一个节点
			do
			{				
				if(entry.getKey().equals(k))//若找到相同的key,则更新value
				{
					oldValue=entry.getValue(); ///获取节点的原value
					entry.value=v;
					return oldValue;
				}
				entry=entry.next;  //下一个节点
			}
			while(entry!=null);  //下一个节点不为空,则继续判断

			Entry<K, V> entry1 = new Entry<K,V>(k, v,table[i]); //新建一个节点指向原节点
			table[i]=entry1;
			entryUseSize++;
		}
		return oldValue;
	}
	/**
	 * 获取指定K值的数据
	 */
	public V get(K k) {
		V value = null;
        int i = hash(k)&(length-1);
        if(table[i]!=null)   //table不为null
        {
        	Entry<K, V> entry = table[i];  //获取entry节点
        	do
        	{
        		if(entry.getKey().equals(k)||entry.getKey()==k)
        		{
        			return entry.value;
        		}
        		entry=entry.next;	
        	}
        	while(entry!=null);
        }		
		return value;
	}
	/**
	 * 获取指定key值的hash值(一种哈希函数)
	 * @param t
	 * @return
	 */
	private int hash(K k) {
        int hashCode=k.hashCode();//获取HashCode
        hashCode^=(hashCode>>>12)^(hashCode>>>20);
        return hashCode^(hashCode>>>7)^(hashCode>>>4);
	}		
}	

3、测试类(有两种键值对)

public class Test{	
	public static void main(String[] args)
	{
		System.out.println("存取int和String键值对:");
		MyHashMap<Integer, String> mp = new MyHashMap();  				
		mp.put(123, "asdasdad");
		mp.put(346,"ddddd");
		mp.put(3333, "sssss");
		mp.put(4444, "xxxxx");
		mp.put(789, "aaaaa");
		System.out.println(mp.get(123));
		System.out.println(mp.get(346));
		System.out.println(mp.get(3333));
		System.out.println(mp.get(4444));
		System.out.println(mp.get(789));		
		System.out.println("存取String和String键值对:");
		MyHashMap<String, String> mp1 = new MyHashMap();  		
		mp1.put("ss", "cvcvv");
		mp1.put("aaaa","uu");
		mp1.put("dr", "yyy");
		mp1.put("qwe", "rrrrr");
		mp1.put("vv", "dsfsf");		
		System.out.println(mp1.get("ss"));
		System.out.println(mp1.get("aaaa"));
		System.out.println(mp1.get("dr"));
		System.out.println(mp1.get("qwe"));
		System.out.println(mp1.get("vv"));			
	}		
}

4、输出结果测试
在这里插入图片描述
以上测试结果表明HashMap的基本概念得到了实现。

关于HashMap一些问题的分析

1、一个key多个value如何实现?
key值是唯一的,但由于键值对是以节点的形式存储,那么在key值唯一的前提下,可以在节点存储value1,value2等数据。我们可以写获取节点的方法,然后读取不同value,或者重写get方法返回数组,都可以实现一个key获取多个value。
2、HashMap是否线程安全?
答案是否定的,HashTable是线程安全的,而HashMap不是,我们在HashMap的底层源码中找不到synchronize关键字,如果在多线程并发访问的时候是有可能出现错误的。那么如果避免呢?我们需要在程序层面人为地实现同步,确保同一时间只有一个线程访问HashMap。虽然HashMap不是线程安全的,但它也有优点,比如效率高,速度快等。
3、为什么数组的长度初值为2的指数次?
在进行按位与的时候可以显现出它的优点,(长度-1)转化为二进制可以得到的二进制各位都是1,不会造成存储空间的浪费。
4、为什么计算HashCode时使用31这个奇质数?
如果数值太小会使得最后得到的范围很小,容易发生哈希碰撞;如果数值超过100会使得到的数值超出int的范围;31+1得到的32是2的指数次,可以方便JVM进行移位运算,如:31 * i = (i << 5) - i

猜你喜欢

转载自blog.csdn.net/mayifan_blog/article/details/85264064