HashMap源码解读(一)
2012-11-06 16:55
316 查看
MY:
void clear()
从此映射中移除所有映射关系。
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射关系的 Set 视图。
V get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
boolean isEmpty()
如果此映射不包含键-值映射关系,则返回 true。
Set<K> keySet()
返回此映射中所包含的键的 Set 视图。
V put(K key, V value)
在此映射中关联指定值与指定键。
void putAll(Map<? extends K,? extends V> m)
将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
V remove(Object key)
从此映射中移除指定键的映射关系(如果存在)。
int size()
返回此映射中的键-值映射关系数。
Collection<V> values()
返回此映射所包含的值的 Collection 视图。
在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。
而在hashmap中有方法static int hash(int h) ,参数就是每个对象的哈希码,所以是个整数。为什么要hash(a.hashCode())呢,是因为table上的key的均匀分布可能需要对key的hashCode做一些处理。之后static
int indexFor(int h, int length) ,是去得对象在hashmap这个数组中的具体位置
fail-fast策略(速错)HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛ConcurrentModificationException,这就是所谓fail-fast策略(速错),这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了
。
对于NULL,如果有,则必然是放在talbe[0]这个位置,当然table[0]这个位置可能还有其他的key,但是map中的key是不可以重复的。value可以重复。而起方法values()可以获得所有的value的集合。是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。
如果上面for循环没找到。则将这个元素添加到talbe[0]链表的表头。
hashmap很费时间的一个地方是,rehash(),这是在容量达到当前容量乘以加载因子时,要把容量加倍,此时要重建内部数据结构。原因很明显,这个函数static int indexFor(int h, int length)的返回值此时已经变了。所以要尽量减少重建,但是把初始容量设置过大,又浪费空间。
hashmap是一个数组,我们判断是否resize时参考的容量是这个数组已经使用的容量,并不是所有map对的数目,或者说所有key的数量。为标准的。
关于hashcode:
对于Entry这个类,里面有个成员是 int hash,它的值是inthash=hash(key.hashCode());可以在put方法里看见,先是算出这个hash,然后如果在table里面没有找到,
则调用addEntry(hash,key,value,i);而addEntry方法呢又调用了
table[bucketIndex]=newEntry<K,V>(hash,key,value,e);而Entry的构造函数是这样的
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
对于hashmap的初始化
Java代码
transient Entry[] table;
而Entry的定义如下:
Java代码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
关键就是table,这是HashMap这个类的一个成员变量,也最重要,是用来存储数据的。
---------------------------------------------------------------------------------------------------------
HashMap源码解读(一)
1、HashMap的存储结构
2、HashMap的初始化
3、元素Hash值获取及通过hash值找到talbe下标索引
4、元素添加方法addEntry
5、HashMap扩容
6、老table重新hash成新table
7、key为null,存到哪去了
8、查找元素get(Object key)
9、根据key删除元素
1、HashMap的存储结构
在HashMap的Field中有:
Java代码
transient Entry[] table;
而Entry的定义如下:
Java代码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
简单说就是一个数组+链表,结构如下图:
2、HashMap的初始化
Java代码
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
构造方法中出现的几个关键字段:loadFactor ,threshold,CAPACITY,table
其中table上面讲了,是HashMap的存储结构。CAPACITY这个是构建HashMap的时候的容量,这里使用了系统默认的初始容量,loadFactor 是加载因子,用处是和CAPACITY相乘获得threshold,这个文档的说明如下:The next size value at which
to resize (capacity * load factor)。其实就是HashMap扩容的临界值,超过这个值,则重新扩容。
这样就说明了loadFactor 的用处了。这里有人要问了。为什么要这个东西。这里就涉及到HashMap的原理了。HashMap中存储元素的时候,首先得先通过其自己的hash算法找到存储在talbe数组的索引值。但是这个hash算法并不能保证,每一个元素对应不同的talbe数组的索引值,当放入HashMap的元素过多的时候,就容易出现相同的索引值,在算法里叫冲突,这时候元素就会被加到该索引值下的链表当中,这样查找的效率就会大大降低,这显然违背了HashMap快速查找的初衷了。所有HashMap在设计的时候,就是用了这样一个加载因子,如果存储的元素个数占table长度的比例大于loadFactor
加载因子的时候,冲突加剧,这样我们就得扩容解决这样的问题。
所以总结影响HashMap效率的两个因素:1.初始容量 2.加载因子。解决的本质无非就是减少hash冲突。
3、元素Hash值获取及通过hash值找到talbe下标索引
Java代码
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这个不深究,结果是获得一个随机点的hash值
Java代码
static int indexFor(int h, int length) {
return h & (length-1);
}
这个就是获得元素对应table下标索引的方法,h是通过上面的hash(int h)方法获得,length是table的长度
4、元素添加方法addEntry
Java代码
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
//Entry的构造方法
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
addEntry方法里出现的几个参数分别是:hash-->元素key的hash值,key,value不用说了,bucketIndex是计算出来的该元素对应的table下标索引。方法的前两句是,根据传入的参数生成一个Entry元素,他的next为现有table[bucketIndex]。
说白了就是将新元素加到该元素对应table[bucketIndex]链表的表头。流程如下图:
5、HashMap扩容
Java代码
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];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
在元素添加方法addEntry中,添加完元素后,有下面两行代码:
Java代码
if (size++ >= threshold)
resize(2 * table.length);
size表示的是HashMap中有多少个元素,当元素的个数超过临界值时,会自动调用扩容方法,可以看出HashMap的扩容是翻番的扩2 * table.length。我们在来看看resize扩容方法。
前面几行是判断扩容后是否好过了最大的int值。后面几行是将原来的table中的元素,重新hash放到新的扩容后的table中。可能大家对transfer(newTable)这个方法很困惑。接下来,我们来解读这个方法的实现。
6、老table重新hash成新table
Java代码
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;
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);
}
}
}
这个方法的主要作用就是,将老的table中的所有不为空的元素,重新hash放到新的table中去。估计在do之前的大家能很好理解。就是遍历table中不为空的元素。这时候找出来的e = src[j]是一个Entry链表。所以,如果不为空,还要遍历这个链表中的每一个元素,并将这些元素重新hash到新table中。下面我们对于代码讲解。
//将第一个元素e后的链表截取出来
Entry<K,V> next = e.next;
//找到e对应新table的下标索引
int i = indexFor(e.hash, newCapacity);
//将e插入到新table下标索引链表的表头
e.next = newTable[i];
//将该新table下标索引重新定位为e,这样就完成了一个元素的重新hash
newTable[i] = e;
//将截取的剩余的链表继续hash
e = next;
示意图如下:
1、Entry<K,V> next = e.next;
2、e.next = newTable[i];
即这里的e就是Entry[j],也就是
3、newTable[i] = e;
因为newTable[i]本身是一个指向浅蓝色Entry[i]的引用,这个时候,我们在将这个引用指向红色Entry[j],这样就完成了老table中一个元素的重新hash到新table中。
7、key为null,存到哪去了
在put方法里头,其实第一行就处理了key=null的情况。
Java代码
if (key == null)
return putForNullKey(value);
//那就看看这个putForNullKey是怎么处理的吧。
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;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
可以看到,前面那个for循环,是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。
如果上面for循环没找到。则将这个元素添加到talbe[0]链表的表头。
8、查找元素get(Object key)
Java代码
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
前面两行是找key为null的元素,前面说过,key为null的元素,是放在table[0]这个链表的。所以要找的话,直接到table[0]中查找就行了。
如果没找到的话。则根据key的hash值找到元素所在table中下标索引,根据其在找到元素所在链表,在遍历链表,找到该元素并返回其value,否则返回null。
9、根据key删除元素
Java代码
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
调用的还是下面的方法
final Entry<K,V> removeEntryForKey(Object key) {
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;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
这里while循环外面的很好看懂,我们讨论while循环里的。
Entry<K,V> next = e.next;把原有的链表截出表头元素,然后判断这个表头元素的key是否就是我们要找的key。如果找出的第一个元素就是的话,我们直接将这个链表的第一个元素删除就OK。
if (prev == e)
table[i] = next;
如果不是,则遍历这个链表,下图展示了这个过程:
步骤1、初始情况
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
步骤2、没找到
Entry<K,V> next = e.next;
……..
prev = e;
e = next;
如果e这个元素不是要删除的话,则遍历下一个元素。
步骤3、找到
prev.next = next;
return e;
将prev的下一个元素指向e.next。这样就相当于删除了e
最后的结果如下:
未完待续。。。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
本篇对HashMap实现的源码进行简单的分析。 所使用的HashMap源码的版本信息如下:
在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。
例如String的hashCode方法为:
可以看得出,一个字符串的哈希值为s[0]31(n-1) + s[1]31(n-2) + …
+ s[n-1],是一个整数。也就是说所有的字符串可以通过hashCode()将其映射到整数的区间中,由于在java中整数的个数是有限的(四个字节有正负,第一位为符号位-231 ~ 231 -1),当s[0]31(n-1) +
s[1]31(n-2) + … + s[n-1]足够大的时候可能会溢出,导致其变成负值。从上面的情况我们可以看出两个不同的字符串可能会被映射到同一个整数,发生冲突。因此java的开发人员选择了31这个乘数因子,尽量使得各个字符串映射的结果在整个java的整数域内均匀分布。
谈完java对象的哈希码,我们来看看今天的主角HashMap,HashMap可以看作是Java实现的哈希表。HashMap中存放的是key-value对,对应的类型为java.util.HashMap.Entry,所以在HashMap中数据都存放在一个Entry引用类型的数组table中。这里key是一个对象,为了把对象映射到table中的一个位置,我们可以通过求余法来,所以我们可以使用 [key的hashCode % table的长度]来计算位置(当然在实际操作的时候由于需要考虑table上的key的均匀分布可能需要对key的hashCode做一些处理)。
相关属性 首先肯定是需要一个数组table,作为数据结构的骨干。
这边定义了一个Entry数组的引用。 继续介绍几个概念把
capacity容量 是指数组table的长度
loadFactor 装载因子,是实际存放量/capacity容量 的一个比值,在代码中这个属性是描述了装载因子的最大值,默认大小为0.75
threshold(阈值)代表hashmap存放内容数量的一个临界点,当存放量大于这个值的时候,就需要将table进行夸张,也就是新建一个两倍大的数组,并将老的元素转移过去。threshold = (int)(capacity * loadFactor);
put方法详解
在HashMap中我们的key可以为null,所以第一步就处理了key为null的情况。
当key为非null的时候,你也许会认为:恩,直接和table长度相除取模吧,但是这里没有,而是又好像做了一次哈希,这是为什么呢?这个还得先看indexFor(hash, table.length)方法,这个方法是决定存放位置的
明眼的都可以发现,因为在HashMap中table的长度为2n (我们把运算都换成二进制进行考虑),所以h & (length-1)就等价于h%length,这也就是说,如果对原本的hashCode不做变换的话,其除去低length-1位后的部分不会对key在table中的位置产生任何影响,这样只要保持低length-1位不变,不管高位如何都会冲突,所以就想办法使得高位对其结果也产生影响,于是就对hashCode又做了一次哈希
当找到key所对应的位置的时候,对对应位置的Entry的链表进行遍历,如果以及存在key的话,就更新对应的value,并返回老的value。如果是新的key的话,就将其增加进去。modCount是用来记录hashmap结构变化的次数的,这个在hashmap的fail-fast机制中需要使用(当某一个线程获取了map的游标之后,另一个线程对map做了结构修改的操作,那么原先准备遍历的线程会抛出异常)。addEntry的方法如下
get方法
get方法其实就是将key以put时相同的方法算出在table的所在位置,然后对所在位置的链表进行遍历,找到hash值和key都相等的Entry并将value返回。
http://uuubd.iteye.com/blog/1447129
http://geeklu.com/2010/07/java-hashmap/
http://zengzhaoshuai.iteye.com/blog/1131890
void clear()
从此映射中移除所有映射关系。
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射关系的 Set 视图。
V get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
boolean isEmpty()
如果此映射不包含键-值映射关系,则返回 true。
Set<K> keySet()
返回此映射中所包含的键的 Set 视图。
V put(K key, V value)
在此映射中关联指定值与指定键。
void putAll(Map<? extends K,? extends V> m)
将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
V remove(Object key)
从此映射中移除指定键的映射关系(如果存在)。
int size()
返回此映射中的键-值映射关系数。
Collection<V> values()
返回此映射所包含的值的 Collection 视图。
在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。
而在hashmap中有方法static int hash(int h) ,参数就是每个对象的哈希码,所以是个整数。为什么要hash(a.hashCode())呢,是因为table上的key的均匀分布可能需要对key的hashCode做一些处理。之后static
int indexFor(int h, int length) ,是去得对象在hashmap这个数组中的具体位置
fail-fast策略(速错)HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛ConcurrentModificationException,这就是所谓fail-fast策略(速错),这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了
。
对于NULL,如果有,则必然是放在talbe[0]这个位置,当然table[0]这个位置可能还有其他的key,但是map中的key是不可以重复的。value可以重复。而起方法values()可以获得所有的value的集合。是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。
如果上面for循环没找到。则将这个元素添加到talbe[0]链表的表头。
hashmap很费时间的一个地方是,rehash(),这是在容量达到当前容量乘以加载因子时,要把容量加倍,此时要重建内部数据结构。原因很明显,这个函数static int indexFor(int h, int length)的返回值此时已经变了。所以要尽量减少重建,但是把初始容量设置过大,又浪费空间。
hashmap是一个数组,我们判断是否resize时参考的容量是这个数组已经使用的容量,并不是所有map对的数目,或者说所有key的数量。为标准的。
关于hashcode:
对于Entry这个类,里面有个成员是 int hash,它的值是inthash=hash(key.hashCode());可以在put方法里看见,先是算出这个hash,然后如果在table里面没有找到,
则调用addEntry(hash,key,value,i);而addEntry方法呢又调用了
table[bucketIndex]=newEntry<K,V>(hash,key,value,e);而Entry的构造函数是这样的
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
对于hashmap的初始化
Java代码
transient Entry[] table;
而Entry的定义如下:
Java代码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
关键就是table,这是HashMap这个类的一个成员变量,也最重要,是用来存储数据的。
---------------------------------------------------------------------------------------------------------
HashMap源码解读(一)
1、HashMap的存储结构
2、HashMap的初始化
3、元素Hash值获取及通过hash值找到talbe下标索引
4、元素添加方法addEntry
5、HashMap扩容
6、老table重新hash成新table
7、key为null,存到哪去了
8、查找元素get(Object key)
9、根据key删除元素
1、HashMap的存储结构
在HashMap的Field中有:
Java代码
transient Entry[] table;
而Entry的定义如下:
Java代码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
.........
}
简单说就是一个数组+链表,结构如下图:
2、HashMap的初始化
Java代码
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold=(int)(DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
构造方法中出现的几个关键字段:loadFactor ,threshold,CAPACITY,table
其中table上面讲了,是HashMap的存储结构。CAPACITY这个是构建HashMap的时候的容量,这里使用了系统默认的初始容量,loadFactor 是加载因子,用处是和CAPACITY相乘获得threshold,这个文档的说明如下:The next size value at which
to resize (capacity * load factor)。其实就是HashMap扩容的临界值,超过这个值,则重新扩容。
这样就说明了loadFactor 的用处了。这里有人要问了。为什么要这个东西。这里就涉及到HashMap的原理了。HashMap中存储元素的时候,首先得先通过其自己的hash算法找到存储在talbe数组的索引值。但是这个hash算法并不能保证,每一个元素对应不同的talbe数组的索引值,当放入HashMap的元素过多的时候,就容易出现相同的索引值,在算法里叫冲突,这时候元素就会被加到该索引值下的链表当中,这样查找的效率就会大大降低,这显然违背了HashMap快速查找的初衷了。所有HashMap在设计的时候,就是用了这样一个加载因子,如果存储的元素个数占table长度的比例大于loadFactor
加载因子的时候,冲突加剧,这样我们就得扩容解决这样的问题。
所以总结影响HashMap效率的两个因素:1.初始容量 2.加载因子。解决的本质无非就是减少hash冲突。
3、元素Hash值获取及通过hash值找到talbe下标索引
Java代码
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这个不深究,结果是获得一个随机点的hash值
Java代码
static int indexFor(int h, int length) {
return h & (length-1);
}
这个就是获得元素对应table下标索引的方法,h是通过上面的hash(int h)方法获得,length是table的长度
4、元素添加方法addEntry
Java代码
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
//Entry的构造方法
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
addEntry方法里出现的几个参数分别是:hash-->元素key的hash值,key,value不用说了,bucketIndex是计算出来的该元素对应的table下标索引。方法的前两句是,根据传入的参数生成一个Entry元素,他的next为现有table[bucketIndex]。
说白了就是将新元素加到该元素对应table[bucketIndex]链表的表头。流程如下图:
5、HashMap扩容
Java代码
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];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
在元素添加方法addEntry中,添加完元素后,有下面两行代码:
Java代码
if (size++ >= threshold)
resize(2 * table.length);
size表示的是HashMap中有多少个元素,当元素的个数超过临界值时,会自动调用扩容方法,可以看出HashMap的扩容是翻番的扩2 * table.length。我们在来看看resize扩容方法。
前面几行是判断扩容后是否好过了最大的int值。后面几行是将原来的table中的元素,重新hash放到新的扩容后的table中。可能大家对transfer(newTable)这个方法很困惑。接下来,我们来解读这个方法的实现。
6、老table重新hash成新table
Java代码
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;
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);
}
}
}
这个方法的主要作用就是,将老的table中的所有不为空的元素,重新hash放到新的table中去。估计在do之前的大家能很好理解。就是遍历table中不为空的元素。这时候找出来的e = src[j]是一个Entry链表。所以,如果不为空,还要遍历这个链表中的每一个元素,并将这些元素重新hash到新table中。下面我们对于代码讲解。
//将第一个元素e后的链表截取出来
Entry<K,V> next = e.next;
//找到e对应新table的下标索引
int i = indexFor(e.hash, newCapacity);
//将e插入到新table下标索引链表的表头
e.next = newTable[i];
//将该新table下标索引重新定位为e,这样就完成了一个元素的重新hash
newTable[i] = e;
//将截取的剩余的链表继续hash
e = next;
示意图如下:
1、Entry<K,V> next = e.next;
2、e.next = newTable[i];
即这里的e就是Entry[j],也就是
3、newTable[i] = e;
因为newTable[i]本身是一个指向浅蓝色Entry[i]的引用,这个时候,我们在将这个引用指向红色Entry[j],这样就完成了老table中一个元素的重新hash到新table中。
7、key为null,存到哪去了
在put方法里头,其实第一行就处理了key=null的情况。
Java代码
if (key == null)
return putForNullKey(value);
//那就看看这个putForNullKey是怎么处理的吧。
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;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
可以看到,前面那个for循环,是在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。
如果上面for循环没找到。则将这个元素添加到talbe[0]链表的表头。
8、查找元素get(Object key)
Java代码
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
前面两行是找key为null的元素,前面说过,key为null的元素,是放在table[0]这个链表的。所以要找的话,直接到table[0]中查找就行了。
如果没找到的话。则根据key的hash值找到元素所在table中下标索引,根据其在找到元素所在链表,在遍历链表,找到该元素并返回其value,否则返回null。
9、根据key删除元素
Java代码
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
调用的还是下面的方法
final Entry<K,V> removeEntryForKey(Object key) {
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;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
这里while循环外面的很好看懂,我们讨论while循环里的。
Entry<K,V> next = e.next;把原有的链表截出表头元素,然后判断这个表头元素的key是否就是我们要找的key。如果找出的第一个元素就是的话,我们直接将这个链表的第一个元素删除就OK。
if (prev == e)
table[i] = next;
如果不是,则遍历这个链表,下图展示了这个过程:
步骤1、初始情况
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
步骤2、没找到
Entry<K,V> next = e.next;
……..
prev = e;
e = next;
如果e这个元素不是要删除的话,则遍历下一个元素。
步骤3、找到
prev.next = next;
return e;
将prev的下一个元素指向e.next。这样就相当于删除了e
最后的结果如下:
未完待续。。。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
本篇对HashMap实现的源码进行简单的分析。 所使用的HashMap源码的版本信息如下:
/* * @(#)HashMap.java 1.73 07/03/13 * * Copyright 2006 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */
一.概述
在Java中每一个对象都有一个哈希码,这个值可以通过hashCode()方法获得。hashCode()的值和对象的equals方法息息相关,是两个对象的值是否相等的依据,所以当我们覆盖一个类的equals方法的时候也必须覆盖hashCode方法。例如String的hashCode方法为:
public int hashCode() { int h = hash; if (h == 0) { int off = offset; char val[] = value; int len = count; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
可以看得出,一个字符串的哈希值为s[0]31(n-1) + s[1]31(n-2) + …
+ s[n-1],是一个整数。也就是说所有的字符串可以通过hashCode()将其映射到整数的区间中,由于在java中整数的个数是有限的(四个字节有正负,第一位为符号位-231 ~ 231 -1),当s[0]31(n-1) +
s[1]31(n-2) + … + s[n-1]足够大的时候可能会溢出,导致其变成负值。从上面的情况我们可以看出两个不同的字符串可能会被映射到同一个整数,发生冲突。因此java的开发人员选择了31这个乘数因子,尽量使得各个字符串映射的结果在整个java的整数域内均匀分布。
谈完java对象的哈希码,我们来看看今天的主角HashMap,HashMap可以看作是Java实现的哈希表。HashMap中存放的是key-value对,对应的类型为java.util.HashMap.Entry,所以在HashMap中数据都存放在一个Entry引用类型的数组table中。这里key是一个对象,为了把对象映射到table中的一个位置,我们可以通过求余法来,所以我们可以使用 [key的hashCode % table的长度]来计算位置(当然在实际操作的时候由于需要考虑table上的key的均匀分布可能需要对key的hashCode做一些处理)。
二.源码解析
相关属性 首先肯定是需要一个数组table,作为数据结构的骨干。transient Entry[] table;
这边定义了一个Entry数组的引用。 继续介绍几个概念把
capacity容量 是指数组table的长度
loadFactor 装载因子,是实际存放量/capacity容量 的一个比值,在代码中这个属性是描述了装载因子的最大值,默认大小为0.75
threshold(阈值)代表hashmap存放内容数量的一个临界点,当存放量大于这个值的时候,就需要将table进行夸张,也就是新建一个两倍大的数组,并将老的元素转移过去。threshold = (int)(capacity * loadFactor);
put方法详解
public V put(K key, V value) { if (key == null) return putForNullKey(value); 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; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
在HashMap中我们的key可以为null,所以第一步就处理了key为null的情况。
当key为非null的时候,你也许会认为:恩,直接和table长度相除取模吧,但是这里没有,而是又好像做了一次哈希,这是为什么呢?这个还得先看indexFor(hash, table.length)方法,这个方法是决定存放位置的
static int indexFor(int h, int length) { return h & (length-1); }
明眼的都可以发现,因为在HashMap中table的长度为2n (我们把运算都换成二进制进行考虑),所以h & (length-1)就等价于h%length,这也就是说,如果对原本的hashCode不做变换的话,其除去低length-1位后的部分不会对key在table中的位置产生任何影响,这样只要保持低length-1位不变,不管高位如何都会冲突,所以就想办法使得高位对其结果也产生影响,于是就对hashCode又做了一次哈希
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所对应的位置的时候,对对应位置的Entry的链表进行遍历,如果以及存在key的话,就更新对应的value,并返回老的value。如果是新的key的话,就将其增加进去。modCount是用来记录hashmap结构变化的次数的,这个在hashmap的fail-fast机制中需要使用(当某一个线程获取了map的游标之后,另一个线程对map做了结构修改的操作,那么原先准备遍历的线程会抛出异常)。addEntry的方法如下
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
get方法
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); 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.equals(k))) return e.value; } return null; }
get方法其实就是将key以put时相同的方法算出在table的所在位置,然后对所在位置的链表进行遍历,找到hash值和key都相等的Entry并将value返回。
http://uuubd.iteye.com/blog/1447129
http://geeklu.com/2010/07/java-hashmap/
http://zengzhaoshuai.iteye.com/blog/1131890
相关文章推荐
- 深入理解JAVA集合系列一:HashMap源码解读
- Java8 HashMap主要方法源码解读
- Java HashMap 核心源码解读
- HashMap源码解读
- JDK源码之解读hashMap 的put和get方法的实现原理
- jdk 1.8 hashmap源码解读(详细)(上)
- Java拾遗:002 - HashMap源码解读
- HashMap源码的解读
- JDK源码之解读hashMap 的put和get方法的实现原理
- java容器源码--hashmap源码解读
- HashMap源码解读
- Java 集合深入理解(16):HashMap 主要特点和关键方法源码解读
- Java HashMap 核心源码解读
- HashMap 主要特点和关键方法源码解读
- JDK8之HashMap源码解读
- Java HashMap 核心源码解读
- JAVA源码解读---HashMap目录扩展的奥秘
- JDK之HashMap源码解读
- HashMap 主要特点和关键方法源码解读
- HashMap源码解读