HashMap原理

 

 

对于HashMap的理解如果只停留在hash数据结构的存储,key/value可以是null,那就太片面了,更深层次的理解HashMap,需要知道HashMap其实就是数组+链表,HashMap有个关于bucket(桶)的概念,这个bucket就是数组实现的,每个bucket里面可以存储的Entry<K,V>,这个Entry<K,V>可以是多个,当有多个时碰撞就发生了,当bucket里需要存储多个Entry<K,V>时使用链表存储,HashMap的基本结构就是这样的。下图来来自网络

 

通过查看Entry的源码,Entry中定义的一些类变量,证明bucket里面存储的Entry是一个链表结构

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//针对一下元素的引用
        int hash;
} 

 

HashMap中定义的一些默认常量,比如HashMap的默认容量, resize的尺寸,还有最重要的bucket是个Entry[]数组

// HashMap的默认初始容量 必须为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 二进制1向左移动4位=16
//HashMap的最大容量,可以认为是int的最大值  
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空Entry[]
static final Entry<?,?>[] EMPTY_TABLE = {};
//用来存储数据的数组,所以说HashMap的底层存储是个数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 

HashMap中put元素的过程:

1、根据key的hashcode获得bucket位置

2、如果bucket是空的,直接存入

3、如果bucket不为空,就发生了碰撞,这时把元素放在bucket中链表的头部

通过源码来分析put()操作的这一过程

public V put( K key, V value ) {
		//存储bucket的数组为空就初始化
		if ( table == EMPTY_TABLE ) {
			inflateTable(threshold);
		}
		//key为null时特殊处理,总是放在table[0]的位置
		if ( key == null ) {
			return putForNullKey(value);
		}
		//计算key的hashcode
		int hash = hash(key);
		//通过hashcode获取bucket的位置
		int i = indexFor(hash, table.length);
//遍历table[i]位置的bucket链表,查找相同的key,若找到则使用新的value替换掉原来的oldValue并返回oldValue,这就是hashcede碰撞发生时的种情况
		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;
			}
		}
//若没有在table[i]位置找到相同的key,则添加key到table[i]位置,新的元素总是在table[i]位置的第一个元素
		modCount++;
		addEntry(hash, key, value, i);
		return null;
	}
	void addEntry( int hash, K key, V value, int bucketIndex ) {
		//检查Entry[]大小,容量的75%扩大容量,减少碰撞发生
		if ( (size >= threshold) && (null != table[bucketIndex]) ) {
			resize(2 * table.length);//当前Entry[]大小*2
			hash = (null != key) ? hash(key) : 0;
			bucketIndex = indexFor(hash, table.length);
		}
		createEntry(hash, key, value, bucketIndex);
	}
	//这个方法就是真正的把key/value添加到Entry中的操作
	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++;
	}

 

HashMap中get元素的过程:

1、根据key的hashcode获得bucket位置

2、如果bucket里面只有一个元素,这时性能最好的,直接取出

3、如果bucket中有多个元素,则通过key的equals()方法,在bucket中链表上查找

通过源码来分析get()操作过程

public V get( Object key ) {
		//ke为null时的处理
		if ( key == null ) {
			return getForNullKey();
		}
		//通过key获取Entry
		Entry<K, V> entry = getEntry(key);
		return null == entry ? null : entry.getValue();
}
final Entry<K, V> getEntry( Object key ) {
		if ( size == 0 ) {
			return null;
		}
		//对key进行hash获取bucket的位置
		int hash = (key == null) ? 0 : hash(key);
		//循环bucket里面的链表根据key的hash值及对key进行equals
		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 != null && key.equals(k))) ) {
				return e;
			}
		}
		return null;
}

 

减少碰撞发生可以提高HashMap的性能,一个存入HashMap的自定义对象都要覆写Object的hashcode()和equals(),覆写equals时有写原则

(1)自反性:就是说a.equals(a)必须为true。 

(2)对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。 

(3)传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。

 

计算bucket的算法,并没有使用key.hashcode()%Entry[].length的方式而是采用了如下方式,减少碰撞的发生,bucket的大小为2的幂时效率最高

 static int indexFor(int h, int length) {
        return h & (length-1);
}

 

resize是指Entry[]中被使用的位置大于Entry.length*0.75倍时,resieze就发生了,对原来的Entry[]进行扩容,默认Entry[]大小为16,也就是16*0.75=12,当Entry[]大小为16时,被使用的大小超过12个,就要对原有的Entry[]进行扩容,原有Entry[].length*2,通过源码来分析这个过程

void resize( int newCapacity ) {
		Entry[] oldTable = table;
		int oldCapacity = oldTable.length;
		//原有bucket=1<<30 = 1 073 741 824,时就创建一个大小为0x7fffffff的bucket
		if ( oldCapacity == MAXIMUM_CAPACITY ) {
			threshold = Integer.MAX_VALUE;
			return;
		}
		//创建新的Entry[]
		Entry[] newTable = new Entry[newCapacity];
		//把当前Entry[]中的内容放到新的Entry[]中,并对它们在新的Entry[]中的位置重新计算
		transfer(newTable, initHashSeedAsNeeded(newCapacity));
		table = newTable;
                //重新计算需要扩容的临界值
		threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
	}

	void transfer( Entry[] newTable, boolean rehash ) {
		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;
			}
		}
	}

 resize在以下方法中被调用到了,如果为了性能考虑也可以在初始化一个HashMap时就指定bucket大小,但最好是2的幂

void addEntry(int hash, K key, V value, int bucketIndex)
public void putAll(Map<? extends K, ? extends V> m) 

 

清空HashMap中的所有数据,其实就是把Entry[]数组全部置中为null,然后把Entry[].size设置为0

 public void clear() {
        modCount++;
        Arrays.fill(table, null);
        size = 0;
    }

 

hashmap中其它的一下方法,后续还会补充进来

 

 

以上源码来自jdk1.7,版本如下

java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

 
 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值