HashMap实现原理,你能看完这一篇就足够了!

一,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的读取!!!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值