一、前言

写在前面:小编码字收集资料花了一天的时间整理出来,对你有帮助一键三连走一波哈,谢谢啦!!

HashMap在我们日常开发中可谓经常遇到,HashMap 源码和底层原理在现在面试中是必问的。所以我们要掌握一下,也是作为两年开发经验必备的知识点!HashMap基于Map接口实现,元素以<Key,Value>的方式存储,并且允许使用null 键和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap是无序的、线程不安全的。

HashMap继承类图快捷键Ctrl+alt+U

二、存储结构介绍

Jdk7.0之前 数组 + 链表

Jdk8.0开始 数组 + 链表 + 二叉树

链表内元素个数>8个 由链表转成二叉树

链表内元素个数<6个 由二叉树转成链表

红黑树,是一个自平衡的二叉搜索树,因此可以使查询的时间复杂度降为O(logn)

链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)

Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置。简称头插法(并发时可能出现死循环)

Jdk1.8中链表新元素添加到链表的尾结点。简称尾插法(解决了头插法死循环)

底层结构实例图

三、源码分析之常用变量解读

/**
* 默认初始容量 - 必须是 2 的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /**
* 最大容量,如果一个更高的值被任何一个带参数的构造函数隐式指定使用。必须是 2 <= 1<<30 的幂。
* 简单理解为:最大容量2的30次幂,如果带容量也会转化为2的次幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30; /**
* 构造函数中未指定时使用的负载因子。经过官方测试测出减少hash碰撞的最佳值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; /**
* 使用红黑树的计数阈值,超过8则转化为红黑树
*/
static final int TREEIFY_THRESHOLD = 8; /**
* 使用红黑树的计数阈值,低于6则转化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6; /**
* 链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会转化为红黑树。
* 这是为了避免,数组扩容和树化阈值之间的冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64; /**
* 在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。
* (我们还在某些操作中允许长度为零,以允许当前不需要的引导机制)
*/
transient Node<K,V>[] table; /**
* 保存缓存的 entrySet(),也就是存放的键值对
*/
transient Set<Map.Entry<K,V>> entrySet; /**
* 此映射中包含的键值映射的数量,就是数组的大小
*/
transient int size; /**
* 记录集合的结构变化和操作次数(例如remove一次进行modCount++)。
* 该字段用于使 HashMap 的 Collection-views 上的迭代器快速失败。如果失败也就是我们常见的CME
* (ConcurrentModificationException)异常
*/
transient int modCount; /**
* 数组扩容的大小
* 计算方法 capacity * load factor 容量 * 加载因子
* @serial
*/
int threshold; /**
* 哈希表的负载因子。
* @serial
*/
final float loadFactor;

四、源码分析之构造方法解读

/**
* 构造一个具有指定初始容量和负载因子的构造方法
* 不建议使用这个构造方法,加载因子经过官方测试是效率最好的,所以为了效率应该保持默认
*/
public HashMap(int initialCapacity, float loadFactor) {
// 参数为负数会抛出IllegalArgumentException异常
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;
// 初始化的参数默认如果不是2的次幂,直接给你转化为2的次幂
// 传的参数为11,会自动转化为比参数大的最近的2的次幂的值,也就是16。
// 我们后面会详细说这个方法怎么进行转化的
this.threshold = tableSizeFor(initialCapacity);
} /**
* 构造一个只有指定初始容量的方法
*/
public HashMap(int initialCapacity) {
// 我们会发现还是调用上面两个参数的构造方法,自动帮我们设置了加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* 构造一个具有默认初始容量(16) 和默认负载因子(0.75)的构造方法
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} /**
* 指定map集合,转化为HashMap的构造方法
*/
public HashMap(Map<? extends K, ? extends V> m) {
// 使用默认加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
//调用PutMapEntries()来完成HashMap的初始化赋值过程
putMapEntries(m, false);
}
/**
* 将传入的子Map中的全部元素逐个添加到HashMap中
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 获得参数Map的大小,并赋值给s
int s = m.size();
// 判断大小是否大于0
if (s > 0) {
// 证明有元素来插入HashMap
// 判断table是否已经初始化 如果table=null一般就是构造函数来调用的putMapEntries,或者构造后还没放过任何元素
if (table == null) { // pre-size
// 如果未初始化,则计算HashMap的最小需要的容量(即容量刚好不大于扩容阈值)。这里Map的大小s就被当作HashMap的扩容阈值,然后用传入Map的大小除以负载因子就能得到对应的HashMap的容量大小(当前m的大小 / 负载因子 = HashMap容量)
// ((float)s / loadFactor)但这样会算出小数来,但作为容量就必须向上取整,所以这里要加1。此时ft可以临时看作HashMap容量大小
float ft = ((float)s / loadFactor) + 1.0F;
// 判断ft是否超过最大容量大小,小于则用ft,大于则取最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 暂时存放到扩容阈值上,tableSizeFor会把t重新计算到2的次幂给扩容数组大小
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果当前Map已经初始化,且这个map中的元素个数大于扩容的阀值就得扩容
// 这种情况属于预先扩大容量,再put元素
else if (s > threshold)
// 后面展开说
resize();
// 遍历map,将map中的key和value都添加到HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 调用HashMap的put方法的具体实现方法putVal来对数据进行存放。该方法的具体细节在后面会进行讲解
// putVal可能也会触发resize
putVal(hash(key), key, value, false, evict);
}
}
}

五、源码分析之常用方法解读

1、tableSizeFor方法解读

/**
* 返回给定目标容量的 2 次方。
*/
static final int tableSizeFor(int cap) {
// cap = 10
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 小于0就是1,如果大于0在判断是不是超过最大容量就是n=15+1 = 16,超过就按最大容量
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

cap为10为例:(右移规则为:无符号位右移)

10的二进制为:0000 0000 0000 0000 0000 0000 0000 1010

执行int n = cap - 1;

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1001

执行n |= n >>> 1;(先进行右移,结果和原来的数进行或运算[==有1则1==])

0000 0000 0000 0000 0000 0000 0000 1001

0000 0000 0000 0000 0000 0000 0000 0100 n>>1结果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1101 n |= n >> 1 结果

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1101

执行n |= n >>> 2;

0000 0000 0000 0000 0000 0000 0000 1101

0000 0000 0000 0000 0000 0000 0000 0011 n>>2结果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 2 结果

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1111

执行n |= n >>> 4;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>4结果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 4 结果

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1111

执行n |= n >>> 8;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>8结果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 8 结果

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1111

执行n |= n >>> 16;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>16结果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 16 结果

二进制结果为:0000 0000 0000 0000 0000 0000 0000 1111

我们会发现,当数小的的时候进行到4时就不会变了我们得到的值为15,即输入10,经过此方法后得到15。遇到大的数才会明显,大家可以找个大的数字进行试试就是先右移在进行或运算。最后进行三门运算进行+1操作,最后结果为16=2^4

2、hash方法解读

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

先判断key是否为空,若为空则返回0。上面说了HashMap仅支持一个key为null的。若非空,则先计算key的hashCode值,赋值给h,然后把h右移16位,并与原来的h进行异或处理(相同为1,不同为0)。

注释进行谷歌翻译:

计算 key.hashCode() 并传播(XOR)更高位的哈希降低。 由于该表使用二次幂掩码,因此仅在当前掩码之上位变化的散列集将始终发生冲突。 (已知的例子是在小表中保存连续整数的 Float 键集。)因此,我们应用了一种变换,将高位的影响向下传播。 在位扩展的速度、实用性和质量之间存在折衷。 因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,我们只是以最简单的方式对一些移位的位进行异或,以减少系统损失, 以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。

总结:使用最简易的方式,及减少系统损失又减少了hash碰撞

3、put方法解读

/**
* 将指定的值与此映射中的指定键相关联。即当前key应该存放在数组的哪个下标位置
* 如果映射先前包含键的映射,则替换旧的值。
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 这才是真正的保存方法
* @param hash 经过hash运算后的key
* @param key 你要添加的key
* @param value 你要添加的value
* @param onlyIfAbsent 如果为真,则不要更改现有值,本次为FALSE,可以替换更改
* @param evict 如果为 false,则表处于创建模式。我们为true
*/
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是否为空
if ((tab = table) == null || (n = tab.length) == 0)
// 如果空的话,会先调用resize扩容,resize我们后面展开讲解
n = (tab = resize()).length;
// 把通过hash得到的值与数组大小-1进行与运算,这个运算就可以实现取模运算,而且位运算还有个好处,就是速度比较快。
// 得到key所对应的数组的节点,然后把该数组节点赋值给p,然后判断这个位置是不是有元素
if ((p = tab[i = (n - 1) & hash]) == null)
// key、value包装成newNode节点,直接添加到此位置。
tab[i] = newNode(hash, key, value, null);
// 如果当前数组下标位置已经有元素了,又分为三种情况。
else {
Node<K,V> e; K k;
// 当前位置元素的hash值等于传过来的hash,并且他们的key值也相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 则把p赋值给e,后续需要新值把旧值替换
e = p;
// 来到这里说明该节点的key与原来的key不同,则看该节点是红黑树,还是链表
else if (p instanceof TreeNode)
// 如果是红黑树,则通过红黑树的方式,把key-value存到红黑树中。后面再讲解这个方法
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 来到这里说明结构为链表,把key-value使用尾插法插到链表尾。
// jdk1.7 链表是头插入法,在并发扩容时会造成死循环
// jdk1.8 就把头插入法换成了尾插入法,虽然效率上有点稍微降低一些,但是不会出现死循环
for (int binCount = 0; ; ++binCount) {
// 遍历该链表,知道知道下一个节点为null。
if ((e = p.next) == null) {
// 说明到链表尾部,然后把尾部的next指向新生成的对象
p.next = newNode(hash, key, value, null);
// 如果链表的长度大于等于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 则链表转化成为红黑树 后面再补充
treeifyBin(tab, hash);
break;
}
// 如果在链表中找到了相同key的话,直接退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 说明发生了碰撞,e代表的是旧值,需要替换为新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判断是不是允许覆盖旧值,和旧值是否为空
if (!onlyIfAbsent || oldValue == null)
// 把旧值替换
e.value = value;
// hashmap没有任何实现,我们先不考虑
afterNodeAccess(e);
// 返回新值
return oldValue;
}
}
// fail-fast机制每次对结构改变进行+1
++modCount;
// 判断HashMap中的存的数据大小,如果大于数组长度*0.75,就要进行扩容
if (++size > threshold)
resize();
// 也是一个空的实现
afterNodeInsertion(evict);
return null;
}

4、resize方法解读

看不清查看原链接:高清图链接

/**
* 初始化或者是将table大小加倍,如果为空,则按threshold分配空间,
* 否则加倍后,每个容器中的元素在新table中要么呆在原索引处,要么有一个2的次幂的位移
*/
final Node<K,V>[] resize() {
// 原来的数据
Node<K,V>[] oldTab = table;
// 获取原来的数组大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
int oldThr = threshold;
// 初始化新数组的容量和阈值
int newCap, newThr = 0;
// 1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
if (oldCap > 0) {
// 容量达到最大
if (oldCap >= MAXIMUM_CAPACITY) {
// 扩容的大小设置为上限
threshold = Integer.MAX_VALUE;
// 直接返回默认原来的,无法在扩了
return oldTab;
}
// 如果旧容量是在16到上限之间
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新数组的容量和阈值都扩大原来的2倍
newThr = oldThr << 1; // double threshold
}
// 2. 到这里 oldCap <= 0, oldThr(threshold) > 0,这就是 map 初始化的时候,
// 第一次调用 resize的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 3. 到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;// 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 12
}
// 只有经过2.才会进入
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 把计算后的ft符合大小,则赋值newThr
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 最后得到要扩容的大小
threshold = newThr;
// 用于抑制编译器产生警告信息
@SuppressWarnings({"rawtypes","unchecked"})
// 在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。
// 这是为了延迟加载,提高效率。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 判断原来的数组有没有值,如果没有则把刚刚创建的数组进行返回
if (oldTab != null) {
// 便利旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 判断当前元素是否为空
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果第一个元素的下一个元素为null,说明链表只有一个
if (e.next == null)
// 则直接用它的hash值和新数组的容量取模就行(这样运算效率高),得到新的下标位置
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树
else if (e instanceof TreeNode)
// 则拆分红黑树,这个小编就不带着往下深究了,感兴趣可以自己点进去看看
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 说明是链表且长度大于1
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;
// 如果当前元素的hash值和oldCap做与运算为0,则原位置不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 不为0则把数据移动到新位置
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;
}

5、get和containsKey方法解读

/**
* 根据key获取对应的value
*/
public V get(Object key) {
Node<K,V> e;
// 调用后存在就获取元素的value返回
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 判断key是否在map中存在
*/
public boolean containsKey(Object key) {
// 调用方法存在及不为null
return getNode(hash(key), key) != null;
}
/**
* 我们发现底层都是getNode来进行干活的
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断数组不能为空,然后取到当前hash值计算出来的下标位置的第一个元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果hash值和key都相等并不为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)
// 则以红黑树的查找方式进行获取到我们想要的key对应的值
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);
}
}
// 找不到key对应的值,返回null
return null;
}

6、remove方法解读

/**
* 如果key存在,就把元素删除
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
} /**
* 删除方法的具体实现
* @param hash 经过hash运算后的key
* @param key 你要移除的key
* @param value 你要移除的value
* @param matchValue 如果为真,则仅在值相等时删除,我们为FALSE,key相同即删除
* @param movable 如果为假,则在删除时不要移动其他节点,我们为TRUE,删除移动其他节点
*/
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;
// 判断table不为空,链表不为空
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
node = p;
// 第一个不是,并且后面还有节点
else if ((e = p.next) != null) {
// 如果是红黑树
if (p instanceof TreeNode)
// 在红黑树中找到返回
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 遍历链表
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 找到要删除的node
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;// p的下一个节点指向到node的下一个节点即可把node从链表中删除了
// 操作+1
++modCount;
// map大小-1
--size;
// 空的实现
afterNodeRemoval(node);
// 返回删除的节点
return node;
}
}
return null;
}

六、总结

Has和Map得底层还是很多的,里面用的一些逻辑和算法,都是很牛皮的、耐人琢磨的。oracle里的人才还是厉害啊,你看都看不明白,人家就能设计出来,实现出来。真的是你我皆为蝼蚁,只不过是一个个搬砖工具人呀。哈哈,对自己的一个自嘲哈,共勉!!看到这里了,点个赞吧,小编码字实属不易呀!!谢谢了!!


环境大家关注小编的微信公众号!谢谢大家!

推广自己网站时间到了!!!

点击访问!欢迎访问,里面也是有很多好的文章哦!

HashMap底层原理及jdk1.8源码解读的更多相关文章

  1. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  2. Java集合:ArrayList (JDK1.8 源码解读)

    ArrayList ArrayList几乎是每个java开发者最常用也是最熟悉的集合,看到ArrayList这个名字就知道,它必然是以数组方式实现的集合 关注点 说一下ArrayList的几个特点,也 ...

  3. JDK1.8HashMap源码解读

    package java.util; import sun.misc.SharedSecrets; import java.io.IOException; import java.io.Invalid ...

  4. Java集合:LinkedList (JDK1.8 源码解读)

    LinkedList介绍 还是和ArrayList同样的套路,顾名思义,linked,那必然是基于链表实现的,链表是一种线性的储存结构,将储存的数据存放在一个存储单元里面,并且这个存储单元里面还维护了 ...

  5. 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  6. Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...

  7. Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  8. HashMap底层实现原理(JDK1.8)源码分析

    ref:https://blog.csdn.net/tuke_tuke/article/details/51588156 http://www.cnblogs.com/xiaolovewei/p/79 ...

  9. jdk1.8.0_45源码解读——HashMap的实现

    jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对 ...

随机推荐

  1. 2022 07 13 第一小组 田龙跃 Java再学习笔记

    1.类名命名规则: 只能由数字字母,下划线,美元符号组成(不能以数字开头,尽量不要用下划线开头) 2.注释(养成多写注释的好习惯) 单行注释 // ctrl+/ 多行注释 // ctrl+shirt+ ...

  2. Note -「序列元素在线段树上的深度」 感悟

    0x01 前言 想法源于一道你谷的毒瘤题目. 这个方面的知识点好像挺新颖的. 于是和 JC 一起想出了该命题的 \(O(n)\) 解法. 0x02 算法本身 总所周知,线段树上的节点都对应表示的原序列 ...

  3. Ask.com用过什么名字?

    搜索引擎 Ask.com 曾是美国第三,世界第六大公网搜索引擎,仅次于 Google 搜索.Bing 和百度.NAVER.Yandex. Ask.com 曾经用过什么名字? Ask Jetson As ...

  4. eplan中数据库运行提速

    access,sql,是指部件库的存储方式,eplan支持两种方式即Microsoft Office access,Microsoft SQL Server,可以通过这两种方式打开部件库,如果要打开数 ...

  5. 从零开始Blazor Server(11)--编辑用户

    用户编辑和角色编辑几乎一模一样,这里先直接贴代码. @page "/user" @using BlazorLearn.Entity @using Furion.DataEncryp ...

  6. openstack 创建虚拟机失败

    虚拟机创建失败    用户创建一台虚拟机,虚拟机使用4个网络平面,所以虚拟机选择了4个不同平面的网络,创建虚拟机一直在孵化的过程中,最后创建虚拟机失败. 失败后返回的报错日志 Build of ins ...

  7. 如何定义 Java 的回调函数,与 JavaScript 回调函数的区别

    JavaScript 中的回调函数 在 JavaScript 中经常使用回调函数,比如:get 请求.post 请求等异步任务.在我们请求之前以及请求之后,都需要完成一些固定的操作,比如:请求之前先从 ...

  8. [HNOI2016]最小公倍数 (可回退并查集,回滚莫队)

    题面 题目链接 题目描述 给定一张 N N N 个顶点 M M M 条边的无向图(顶点编号为 1 , 2 , - , n 1,2,\ldots,n 1,2,-,n),每条边上带有权值.所有权值都可以分 ...

  9. C++中的cout.setf(ios::fixed)是什么意思?

    问题描述:在阅读一段代码时,发现代码的最后一部分出现 ... cout.setf(ios::fixed); cout.setf(ios::showpoint); ... 解决: cout.setf() ...

  10. 【JDBC】学习路径8-连接池

    为什么是连接池? 第一.受我们硬件资源的限制,我们的一些资源使用时有限制的比如我们的数据库 连接数和线程数.为了摆脱这些限制,我们就使用了池化技术来将这些资源限制在一定范围内. 第二.我们创建和销毁这 ...