对于HashMap的理解如果只停留在hash数据结构的存储,key/value可以是null,那就太片面了,更深层次的理解HashMap,需要知道HashMap其实就是数组+链表,HashMap有个关于bucket(桶)的概念,这个bucket就是数组实现的,每个bucket里面可以存储的Entry<K,V>,这个Entry<K,V>可以是多个,当有多个时碰撞就发生了,当bucket里需要存储多个Entry<K,V>时使用链表存储,HashMap的基本结构就是这样的。下图来来自网络
通过查看Entry的源码,Entry中定义的一些类变量,证明bucket里面存储的Entry是一个链表结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//针对一下元素的引用
int hash;
}
HashMap中定义的一些默认常量,比如HashMap的默认容量, resize的尺寸,还有最重要的bucket是个Entry[]数组
// HashMap的默认初始容量 必须为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 二进制1向左移动4位=16
//HashMap的最大容量,可以认为是int的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空Entry[]
static final Entry<?,?>[] EMPTY_TABLE = {};
//用来存储数据的数组,所以说HashMap的底层存储是个数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
HashMap中put元素的过程:
1、根据key的hashcode获得bucket位置
2、如果bucket是空的,直接存入
3、如果bucket不为空,就发生了碰撞,这时把元素放在bucket中链表的头部
通过源码来分析put()操作的这一过程
public V put( K key, V value ) {
//存储bucket的数组为空就初始化
if ( table == EMPTY_TABLE ) {
inflateTable(threshold);
}
//key为null时特殊处理,总是放在table[0]的位置
if ( key == null ) {
return putForNullKey(value);
}
//计算key的hashcode
int hash = hash(key);
//通过hashcode获取bucket的位置
int i = indexFor(hash, table.length);
//遍历table[i]位置的bucket链表,查找相同的key,若找到则使用新的value替换掉原来的oldValue并返回oldValue,这就是hashcede碰撞发生时的种情况
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;
}
}
//若没有在table[i]位置找到相同的key,则添加key到table[i]位置,新的元素总是在table[i]位置的第一个元素
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry( int hash, K key, V value, int bucketIndex ) {
//检查Entry[]大小,容量的75%扩大容量,减少碰撞发生
if ( (size >= threshold) && (null != table[bucketIndex]) ) {
resize(2 * table.length);//当前Entry[]大小*2
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//这个方法就是真正的把key/value添加到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++;
}
HashMap中get元素的过程:
1、根据key的hashcode获得bucket位置
2、如果bucket里面只有一个元素,这时性能最好的,直接取出
3、如果bucket中有多个元素,则通过key的equals()方法,在bucket中链表上查找
通过源码来分析get()操作过程
public V get( Object key ) {
//ke为null时的处理
if ( key == null ) {
return getForNullKey();
}
//通过key获取Entry
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K, V> getEntry( Object key ) {
if ( size == 0 ) {
return null;
}
//对key进行hash获取bucket的位置
int hash = (key == null) ? 0 : hash(key);
//循环bucket里面的链表根据key的hash值及对key进行equals
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;
}
减少碰撞发生可以提高HashMap的性能,一个存入HashMap的自定义对象都要覆写Object的hashcode()和equals(),覆写equals时有写原则
(1)自反性:就是说a.equals(a)必须为true。
(2)对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
(3)传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。
计算bucket的算法,并没有使用key.hashcode()%Entry[].length的方式而是采用了如下方式,减少碰撞的发生,bucket的大小为2的幂时效率最高
static int indexFor(int h, int length) {
return h & (length-1);
}
resize是指Entry[]中被使用的位置大于Entry.length*0.75倍时,resieze就发生了,对原来的Entry[]进行扩容,默认Entry[]大小为16,也就是16*0.75=12,当Entry[]大小为16时,被使用的大小超过12个,就要对原有的Entry[]进行扩容,原有Entry[].length*2,通过源码来分析这个过程
void resize( int newCapacity ) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//原有bucket=1<<30 = 1 073 741 824,时就创建一个大小为0x7fffffff的bucket
if ( oldCapacity == MAXIMUM_CAPACITY ) {
threshold = Integer.MAX_VALUE;
return;
}
//创建新的Entry[]
Entry[] newTable = new Entry[newCapacity];
//把当前Entry[]中的内容放到新的Entry[]中,并对它们在新的Entry[]中的位置重新计算
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;
}
}
}
resize在以下方法中被调用到了,如果为了性能考虑也可以在初始化一个HashMap时就指定bucket大小,但最好是2的幂
void addEntry(int hash, K key, V value, int bucketIndex)
public void putAll(Map<? extends K, ? extends V> m)
清空HashMap中的所有数据,其实就是把Entry[]数组全部置中为null,然后把Entry[].size设置为0
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
hashmap中其它的一下方法,后续还会补充进来
以上源码来自jdk1.7,版本如下
java version "1.7.0_79" Java(TM) SE Runtime Environment (build 1.7.0_79-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)