hashmap底层实现采用的是哈希表(基本结构就是“数组+链表”)。
一个Entry[]对象存储了:
1、key:键对象value:值对象
2:next:下一个节点
3、hash值对象的hash值。
显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:
然后,我们画出Entry[]数组的结构(这也是HashMap的结构):在这里我们把单向链表存储在一个数组里面。
一、存储数据的过程put(key,value)
明白了HashMap的基本结构后,我们继续深入学习HashMap如何存储数据的。此处的核心是如何产生hash值,该值用来对应数组的存储位置。
我们的目的是将“key-value两个对象” 成对的存放到HashMap扥Entry[]数组中,参见以下步骤:
1、获得key对象的hashcode
首先调用key对象的hashcode()方法,获得hashcode。
2、根据hashcode计算出hash值(要求在[0,数组长度-1]区间)
hashcode是一个整数,我们需要将它转化成[0,数组长度-1]区间)的范围。我们要求转化后的hash值尽量均匀地分布在[0,数组长度-1]区间),减少“hash冲突”。
- 一个简单和常用的计算hash值的算法(相除取余算法)
- hash值=hashcode%数组的长度
- 这种算法可以让hash值均匀的分布在[0,数组长度-1]的区间。早期HashTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值=hashcode&(数组长度-1)。
测试Hash算法
public class test {
//测试hash算法
public static void main(String[] args) {
int h=25860399;
int length=16;//length为2的整数次幂,则h&(length-1)相当于length取模
myHash(h,length);
}
public static int myHash(int h,int length){
System.out.println("通过位运算实现取余操作:"+(h&(length-1)));
System.out.println("直接取余操作:"+(h%length));
return h&(length-1);
}
}
运行上面的程序,我们发现直接取余h%length和位运算+(h&(length-1))结果是一致的。事实是为了获得更好的散列效果。
3、生成Entry对象
如上所述,一个Entry对象包含4部分:key对象、value对象、hash值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个Entry对象的引用为null。
4、将Entry对象放到table数组中
如果本Entry对象对应的数组索引位置还没有放Entry对象,则直接将Entry对象存储进数组;如果对应索引位置已经有Entry对象,则将已有Entry对象的next指向本Entry对象,形成链表。
总结上述过程:
当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一个hash值的元素已经放在同一位置了,这是就添加到同一hash值元素的后面,他们在数组同一个位置,就形成了链表,同一个链表上的hash值是相同的,所以说数组存放的是链表。
================================================================================================
二、取数据过程get(key)
我们需要通过key对象获得“键值对”对象,进而返回value对象,明白了存储数据过程,去数据过程就比较简单了,参见以下步骤:
扩容问题
HashMap的位桶数组,初始大小为16.实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组length),就重新调整数组大小变为原来2倍大小。
扩容很耗时的,扩容的额本质是定义新的更大的数组,并将旧的数组内容挨个拷贝到新的数组中。
- 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
- 在链表上挨个比较key对象。调用equel()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回 true的节点对象为止。
- 返回equel()为true的节点对象的value对象。