源码下载地址: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方法.