HashMap源码分析
基于JDK1.7中的HashMap分析
字段说明
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认容器长度(与直接写16有什么区别?这样效率高?)
static final int MAXIMUM_CAPACITY = 1 << 30; //容器最大长度
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认扩容阀值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //瞬态的Entry数组对象,不会被序列化
transient int size; //数据数量
int threshold; //数据长度扩容阀值,即达到此数量的数据时扩容
final float loadFactor; //扩容阀值
Entry,数据单元
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//省略其它getKey、getValue、setValue、equals、hashCode、toString等方法
}
- Entry是个内部类,有4个属性,key、value、hash这就是存进来的数据key,value和hash值
- 重点是Entry<K,V> next,这个是下一个Entry,一个扣一个,可见Entry是链式结构
- 而HashMap的容器table呢,是Entry<K,V>[] table这样的,数组形式的Entry,也就是说 容器是数组+链式结构
- 这样的容器结构的好处是什么呢?数组的优点是便于查找,链式的优点是便于修改,这样可以把两者优点结合
创建对象
- 构造函数是HashMap(int initialCapacity, float loadFactor),还有三个重载函数,HashMap()、HashMap(int initialCapacity),和 HashMap(Map<? extends K, ? extends V> m)
- initialCapacity是指初始化容器长度,默认值为 1 << 4 ,也就是16,指定的长度不能大于MAXIMUM_CAPACITY,也就是 1 << 30,超过就指定为MAXIMUM_CAPACITY
- loadFactor为阀值,默认为0.75f,也就是说在容器内数据达到3/4时,进行容器扩容
- HashMap在创建对象时没有扩张容器,table还是EMPTY_TABLE
基本方法
put(k,v),添加数据
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
}
//添加Entry数据单元
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//创建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++;
}
- 在这可以看到,在添加数据时才判断如是容器为空,则扩张容器
- 如果key为null,先进行特殊处理
- 根据key得到一个hash值,再根据hash值得到一个索引值,接下来先看容器table中对应索引位置的Entry链上有没有重复的key,有的话覆盖原值并返回老值,没有的话继续往后走,进入到addEntry()
- 在addEntry中有个判断,如果数据达到阀值,则扩容数据,长度是现在容器长度的2倍,然后重新获取索引值
- 到这就只需要创建一个新的Entry对象,并链接对应索引位置的Entry,就相于在一个锁链的前端加了一个扣,就完事了
inflateTable(int toSize),扩张容器
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
threshold是创建对象时的initialCapacity,如果没有指定,则默认为16
get(k),获取数据
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//getEntry才是取数据的实际操作
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
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;
}
取数据时也就跟存数据类似了,取hash值,得到索引位置,然后遍历Entry链,找到对应key的value
resize(n) 扩容
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, 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;
}
}
}
补充说下扩容方法,从上面可以看出,扩容时都是扩容成原来的2倍容量,并且需要把原数据都取出来重新计算存入新容器中,所以如果在使用HashMap时提前知道要存放的数据量并定义好,可以减少一些不必要的性能消耗
其它方法
其它的常用方法,remove(k)就是跟put(k,v)反过来,找到对应的Entry,然后从链上去除,别的如containsKey(k),containsValue(v),keySet(),entrySet()等也基本都是包装的getEntry()方法