java容器类:HashMap
HashMap源码分块阅读(2)#
HashMap类定义了属性和方法,其中方法又有共用方法(get,put,remove等)和私用方法(hash,resize等)
HashMap的属性##
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
如果未指定HashMap的容量,那么默认容量就是16,HashMap的容量都是偶数.- static final int MAXIMUM_CAPACITY = 1 << 30;
如果指定的容量超过这个这个值,那么这个值将会被启用.应该是用来防止容量过大的.- static final float DEFAULT_LOAD_FACTOR = 0.75f;
如果未指定HashMap负载因子,那么默认的因子就是0.75.- static final Entry[] EMPTY_TABLE = {};
一个空的Entry数组,后面会被用来下面的table比较- transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
用于存储HashMap数据的数组对象.用transient修饰表示这个对象不可被序列化.- transient int size;
代表HashMap的大小,由HashMap中的键值对决定- int threshold;
HashMap的阀值,假设容量和负载因子为默认值,那么HashMap中存储的键值对超过16*0.75,也就是阀值等于12,那么HashMap就会扩容.- final float loadFactor;
负载因子- transient int modCount;
HashMap的结构被改变的次数,结构性改变是指改变HashMap键值对映射的数量或者修改内部结构(例如:再哈希).这个域用来造成Collecttion类的迭代器的快速失败.- HashMap采用除数取余法实现散列,假定一个键值对的key为一个String字符串,那么用String类的hashCode()方法获得hashCode.hashCode/size得到的余数就是这个键值对要存储在HashMap中的位置.我使用的jdk版本是1.7,类中还提供了一个替代哈希方法.如果启用替代hash方法,若key为String类型,那么就将计算哈希码的方法从hashCOde()替换为StringHash32().是否使用替代哈希取决于系统变量jdk.map.althashing.threshold,然而我并不知道怎么设置这个变量,System.setProperty方法也没有用.
- 这些转换都是在静态内部类Holder中处理的
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
HashMap运行步骤##
- 1.初始化: HashMap有三种构造方法:1.HashMap(int initialCapacity, float loadFactor),2.HashMap(int initialCapacity),3.HashMap().
第一种可以指定容量和负载因子,第二种指定容量,负载因子默认为0.75,第三种调用第一种的构造方法,容量和负载因子都是默认值:16,0.75- 2.插入数据:最基础的就是put()方法了,put步骤分5步:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
//1.判断表是否为空,若为空,则进行膨胀.
//inflateTable方法做了什么?:
//a.将容量扩充最小的2的n次幂,且大于之前设置的容量,例如设置为5,那么会膨胀为8;如果是9,那么膨胀为16.
//b.初始化阀值threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//c.初始化table = new Entry[capacity];
//调用initHashSeedAsNeeded(capacity)方法初始化hashseed,这个变量决定要不要使用StringHash32()方法生成哈希码
}
if (key == null)
return putForNullKey(value);
//2.若key为空,调用putForNullKey()方法,key为null的值默认保存在table数组的第一个位置,即table[0],通过遍历table[0]的Entry节点来定位和处理数据.
int hash = hash(key);
//3.hash方法计算得出传入key的hashCode
int i = indexFor(hash, table.length);
//4.indexFor方法得出键值对在table中的位置
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;
}
//5.遍历Entry节点定位并处理数据
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
- 这是HashMap的数据结构图,table是一个Entry数组,通过put方法添加的键值对将以Entry类的形式添加到table[i]中,i即为indexFor方法得出的余数,若多个不同的hashCode得到的余数一致,那么就会像table[1]一样挂了好多的Entry对象,这被称为散列冲突,冲突越厉害,那么对HashMap的性能影响越大.
- 可以分析一下addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//如果Entry的数量高于阀值,而且这个索引的table中没有存放任何Entry,那么进行扩容
resize(2 * table.length);//将table的长度扩充一倍,并将table中存储的Entry在重新通过除数取余法进行排布,工作量比较大,很影响性能.
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//重新计算存放的位置
}
createEntry(hash, key, value, bucketIndex);//实例化一个Entry对象,将key和value存入其中
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //得到当前节点存储的Entry
table[bucketIndex] = new Entry<>(hash, key, value, e); //然后将新的Entry放入table的对应节点,并将刚刚得到的Entry挂在新的Entry尾部.
size++; //扩充Entry的数量
}
上述三张图就是一个键值对存入HashMap中的过程
- 3.查找数据
查找和添加类似,首先根据key计算出数组的索引值(如果key为null,则索引值为0),然后顺序查找该索引值对应的Entry链,如果在Entry链中找到相等的key,则表示找到相应的Entry记录,否则,表示没找到,返回null。对get()操作返回Entry中的Value值,对于containsKey()操作,则判断是否存在记录,两个方法都调用getEntry()方法:
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
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;
}
- 4.删除数据
移除操作(remove())也是先通过key值计算数组中的索引号(当key为null时索引号为0),从而找到Entry链,查找Entry链中的Entry,并将该Entry删除。
- 5.遍历
HashMap中实现了一个HashIterator,它首先遍历数组,查找到一个非null的Entry实例,记录该Entry所在数组索引,然后在下一个next()操作中,继续查找下一个非null的Entry,并将之前查找到的非null Entry返回。为实现遍历时不能修改HashMap的内容(可以更新已存在的记录的值,但是不可以添加、删除原有记录),HashMap记录一个modCount字段,在每次添加或删除操作起效时,将modCount自增,而在创建HashIterator时记录当前的modCount值(expectedModCount),如果在遍历过程中(next()、remove())操作时,HashMap中的modCount和已存储的expectedModCount不一样,表示HashMap已经被修改,抛出ConcurrentModificationException。即上面提到的fail fast。
在HashMap中返回的key、value、Entry集合都是基于该Iterator实现.