HashMap的实现过程

 HashMap的实现过程

 

一、从Hash说起

还记得,我们第一次接触Hash这个词,是在学数据结构,讲到查找一节,引入哈希表的时候。

对于像顺序查找、折半查找、二叉排序树查找等查找,关键字在存储结构中的位置是随机的,即关键字与它的存储位置之间存在着不确定性的关系,因而这样的查找方法是建立在“比较”的基础上。而查找的效率也主要依赖于查找过程中所进行比较的次数。

哈希表的引入,提供了有别于上面所说查找的另外一种查找方式。即关键字与它所存储的位置之间有着一个确定的对应关系,这样当我们要查找指定关键字时,只要通过这样的一个映射关系,就可以得到它的存储地址,从而可以很快地找到关键字。

而这种映射关系就是所谓的哈希函数了,哈希函数的作用在于,它提供关键字到其存储地址的一种映射。不同的哈希函数,一般来说,就会有不同的映射结果。对于单值的映射来说,只要哈希函数一确定,输入相同的关键字,就会得到相同的映射结果,而输入不同的关键字,也有可能得到相同的映射结果。不过一旦出现这种现象,可就糟了,因为,如果我们直接将映射结果所指空间作为关键字的存储位置,那这样将会出现两个不同的关键字被存储在同一个存储空间中,这样肯定是不行的,所以我们必须对这样的结果还需进行一个加工处理。对于这样现象,我们叫它冲突Hash冲突),而当冲突出现时,我们需要对它进行处理,以使得最后每个关键字都能够找到它唯一的一个存储空间。

对于冲突的处理有许多方法,在这里,我们用的是挂链法。即,当我们用哈希函数处理不同的关键字,得到相同的映射结果,我们姑且把这样相同的映射结果所对应的空间叫做“第一地址”,然后我们在“第一地址”上挂一个链表(即“第一地址”存储链表的首地址),而链表里的元素就是实实在在所保存的关键字,只要当不同的关键字通过该哈希函数得到相同的映射结果,我们就把该关键字加到“第一地址”下的链表中存储。当我们访问该关键字时,首先通过哈希函数找到“第一地址”,然后遍历链表即可。

我们用哈希这一名词来表示存储这些关键字的结构。在这里,哈希表就是我们常说的“数组+链表”的结构了。数组空间存储就是上面所说的“第一地址”,即数组下标索引指的就是“第一地址”,通过索引,我们可以找到存有关键字的链表,然后再遍历链表就可以找到指定的关键字了。

附“哈希表”的定义(数据结构(严蔚敏版)):

哈希表:根据设定的哈希函数H(key)和处理冲突的方法,将一组关键字映像到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为哈希表,这一映像过程称为哈希造表散列,所得存储位置称哈希地址散列地址

 



 

针对以上构造的哈希表,当我们存储的元素不是单值元素,而是像Map中键值对的形式的元素时,这时这种结构就是我们谈论的HashMap了,不过实际中的HashMap存储的元素还有hash值,以及指向下一个结点的引用。具体的东西,我们一看源码即可知晓。

对于Map,我们知道,有put(Object key , Object value)get(Object key)remove(Object key)containsKey(Object key)containsValue(Object value)等方法,同样,HashMap中也含有这些方法,下面我们从HashMap的源码中截取这些方法,自定义一个HashMap

二、从源码中截取经典自定义HashMap

1、定义内部类Entry<K,V>,封装所存数据

 

	/**
	 * 内部类:封装数据,作为链表的结点
	 * @author wanghaoliang
	 *
	 * @param <K>
	 * @param <V>
	 */
	static class Entry<K,V>{
		K key;
		V value;
		int hash;//参与h & (length-1)的hash码值
		Entry<K,V> next;
		
		Entry(K k,V v,int h,Entry<K,V> n){
			key=k;
			value=v;
			hash=h;
			next=n;
		}
	}

 

2、自定义带有初始容量和装载因子的构造器

 

	/**
	 * 指定初始容量和装载因子的构造器
	 * @param initialCapacity
	 * @param loadFactor
	 */
	public MyHashMap(int initialCapacity,float loadFactor){
		//IllegalArgumentException非法参数处理
		if(initialCapacity<0)
			throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
		if(initialCapacity>MAXIMUM_CAPACITY)
			initialCapacity=MAXIMUM_CAPACITY;
		if(loadFactor<=0||Float.isNaN(loadFactor))
			throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);

		//初始化参数
		int capacity=1;
		while(capacity<initialCapacity)
			capacity<<=1;
		
		this.loadFactor=loadFactor;
		//计算阀值
		threshold=(int)(capacity*loadFactor);
		//对hash表初始化
		table=new Entry[capacity];
	}

 

3、定义put(K key,V value)等函数,关联指定的KeyValue,并加入到链表中 

 

	/**
	 * 关联指定的Key和Value,
	 * 如果map中已经有了指定的Key的映射,则新的value会覆盖老的value
	 * 如果map中没有指定的Key,则调用addEntry方法,添加新的mapping
	 * 返回老的value值
	 * 如果返回Null,则说明map中不存在指定
	 * @param key
	 * @param value
	 * @return
	 */
	public V put(K key,V value){
		//HashMap中允许存放null键和null值
		if(key==null)
			return putForNullKey(value);
		int hash = hash(key.hashCode());
		int i= indexFor(hash, table.length);
		//遍历table,查看其中是否已有指定的Key,若有,则用新的Value覆盖旧的Value
		for(Entry<K,V> e=table[i];e!=null;e=e.next){
			Object k=e.key;
			if(e.hash==hash&&(key==k||key.equals(k))){
				V oldValue=e.value;
				e.value=value;
				return oldValue;
			}
		}
		//table中没有指定Key的映射,故加入该映射到链表中
		addEntry(hash,key, value, i);
		return null;
	}

 

4、定义get(K key)等函数,通过指定键获取对应的Value

 

	/**
	 * 返回指定的键(Key)所对应的值(Value)
	 * @param key
	 * @return
	 */
	public V get(K key){
		if(key==null)
			return getForNullKey();
		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=e.key;
			//先比较hash,再比较key值
			if(e.hash==hash&&(key==k)||key.equals(k))
				return e.value;
		}
		return null;
	}

 

5、定义remove(K key)等函数,通过指定键移除对应的映射关系

 

	/**
	 * 移除指定的Key所对应的映射
	 * 返回Entry<K,V>
	 * @param key
	 * @return
	 */
	public Entry<K,V> removeEntryForKey(K key){
		//如果key为null,则对应的hash值为0
		int hash=(key==null)?0:hash(key.hashCode());
		int i=indexFor(hash,table.length);
		Entry<K,V> prev=table[i];
		Entry<K,V> e=prev;
		
		while(e!=null){
			Entry<K,V> next=e.next;
			Object k=e.key;
			//遍历链表,找到指定的结点
			if(e.hash==hash&&(key==k||key.equals(k))){
				size--;
				if(prev==e)//要删除的结点为首结点
					table[i]=next;
				else
					prev.next=next;
				return e;
			}
			prev=e;
			e=next;
		}
		return e;
	}

 

6、定义containsKey(K key)containsValue(V value)函数,判断指定的KV是否存在于HashMap

 

 

	/**
	 * 判断table中是否含有指定的Key
	 * @param key
	 * @return
	 */
	public boolean containsKey(K key){
		return getEntry(key)!=null;
	}

 

	/**
	 * 判断table中是否含有指定的Value
	 * @param value
	 * @return
	 */
	public boolean containsValue(V value){
		if(value==null)
			return containsNullValue();
		Entry[] tab=table;
		for(int i=0;i<tab.length;i++){
			for(Entry e=tab[i];e!=null;e=e.next)
				if(value.equals(e.value))
					return true;
		}
		return false;
	}

 

7、测试

 

三、源码要点分析

 

1、为什么table表的大小为2的幂次方?

 

   /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;

 

 

我们知道,选择哈希函数构造哈希表时,应尽量避免冲突,即构造一个均匀的哈希表,使得表中的每个索引空间被映射到的概率都是均等的。对于一片有限的连续的存储空间(数组),如果将不同的关键字比较均匀地映射到这一片存储空间呢?是的,我们一般采用“除留取余法”,即拿hash%table.length所得值,作为索引空间。但是计算机对于%(取余运算)较耗时间,而计算机对于&(位与运算),即hash & (table.length-1) 却是大大地节省了时间,而只有当table.length2的幂次方时,上面的hash & (table.length-1)才具有等价的hash%table.length效果。

2、为什么还要再哈希?

即为什么当我们调用key.hashcode()返回一个哈希值时,为什么不直接将key.hashcode()作为最终的hash值,而还要经过如下的这一操作?

 

    /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    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);
    }

 

 

大概看了一下源码中的对此函数的注释可知,原始的key.hashcode()返回得到的hash值,如果参与hash & (table.length-1)运算,在低字节上很容易相同,即就很容易产生冲突,为了使得冲突减少,所以系统增加了上面的这一操作,它是原始hash的补充,旨在使得key的最终的hash码值,在参与hash & (table.length-1)运算能够达到一个分布均匀的结果值,即最大化地减少冲突。

 

附:完整代码

 

package whlHashMap2;

import java.util.Map;

/**
 * 自定义HashMap
 * @author wanghaoliang
 *
 */
public class MyHashMap<K,V> {

	//最大的初始容量,必须<=2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //阀值=capacity * load factor
    int threshold;
    //装载因子
    final float loadFactor;
    //hash表
    transient Entry[] table;
    //键值对的个数
    transient int size;

	/**
	 * 指定初始容量和装载因子的构造器
	 * @param initialCapacity
	 * @param loadFactor
	 */
	public MyHashMap(int initialCapacity,float loadFactor){
		//IllegalArgumentException非法参数处理
		if(initialCapacity<0)
			throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
		if(initialCapacity>MAXIMUM_CAPACITY)
			initialCapacity=MAXIMUM_CAPACITY;
		if(loadFactor<=0||Float.isNaN(loadFactor))
			throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);

		//初始化参数
		int capacity=1;
		while(capacity<initialCapacity)
			capacity<<=1;
		
		this.loadFactor=loadFactor;
		//计算阀值
		threshold=(int)(capacity*loadFactor);
		//对hash表初始化
		table=new Entry[capacity];
	}
	
	/**
	 * 补充性的hash函数
	 * @param h
	 * @return
	 */
	static int hash(int h){
		h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
	}
    /**
     * 返回对应的hashCode在数组中的索引
     * 等价于h%length操作
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    /**
     * 添加链表结点
     * @param hash:关键字所对应的hash值
     * @param key:键值对中的Key
     * @param value:键值对张的Value
     * @param bucketIndex数组索引
     */
    void addEntry(int hash,K key,V value,int bucketIndex){
    	Entry<K,V> next=table[bucketIndex];
    	table[bucketIndex]=new Entry<K,V>(key,value,hash,next);
    	if(size++ >= threshold)
    		resize(2*table.length);
    }
    /**
     * 改变table的大小
     * @param newCapacity:新的大小容量
     * newCapacity必须满足:不超过MAXIMUM_CAPACITY,并且为2的幂次方
     */
    void resize(int newCapacity){
    	//参数处理
    	Entry[] oldTable=table;
    	int oldCapacity=oldTable.length;
    	if(oldCapacity==MAXIMUM_CAPACITY){
    		threshold=Integer.MAX_VALUE;
    		return;
    	}
    	
    	Entry[] newTable=new Entry[newCapacity];
    	//从oldTable中转移数据到newTable中
    	transfer(newTable);
    	table=newTable;
    	threshold=(int)(newCapacity*loadFactor);
    }
    
    /**
     * 从oldTable中转移数据到newTable中
     * @param newTable
     */
    void transfer(Entry[] newTable){
    	Entry[] src=table;
    	int newCapacity=newTable.length;
    	for(int j=0;j<src.length;j++){
    		Entry<K,V> e=src[j];
    		if(e!=null){
    			src[j]=null;//清空原始Hash表
    			//原始链表数据在新的链表中顺序被倒置了
    			do{
    				Entry<K,V> next=e.next;
    				int i=indexFor(e.hash, newCapacity);
    				e.next=newTable[i];
    				newTable[i]=e;
    				e=next;
    			}while(e!=null);
    		}
    	}
    }
	
	/**
	 * 关联指定的Key和Value,
	 * 如果map中已经有了指定的Key的映射,则新的value会覆盖老的value
	 * 如果map中没有指定的Key,则调用addEntry方法,添加新的mapping
	 * 返回老的value值
	 * 如果返回Null,则说明map中不存在指定
	 * @param key
	 * @param value
	 * @return
	 */
	public V put(K key,V value){
		//HashMap中允许存放null键和null值
		if(key==null)
			return putForNullKey(value);
		int hash = hash(key.hashCode());
		int i= indexFor(hash, table.length);
		//遍历table,查看其中是否已有指定的Key,若有,则用新的Value覆盖旧的Value
		for(Entry<K,V> e=table[i];e!=null;e=e.next){
			Object k=e.key;
			if(e.hash==hash&&(key==k||key.equals(k))){
				V oldValue=e.value;
				e.value=value;
				return oldValue;
			}
		}
		//table中没有指定Key的映射,故加入该映射到链表中
		addEntry(hash,key, value, i);
		return null;
	}
	/**
	 * 当Key为Null时的put(K,V)方法处理
	 * @param value
	 * @return
	 */
	private V putForNullKey(V value){
		for(Entry<K,V> e=table[0];e!=null;e=e.next){
			if(e.key==null){
				V oldValue=e.value;
				e.value=value;
				return oldValue;
			}
		}
		addEntry(0, null, value, 0);
		return null;
	}
	/**
	 * 返回指定的键(Key)所对应的值(Value)
	 * @param key
	 * @return
	 */
	public V get(K key){
		if(key==null)
			return getForNullKey();
		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=e.key;
			//先比较hash,再比较key值
			if(e.hash==hash&&(key==k)||key.equals(k))
				return e.value;
		}
		return null;
	}
	/**
	 * 当Key为Null时的get(K)方法处理
	 * @return
	 */
	private V getForNullKey(){
		for(Entry<K,V> e=table[0];e!=null;e=e.next){
			if(e.key==null)
				return e.value;
		}
		return null;
	}
	/**
	 * 判断table中是否含有指定的Key
	 * @param key
	 * @return
	 */
	public boolean containsKey(K key){
		return getEntry(key)!=null;
	}
	/**
	 * 判断table中是否含有指定的Value
	 * @param value
	 * @return
	 */
	public boolean containsValue(V value){
		if(value==null)
			return containsNullValue();
		Entry[] tab=table;
		for(int i=0;i<tab.length;i++){
			for(Entry e=tab[i];e!=null;e=e.next)
				if(value.equals(e.value))
					return true;
		}
		return false;
	}
	/**
	 * 当Value为Null是的containsValue(V)方法处理
	 * @return
	 */
	private boolean containsNullValue(){
		Entry[] tab=table;
		for(int i=0;i<tab.length;i++)
			for(Entry e=tab[i];e!=null;e=e.next)
				if(e.value==null)
					return true;
		return false;
	}
	/**
	 * 返回指定Key所对应的映射
	 * @param key
	 * @return
	 */
	public Entry<K,V> getEntry(K key){
		//若key为null,则对应hash码值为0
		int hash=(key==null)?0:hash(key.hashCode());
		int i=indexFor(hash, table.length);
		for(Entry<K,V> e=table[i];e!=null;e=e.next){
			Object k=e.key;
			if(e.hash==hash&&(key==k||key.equals(k)))
				return e;
		}
		return null;
	}
	/**
	 * 移除指定的Key所对应的映射
	 * 返回Value
	 * @param key
	 * @return
	 */
	public V remove(K key){
		Entry<K,V> e =removeEntryForKey(key);
		return  (e ==null ? null:e.value);
	}
	/**
	 * 移除指定的Key所对应的映射
	 * 返回Entry<K,V>
	 * @param key
	 * @return
	 */
	public Entry<K,V> removeEntryForKey(K key){
		//如果key为null,则对应的hash值为0
		int hash=(key==null)?0:hash(key.hashCode());
		int i=indexFor(hash,table.length);
		Entry<K,V> prev=table[i];
		Entry<K,V> e=prev;
		
		while(e!=null){
			Entry<K,V> next=e.next;
			Object k=e.key;
			//遍历链表,找到指定的结点
			if(e.hash==hash&&(key==k||key.equals(k))){
				size--;
				if(prev==e)//要删除的结点为首结点
					table[i]=next;
				else
					prev.next=next;
				return e;
			}
			prev=e;
			e=next;
		}
		return e;
	}
	/**
	 * 直接移除所指的映射
	 * @param o:被移除的映射对象
	 * @return
	 */
	public Entry<K,V> removeMapping(Object o){
		if(!(o instanceof Map.Entry))
			return null;
		
		Map.Entry<K, V> entry=(Map.Entry<K, V>)o;
		Object key=entry.getKey();
		int hash=(key==null)?0:hash(key.hashCode());
		int i=indexFor(hash, table.length);
		Entry<K,V> prev=table[i];
		Entry<K,V> e=prev;
		
		while(e!=null){
			Entry<K,V> next=e.next;
			Object k=e.key;
			if(e.hash==hash&&(key==k||key.equals(k))){
				size--;
				if(prev==e)
					table[i]=next;
				else
					prev.next=next;
				return e;
			}
			prev=e;
			e=next;
		}
		return e;
	}
	/**
	 * 内部类:封装数据,作为链表的结点
	 * @author wanghaoliang
	 *
	 * @param <K>
	 * @param <V>
	 */
	static class Entry<K,V>{
		K key;
		V value;
		int hash;//参与h & (length-1)的hash码值
		Entry<K,V> next;
		
		Entry(K k,V v,int h,Entry<K,V> n){
			key=k;
			value=v;
			hash=h;
			next=n;
		}
	}
	
}

 

 

<!--EndFragment-->

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值