1 HashMap简介

HashMap是实现map接口的一个重要实现类,在我们无论是日常还是面试,以及工作中都是一个经常用到角色。它的结构如下:

它的底层是用我们的哈希表和红黑树组成的。所以我们在学习HashMap底层原理的时候,需要有这两种数据结构的知识做铺垫,才能有更好的理解!

1.1 哈希表

散列表是由我们的数组和链表组成的,集成了两种数据结构的优点,我们先简单介绍一下这两种数据结构。

数组:数组存储区间是连续的,占用内存严重,故空间复杂度很大,但数组的二分查找时间复杂度很小,为 o(1),数组的特点:查找速度快、插入和删除效率低

链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,为 o(n),链表的特点:查找速度慢、插入和删除效率高

哈希表:哈希表为每个对象计算出一个整数,称为哈希码根据这些计算出来的整数(哈希码)保存在对应的位置上!如果遇到了哈希冲突,也就是同一个坑遇到了被占用的情况下,那么我们就会以链表的形式添加在后面。

1.2 红黑树

关于红黑树的知识点比较多,如果过多介绍红黑树的话,那么HashMap就不好介绍了。这里给上一个连接,一篇关于红黑树非常好的文章。点击这里

2 源码解析

好了,开始解析我们的源码,通过解析源码更好的了解HashMap后,对那么常见的面试题也可以更加的吃透!

2.1 基本属性

首先就是介绍我们的HashMap的基本属性,对基本属性介绍完之后,对后面方法里使用时才不会迷惑

1、我们的默认的初始化的hashmap的容量,如果没有指定的话,就是我们的默认,1<<4就是16。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

2、我们的hashmap最大容量,2的30次方。

    static final int MAXIMUM_CAPACITY = 1 << 30;

3、默认的装载因子,0.75。有什么用呢?比如我们的容量现在是16,16*0.75=12,也就是说,当我们的实际容量到了12的时候,那么就会触发扩容机制,进行扩容!

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

4、我们知道哈希表是由数组和链表组成的,每一个位置都可以说是一个哈希桶。我们的哈希桶默认是链表,但是在JDK1.8之后我们的哈希桶中当有TREEIFY_THRESHOLD个节点的时候,也就是下面默认的8,我们桶中的链表会被转换为红黑树的结构。

    static final int TREEIFY_THRESHOLD = 8;

5、与上面相同,不过不同的是,会将红黑树转换成链表。

    static final int UNTREEIFY_THRESHOLD = 6;

6、当哈希桶的结构转换成树之前,还会有一次判断,只有键值对大于64才会转换!也就是我们下面定义的最小容量,这是为了避免哈希表建立初期多个键值对恰巧都在一个哈希桶上面,而导致了没必要的转换。

    static final int MIN_TREEIFY_CAPACITY = 64;

7、内部结构静态内部类

8、其他成员变量

2.1.1 思考

这里同时引发了我们一些思考?为什么要将转换成树形结构的阈值设置为8呢?为什么不将转换成链表结构的阈值也设置为8呢?这里我们在最后面试题分析的时候统一进行回答!

2.2 构造方法

hashmap的构造方法有四个,不过我们重点介绍其中的一个,因为这一个理解了,其他的也不成问题。

//initialCapacity:初始大小
//loadFactor:装载因子
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);
}

总结了构造方法进行的操作:

  1. 首先是边界处理,如果初始大小小于0,抛异常。如果大于最大,那也只能赋予我们默认的最大值!如果装载因子小于0或者不是数字的话,抛异常!
  2. 然后就是进行我们的赋值,装载因子赋值,还有就是调用我们的tableSizeFor来返回一个大于等于initialCapacity的2次幂。
    static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

关于为什么做了位运算后可以返回大于等于它的二次幂,可以看一下这篇博文!点击跳转

这里的threshold也就是我们的阈值,当达到了这个阈值的时候我们会进行扩容!但是这里可能也会觉得疑惑,阈值不是容量*装载因子吗?不应该写成下面这样子吗?

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;

注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会用到resize()方法,然后对threshold重新计算。后面我们对方法分析时会谈到。

2.3 核心方法

关于hashmap和核心方法和考点,其实都集中在put方法和resize()方法,这也会是我们下面重点要介绍到的。

2.3.1 put方法

我们首先来看put方法

    public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

调用了我们的putval方法,参入了一个以key计算的哈希值,key,value,还有两个其他参数。在看putVal方法之前先来看一下hash方法,看看它是如何计算哈希值。

    static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这是一个三目运算符,如果key不为null的话,返回我们的key的哈希值(低十六位)同时与高16位的异或运算。这一步的操作意义何为呢?我们先临时跳到putVal方法里面可以看到有这么一步操作

它将我们计算出来的哈希值,与我们的哈希表长度-1(为了获得)进行&运算,这是为了获取我的table下标。至于为什么-1呢?因为我们的长度都是2的整数次幂,转换成2进制也就是1000000....这种的形式,为了更好的随机,所有我们进行了-1操作,也就是变成11111111这种。因为&操作是都为1的时候才会为1,所以我的的1多的时候随机性才会更大,毕竟一个1能干过那么多的1吗?这是减少哈希冲突的第一步操作。举个例子说明一下:

比如我们的长度转换为2进制为 1000 0000 ,进行-1操作后就是 0111 1111
而这个时候我们原来的二进制数
1000 0000
&
0101 1011 = 0000 0000
与任何最高位不为1的数进行&运算,都会变成0,也就让我们的哈希冲突变大了!
而我们-1操作后
0111 1111
&
0101 1011 = 0101 1011
可以看出来,这样比原来的减少了很多的哈希冲突。
同时这也是为什么我们要让哈希的容量大小一定要为2的整数次幂

好了,我们要回答一下再上面那个问题了,为什么要返回低16位与高16位的异或作为key的最终hash值呢?同样举个例子演示一下这个流程:

假设length为8,HashMap的默认初始容量为16;

length = 8 ,(length-1) = 7 , 转换二进制为111;

假设一个key的 hashcode = 78897121 ,转换二进制:100101100111101111111100001,与(length-1)& 运算如下

    0000 0100 1011 0011 1101 1111 1110 0001

&运算

    0000 0000 0000 0000 0000 0000 0000 0111

=   0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,所以下标为1)

上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。如果让哈希值的低三位更加随机,那么&结果就更加随机,就更能减少我们的哈希冲突了。如何让哈希值的低三位更加随机,那么就是让其与高位异或,所以我们才在返回的时候与高位异或了再返回。低位与高位异或的过程举个例子如下:

然后总结一下在与我们与哈希值进行运算的时候有这么一个规律:

  • 当length=8时 下标运算结果取决于哈希值的低三位

  • 当length=16时 下标运算结果取决于哈希值的低四位

  • 当length=32时 下标运算结果取决于哈希值的低五位

  • 当length=2的N次方, 下标运算结果取决于哈希值的低N位。

好了,我们继续回到我们的putVal方法。下面我直接在注释里面进行分析

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//当我们的table为空的时候调用resize()进行扩容初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 没有发生碰撞,初始化我们的第一个节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//发生碰撞
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//hashcode和key相等,记录下原先的值
e = p;
//如果这个时候我们的哈希桶已经是红黑树结构,那么调用树的插入函数
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//链表结构,同时我们的hashcode不相等
//找到与key相等的节点,更新value,退出循环
//如果没有找到与key相等的节点,在链表尾部插入,如果插入后节点数量大于
//我们变成红黑树的阈值,那么进行转换成红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//达到临界值转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//新值覆盖旧值,返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空实现,为LinkedHashMap预留
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//键值对达到阈值,进行扩容
if (++size > threshold)
resize();
//空实现,为LinkedHashMap预留
afterNodeInsertion(evict);
return null;
}

2.3.2 resize()方法

我们在上面不管是源码分析还是在哪分析,都说到了我们的resize()方法,下面我们将正式开始讲到

   final Node<K,V>[] resize() {
//原table数组赋值
Node<K,V>[] oldTab = table;
//如果原数组为null,那么原数组长度为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//赋值阈值
int oldThr = threshold;
//newCap 新数组长度
//newThr 下次扩容的阈值
int newCap, newThr = 0;
// 1. 如果原数组长度大于0
if (oldCap > 0) {
//如果大于最大长度1 << 30 = 1073741824,那么阈值赋值为Integer.MAX_VALUE后直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 2. 如果原数组长度的2倍小于最大长度,并且原数组长度大于默认长度16,那么新阈值为原阈值的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 3. 如果原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 4. 如果原数组长度为0,阈值为0,那么新数组长度,新阈值都初始化为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 5.如果新的阈值等于0
if (newThr == 0) {
//计算临时阈值
float ft = (float)newCap * loadFactor;
//新数组长度小于最大长度,临时阈值也小于最大长度,新阈值为临时阈值,否则是Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//计算出来的新阈值赋值给对象的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//用新计算的数组长度新建一个Node数组,并赋值给对象的table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//后面是copy数组和链表数据逻辑
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;
}

这个时候我们以最初的三种构造方法来模拟一下流程。上面每一个扩容情况都标注了记号

//①
Map<String, String> map = new HashMap<>();
map.put("1", "1");
//②
Map<String, String> map1 = new HashMap<>(2);
map1.put("2", "2");
//③
Map<String, String> map2 = new HashMap<>(2, 0.5f);
map2.put("3", "3");
  • ① 没有设置initialCapacity,也没有设置负载因子,第一次put的时候会触发扩容。第一次的时候,数组长度为默认值16,阈值为160.75=12,走的代码4逻辑,等到数组长度超过阈值12后,触发第二次扩容,此时table数组,和threshold都不为0,即oldTab、oldCap、oldThr都不为0,先走代码1,如果oldCap长度的2倍没有超过最大容量,并且oldCap 长度大于等于 默认容量16,那么下次扩容的阈值 变为oldThr大小的两倍即 12 2 = 24,newThr = 24,newCap=32
  • ② 设置了initialCapacity,没有设置负载因子,此时hashMap使用默认负载因子0.75,本实例设置的初始容量为2,通过计算阈值为2,第一次put的时候由于还没初始化table数组,因此触发第一次扩容。此时oldCap为0,oldThr为2,走代码3,确定这次扩容的新数组大小为2,此时还没有确定newThr 下次扩容的大小,于是进入代码5 确定newThr为 2 0.75 = 1.5 取整 1 ,及下次扩容阈值为1。当数组已有元素大于阈值及1时,触发第二次扩容,此时oldCap为1,oldThr为1,走代码1newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,此时由于newThr等于0,进入代码5,确定newThr为 4 0.75 = 3,下次扩容阈值为3
  • ③ 设置了initialCapacity=2,负载因子为0.5,通过tableSizeFor计算阈值为2,第一次put的时候,进行扩容,此时oldCap为2,oldThr为2,进入代码1,同实例②,newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,进入代码5,确定newThr为 4 * 0.5 = 2,下次扩容阈值为2

2.3.3 get()方法

    public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

获取了我们的key的hashcode然后作为参数传入getNode方法中!

    final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果计算出来的哈希值是在哈希表上
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果刚好在哈希桶的第一个节点上,返回
if (first.hash == hash && // always check first node
((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);
//对我们的链表进行遍历查找元素
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果没有找到的话,返回null
return null;
}

2.3.4 remove方法

    public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

首先是计算出我们的hash,然后调用removeNode方法来移除

    final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//我们的哈希桶不为空,同时要映射的哈希值也在
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//刚好我们的哈希桶首位就是要删除的,记录下来
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果不是,进行遍历查找
else if ((e = p.next) != null) {
//如果是红黑树结构的话,调用树的查找方法
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//对链表进行查找key
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//找到了之后就去删除,分红黑树,桶的首位,链表中,
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//红黑树,调用树删除节点的方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//在桶首位,直接将下一个节点赋值给首位
else if (node == p)
tab[index] = node.next;
//在链表中,将下一个结点赋值给前一个节点的下一个节点,删除自我
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}

3 总结

  1. 扩容是一个特别耗性能的操作,所以当我们在使用 HashMap,正确估算 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。

  2. 负载因子 loadFactor 是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况特殊。

  3. HashMap 是非线程安全的,不要在并发的情况下使用 HashMap,建议使用 ConcurrentHashMap!

4 面试题分析

关于HashMap的源码就分析这些,因为这些足够我们去了解它的一些基本特性和常见面试足够用了。下面我收集了一些面试题和我们上面的留下的思考题进行分析!

1、为什么要将转换成树形结构的阈值设置为8呢?为什么不将转换成链表结构的阈值也设置为8呢?

  1. 当初始阈值为8时,链表的长度达到8的概率变的很小,如果再大概率减小的并不明显

  2. 树结构查找的时间复杂度是O(log(n)),而链表的时间复杂度是O(n),当阈值为8时,long8 = 3,相比链表更快,但树结构比链表占用的空间更多,所以这是一种时间和空间的平衡

至于为什么不将转换链表的阈值也设置为8,是因为如果两个值太接近的话,就会造成频繁的转换,导致我们的时间复杂度变高。而在6是经过计算后最合适的数值

2、HashMap 为什么不用平衡树,而用红黑树?

这一题应该归类与数据结构了,不过这里同样给出分析

  • 红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树可以是2倍

  • 红黑树在插入、删除的时候旋转的概率比平衡树低很多,效率比平衡树高

查找时间复杂度都维持在O(logN),具体的还望查看红黑树的特性,上面最开始也给了一篇关于红黑树的介绍。

3、HashMap在并发下会产生什么问题?有什么替代方案?

HashMap并发下产生问题:由于在发生hash冲突,插入链表的时候,多线程会造成环链,再get的时候变成死循环,Map.size()不准确,数据丢失。

关于为什么会造成环链的话,可以看这里!

替代方案:

  • HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态
  • ConcurrentHashMap:

    1.7 采用锁分段技术提高并发访问率

    1.8 数据依旧是分段存储,但锁采用了synchronized

4、HashMap中的key可以是任何对象或数据类型吗?

  • 可以是null,但不能是可变对象,如果是可变对象,对象中的属性改变,则对象的HashCode也相应改变,导致下次无法查找到已存在Map中的数据

  • 如果要可变对象当着键,必须保证其HashCode在成员属性改变的时候保持不变

5、为什么不直接将key作为哈希值而是与高16位做异或运算?

这个我们在上面说过了,还用图和样例解释,是为了更好的随机性,解决哈希碰撞。

6、关于更多的面试题

这里提供了一篇关于面试题挺多的博文,通过阅读源码,里面大部分的面试题都可以解答了!

点击这里看面试题

5 HashMap与HashTable有什么不同?

因为HashTable和HashMap很是类似,就跟我们的Vector与ArrayList的关系一样。提供了线程安全的解决方案,所有我们在这里通过区别,就相当与对HashTable进行了源码分析!

从存储结构和实现来讲基本上都是相同的。

它和HashMap的最大的不同是它是线程安全的,另外它不允许key和value为null。

Hashtable是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换或者Collections的synchronizedMap方法使HashMap具有线程安全的能力。

不同点 HashMap HashTable
数据结构 数组+链表+红黑树 数组+链表
继承的类不同 继承AbstractMap 继承Dictionary
是否线程安全
性能高低
默认初始化容量 16 11
扩容方式不同 原始容量*2 原始容量*2+1
底层数组的容量为2的整数次幂 要求为2的整数次幂 不要求
确认key在数组中的索引的方法不同 i = (n - 1) & hash; index = (hash & 0x7FFFFFFF) % tab.length;
遍历方式 Iterator(迭代器) Iterator(迭代器)和Enumeration(枚举器)
Iterator遍历数组顺序 索引从小到大 索引从大到小

6 参考资料

公众号《Java3y》文章

知乎专栏《Java那些事儿》

阿提说说

4.Java容器-HashMap详解

HashMap源码注解 之 静态工具方法hash()、tableSizeFor()(四)

HashMap中hash(Object key)原理,为什么(hashcode >>> 16)。

HashMap:从源码分析到面试题的更多相关文章

  1. HashMap的源码分析与实现 伸缩性角度看hashmap的不足

    本文介绍 1.hashmap的概念 2.hashmap的源码分析 3.hashmap的手写实现 4.伸缩性角度看hashmap的不足 一.HashMap的概念 HashMap可以将其拆分为Hash散列 ...

  2. HashMap的源码分析

    hashMap的底层实现是 数组+链表 的数据结构,数组是一个Entry<K,V>[] 的键值对对象数组,在数组的每个索引上存储的是包含Entry的节点对象,每个Entry对象是一个单链表 ...

  3. Java——HashMap底层源码分析

    1.简介 HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的. HashMap 最多只允许一条记录的key为 nu ...

  4. Java中HashMap的源码分析

    先来回顾一下Map类中常用实现类的区别: HashMap:底层实现是哈希表+链表,在JDK8中,当链表长度大于8时转换为红黑树,线程不安全,效率高,允许key或value为null HashTable ...

  5. HashMap方法源码分析

    本文将分析put(),resize(),get()和remove()方法的源码 putval()方法 大致步骤:计算key的hash值:根据hash值计算数组下标:判断下标处是否有节点,无节点则直接插 ...

  6. Java源码——HashMap的源码分析及原理学习记录

    学习HashMap时,需要带着这几个问题去,会有很大的收获: 一.什么是哈希表 二.HashMap实现原理 三.为何HashMap的数组长度一定是2的次幂? 四.重写equals方法需同时重写hash ...

  7. HashMap LinkedHashMap源码分析笔记

    MapClassDiagram

  8. Java HashMap实例源码分析

    引言 HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢? 一 Entry Entry是Map接口中的一个内部接口,它是实现键值对存储关键.在HashMap中,有Entry的实现 ...

  9. HashMap的源码分析(一)

    1.hashMap的关键值 DEFAULT_INITIAL_CAPACITY:默认初始容量16,∈(0,1<<30),实际大小为2的整数次幂: DEFAULT_LOAD_FACTOR:默认 ...

随机推荐

  1. 数据可视化之分析篇(七)Power BI数据分析应用:水平分析法

    https://zhuanlan.zhihu.com/p/103264851 首先,以财务报表分析为例,介绍通用的分析方法论,整体架构如下图所示: (点击查看大图) 接下来我会围绕这五种不同的方法论, ...

  2. 史上最全SpringBoot整合Mybatis案例

    摘要:如果小编说,SpringBoot是目前为止最好的框架,应该没有人会反驳吧?它的出现使得我们很容易就能搭建一个新应用.那么,SpringBoot与其他第三方框架的整合必定是我们需要关注的重点. 开 ...

  3. swagger -- 前后端分离的API接口

    文章目录 一.背景 二.swagger介绍 三.在maven+springboot项目中使用swagger 四.swagger在项目中的好处 五.美化界面 参考链接:5分钟学会swagger配置 参考 ...

  4. 面试题五十七:和为s的数字

    题目一:和为s的数字,在一个递增数组中寻找两个数字的和等于s 方法:双指针法,一个在头一个在尾:如果两个指针指向的和小于,那么be++:大于end--: 题目二:打印所有和为s的连续正数序列 方法:双 ...

  5. OceanBase安装和使用

    链接 https://mp.weixin.qq.com/s?spm=a2c6h.12873639.0.0.41f92c9bH5FL2Y&__biz=MzU3OTc2MDQxNg==&m ...

  6. windows异常-环境变量

    问题现象: 高级设置:windows 找不到文件 %windir%\systempropertiesadvanced.exe 请确定文件是否正确后,再试一次 基础信息: windows7 专业版 问题 ...

  7. Fortify Audit Workbench 笔记 Unreleased Resource: Database( 未释放资源:数据库)

    Unreleased Resource: Database 未释放资源:数据库 Abstract 程序可能无法成功释放某一项系统资源. Explanation 程序可能无法成功释放某一项系统资源. 资 ...

  8. Day04_乐优商城项目搭建

    学于黑马和传智播客联合做的教学项目 感谢 黑马官网 传智播客官网 微信搜索"艺术行者",关注并回复关键词"乐优商城"获取视频和教程资料! b站在线视频 0.学习 ...

  9. Python访问列表中的值

    Python访问列表中的值: 列表中可以包含所有数据类型: # 列表中可以存放 数字数据类型数据 # int 型数据 lst = [1,2,3] print(lst) # [1, 2, 3] # fl ...

  10. Seaborn实现单变量分析

    import numpy as np import pandas as pd from scipy import stats,integrate import matplotlib.pyplot as ...