一,HashMap底层数据结构
在jdk1.7中的HashMap是位桶+链表实现
在jdk1.8中的HashMap是位桶+链表+红黑树实现
未超过8个节点时,是位桶+链表实现,在节点数超过8个节点时,是位桶+红黑树实现。
二,HashMap的实现原理
jdk1.7中的HashMap
HashMap的主干是一个Entry数组,数组中的每一项都是一个Entry,为了解决哈希冲突,所以就采用了数组+链表的方法
1 来看看声明Entry的代码结构吧
transient Node<k,v>[] table;//用来存储键值对的数组<k,v>
同时Entry是HashMap中的一个静态内部类,如下
static class Entry<K,V) implements Map.Entry<K,V>{
final K key;
v value;
Entry<K,V> next;//存储指向下一个Entry的引用,为单链表结构
int hash;//根据Key执行hashCode()方法计算得到
2了解了Entry的结构之后,我们就来创建一个简单的Entry!
//一个Entry中包含如下4个参数,hash值,key值,value值和Entry的next
Entry(int h,K k,V v,Entry<K,V> n)
{
hash=h;
key=k;
value=V;
next=n;
}
//通过hash值得到对应的数组下标
static int indexFor(int h,int length){
//该方法其实就是一个与运算的执行
return h&(length-1);
3看了简单的Entry的构造过程之后,我们来讲述一下Entry的原理!
简述一下Entry的存储过程:
当准备存入键值对时,
1根据其key值来获取对应的hash值hashCode()
2根据hash值获得对应的数组下标hash.indexFor()
3根据数组下标存入该Entry
3.1若该下标处没有对应的值,则直接将该Entry存入
3.2若下标处已有值,则比较他们的hash值
3.3若hash值不相同,则将该Entry存入到该对应的Entry链头
3.4若hash值相同,则将该Entry替换掉此处的Entry值
大致了解其原理之后,接下来就来看看其源码是如何实现的
三,源码分析
首先来全面查看一下HashMap的全部关键属性
transient Entry[] table;//存储元素的实体数组
transient int size;//存放元素的个数
int threshold;//临界值 当实际大小超过临界值时,会进行扩容//threshold=数组容量*填充因子
final float loader;//填充因子,通常默认为0.75
transient int modCount;//被修改的次数
这里提一下HashMap和HashTable的区别
其中哈希表的初始长度为16,且以2倍的速度进行扩容
而hashTable的初始长度为11,以2n+1的速度进行扩充。
1,HashMap构造器详解
hashMap共有四个构造器,重点讲一下其默认的一个构造器
语法:(容量,填充因子)
public HashMap(int initialCapacity,float loadFactory){
//对此传入的初始容量进行校验,最大的容量不能超过2的30次方 1<<30,左移30位,低位补0)
//如果该容量小于0,则抛出异常
if(initialCapacity<0) throw new IllegalArgumentException("Illegal inital capacity:"+initialCapacity);
//如果该初始容量大于最大容量限制,那么我们就将该容量设置为最大容量,
if(initialCapacity >MAXIMUM_CAPACITY)
initicalCapacity=MAXIMUM_CAPACITY;
if(loadFactor <=0||Float.isNAN("illegal loadFactor:"+loadFactor);
this.loadFactor=loadFactor;
threshold=initialCapacity;
init();}
上述代码,为我们的HashMap分配了容量和加载因子,但是其并没有构建table数组·
这是因为他比较懒惰,当我们需要我们进行put操作的时候,才会给我们创建table数组
2 HashMap数组详解
public V put(K key,V value){
//1判断此=数组是否为空
if(table==EMPTY_TABLE){
//此时threshold为initialCapacity,默认是1>>4(16)
inflateTable(threshold);//这里现在不懂没关系,等下接下来会有详解
}
//2数组容量确定之后,就开始进行put操作了
if(key==null)
return putForNullKey(value);//这里不懂也没关系,接下来会有详解
int hash=hash(key);//对key的hashCode进一步计算,可以更加确保散列均衡
//数组下标,该方法会在接下来进行解析
int idx=indexFor(hash,table.length);
//3现在得到了对应的数组下标,就要开始来存入数据了,涉及到的判断在上面已经讲了,不熟悉的可以回到上面看看
//4通过第3步得到的下标i,找到对应的那条数组链表,接着对该数组链表进行遍历,看该链表中是否已存在对应的键值对
for(Entry<K,V> e=table[i];e!=null;e=e.next){
//5如果该对应的数据存在,那么就执行覆盖操作,并返回之前的value
Object k;
//5.1判断该hash值和该key值是否相同
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
//5.2若该hash值和key的值均相等,那么就进行替换,用新的代替旧的
V oldValue=e.value;
e.value=value;
e.recordAccess(this);//这个C方法接下来也会有讲解
//返回旧值
return oldValue;
}
}
modCount++;//保证并发访问时,等下去深究
addEntry(hash,key,value,i);//添加一个entry,/四元素 hash,key value和下标i
return null;
}
解析上面方法之一:
重点重点!!!
2.1 inflateTable:看其是如何给数组赋初始化值的,即确定数组的容量,并构建数组
上面的疑问,如下
//1判断此=数组是否为空
if(table==EMPTY_TABLE){
//此时threshold为initialCapacity,默认是1>>4(16)
inflateTable(threshold);//这里现在不懂没关系,等下接下来会有详解
}
inflateTable(threshold)解析
private void inflateTable(int toSize){
//相信大家也知道,因为hashmap初始为16,是以2的幂次方增长,所以这里的capacity是数组的容量,同时也必须是2的次幂,那么是如何确保其永远都会是2的次幂呢? 答案就是通过roundUpToPowerOf2(toSize)这个方法来完成,接下来会有解析
//1确定数组容量
int capacity=roundUpToPowerOf2(toSize);
//2根据数组容量,确定该数组的临界值
threshold=(int)Math.min(capacity*loadFactor,MAXIMUM_CAPACITY+1);//此处为threshold赋值也就是该HashMap的临界值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capacity一定不会超过MAXIMUM_CAPACITY,该值为1>>30
//3初始化数组,并根据我们已确定的数组容量来进行初始化
table=new Entry[capacity];
initHashSeedAsNeed(capacity);
}
总结:inflateTable 根据capacity来创建对应的数组,而capacity又是根据所传入的数组的容量
2.2 解析其中的roundUpToPowerOf2(tosize)方法
作用:根据传入的值,确定该数组的容量永远是最接近该数组容量的2的倍数:
2.2.1 保证该数组容量大于或等于16,且为2的次幂,且为最接近该数字的容量
private static int roundUpToPowerOf2(int number){
return number>=MAXIMUM_CAPACITY?MAXIMUMCITY:(number>1)?Integer.highestOneBit((number-1)<<1):1;
结果解析
1当number大于最大限制值时,赋该最大值也即是2的30次方
2否则执行(number>1)?Integer.highestOneBit((number-1)<<1):1
3number>1执行Integer.highestOneBit((number-1)<<1)
4该Integer.highestOneBit原理:获取该number的值并减去1,接着取该
减一之后,在将该数化为二进制数,并取该最左边的数往右移一位
3Hash值的获取
主要运用的手段:使用异或,移位等运算来保证最后的存储位置分布均匀
final int hash(Object k){
int h=hashSeed;
if(h!=0&&k instanceof String){
return sun.misc.Hashing.stringHash32((String) k);
}
h^=k.hashCode();
h^=(h>>>20)^(h>>>12);
return h^(h>>>7)^(h>>>4);
}
4存储位置/indexFor()方法
//这个就比较简单了,主要是执行一个与运算
static int indexFor(int h,int length){
return h&(length-1);
}
总结一下,是如何确定数组下标的:
key----->hashCode()----->hash()----->hash值----->indexFor()----->存储下标
上面我们实现了HashMap中Entry数组的初始化,接着我们就要说说在HashMap中添加Entry数组了
4看看addEntry的实现
//说明一下,这里的bucketIndex就是我们之前使用IndeFor()方法计算出的下标值
void addEntry(int hash,k key,v value,int bucketIndex){
//判断该数组的大小是否超出了该临界值,若超时临界值,就会发生hash冲突,就需要进行2倍扩容
if((size>=threshold)&&(table[bucketIndex])){
resize(2*table.length);//接下来会对该方法进行讲解其实如何进行扩容的
hash=(key!=null)?hash(key):0;//若该key为空,则给其赋值为0
}
//构建一个新的Entry数组在该对应的下标处
createEntry(hash,key,value,buckerIndex);
}
4.1resize()扩容机制
void resize(int newCapacity){
//1判断该旧数组的容量是否为HashMap的最大容量
Entry[] oldTable=table;
int oldCapacity=oldTable.length;
if(oldCapacity==MAXIMUN_CAPACITY){
threshold=Integer.MAX_VALUE;
return ;
}
Entry[] newTable =new Entry[newCapacity];
//将旧Table的元素全部进行转移到我们新建的Table数组上
transfer(newTable);
//然后再将我们新建的table赋值给table
table=newTable;
//计算临界值
threshold=(int)(newCapacity*loadFactory);
如上所示,在进行扩容的时候,会首先判断一下该旧数组的容量是否为数组的最大容量,若是,则无需进行扩容,
若不是,则进行扩容,使用Transfer方法,将旧数组中的元素全部转移到新数组中,并计算临界值
5HashMap读取
前面说了hashMap的初始化及元素的添加,接下来就来说说HashMap是如何读取数据!
总所周知,HashMap读取数据是通过get方法进行读取的,所以接下来自热而然就是对该get方法的解析了!
public V get(Object key){
//1判断该key是否为空,若为空,则进入getForNullkey这个方法
if(key==null)
return getForNullkey();
//若不为空,则获取该key的hash值
int hash=hash(key.hashCode());
//根据hash值获得该下标值
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)))
return e.value;
}return null;
}
这样我们就完成了该HashMap的读取!!!