本篇文章主要讲解和实例演示HashMap两大核心源码:hash算法及indexFor算法,希望大家通过阅读本篇文章后,能够彻底理解这两大源码的方法和作用
一.HashMap的介绍
先简单过一下HashMap的概念,我们都知道Java集合有两大接口Collection和Map,本篇介绍的集合是实现Map接口的HashMap。
特点:我们知道HashMap是用来存储键值对的一种集合,存入元素时,以key和value同时存入HashMap中。
HashMap的作用(有什么好处我们为什么要用它):我之前的文章也介绍过Java中的另外两种集合,ArrayList和LinkedList,这两种集合的数据结构是数组和链表,也分析了这两种集合的优劣,ArrayList集合的查询效率高,以及LinkedList的增删效率高,那么讲到这,我们就在想有没有这样一种集合,既能发挥ArrayList的高效查询效率,又能发挥LinkedList的高效增删?于是就有了HashMap。
ArrayList和LinkedList相关文章: ArrayList及LinkedList.
HashMap的数据结构:数组+链表+红黑树(JDK1.8及以后),HashMap使用了数组和链表的链地址的数据结构,同时发挥高效查询以及高效增删。红黑树的主要作用是解决链表冲突,设计的好处是避免在最极端的情况下冲突的链表变得很长很长,在查询的过程中,效率变得非常慢(这部分的详细内容会在后面进行讲解),不得不说,程序的设计者考虑问题还是非常全面的。
二.HashMap的存储方式
存储的数据类型
前面已经了解HashMap的存储是以key-value的形式存储元素,其方法是使用Map.put(k,v),其中k的数据类型是引用类型,主要原因是,我们在插入元素时需要对key进行去重操作,使用的是hashcode和equals方法(需要重写),而基本数据类型是没有方法的,所以需要使用引用数据类型。
HashMap的存储方式演示: HashMap存储实例演示.
存储方式
上面已经讲过,使用HashMap存储时,以它的key作为键找到对应在数组中的位置,然后插入value。一张图演示HashMap的数据机构:
数组存放的是key的位置,链表是用来存放出现同一个key时不同的元素的空间。其中Entry中有每一个键值对(key和value)。
可能有人会有疑问:为什么key相同还会有不同的元素对象?
HashMap中的hashcode()
什么是hashcode,为什么HashMap要使用它?
hashcode是一个对象的散列码,主要是用于查找的快捷性,当我们要存放一个元素进HashMap中时,它先检查此元素的key是否重复,我们使用重写过的hashcode来计算key的散列值,将它存放在散列表里(hashcode的重写方式很多),其实hashcode就是计算存放元素的索引,这个索引尽量不出现重复,最好是不同的元素,存放在不同的位置。hashcode就是为了尽量让存进集合中的元素的位置更加分散、少出现元素位置冲突。
如何解决同一个key可能是不同的对象?
我们已经知道,通过计算key的hashcode来找到存放元素的位置,但是这种方式总会出现相同的hashcode值的现象,即,不同的对象有着同样的hashcode。
举例:
public static void main(String[] args){
String A ="abc";
Integer B=96354;
System.out.println("A的hashcode为:"+A.hashCode());
System.out.println("B的hashcode为:"+B.hashCode());
System.out.println(A.equals(B));
}
结果:
这就出现了不同的对象有着相同的hashcode,所以我们需要再使用重写的equals方法对对象的变量进行比较。
为何需要重写hashcode和equasl方法? hashcode和equals方法的重写.
为了使得hashcode的值更加地散列(降低出现相同hashcode值的概率),由此引出hash算法。
三.HashMap中的hash算法
首先看一下hash算法的源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
看上去很简单,定义int型变量h,(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)这个分步理解为:
1.如果key的值是null,那么hash算法返回的值为0。
2.如果key不为null,就执行(h = key.hashCode()) ^ (h >>> 16)。
3.计算key的hashcode然后与此hashcode的高16位进行异或运算。
那么为何hashcode要右移16位,且和自己进行异或运算?
在讨论这个问题前我们需要看HashMap中另一个核心方法的源码indexFor()方法(此方法为JDK1.7及以前的方法,JDK1.8已经省略了,但实际的作用还在使用)。
四.HashMap中的indexFor算法(JDK1.7)
先看源码
static int indexFor(int h, int length) {
return h & (length-1);
}
以上的方法是HashMap中put方法中计算数组下标值index的算法,简单介绍就是HashMap在插入元素时调用put方法,而put方法又用到indexFor()方法。
JDK1.8使用的indexFor()方法
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断node数组已经初始化,根据key的hash找到first node
if ((tab = table) != null
&& (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 判断桶上第一个node的hash和key的hash一致,并判断key的值和first node 的key是否相同,相同就返回第一个Node
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 遍历桶中元素
if ((e = first.next) != null) {
// 如果是红黑树,走红黑树遍历查找方式 后文详解
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 链表方式查找Node: 采用do while形式遍历,保证循环至少走一次。
do {
// 依次遍历,和遍历桶顶元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
其中**tab[(n - 1) & hash]**部分和JDK1.7中的indexFor()方法作用一致
那么,为何要使用indexFor()方法呢?
我们来看一下这个方法的具体操作:
return h & (length-1);
找出在HashMap数组中的index,所以 h & (length-1) 就是key的hashcode在数组的位置,这么做有两种好处:
保证不发生数组越界
在HashMap中,数组的长度规定是2的幂次,那么长度转换为二进制为1000……00,1后面偶数个0,如果length-1转换为二进制为01111……111,0后面偶数个1,那么,length-1和h进行&操作,结果的值不会超过数组的大小,防止数组越界(主要作用)。
为何数组长度为2的幂次?
假设此时数组大小为16,length-1位15二进制为1111,假设有两个元素待插入,它们的hashcode为8和9,对应的二进制为1000和1001,分别&操作结果为:
hashcoe为8的元素:
1000
1111&操作结果为:
1000
hashcoe为9的元素:
1001
1111&操作结果为:
1001
发现不同hashcode的元素放入数组的位置不同,符合要求。
而如果数组长度不是2的幂次,那么会是什么结果呢?
假设数组长度为15,length-1为14,二进制为1110
hashcoe为8的元素:
1000
1110&操作结果为:
1000
hashcoe为9的元素:
1001
1110&操作结果为:
1000
发现不同hashcode的元素放在了数组同一个位置,发生hash冲突,两个元素都放在1000为数组idex下标为8的位置,而且,length-1二进制的最后一位为0,那么和任何hashcode&,最后一位都是0,这样,所有末尾为1的位置都用不到,不仅造成hash冲突,还会造成数组空间资源浪费。因此,我们通常规定HashMap数组长度为2的幂次,通过length-1后保证二进制最后位为1.
那么这样是否就解决问题了呢?(为何需要hash算法)
我们再举个例子,假设数组length的长度为8,length-1为7,二进制为111,此时有个元素key的hashcode为100001110110101,进行&运算
100001110110101
000000000000111
进行&运算后结果为: 000000000000101,我们发现,结果只与hashcode的后三位有关,不管前面位数的值是多少,由于数组长度固定,而且数组长度越小,那么使用到的hashcode的位数越少,正常情况是:
length为8,数组下标取决于hashcode后三位
length为16,数组下标取决于hashcode后四位
length为32,数组下标取决于hashcode后五位
length为2的n次方,数组下标取决于hashcode后n位(hashcode的长度一般不超出2的16次方)。
因此,为了让hashcode的后16位更加散列随机,就将hashcode的前16位参与运算,这样就保留了hashcode的前16位的特征,而将hashcode的前16位参与运算,最好的方法就是移动高位的16位。
为什么使用^不用&或者|
经过实验得知&或者|的结果都偏向1或者0,相比^并不均匀,所以,我们使用
^进行运算。
讲到这,我们就发现为何需要hash算法:
1.为了hashcode能够进一步实现散列随机
2.使得hashcode所有部位都能参与运算,实现hashcode的所有特征
HashMap极限容量
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//将threshold置为Integer.MAX_VALUE
return;//当HashMap的容量已经是2的31次方的时候,直接返回。
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
通过HashMap的扩容源码可知,当数组容量达到2的31次方时已经不会扩容了,而数组带下为2的幂次,所以可以发现绝大多数情况的数组大小都不会超过2的16次方。
总结:本文主要讲解了HashMap中的两大核心源码:hash算法和indexFor算法,通过讲解,希望大家都能够很好地理解这两大核心源码,最后,希望大家国庆玩得开心,但也不要忘了这段宝贵的学习时间。
别人结婚,我学Java。