Java - 提高-源码(10) - HashMap

源码下载地址:jdk1.7源码下载


HashMap的实现

源码分析对应JDK1.7

参考资料:
http://www.codeceo.com/article/java-hashmap-from-code-layer.html#0-tsina-1-24433-397232819ff9a47a7b7e80a40613cfe1
http://www.cnblogs.com/chenssy/p/3521565.html


先记住结论
HahsMap  继承AbstractMap;
HashMap  的key和value都是可以为null;
HashMap  是无序的;
HashMap  不是同步的,如果需要线程安全的HahsMap,可以通过Collections类的静态方法synchronizedMap获得
      线程安全的HashMap。


HashMap 中有两个重要参数:"初始容量","加载因子"
初始容量:hashmap底层table数组(entry)的长度
加载因子:算是一种阀值,当table数组中有效数据条目超出 加载因子与当前容量(table数组总长度)的乘积时,
则对哈希表进行扩容。
(有效数据条目意思:数组长度16,里面存了10条数据,有效条目就是10条数据,不是数组总长)


HashMap的数据结构
说到数据结构,想到这个图,画的很好,直接拿来用,感谢作者


之前在看上面这张HashMap结构图的时候,一直很好奇什么情况下链表会出现多个?
后来偶然测试发现一个例子:
这里我将hashMap源码中的代码拿出来测试

HashMap<String, Integer> map = new HashMap<String, Integer>();
 map.put("语文", 1);
 map.put("数学", 2);
 map.put("英语", 3);
 map.put("历史", 4);
 map.put("政治", 5);
 map.put("地理", 6);
 map.put("生物", 7);
 map.put("化学", 8);
 map.put("化学", 9);

在put的时候,"历史" 和 "语文" 就出现了entry链表
然后我将hashMap put时的源码拿出来测试了一下。

public static void main(String[] args) {

		int str = "语文".hashCode();
		int str2 = "历史".hashCode();

		System.out.println("hashCode : " + str);
		System.out.println("hashCode : " + str2);

		System.out.println("table 数组下标 : " + hash(str) % 16);
		System.out.println("table 数组下标 : " + (hash(str2) % 16));
		}

		/**
		 * 根据key的hash计算出table数组的下标.
		 * 
		 * @param h
		 * @return
		 */
		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);
		}
		
输出结果:
hashCode : 1136442
hashCode : 684332
table 数组下标 : 0
table 数组下标 : 0
你看,通过hashMap中的hash算法之后,计算出他们两个的下标是一样的,然后就出现了entry链表。



HashMap中 Entry的源码:

static class Entry<K, V> implements Map.Entry<K, V> {
	final K key;
	V value;
	// 当put发生碰撞时,指向下一个节点
	Entry<K, V> next;
	int hash;

	/**
	 * Creates new entry.<br>
	 * 构造函数<br>
	 * 
	 */
	Entry(int h, K k, V v, Entry<K, V> n) {
			value = v;
			next = n;// 下一个节点(entry链表)
			key = k;
			hash = h;
	}

	public final K getKey() {
			return key;
	}

	public final V getValue() {
			return value;
	}

	public final V setValue(V newValue) {
			V oldValue = value;
			value = newValue;
			return oldValue;
	}

	/**
	 * entry中的equlas方法,判断两个entry是否相等<br>
	 * 若两个Entry的key和value都相等,则返回true<br>
	 * 否则返回false
	 * 
	 */
	public final boolean equals(Object o) {
			if (!(o instanceof Map.Entry))
					return false;
			Map.Entry e = (Map.Entry) o;
			Object k1 = getKey();
			Object k2 = e.getKey();
			if (k1 == k2 || (k1 != null && k1.equals(k2))) {
					Object v1 = getValue();
					Object v2 = e.getValue();
					if (v1 == v2 || (v1 != null && v1.equals(v2)))
							return true;
			}
			return false;
	}

	// entry中的hashCode方法
	public final int hashCode() {
			return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
	}

	public final String toString() {
			return getKey() + "=" + getValue();
	}

	/**
	 * This method is invoked whenever the value in an entry is overwritten
	 * by an invocation of put(k,v) for a key k that's already in the
	 * HashMap.
	 */
	void recordAccess(HashMap<K, V> m) {
	}

	/**
	 * This method is invoked whenever the entry is removed from the table.
	 */
	void recordRemoval(HashMap<K, V> m) {
	}
	}
通过上面那个图,可以看出,HashMap底层是一个数组,数组中存放的是Entry<K,V>;
通过entry的源码,发现每个Entry<K,V>的next,维护了entry链表

HashMap的构造方法

HashMap():
构造一个默认初始容量为16,默认加载因子为0.75的空HahsMap
HashMap(int initialCapacity):
构造一个  带指定初始容量 ,默认加载因子为0.75的空HashMap
HashMap(int initialCapacity, float loadFactor):
构造一个  带指定初始容量 和 加载因子的空HashMap
HashMap(Map<? extends K, ? extends V> m):
构造一个 映射关系与指定Map相同的新HashMap


下面看个通用的构造方法:
HashMap(int initialCapacity) 和 HashMap() 构造函数最后都是指向这个构造方法。

/**
* 指定容量大小和加载因子的构造函数
* 
* @param initialCapacity
* @param loadFactor
*/
public HashMap(int initialCapacity, float loadFactor) {

	// 参数校验
	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);

	// Find a power of 2 >= initialCapacity
	// 找出"大于initialCapacity(指定容量)的最小2的幂"
	int capacity = 1;
	while (capacity < initialCapacity)
			capacity <<= 1;

	// 加载因子赋值
	this.loadFactor = loadFactor;

	// 设置HashMap的阀值,当HashMap中存储的数据量达到threshold时,需要扩容
	// Math.min():选择一个小的参数
	threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

	// 创建指定长度的table数组(entry)
	table = new Entry[capacity];

	useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
	init();
}
这里可能有个疑问,或者面试的时候回问道:
为什么扩容一定是2的幂次?

这里直接说结论:

当HahsMap的容量是2的幂次时,不同的hash值发生碰撞的概率比较小,这样数据在table数组中分布的均匀,查询速度也快。


分析:

HashMap的底层数组长度总是2的幂次,在构造方法中有这样几行代码

int capacity = 1;
while (capacity < initialCapacity)
	capacity <<= 1;
这段代码的作用是:找出"大于initialCapacity(指定容量)的最小2的幂"。
当length为2的幂次时indexFor()方法
static int indexFor(int h, int length) {
	return h & (length - 1);
}
就相当于对length取摸,而且速度比直接取模快的多,这是HashMap在速度上的一个优化。

indexFor方法,该方法仅有一条语句:h & (length - 1),
这段代码除了上面说的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。

这里我们假设length为16 和 15,其中16为2的幂次,h为5,6,7


当length为15时,h的6,7的结果是一样的,这就表示它们在table数组中的存储位置是相同的,也就是产生了碰撞。
6,7就会在一个位置上形成链表,这样子就会导致查询速度降低。

我们扩大h的值,从0 ~ 15,length为15

从上面这个图中发现,一共发生了8次碰撞,同时发现浪费的空间非常大,
1,3,5,7,9,11,13,15处没有记录,也就是没有存放数据。

而当length=16时,length-1=15,即1111,那么进行低位&运算时,值总是与原来得hash值相同,而进行高位运算时,
其值等于其低位值。(0~14 与 0~15 ,与table数组长度比较)

所以说当length=2^n时,不同的hash值发生碰撞的概率比较小,这样会使得数据在table数组中分布均匀,查询速度也快



put(K,V)方法

public V put(K key, V value) {

	// 如果key为空,将null存放在table[0]第一个位置,这就是HashMap允许存null的原因
	if (key == null)
			return putForNullKey(value);

	// 计算key的hash值
	int hash = hash(key);    -----------------(1)

	// 根据hash码和数组长度,计算table数组下标
	int i = indexFor(hash, table.length);  ---------------(2)

	// 从i处开始迭代entry链表,找到key保存的位置
	for (Entry<K, V> e = table[i]; e != null; e = e.next) {
			Object k;
			// 判断该链条上是否有hash值相同的(key相同)
			// 若存在key相同,直接覆盖value,返回旧的value
			if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
					V oldValue = e.value;// 取出旧值
					e.value = value;// 赋新值
					e.recordAccess(this);
					return oldValue;// 返回旧值
			}
	}

	// 修改次数+1
	modCount++;

	// i处没有entry链表(该位置为空),将key,value添加至i处
	addEntry(hash, key, value, i);

	return null;
}
a. 判断key是否为null,为null直接调用putForNullKey()方法处理
b. key不为null,计算key的hash值
c. 根据hash值,计算table数组中的下标位置
d. 如果该下标位置有entry,则比较是否是相同的key,如果是相同的,覆盖value
key不同则将新的key/value存入entry链表表头,旧的往后移。
e. 如果下标处没有entry,则直接存储


在上面代码的for循环处,此处的迭代为了防止存在相同的key值,如果两个hash值(key)相同,
HashMap的处理方式是用新的value替换旧的value,并没有处理key,这就解释了HashMap中没有两个相同的key,
(以及后面的HashSet,key是唯一的,这个后面再说)


在(1),(2)处,这里是比较关键的地方:
hash()方法

final int hash(Object k) {
	int h = 0;
	if (useAltHashing) {
			if (k instanceof String) {
					return sun.misc.Hashing.stringHash32((String) k);
			}
			h = hashSeed;
	}

	h ^= k.hashCode();

	// 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);
}
首先获得k的hashCode,然后对hashCode值进行计算(纯数学计算,各种移位计算)

indexFor()方法

static int indexFor(int h, int length) {
		return h & (length - 1);
}
对于HashMap的table[]而言,数据分布需要均匀(最好每个下标只有一个元素,直接取值),
不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。
indexFor()方法的责任就是:均匀的分布table[]中的数据和充分利用空间

putForNullKey()方法,这个方法处理key为null的情况

/**
* 该方法的作用是将"key为null"的键值对,存放到table[0]位置
*/
private V putForNullKey(V value) {

	// 直接遍历table[0]位置的Entry链表
	for (Entry<K, V> e = table[0]; e != null; e = e.next) {

	// 寻找链表中key为null的Entry
	if (e.key == null) {
			V oldValue = e.value;// 取出旧value值
			e.value = value;// 赋值新的value
			e.recordAccess(this);
			return oldValue;// 返回旧value
	}
	}

	// 如果没有找到key为null的entry,说明table[0]位置没有Entry
	// HashMap被改变数计数器+1
	modCount++;

	// 将key为null的键值对添加入到Entry中
	addEntry(0, null, value, 0);

	return null;
}

最终将key,value插入Entry的两个方法

void addEntry(int hash, K key, V value, int bucketIndex) {

	// 首先判断是否需要扩容
	// 'hashMap的大小' 大于等于 '阀值(加载因子*容量)' && table数组对应下标位置有数据
	if ((size >= threshold) && (null != table[bucketIndex])) {
			// 容量扩大两倍
			resize(2 * table.length);

	// key为null,hash取0
	// key不为null,根据key计算hash
	hash = (null != key) ? hash(key) : 0;

	// 重新计算哈希码的索引
	bucketIndex = indexFor(hash, table.length);
	}

	// 创建entry
	createEntry(hash, key, value, bucketIndex);
}

/**
* 
* @param hash
*            hash值
* @param key
* @param value
* @param bucketIndex
*            table数组下标
*/
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大小+1
}

有两点需要注意:
a. 链表的产生
HahsMap总是将新的Entry对象添加到bucketIndex处。
如果bucketIndex处已经有了对象,那么新添加的Entry将指向原有的Entry对象,形成一条Entry链。
但如果bucketIndex处没有Entry对象,直接将数据塞进去,不会形成链表

b. 扩容问题
随着HahsMap中元素得数量越来越多,发生碰撞的概率越来越大,所以产生的链表会越来越长,
为了保证HashMap的速度以及效率,系统必须进行扩容处理,而扩容处理非常耗时,所以如果能
预知HashMap中元素的数量,在构造的时候,直接设置。


get(Object key)方法

get方法源码

public V get(Object key) {

	// 如果key为null,直接调用getForNullKey()方法
	if (key == null)
			return getForNullKey();

	Entry<K, V> entry = getEntry(key);

	// 返回value
	return null == entry ? null : entry.getValue();
}

private V getForNullKey() {
	// 默认重table[]数组第一位取Entry
	for (Entry<K, V> e = table[0]; e != null; e = e.next) {
			if (e.key == null)
					return e.value;// 返回value
	}
	return null;
}

final Entry<K, V> getEntry(Object key) {
	// 计算key的hash值
	int hash = (key == null) ? 0 : hash(key);

	// 根据hash值,算出下标位置,从table数组中取出Entry
	for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
			Object k;
			// 查找的key与entry中的key相同,则返回对应的value
			if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
					return e;
	}
	return null;
}

get()方法还是挺简单的,通过key的hash值找到table数组对应下标位置的Entry,然后取出value


其他的方法,看看源码大概都明白了,个人感觉HahsMap最主要的是Entry、hash计算、下标计算(indexFor)


HashMap小结

1. 什么时候会使用HashMap?它有什么特点?
HashMap是基于Map接口的实现,存储键值对时使用。
HashMap可以存储null的键值,是非同步的,HashMap存储着Entry对象

2. HashMap的工作原理
HashMap通过put / get方法存储和获取对象。
存储对象时,我们将kv传给put方法,它调用hashCode计算出hash值,从而得出存储在table数组中的位置,然后进一步存储。
每次存储时,HashMap会根据table数组的容量,自动调整容量(2的幂次方)
如果发生碰撞时,HashMap通过链表将产生碰撞的元素组织起来.

获取对象时,我们将key传给get,它调用hashCode计算hash从而得出key在table数组中的位置,并进一步调用equals方法确定键值对.

3. HashMap的put和get原理,equals和hashCode的作用是什么?
通过key的hashCode(),再计算出hash值,并计算出下标,从而获得在table数组中的位置.
如果产生碰撞,则利用key.equals()方法区链表中查找对应的节点.

4. 如果HashMap的大小超过了负载因子定义的内容,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并重新调用hash方法.


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值