目录
简单说一下什么是哈希表
其实就是在数组上面,然后不按照固定的索引去存放数据,这也是利用数组支持按照索引下标进行随机性访问的特性。其中,索引的随机生成并且保证它的唯一性,就是我们哈希表的重要任务
那么这个哈希值怎么算出来呢,一般我们会通过一个散列函数去进行计算得到,比如在HashMap的源码里面有下面这个散列函数
上面这个哈希函数可以自行设计,它的设计思想就是先拿到key的原始哈希值,这个是系统给我们实现的函数函数,在Java中,每个类都继承自Object
类,而Object
类中有一个默认的hashCode()
方法。这默认的hashCode()
方法是根据对象的内存地址计算的,因此相同内容的对象在内存中不同的位置,其hashCode()
值也会不同。
总之,它能先初步计算出一个哈希值,在把这个hashCode的高16位和低16位进行混合,使得高位和低位的信息更好地交叉,进一步增加混淆性。减少了哈希冲突,哈希冲突也就是不同的对象进来你生成的哈希值一样。
那这里提一点,如果是相同的对象,你肯定要生成相同的哈希值,哈希函数也不能太复杂,不然也会耗费大量的计算时间,尽可能让算出来的哈希值随机且均匀的分布,这样也能减少冲突
一些常见的设计就比如:处余法,平方取中间数字,稍微了解一下
哈希冲突的常见解决办法
一般来说,分为两类方法来解决散列冲突:开放寻址法,链表法
1.开放寻址法
1.1线性的地址检测
核心:一旦出现了散列冲突,我们就重新去寻址一个空的散列地址,比如从当前位置,依 次往后面找,看看是否有空闲,直到找到空闲位置为止
1.2二次检测
和上面差不多的思想,只不过上面是一个地址一个地址移动往下面检测,这里把检测不上变成了原来的二次方,也就是一次可以跨很多位置进行检测
1.3双重散列
先用一个哈希函数计算出一个存储位置,如果位置白占用,在用第二个散列函数,直到找到空闲位置为止
2.链表法
我们首先把数组下标的每一个位置我们可以称之为“桶(bucket)”,每一个桶通常会对应一条链表,所有的散列值相同的元素我们放到这个桶对应的链表里面
HashMap就是采用这样的方式存储数据的
HashMap的源码讲解
整个HashMap采用了哈希表 + 单链表 + 红黑树结构来进行设计
常见属性
/**
* The default initial capacity - MUST be a power of two.
* 默认的初始化容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* 最大的hash表的容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 加载因子,可以这样理解
* 当表里面的数据达到了75%的时候,就要触发扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 进行树化的阈值,默认某个桶结点数大于8进行树化
* 但是还是有个条件是,整个桶的所有结点满足最小的树化容量(MIN_TREEIFY_CAPACITY = 64)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 链化的阈值,当结点数小于6时,红黑树会退化成单链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 树化一个条件:表的最小容量数目
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
* 每一个结点
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
这里先来说jdk1.8的源码
初始化对象的几个构造函数
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//上面几个都是一些常规的安全检查
this.loadFactor = loadFactor;//赋值你的负载因子
//这个函数返回的阈值是一个最接近当前初始容量的二次方数
this.threshold = tableSizeFor(initialCapacity);//返回一个阈值
//需要注意一点,调用这个构造方法的时候
//还没有给我们返回容量,也就是这个表还没有被初始化
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
* 还是调用上面那个构造函数
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
* 给了一个默认的负载因子
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
* 一样给一个默认的负载因子
* 但是这里会调用一个putMapEntries方法进行数据的追加
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Implements Map.putAll and Map constructor.
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
* 当使用HashMap(map)初始化一个对象的时候,会进入这个构造方法
* 这个方法会计算出一个阈值,这个阈值一般和你传入的map大小是有关系的
* 然后在
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//表为null的时候,计算一个扩容阈值
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold) //原始阈值肯定为0,进来
threshold = tableSizeFor(t);//变成当前值最近的二次方,这个数组大小和s大小有关系
}
else if (s > threshold)
resize();//表如果不为空,容量大于了阈值,就进行扩容
//循环添加数据
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//内部还是调用了putVal开始添加数据
putVal(hash(key), key, value, false, evict);
}
//上面第一次进来,也会先算阈值,然后进行循环调用putVal进行数据的添加
}
}
这里就得引入一个问题,就是哈希表在什么时候进行初始化,上面看了一下构造函数,在初始化的时候new HashMap()与new HashMap(capacity,factor)他们其实都没有创建一张哈希表,只是给我们创建了一个负载因子(空参构造只会创建负载因子)和阈值,他们初始化哈希表都是在调用put方法添加数据的时候进行的,那我们在看new HashMap(Map)这个构造函数,它虽然也会给我们先算一个阈值出来,但是后面会调用一个for循环调用putVal方法进行数据添加,这个时候就会初始化一张表
如果说非要问,哈希表是在什么时候进行初始化的?
我们可以这样回答,空参构造和带容量的构造函数初始化一个对象的时候,没有初始化表,是在调用put方法的时候进行初始化的,而我们利用map集合进行初始化一个对象的时候,它是会给我们初始化表的,但是本质也是调用了putVal方法
构造对象看完了,下面就来看put和putVal这两个初始化一张表的方法
其实put方法内部就是调用了putVal进行一个数据的添加,下面我们看一下表的初始化过程
这里面有putVal方法的详细注释
下面我引入了完整的put方法
我们可以这样来看
第一次put数据会先进入下面这部分代码里面
然后去调用扩容方法reszie,那么进入到resize之后,会直接干到if (oldTab != null)的部分,我把这部分代码贴过来
/**
*
*扩容的函数
* @return the table
* 先来分析一下他会进入扩容的一些情况
* 1.第一次put的时候,会调用putVal方法,然后内部,会调用resize()方法进行初始化一个哈希表
*阈值对于HashMap(initialCapacity, loadFactor)来说会直接根据capacity算出一个阈值,new一个构造对象的时候就会算出来
*对于HashMap()来说,是在第一次初始化算阈值,也就是在第一次put的时候计算阈值
*
*
*/
final Node<K,V>[] resize() {
//oldTab:引用扩容前的哈希表
//这个表对于所有构造函数第一次put进来的时候来讲是null
Node<K,V>[] oldTab = table;
//为null长度自然就为0,否则返回之前表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//拿到当前对象的阈值
int oldThr = threshold;
//新长度,新的阈值
int newCap, newThr = 0;
//这一块计算新的长度与阈值,分为不同的块来看
//原来的表有长度,那么肯定不是new HashMap(),因为它初始化的时候,不可能有长度
//那么也不是new HashMap(cap,factor)这个,这个只会根据cap计算一个2次方的阈值,也没有长度
//考虑为第一次为初始化map的时候,new HashMap(map),这个也不会有长度,但是会根据map的大小
//计算出来一个阈值,这个阈值也是2的次方数目,然后就在添加数据进行表的初始化,也没有长度
if (oldCap > 0) {//第一把初始化时构造函数的时候,都不可能走进oldCap > 0 这条线
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//这里就是oldCap等于0的情况,但是它又是一个有阈值的情况
//什么情况在初始化的时候会有阈值呢?那么肯定就是new HashMap(cap,factor), new HashMap(map)都有
//那么他们就会走这初始化,把原来的阈值变为新的长度,但是newThr在这还没有初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//下面就是oldCap等于0,oldThr=0
//就是空参new HashMap(),初始化的时候无长度,无阈值
else { // zero initial threshold signifies using defaults
//问题:HashMap无参构造调用的时候,什么时候会初始化一张哈希表
//在第一次put的时候,会初始化一表,他会走oldCapacity=0与oldThreshold=0的情况
//把长度变为默认长度16,把阈值变为默认负载因子*默认容量=0.75*16=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的阈值为0,经过上面,只有new HashMap(cap,factor)与new HashMap(map)新的阈值为0
if (newThr == 0) {
//这里对于HashMap(map),它的负载因子也是0.75
float ft = (float)newCap * loadFactor;//算新的阈值 = 老的阈值(因为之前是老的阈值给到了新的长度)*他们传入的负载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//然后把当前对象的阈值赋值上去,什么new初始化都是在这里赋值新的阈值
threshold = newThr;
//上面做完了,该初始化的都初始化完了
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//建立一个新的哈希表,如果是初始化,就是第一次在这里建立一张表
table = newTab;//然后赋值给table;
//以上初始化到这一步就全部结束掉,因为老的表是没有数据的
//对所有的new HashMap(....)都是一样的结果,也就是在put里面第一次调用resize()走到这就结束了
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
下面是完整的puVal方法注释
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab:引用当前hashMap的散列表
//p: 当前散列表的元素
//n: 散列表数组的长度
//i: 根据上面的hash进一步计算出的数据存放在哈希表的位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
//不管用什么构造函数进行初始化一个HashMap
//他们都没有初始化一张hash表
//所以都会进入到这个里面,进行扩容,并且把长度赋值给n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//按照分析
//所有的HashMap构造函数都会在上面一步进行一个扩容
//与其说是扩容,不如说是初始化一张哈希表
//然后在进入下面的判断
//上面哈希表就已经被初始化出来了
//i = (n - 1) & hash 利用之前hash码算一个哈希位置出来
//并把当前元素赋值给p
//如果当前值为NULL
if ((p = tab[i = (n - 1) & hash]) == null)//这个里面的n在每一次扩容之前都不会变,换句话,如果每次的key一样,并且没有主动去修改此对象的hashCode,大多返回的i都一样
//直接把值放进去
tab[i] = newNode(hash, key, value, null);
//如果这个位置已经有数据了
//注意一个问题是位置有数据,并不代表key就相等
//这里很可能就是不同的key生成后几位相同hash码
//从而算出来的位置是一样的
//这个else大体的判定条件是!= null
else {
//e: 找到一个与插入key相等的结点,这里也就是key相等的结点
//k: 一个临时的k
Node<K,V> e; K k;
//上面if判断的时候就已经把当前元素赋值给了p
//当前元素hash码相等 && key相等
//其实这里完全可以理解为key相等,计算的hash码就是相等的,自然i索引位置相等
//这里也就是在第一个位置,也就是表的桶位,进行了判定
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//key相等的索引结点赋值给e,后面需要去把e的value替换掉
//上面没进去说明key不一样
//然后我们判断下面当前结点是不是一棵红黑树
//如果是红黑树,就按照红黑树的方式插入进去
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//上面没有相等的key替换,也没有被树化
//那就是链表处理
else {
//走一个循环插入
//jdk1.8是尾插,所以要走指针到链表的最后一个位置
for (int binCount = 0; ; ++binCount) {
//如果当前结点的.next等于null的时候,表明已经走到了链表的最后一个结点
if ((e = p.next) == null) {
//直接把这个结点连接到当前结点的next后面
p.next = newNode(hash, key, value, null);
//binCount记录了这个链表走了多少次
//TREEIFY_THRESHOLD=8这是树化的条件
//当binCount = 7的时候,也就是达到了树化的条件,循环了八次
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在循环的过程中找到key相等的位置,就需要替换值
//因为链表里面可能也存在相等的key啊
//直接跳出这个循环就行
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//这就是让p与e交替往下面轮替
}
}
//替换e结点
if (e != null) { // existing mapping for key
V oldValue = e.value;//保留老值,等会返回
//onlyIfAbsent表示存在某个key就不插入这里默认是false
//不为false,进入,表示已经存在某个key值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//表的改变次数,包含了添加操作,删除操作等改变
//哈希表的长度大于了阈值,就要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
实际的扩容分析
扩容会走下面这条路
如果表里面的数量大于了阈值,就开始扩容,下面是resize扩容部分的代码分析
final Node<K,V>[] resize() {
//拿到老表
Node<K,V>[] oldTab = table;
//老表这里,肯定会拿到长度
//这里说一下如果是空参构造,在上面初始化put之后长度就是16(值默认)
//如果是带容量的构造函数,在上面初始化后容量是2次方的值,至于具体是多少
//这个要根据你传入的initialCapacity决定,因为他会先算一个阈值,然后在下次
//初始化表的时候,把这个阈值当做容量传递过去,然后在根据你的容量和传入的负载因子
//算出一个新的阈值
//而对于利用map集合进行初始化一个HashMap的时候,它的长度取决于map集合
//的长度,因为他是利用map集合的长度来算出了一个2次方的值作为阈值,下次初始化
//一张表的时候,在把这个阈值拿进来作为表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//拿到当前对象的阈值,也是有的
int oldThr = threshold;
//新长度,新的阈值初始化为0
int newCap, newThr = 0;
//这里就不是初始化动作了
//这里全部都是扩容,所以都走这条if路线,容量全部都是大于0的
if (oldCap > 0) {
//直接封顶了,也就是扩容不了,直接返回老表
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//重点在这个位置
//新的表的长度一定会扩展为原来表长度的2倍
//新的阈值这里分了一个情况:只有老表的长度大于了默认的容量
//才会把新的阈值扩展为原来的2倍
//那么对于空参构造来说,这里肯定就是新的长度与阈值全都变为原来的2倍
//因为它的初始化长度就是16默认值
//那么什么情况下会出现oldCap小于DEFAULT_INITIAL_CAPACITY这个容量呢
//那么就是当用另外两个构造函数初始化一个哈希表的时候
//你传入的capacity与map集合的大小非常小的时候
//这里拿capacity来说,当它的值是8以下的时候
//他会先算出来一个初始阈值,这个初始阈值在第一次初始化哈希表的时候
//就会给这张表作为初始容量,假设是8以下,比如4,那么初始容量算出来就是8
//阈值就是,假设负载因子传入的是0.5,阈值=8*0.5=4也就是olcCap=8,oldThr=4
//下一次进来扩容长度变为16,因为老的长度是8<16,所以它的阈值扩容不会直接走
//什么下面的两倍,而是跳到下面我标注(1)的位置
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//(1)
//如果到这个位置
//那么肯定是oldCapacity长度<16
if (newThr == 0) {
//它按照新长度和它自己的负载因子
//上面我们假设了负载因子是0.5,新长度是16
//那么乘以新的负载因子就是16*0.5= 8;
//我们会注意到阈值也还是变为了原来的2倍
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一张新表
table = newTab;//然后赋值给table;
//扩容老表肯定不等于null的,所以必走下面这条路
if (oldTab != null) {
//遍历整个表,我们重点在于对原来表的数据的迁移
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;//中间的移动变量
//当这个位置有值了,让e结点去指向这个位置
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//把当前位置的数据变为null
//如果当前位置下面没有结点
//说明了一个问题就是当前结点是一个唯一的key
//也就是说按照之前的hash算出来的位置没有重复值
//那么直接算出一个位置,放到新表里面
//这个位置不可能重复
if (e.next == null)
//算出新的位置然后放值
newTab[e.hash & (newCap - 1)] = e;
//如果有结点,然后就判断有没有被树化
else if (e instanceof TreeNode)
//如果是一棵红黑树,那么就按照红黑树处理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//也不是红黑树,那么就按照链表处理
//需要将这个桶的链表进行迁移
else {
//这里分为迁移到低位链表还是高位链表
//低位链表原来的链表长度之内
//高位链表扩展出来的链表长度之内
Node<K,V> loHead = null, loTail = null;//低位链表
Node<K,V> hiHead = null, hiTail = null;//高位链表
Node<K,V> next;//中间的移动指针
do {
next = e.next;
//如果最高位为0的数据,依旧放在原来的表的位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//这个只会进来一次
loHead = e;//当前loHead是表头,到时候挂到桶位置上的表头
else
loTail.next = e;
loTail = e;//loTail会指向e的上一个结点
}
else {
if (hiTail == null)
hiHead = e;//同理
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//这里刚好是这个链表的最后一个数据
//它的next直接就是null
//假如这里不是当前链表的最后一个数据,他可能下面还有数据
//因为他已经要搬到新位置上去
//所以直接把这里的next直接挂为null
loTail.next = null;//低位表最后一个位置next挂为null
newTab[j] = loHead;
}
if (hiTail != null) {
//同理
hiTail.next = null;
//扩展到了新的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
简单说一下如果表的长度达到了最大值处理方式
这里还得重点来说一下关于链表位置的迁移计算
那么上面就知道了e.hash & oldCap结果如果为0的情况,那就是最高位一定为0的情况,最高位如果为1,它永远不会出现为0的情况,那么继续往下面看
下面我们就可以拿到当前的结点的hash值我们去计算一下位置
很明显,高位为0的结点的hash值,它的位置只与后面四位有关,换句话说说,上面的hash值不管是与老的长度位比较还是新的长度位比较,结果都一样,所以他们全都放在低位链表的位置,换句话,这几个结点的位置全都不动
那么我们现在去看一下扩展到高位的链表
这里也就说明了,为什么高位链表要扩展到这个位置的原因
几个常见问题
手写一个简单的哈希表
package com.pxx.test.hashmap.myhashmap1;
public class MyHashMap<K, V> {
//默认容量
private static final int DEFAULT_CAPACITY = 16;
//容量的最大值
private static int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//哈希表的容量
private int capacity;
//负载因子
private float loadFactor;
//阈值
private int threshold;
//当前哈希映射中键值对的数量
private int size;
//存储键值对的数组,每一个元素都是一个链表头结点
private Node<K, V>[] table;
public MyHashMap() {
//使用默认的容量和负载因子创建哈希映射实例
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* //带参,使用指定容量和负载因子创建哈希映射实例
* @param capacity
* @param loadFactor
*/
public MyHashMap(int capacity, float loadFactor) {
this.capacity = capacity;
this.loadFactor = loadFactor;
this.table = new Node[capacity];//这里已经把数据表给初始化了
//把阈值在初始化的时候也给上
this.threshold = (int) (capacity * loadFactor);
}
/**
* 内部静态节点类,表示哈希映射中的结点
* 这个结点的设计除了键值以外
* 应该还包含一个指向下一个node的指针,
* 因为当某个索引算出来
* 但是又被占用了之后,后面就要接链表了,不停的往下指向
*/
private static class Node<K, V> {
K key;
V value;
Node<K, V> next;//每个结点后面可能变成一个链表,所以必须有一个next指针
int hash;//给一个hash值用于后面进行扩容操作
public Node(int hash , K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
//这里提供一个哈希函数,计算哈希索引的值
private int hash(K key) {
return key.hashCode();
}
//完善添加方法
//这里面采用了链接的方法解决了冲突
//如果有相同的键进来,就要被后面的值进行更新
public V put(K key, V value) {
int index = hash(key);//拿到哈希值
//拿到表的长度
int n = table.length;
//当前这个结点就是数组的桶,也就是链表的头结点
Node<K, V> node = table[index & (n - 1)];//从这个当前哈希索引里面拿一个节点看看
//如果不等于null
//表明这个位置有值,我们就要插入到链表里面去
//那么是头插还是尾插呢
//在JDK源码里面1.8是尾插 1.8之前是头插
//那我们这里实现尾插
if (node != null) {
while (node.next != null) {
//还必须知道如果存在相同的键,那么就需要把值进行更新
//但是如果更新了我们需要返回老值
if (node.key.equals(key)) {
V oldVal = node.value;
//然后替换值
node.value = value;
return oldVal;
}
node = node.next;//最后跳出循环会移动到最后一个指针的位置
}
Node<K,V> newNode = new Node<>(index,key, value, null);
node.next = newNode;
} else {
//直接放到这个位置就行了
table[index] = new Node<>(index, key, value, null);
}
//除了替换老值,不进入size的计算之内
//其他的都要进入到size的计算之内
size++;
//看看是否达到了负载因子
//默认负载因子
//private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//这里的负载因子默认是75%,比如容量是16 * 0.75 = 12
//如果达到了12,这个哈希表的容量也就达到了一个阈值
//负载因子用于控制哈希表的填充程度
//当负载因子设置得较小时,哈希表会更早地进行扩容,以减少冲突和提高性能
if (size > threshold) {
resize();
}
return null;//没有新的值
}
//我限定的是只要进入这个方法就是扩容
//表的初始化动作没有在这里进行
private Node<K,V>[] resize() {
Node<K, V>[] oldTab = table;
//拿到老表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//拿到阈值
int oldThr = threshold;
//新长度,新阈值
int newCap = 0, newThr = 0;
if (oldCap > 0) {
//容量如果是最大值,封顶
//阈值给个最大值,返回老表
if (oldCap >= MAXIMUM_CAPACITY) {
//直接把阈值给到最大
threshold = Integer.MAX_VALUE;
return oldTab;
}
//这里我们直接扩展两倍
newCap = oldCap << 1;
newThr = oldThr << 1;
}
//新的阈值赋值
threshold = newThr;
//创建新表,注意强制类型转换,编译器无法强制类型转换成泛型数组
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
Node<K, V> e;//给一个中间结点,等会用来移动
//遍历老表
for (int j = 0; j < oldCap; ++j) {
e = oldTab[j];
//当前结点有值
if (e != null) {
//把老表的位置释放掉
oldTab[j] = null;
//下面去判断是直接放值,还是进行链化迁移
//没有一个重复的索引位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else {
//考虑为链表的迁移
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
//最高位如果为0,放原来的位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;//用loTail不断去拼接这个链表
loTail = e;//loTail不停往下面走,然后会卡在其中某一个位置
} else {
//最高位不等于0的时候,考虑为1的时候
//会迁移到原来的位置 + 最高位数
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//然后整体把e向下移动
e = next;
} while (e != null);//直到结束大家都链好了各自的数据
//上面循环完了之后,要把链表挂到相应的位置上去
//这里就是真正的迁移
if (loTail != null) {
//这里刚好是这个链表的最后一个数据
//它的next直接就是null
//假如这里不是当前链表的最后一个数据,他可能下面还有数据
//因为他已经要搬到新位置上去
//所以直接把这里的next直接挂为null
loTail.next = null;//低位表最后一个位置next挂为null
newTab[j] = loHead;
}
if (hiTail != null) {
//同理
hiTail.next = null;
//扩展到了新的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
}