一、HashMap(JDK1.8)

1、基本知识、数据结构

(1)时间复杂度:用来衡量算法的运行时间。
  参考:https://blog.csdn.net/qq_41523096/article/details/82142747

(2)数组:采用一段连续的存储空间来存储数据。查找方便,增删麻烦。

(3)链表:采用一段不连续的存储空间存储数据,每个数据中都存有指向下一条数据的指针。即 n 个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一个后续节点。增删方便,查找麻烦,

(4)红黑树:一种自平衡的二叉查找树,时间复杂度 O(log n)。

(5)散列表、哈希表:结合数组 与 链表的优点。通过 散列函数 计算 key,并将其映射到 散列表的 某个位置(连续的存储空间)。对于相同的 hash 值(产生 hash 冲突),通常采用 拉链法来解决。简单地讲,就是将 hash(key) 得到的结果 作为 数组的下标,若多个key 的 hash(key) 相同,那么在当前数组下标的位置建立一个链表来保存数据。

(6)HashMap:基于 哈希表的 Map 接口的非同步实现(即线程不安全),提供所有可选的映射操作。底层采用 数组 + 链表 + 红黑树的形式,允许 null 的 Key 以及 null 的 Value。不保证映射的顺序且不保证顺序恒久不变。

2、HashMap JDK1.8 底层数据结构

(1)采用 数组 + 链表的形式。
  HashMap 采用 Node 数组来存储 key-value 键值对,且数组中的每个 Node 实际上是一个单向的链表,内部存储下一个 Node 实体的指针。

transient Node<K,V>[] table;

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;
}
}

(2)当前数组长度大于某个阈值(默认为 64),且链表长度大于某个阈值(默认为 8)时,链表会转为 红黑树。

二、HashMap JDK1.8 源码分析

1、基本常量、成员变量

/**
* 初始数组容量,必须为 2 的整数次幂。默认为 2^4 = 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /**
* 最大数组容量, 默认为 2^30。
*/
static final int MAXIMUM_CAPACITY = 1 << 30; /**
* 负载因子,默认为 0.75。
* 用于计算 HashMap 容量。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; /**
* 树化的第一个条件:
* 链表转红黑树的阈值,默认为 8。
* 即链表长度大于等于 8 时,当前链表会转为红黑树进行存储。
*/
static final int TREEIFY_THRESHOLD = 8; /**
* 红黑树转链表的阈值,默认为 6。
* 即红黑树节点小于等于 6 时,当前红黑树会转为链表进行存储。
*/
static final int UNTREEIFY_THRESHOLD = 6; /**
* 树化的第二个条件:
* 树化最小容量,默认为 64。
* 当前数组长度大于等于 64 时,才可以进行 链表转红黑树。
*/
static final int MIN_TREEIFY_CAPACITY = 64 /**
* 数组,用于存储 Node<K, V> 链表
*/
transient Node<K,V>[] table; /**
* 用于存储 Node<K, V> 的总个数
*/
transient int size; /**
* 数组长度阈值,当超过该值后,会调整数组的长度。一般通过 capacity * load factor 计算
*/
int threshold; /**
* 负载因子,用于计算阈值,默认为 0.75
*/
final float loadFactor; /**
* 用于快速失败(fail-fast)机制,当对象结构被修改后会改变。
*/
transient int modCount;

2、核心构造方法

(1)源码:

/**
* 常用无参构造方法,以默认值构造 HashMap。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} /**
* HashMap 核心构造方法,根据 初始化容量 以及 负载因子创建 HashMap.
* @param initialCapacity 初始化容量
* @param loadFactor 负载因子
* @throws IllegalArgumentException 非法数据异常
*/
public HashMap(int initialCapacity, float loadFactor) {
// 如果初始化容量 小于 0 ,则会抛出 非法数据 异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 如果初始化容量 大于 最大容量值,则给其赋值为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY; // 若负载因子小于 0 或者 不合法, 抛出 非法数据异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 若上述条件均成立,则保存 负载因子的值
this.loadFactor = loadFactor; // 若上述条件均成立,则保存 数组长度的阈值(2的整数次幂)。
this.threshold = tableSizeFor(initialCapacity);
} 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;
}

(2)分析:
  上例的构造函数,根据 初始化容量以及 负载因子去创建 HashMap,没有去 实例化 Node 数组,数组的实例化 需要在 put 方法里实现。
  数组长度阈值 通过 tableSizeFor() 方法实现,能返回一个比给定容量大的 且 最小的 2 的次幂的数。比如 initialCapacity = 21, tableSizeFor() 返回的结果为 32。

3、hash(key)

  用于计算 key 的 hash 值。
(1)源码:

/**
* 计算 key 的 hash 值的方法
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} // Node 数组
transient Node<K,V>[] table; // 获取某个 key 所在位置时,通过 (table.length - 1) & hash(key) 去计算数组下标
table[(table.length - 1) & hash(key)]

(2)分析
  采用 高 16 位 与 低 16 位 异或,然后再进行移位运算。主要是为了减少冲突。

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} (length - 1) & hash(key) 【举例:】
假设某个值经过 hashCode 计算后为:
1111 0101 1010 0101 1101 1110 0000 0000
数组长度为 16,那么 length -1 = 15,如下:
0000 0000 0000 0000 0000 0000 0000 1111
此时进行 (length - 1) & hash(key) 操作后,
1111 0101 1010 0101 1101 1110 0000 0000
&
0000 0000 0000 0000 0000 0000 0000 1111
=
0000 0000 0000 0000 0000 0000 0000 0000
即只要 hashCode 计算出的值最后四位为0,得到的结果就一定为 0,此时冲突会大大提高。 采用 高16位 与 低16位 异或,计算为:
1111 0101 1010 0101 1101 1110 0000 0000
^
0000 0000 0000 0000 1111 0101 1010 0101
=
1111 0101 1010 0101 0010 1011 1010 0101
此时进行 (length - 1) & hash(key) 操作后,
1111 0101 1010 0101 0010 1011 1010 0101
&
0000 0000 0000 0000 0000 0000 0000 1111
=
0000 0000 0000 0000 0000 0000 0000 0101
此时计算出来的,是hashcode结果的后几位的值,这样就可以减少冲突的发生。

4、put、putVal

方法作用:
  Step1: 给 HashMap 的数组 初始化。
  Step2: 定义 链表 转为 红黑树的条件。
  Step3: 定义数据存储的动作(存储的方式:链表还是红黑树)。

(1)分析 put 过程
  Step1:put 内部调用 putVal() 方法。
  Step2:先判断 数组是否为 null 或者 长度为0,是的话,则调用 resize 方法给数组扩容。
  Step3:对 key 进行 hash 并执行位运算((length - 1) & hash(key)),得到数组下标。若不冲突,即当前数组位置不存在元素,直接在此处添加一个节点即可。
  Step4:若冲突,即当前数组位置存在元素,则根据节点的情况进行判断。
    如果 恰好是第一个 元素,则进行替换 value 的操作。
    如果不是第一个元素,则判断是否为 红黑树结构,是则添加一个树节点。
    如果不是红黑树结构(即链表),则采用尾插法给链表插入一个节点,链表长度大于等于 8 时,将链表转为红黑树结构。
  Step5:若 Node 长度大于阈值,还得重新 resize 扩容。

(2)源码:

// Node 数组
transient Node<K,V>[] table; // 插入数据的操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} /**
* 真正的插入数据的方法。
* @param key 的 hash 值
* @param key
* @param value
* @param onlyIfAbsent为 true,插入数据若存在值时,不会进行修改操作
* @param evict if false, the table is in creation mode.
* @return 上一个值,若不存在,则返回 null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果 Node 数组为 null 或者 长度为 0 时,即 Node 数组不存在,则调用 resize() 方法,重新获取一个调整大小后的 Node 数组。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; // 如果当前数组元素没有值,即不存在 哈希冲突的情况,直接添加一个 Node 进去(多线程时,此处可能导致线程不安全)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 存在哈希冲突的情况下,需要找到 插入或修改 的节点的位置,然后再操作(插入或修改)
Node<K,V> e; K k; // Step1:找到节点的位置1
// 判断第一个节点 是不是我们需要找的,判断条件: hash 是否相等、 key 是否相等。都相等则保存该节点,后续会修改。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 判断是否为 红黑树节点,是则添加一个树节点,返回节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 是链表节点,遍历查找节点
for (int binCount = 0; ; ++binCount) {
// 第一个条件判断得知 第一个节点不是我们要的,所以可以直接从第二个节点开始(p.next),然后遍历得第三、四个节点。
if ((e = p.next) == null) {
// 如果第二(三、四。。。)个节点没有值,直接添加一个 Node 即可,此时的 e 为 null。
p.next = newNode(hash, key, value, null); // 如果链表长度大于等于 8,则转为红黑树 ,并结束遍历操作
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;
}
} // 当 e 不为 null 时,对值进行修改,并将旧值返回
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 此处是 空实现,LinkedHashMap使用
afterNodeAccess(e);
return oldValue;
}
} // 添加节点的后续操作
// 修改次数加1
++modCount; // 当Node节点数 size 大于 阈值时,需要执行 resize 方法调整数组长度。
if (++size > threshold)
resize();
// 此处是 空实现,LinkedHashMap使用
afterNodeInsertion(evict); // 添加节点成功,返回 null
return null;
}

5、resize

  用于给数组扩容。
(1)resize 过程
  Step1:计算新数组的阈值、新数组的长度。
  Step2:给新数组复制。对于链表节点采用 e.hash & oldCap 去确定元素的位置,新位置只有两种可能(在原位置、或者在原位置的基础上增加 旧数组长度)

【举例:】
e.hash = 10 = 0000 1010, oldCap = 16 = 0001 0000
则 e.hash & oldCap = 0000 0000 = 0 e.hash = 18 = 0001 0010, oldCap = 16 = 0001 0000
则 e.hash & oldCap = 0001 0000 = 16 当 e.hash & oldCap == 0 时,新位置为 原数据所在的位置。即 table[j]
当 e.hash & oldCap != 0 时,新位置为 原数据所在的位置 + 原数组的长度。即 table[j + oldCap]

(2)源码:

/**
* 给数组扩容
*/
final Node<K,V>[] resize() {
// Step1:判断数组是否需要扩容,若需要则扩容
// 记录原数组
Node<K,V>[] oldTab = table; // 记录原数组长度,若为 null,则为 0, 否则为 数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 记录原数组的阈值
int oldThr = threshold; // 记录新数组的长度、阈值
int newCap, newThr = 0; // 如果原数组已被初始化
if (oldCap > 0) {
// 若数组长度超过最大的容量,则直接返回原数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 若数组长度2倍扩容仍小于最大容量,则阈值加倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 原数组为null,若旧阈值大于0, 则数组长度为 阈值大小
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 原数组为 null,旧阈值小于等于0, 则数组长度、阈值均为默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 若新阈值为 0,则根据新数组长度重新计算阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr; // Step2:将原数组的数据复制到新数组中(重新计算元素新的位置)
@SuppressWarnings({"rawtypes","unchecked"})
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;
// 如果原数组节点中只有一个值,那么直接复制到新数组
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;
// 判断节点是否需要移动,位运算 为 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);
// 将链表的尾节点置 null,并将头节点放到新位置
if (loTail != null) {
loTail.next = null;
// 新位置为 原始位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 新位置为 原始位置 + 原始数组长度
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

6、get、getNode

  用于获取节点的 value 值。
(1)分析 get 的过程
  Step1:先获取节点,内部调用 getNode() 方法。
  Step2:判断 数组是否为 null 或者 长度为0,是则直接返回 null。对 key 进行 hash 并执行位运算((length - 1) & hash(key)),得到数组下标,若当前数组下标位置数据为null,也返回 null。
  Step3:若当前数组下标位置有值。
    若 恰好是第一个元素,直接返回第一个节点即可。
    若不是第一个元素,则判断是否为 红黑树结构,是则返回树节点。
    若不是树结构,则遍历链表,返回相应的节点。

(2)源码:

/**
* 根据 key 获取 value
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} /**
* 真正获取 value 的操作
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 数组长度为 0 或者为 null,或者 节点不存在,直接返回一个 null
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);
}
}
return null;
}

三、常见面试题

1、为什么使用 数组 + 链表 + 红黑树的 数据结构?

  数组用于 确定 数据存储的位置,链表用来解决 哈希冲突(当冲突时,在当前数组对应的位置形成一个链表)。当链表的长度大于等于 8 时,需要将其转为 红黑树,查询效率比链表高。
  采用数组 + 链表的数据结构,可以结合 数组寻址的优势 以及 链表在增删上的高效。

2、HashMap 什么情况下会扩容?

  当数组长度超过阈值时(loadFactor * capacity),默认负载因子(loadFactor) 为 0.75,数组(capacity)长度 为 16。
  此时的阈值为 16 * 0.75 = 12,即只要数组长度大于 12 时,就会发生扩容(resize)。数组、阈值扩大到原来的 2 倍。即 当前数组长度为 16,扩容后变为 32,阈值为 24。

3、数组扩容为什么长度是 2 的次幂?

  为了实现高效、必须减少碰撞,即需要将数据尽量均匀分配,使每个链表长度大致相同。数据 key 的哈希值直接使用肯定是不行的,可以采用 取模运算 ,即 hash(key) % length,得到的余数作为数组的下标( table[hash(key) % length] )。
  但是取模运算的效率没有 移位运算高((length - 1) & hash(key))。length 指的是数组的长度。

// Node 数组
transient Node<K,V>[] table; JDK 1.8 源码给的实现是
(length - 1) & hash(key), // 计算数组下标值
table[(length - 1) & hash(key)] // 定位到数组元素的位置
也即
(length - 1) & hash(key) == hash(key) % length, 想要上面等式成立, length 必须满足 2 的次幂(效率最高), 即 length = 2^n。 为什么必须满足 2 的次幂?
因为只有 2 的次幂, length - 1 的二进制位全为1,使得 hash(key) 后几位都进行 &1 操作, 这样得到的结果等同于 hash(key) 后几位的值。
即 (length - 1) & hash(key) == hash(key) % length
如果 不为 2 的次幂,那么可能存在 某些值永远都不会出现的情况。 举个例子:
【hash(key) = 9, length = 16】
此时 hash(key) % length = 9 % 16 = 9
(length - 1) & hash(key) = 15 & 9 = 1111 & 1001 = 1001 = 9
hash(key) % length == (length - 1) & hash(key) 【hash(key) = 27, length = 16】
此时 hash(key) % length = 27 % 16 = 11
(length - 1) & hash(key) = 15 & 27 = 01111 & 11011 = 1011 = 11
hash(key) % length == (length - 1) & hash(key) 【hash(key) = 9, length = 15】
此时 hash(key) % length = 9 % 15 = 9
(length - 1) & hash(key) = 14 & 9 = 1110 & 1001 = 1000 = 8
hash(key) % length !== (length - 1) & hash(key)
数组长度为 15 时,length -1 = 1110,此时不管如何,最后一位均不可能为 1,也即 1001、1101等这些值永远都获取不到。

4、String 中的 hashCode 方法

  参考:https://segmentfault.com/a/1190000010799123。
  以 31 为权,对每一个字符的 ASCII 码进行运算。
  选用 31 的原因,31 * i = 32 * i - i = (i << 5) - i,31 可以被虚拟机优化成 位运算,效率更高。

public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

5、HashMap 线程不安全?举个例子?

  HashMap 采用尾插法将数据插入链表的尾部,但其 putVal 方法是线程不安全的。putVal 方法中有段代码如下:

if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);

  当线程 A 与线程 B 同时进行 put 操作时,且两个值的 key 经过 hash() 是一致的,即占用同一个数组元素。若此时数组元素为 null,线程 A 执行到这段代码的时候,发现该位置数据为 null,则触发一次 newNode 操作,这时线程 B 恰好也执行到这,同样触发一次 newNode 操作,这时不管是线程 A 还是线程 B成功,都会覆盖当前元素,即线程不安全。
JDK 7 用的头插法,会造成死循环(没有过多研究,有时间再补充)。

6、HashMap、HashTable、ConcurrentHashMap的区别

(1)HashMap 是线程非安全的,允许存在 null 的 key 以及 null 的 value。且只有一个为 null 的key,可以存在多个为 null 的 value。HashMap 的效率比 HashTable 高
(2)HashTable 是线程安全的,不允许存在 null 值。
(3)ConcurrentHashMap 是线程安全的 HashMap,并发能力比 HashTable 强。

HashMap (JDK1.8) 分析的更多相关文章

  1. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  2. HashMap 源码分析 基于jdk1.8分析

    HashMap 源码分析  基于jdk1.8分析 1:数据结构: transient Node<K,V>[] table;  //这里维护了一个 Node的数组结构: 下面看看Node的数 ...

  3. 源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  4. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  5. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  6. 集合框架源码学习之HashMap(JDK1.8)

    目录: 0-1. 简介 0-2. 内部结构分析 0-2-1. JDK18之前 0-2-2. JDK18之后 0-3. LinkedList源码分析 0-3-1. 构造方法 0-3-2. put方法 0 ...

  7. Java源码解析——集合框架(五)——HashMap源码分析

    HashMap源码分析 HashMap的底层实现是面试中问到最多的,其原理也更加复杂,涉及的知识也越多,在项目中的使用也最多.因此清晰分析出其底层源码对于深刻理解其实现有重要的意义,jdk1.8之后其 ...

  8. HashMap源码分析(一)

    基于JDK1.7 HashMap源码分析 概述 HashMap是存放键值对的集合,数据结构如下: table被称为桶,大小(capacity)始终为2的幂,当发生扩容时,map容量扩大为两倍 Hash ...

  9. Java中HashMap源码分析

    一.HashMap概述 HashMap基于哈希表的Map接口的实现.此实现提供所有可选的映射操作,并允许使用null值和null键.(除了不同步和允许使用null之外,HashMap类与Hashtab ...

随机推荐

  1. 在4K屏下以超过VMWare默认的最高分辨率运行Linux系统

    前言 4K 屏,有其优点也有其弊端.优点就是分辨率高,字体和图标看起来如丝一般顺滑:缺点就是字体和图标小,费眼睛.解决这个缺点的方法也很简单粗暴,就是将系统的显示比例放大.在高分屏不很普及的时候,无论 ...

  2. C语言实现matlab的interp2()函数

    项目要用到matlab中的Vq = interp2(X,Y,V,Xq,Yq)函数,即把一个已知经纬度和对应值的矩阵,插值变换到一个给定经纬度网格中,也就是对给定网格填值,需要用到插值,这里使用双线性内 ...

  3. [Effective Java 读书笔记] 第三章类和接口 第二十-二十一条

    第二十条 用函数对象表示策略 函数指针(JAVA的函数指针,是指使用对象的引用来作为参数,传递给另一个对象的方法)主要用来实现策略模式,为了在JAVA中实现这种模式,要申明一个接口来表示该策略,并为每 ...

  4. 数据算法 --hadoop/spark数据处理技巧 --(17.小文件问题 18.MapReuce的大容量缓存)

    十七.小文件问题 十八.MR的大容量缓存 在MR中使用和读取大容量缓存,(也就是说,可能包括数十亿键值对,而无法放在一个商用服务器的内存中).本次提出的算法通用,可以在任何MR范式中使用.(eg:MR ...

  5. Linux bash管道符“|”使用介绍与例子

    https://blog.csdn.net/wangqianyilynn/article/details/75576815

  6. .NET Core之单元测试(三):Mock框架Moq的使用

    编写一个API 新增一个接口 public interface IFoo { bool Ping(string ip); } 接口实现 public class Foo : IFoo { public ...

  7. comm diff 文件对比

    comm: 利用comm命令进行处理的文件必须首先通过sort命令进行排序处理并且是unix格式而非dos格式的文本文件 功能说明:比较两个已排过序的文件.(使用sort排序)语 法:comm [-1 ...

  8. java工作流系统jflow表单引擎字段扩展组件介绍

    关键词:工作流快速开发平台  工作流流设计  业务流程管理   asp.net 开源工作流  bpm工作流系统  java工作流主流框架  自定义工作流引擎 表单设计器  流程设计器 装饰类图片 用于 ...

  9. iOS开发 - 在SwiftUI中显示模态视图

    在SwiftUI中显示模态视图 简介 这里教大家如何弹出一个简单的模态视图.分别有两个页面,ContentView和GCPresentedView,以下对应简称为A和B.我们要做的是在A视图中点击按钮 ...

  10. 【HDU - 1029】Ignatius and the Princess IV (水题)

    Ignatius and the Princess IV  先搬中文 Descriptions:   给你n个数字,你需要找出出现至少(n+1)/2次的数字 现在需要你找出这个数字是多少? Input ...