HashMap集合类是JAVA中常用的数据结构,与此相关的集合还有LinkHashMap和TreeMap,这里主要介绍HashMap
HashMap是一种基于线性存储结构(也就是数组)的数据结构,数据以Entry(K,V)的形式保存在数组table中。Entry本身是一个链表结构,如下所示
Entry{
final K key;//键
V value;//值
Entry<K,V> next;//指向下一个实体
int hash;//key对应的hash值
}
TIPS:看最新的jdk和sdk代码时发现已经用Node implements Entry<K,V>代替了原来的Entry,Entry成了接口,被Node实现。
在存储数据时,先获取key值对应的hashcode值,然后根据table数组长度对hashcode值进行indexFor()取模运算,将具有相同值的(key,value)作为链表头部保存在同一个数组元素中,其中next元素指向原来的Entry对象。
在Java1.7中,整个集合初始capacity容量(集合中Entry实体的数量)为2^4 = 16,Entry被保存在容器大小为16的bucket数组中。在添加数据过程中,如果bucket数组有超过75%的位置(12个)被填充数据时,此时数据容器需要进行扩容一倍。所以HashMap的扩容因子为0.75,每次扩容数组大小增加一倍。
/*h为hashcode,length为数组cap
*/
indexFor(int h,int length){
return h & (length-1);//从这儿可以得知,数组的bucket个数和数组大小capacity的关系近乎为bucket个数^2 = capacity大小
}
扩容阈值threshold = 数组大小 * loadFactor,loadFactor默认为0.75。
在每次添加键值对时,会先对数组容量进行判断,
void addEntry(int hash ,K key,V,value,int bucketIndex){
if((size >= threshold) &&(null != table[bucketIndex])){
resize(2*table*length);//达到阈值条件后,每次扩容两倍,将capacity变成原来的两倍
...
}
createEntry(hash,k,v,bucketIndex);
}
如果数组中元素的个数(非数组的实际长度或容量capacity)超过阈值,则对数组进行扩容。数组容量每次添加两倍,好处是,保持了原来的Entry的bucketIndex保持不变,具体算法可见这篇文章。
下面分析几个常见的api接口:
public V put (K key,V value){
...
int hash = hash(key);//获取key所对应的hash值
int i = indexFor(hash,table.length);//获取该hash值对应的Bucket
for(Entry<K,V>e = table[i];e != null;e = e.next){//遍历链表
Object k;
//判断新添加的key是否存在,如果存在,则更新该key键所对应的value值,并返回原来的oldValue值。
//判断的标准为:先判断该key是否有相同的hashCode,再判断该key是否和原来的key相等或相同(这儿是复写equals方法后需要复写hashCode的关键所在,因为如果hashCode都不相等,后面的equals等判断都不会执行)
if(e.hash == hash && ((k = e.key) == key || key.equals(k))){
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//数组中存储的Entry个数加一
addEntry(hash,key,value,i);//存储Entry,上面有讲解
return null;
}
//内部方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);//这两行表示,先将序号为bucketIndex的桶中的entry提取出来,然后作为next元素赋给新创建的Entry实体;也就是每次新添加的元素被放在链表头部
size++;//每次新建一个Entry实体,size都会加1
}
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
//通过O(1)次查找到key所在的桶,再进行链表的遍历操作
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//key值的比较和put方法中的一样,先比较hashCode,再比较key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
在JDK1.8中,HashMap新增了红黑树数据结构。当bucket数组中某个元素的链表数据Node个数超过8时,会使用红黑树数据结构代替链表。
另外,https://blog.csdn.net/suifeng629/article/details/82179996这篇文章总结也不错