HashMap的结构图示

​ jdk1.7的HashMap采用数组+单链表实现,尽管定义了hash函数来避免冲突,但因为数组长度有限,还是会出现两个不同的Key经过计算后在数组中的位置一样,1.7版本中采用了链表来解决。

​ 从上面的简易示图中也能发现,如果位于链表中的结点过多,那么很显然通过key值依次查找效率太低,所以在1.8中对其进行了改良,采用数组+链表+红黑树来实现,当链表长度超过阈值8时,将链表转换为红黑树.具体细节参考我上一篇总结的 深入理解jdk8中的HashMap

​ 从上面图中也知道实际上每个元素都是Entry类型,所以下面再来看看Entry中有哪些属性(在1.8中Entry改名为Node,同样实现了Map.Entry)。

//hash标中的结点Node,实现了Map.Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
//Entry构造器,需要key的hash,key,value和next指向的结点
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
} public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//equals方法
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//重写Object的hashCode
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
} public final String toString() {
return getKey() + "=" + getValue();
} //调用put(k,v)方法时候,如果key相同即Entry数组中的值会被覆盖,就会调用此方法。
void recordAccess(HashMap<K,V> m) {
} //只要从表中删除entry,就会调用此方法
void recordRemoval(HashMap<K,V> m) {
}
}

HashMap中的成员变量以及含义

//默认初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30; //默认加载因子.一般HashMap的扩容的临界点是当前HashMap的大小 > DEFAULT_LOAD_FACTOR *
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认是空的table数组
static final Entry<?,?>[] EMPTY_TABLE = {}; //table[]默认也是上面给的EMPTY_TABLE空数组,所以在使用put的时候必须resize长度为2的幂次方值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //map中的实际元素个数 != table.length
transient int size; //扩容阈值,当size大于等于其值,会执行resize操作
//一般情况下threshold=capacity*loadFactor
int threshold; //hashTable的加载因子
final float loadFactor; /**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount; //hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算
//hashSeed是一个与实例相关的随机值,用于解决hash冲突
//如果为0则禁用备用哈希算法
transient int hashSeed = 0;

HashMap的构造方法

​ 我们看看HashMap源码中为我们提供的四个构造方法。

//(1)无参构造器:
//构造一个空的table,其中初始化容量为DEFAULT_INITIAL_CAPACITY=16。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//(2)指定初始化容量的构造器
//构造一个空的table,其中初始化容量为传入的参数initialCapacity。加载因子为DEFAULT_LOAD_FACTOR=0.75F
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//(3)指定初始化容量和加载因子的构造器
//构造一个空的table,初始化容量为传入参数initialCapacity,加载因子为loadFactor
public HashMap(int initialCapacity, float loadFactor) {
//对传入初始化参数进行合法性检验,<0就抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果initialCapacity大于最大容量,那么容量=MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//对传入加载因子参数进行合法检验,
if (loadFactor <= 0 || Float.isNaN(loadFactor))
//<0或者不是Float类型的数值,抛出异常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//两个参数检验完了,就给本map实例的属性赋值
this.loadFactor = loadFactor;
threshold = initialCapacity;
//init是一个空的方法,模板方法,如果有子类需要扩展可以自行实现
init();
}

​ 从上面的这3个构造方法中我们可以发现虽然指定了初始化容量大小,但此时的table还是空,是一个空数组,且扩容阈值threshold为给定的容量或者默认容量(前两个构造方法实际上都是通过调用第三个来完成的)。在其put操作前,会创建数组(跟jdk8中使用无参构造时候类似).

//(4)参数为一个map映射集合
//构造一个新的map映射,使用默认加载因子,容量为参数map大小除以默认负载因子+1与默认容量的最大值
public HashMap(Map<? extends K, ? extends V> m) {
//容量:map.size()/0.75+1 和 16两者中更大的一个
this(Math.max(
(int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
//把传入的map里的所有元素放入当前已构造的HashMap中
putAllForCreate(m);
}

​ 这个构造方法便是在put操作前调用inflateTable方法,这个方法具体的作用就是创建一个新的table用以后面使用putAllForCreate装入传入的map中的元素,这个方法我们来看下,注意刚也提到了此时的threshold扩容阈值是初始容量。下面对其中的一些方法进行说明

(1)inflateTable方法说明

​ 这个方法比较重要,在第四种构造器中调用了这个方法。而如果创建集合对象的时候使用的是前三种构造器的话会在调用put方法的时候调用该方法对table进行初始化

private void inflateTable(int toSize) {
//返回不小于number的最小的2的幂数,最大为MAXIMUM_CAPACITY,类比jdk8的实现中的tabSizeFor的作用
int capacity = roundUpToPowerOf2(toSize);
//扩容阈值为:(容量*加载因子)和(最大容量+1)中较小的一个
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建table数组
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}

(2)roundUpToPowerOf方法说明

private static int roundUpToPowerOf2(int number) {
//number >= 0,不能为负数,
//(1)number >= 最大容量:就返回最大容量
//(2)0 =< number <= 1:返回1
//(3)1 < number < 最大容量:
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//该方法和jdk8中的tabSizeFor实现基本差不多
public static int highestOneBit(int i) {
//因为传入的i>0,所以i的高位还是0,这样使用>>运算符就相当于>>>了,高位0。
//还是举个例子,假设i=5=0101
i |= (i >> 1); //(1)i>>1=0010;(2)i= 0101 | 0010 = 0111
i |= (i >> 2); //(1)i>>2=0011;(2)i= 0111 | 0011 = 0111
i |= (i >> 4); //(1)i>>4=0000;(2)i= 0111 | 0000 = 0111
i |= (i >> 8); //(1)i>>8=0000;(2)i= 0111 | 0000 = 0111
i |= (i >> 16); //(1)i>>16=0000;(2)i= 0111 | 0000 = 0111
return i - (i >>> 1); //(1)0111>>>1=0011(2)0111-0011=0100=4
//所以这里返回4。
//而在上面的roundUpToPowerOf2方法中,最后会将highestOneBit的返回值进行 << 1 操作,即最后的结果为4<<1=8.就是返回大于number的最小2次幂
}

(3)putAllForCreate方法说明

​ 该方法就是遍历传入的map集合中的元素,然后加入本map实例中。下面我们来看看该方法的实现细节

private void putAllForCreate(Map<? extends K, ? extends V> m) {
//实际上就是遍历传入的map,将其中的元素添加到本map实例中(putForCreate方法实现)
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}

​ putForCreate方法原理实现

private void putForCreate(K key, V value) {
//判断key是否为null,如果为null那么对应的hash为0,否则调用刚刚上面说到的hash()方法计算hash值
int hash = null == key ? 0 : hash(key);
//根据刚刚计算得到的hash值计算在table数组中的下标
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//hash相同,key也相同,直接用旧的值替换新的值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
//这里就是:要插入的元素的key与前面的链表中的key都不相同,所以需要新加一个结点加入链表中
createEntry(hash, key, value, i);
}

(4)createEntry方法实现

void createEntry(int hash, K key, V value, int bucketIndex) {
//这里说的是,前面的链表中不存在相同的key,所以调用这个方法创建一个新的结点,并且结点所在的桶
//bucket的下标指定好了
Entry<K,V> e = table[bucketIndex];
/*Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}*/
table[bucketIndex] = new Entry<>(hash, key, value, e);//Entry的构造器,创建一个新的结点作为头节点(头插法)
size++;//将当前hash表中的数量加1
}

HashMap确定元素在数组中的位置

​ 1.7中的计算hash值的算法和1.8的实现是不一样的,而hash值又关系到我们put新元素的位置、get查找元素、remove删除元素的时候去通过indexFor查找下标。所以我们来看看这两个方法

(1)hash方法

final int hash(Object k) {
int h = hashSeed;
//默认是0,不是0那么需要key是String类型才使用stringHash32这种hash方法
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//这段代码是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点
//说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对
//最终得到的结果产生影响
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

​ 我们通过下面的例子来说明对于key的hashCode进行扰动处理的重要性,我们现在想向一个map中put一个Key-Value对,Key的值为“fsmly”,不进行任何的扰动处理知识单纯的经过简单的获取hashcode后,得到的值为“0000_0000_0011_0110_0100_0100_1001_0010”,如果当前map的中的table数组长度为16,最终得到的index结果值为10。由于15的二进制扩展到32位为“00000000000000000000000000001111”,所以,一个数字在和他进行按位与操作的时候,前28位无论是什么,计算结果都一样(因为0和任何数做与,结果都为0,那这样的话一个put的Entry结点就太过依赖于key的hashCode的低位值,产生冲突的概率也会大大增加)。如下图所示

​ 因为map的数组长度是有限的,这样冲突概率大的方法是不适合使用的,所以需要对hashCode进行扰动处理降低冲突概率,而JDK7中对于这个处理使用了四次位运算,还是通过下面的简单例子看一下这个过程.可以看到,刚刚不进行扰动处理的hashCode在进行处理后就没有产生hash冲突了。

​ 总结一下:我们会首先计算传入的key的hash值然后通过下面的indexFor方法确定在table中的位置,具体实现就是通过一个计算出来的hash值和length-1做位运算,那么对于2^n来说,长度减一转换成二进制之后就是低位全一(长度16,len-1=15,二进制就是1111)。上面四次扰动的这种设定的好处就是,对于得到的hashCode的每一位都会影响到我们索引位置的确定,其目的就是为了能让数据更好的散列到不同的桶中,降低hash冲突的发生。关于Java集合中存在hash方法的更多原理和细节,请参考这篇hash()方法分析

(2)indexFor方法

static int indexFor(int h, int length) {
//还是使用hash & (n - 1)计算得到下标
return h & (length-1);
}

主要实现就是将计算的key的hash值与map中数组长度length-1进行按位与运算,得到put的Entry在table中的数组下标。具体的计算过程在上面hash方法介绍的时候也有示例,这里就不赘述了。

HashMap的put方法分析

(1)put方法

public V put(K key, V value) {
//我们知道Hash Map有四中构造器,而只有一种(参数为map的)初始化了table数组,其余三个构造器只
//是赋值了阈值和加载因子,所以使用这三种构造器创建的map对象,在调用put方法的时候table为{},
//其中没有元素,所以需要对table进行初始化
if (table == EMPTY_TABLE) {
//调用inflateTable方法,对table进行初始化,table的长度为:
//不小于threshold的最小的2的幂数,最大为MAXIMUM_CAPACITY
inflateTable(threshold);
}
//如果key为null,表示插入一个键为null的K-V对,需要调用putForNullKey方法
if (key == null)
return putForNullKey(value); //计算put传入的key的hash值
int hash = hash(key);
//根据hash值和table的长度计算所在的下标
int i = indexFor(hash, table.length);
//从数组中下标为indexFor(hash, table.length)处开始(1.7中是用链表解决hash冲突的,这里就
//是遍历链表),实际上就是已经定位到了下标i,这时候就需要处理可能出现hash冲突的问题
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//hash值相同,key相同,替换该位置的oldValue为value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//空方法,让其子类重写
e.recordAccess(this);
return oldValue;
}
}
//如果key不相同,即在链表中没有找到相同的key,那么需要将这个结点加入table[i]这个链表中 //修改modCount值(后续总结文章会说到这个问题)
modCount++;
//遍历没有找到该key,就调用该方法添加新的结点
addEntry(hash, key, value, i);
return null;
}

(2)putForNullKey方法分析

​ 这个方法是处理key为null的情况的,当传入的key为null的时候,会在table[0]位置开始遍历,遍历的实际上是当前以table[0]为head结点的链表,如果找到链表中结点的key为null,那么就直接替换掉旧值为传入的value。否则创建一个新的结点并且加入的位置为table[0]位置处。

//找到table数组中key为null的那个Entry对象,然后将其value进行替换
private V putForNullKey(V value) {
//从table[0]开始遍历
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//key为null
if (e.key == null) {
//将value替换为传递进来的value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧值
}
}
modCount++;
//若不存在,0位置桶上的链表中添加新结点
addEntry(0, null, value, 0);
return null;
}

(3)addEntry方法分析

​ addEntry方法的主要作用就是判断当前的size是否大于阈值,然后根据结果判断是否扩容,最终创建一个新的结点插入在链表的头部(实际上就是table数组中的那个指定下标位置处)

/*
hashmap采用头插法插入结点,为什么要头插而不是尾插,因为后插入的数据被使用的频次更高,而单链表无法随机访问只能从头开始遍历查询,所以采用头插.突然又想为什么不采用二维数组的形式利用线性探查法来处理冲突,数组末尾插入也是O(1),可数组其最大缺陷就是在于若不是末尾插入删除效率很低,其次若添加的数据分布均匀那么每个桶上的数组都需要预留内存.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//这里有两个条件
//①size是否大于阈值
//②当前传入的下标在table中的位置不为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//如果超过阈值需要进行扩容
resize(2 * table.length);
//下面是扩容之后的操作
//计算不为null的key的hash值,为null就是0
hash = (null != key) ? hash(key) : 0;
//根据hash计算下标
bucketIndex = indexFor(hash, table.length);
}
//执行到这里表示(可能已经扩容也可能没有扩容),创建一个新的Entry结点
createEntry(hash, key, value, bucketIndex);
}

(4)总结put方法的执行流程

  1. 首先判断数组是否为空,若为空调用inflateTable进行扩容.
  2. 接着判断key是否为null,若为null就调用putForNullKey方法进行put.(这里也说明HashMap允许key为null,默认插入在table中位置为0处)
  3. 调用hash()方法,将key进行一次哈希计算,得到的hash值和当前数组长度进行&计算得到数组中的索引
  4. 然后遍历该数组索引下的链表,若key的hash和传入key的hash相同且key的equals放回true,那么直接覆盖 value
  5. 最后若不存在,那么在此链表中头插创建新结点

HashMap的resize方法分析

(1)resize的大体流程

void resize(int newCapacity) {
//获取map中的旧table数组暂存起来
Entry[] oldTable = table;
//获取原table数组的长度暂存起来
int oldCapacity = oldTable.length;
//如果原table的容量已经超过了最大值,旧直接将阈值设置为最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//以传入的新的容量长度为新的哈希表的长度,创建新的数组
Entry[] newTable = new Entry[newCapacity];
//调用transfer
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//table指向新的数组
table = newTable;
//更新阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

(2)transfer方法分析

​ transfer方法遍历旧数组所有Entry,根据新的容量逐个重新计算索引头插保存在新数组中。

void transfer(Entry[] newTable, boolean rehash) {
//新数组的长度
int newCapacity = newTable.length;
//遍历旧数组
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
//重新计算hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
//这里根据刚刚得到的新hash重新调用indexFor方法计算下标索引
int i = indexFor(e.hash, newCapacity);
//假设当前数组中某个位置的链表结构为a->b->c;women
//(1)当为原链表中的第一个结点的时候:e.next=null;newTable[i]=e;e=e.next
//(2)当遍历到原链表中的后续节点的时候:e.next=head;newTable[i]=e(这里将头节点设置为新插入的结点,即头插法);e=e.next
//(3)这里也是导致扩容后,链表顺序反转的原理(代码就是这样写的,链表反转,当然前提是计算的新下标还是相同的)
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

​ 这个方法的主要部分就是,在重新计算hash之后对于原链表和新table中的链表结构的差异,我们通过下面这个简单的图理解一下,假设原table中位置为4处为一个链表entry1->entry2->entry3,三个结点在新数组中的下标计算还是4,那么这个流程大概如下图所示

(3)resize扩容方法总结

  1. ​ 创建一个新的数组(长度为原长度为2倍,如果已经超过最大值就设置为最大值)
  2. 调用transfer方法将entry从旧的table中移动到新的数组中,具体细节如上所示
  3. 将table指向新的table,更新阈值

HashMap的get方法分析

//get方法,其中调用的是getEntry方法没如果不为null就返回对应entry的value
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}

​ 可以看到,get方法中是调用getEntry查询到Entry对象,然后返回Entry的value的。所以下面看看getEntry方法的实现

getEntry方法

//这是getEntry的实现
final Entry<K,V> getEntry(Object key) {
//没有元素自然返回null
if (size == 0) {
return null;
}
//通过传入的key值调用hash方法计算哈希值
int hash = (key == null) ? 0 : hash(key);
//计算好索引之后,从对应的链表中遍历查找Entry
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//hash相同,key相同就返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

getForNullKey方法

//这个方法是直接查找key为null的
private V getForNullKey() {
if (size == 0) {
return null;
}
//直接从table中下标为0的位置处的链表(只有一个key为null的)开始查找
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//key为null,直接返回对应的value
if (e.key == null)
return e.value;
}
return null;
}

jdk7版本的实现简单总结

​ (1)因为其put操作对key为null场景使用putForNullKey方法做了单独处理,HashMap允许null作为Key

​ (2)在计算table的下标的时候,是根据key的hashcode值调用hash()方法之后获取hash值与数组length-1进行&运算,length-1的二进制位全为1,这是为了能够均匀分布,避免冲突(长度要求为2的整数幂次方)

​ (3)不管是get还是put以及resize,执行过程中都会对key的hashcode进行hash计算,而可变对象其hashcode很容易变化,所以HashMap建议用不可变对象(如String类型)作为Key.

​ (4)HashMap是线程不安全的,在多线程环境下扩容时候可能会导致环形链表死循环,所以若需要多线程场景下操作可以使用ConcurrentHashMap(下面我们通过图示简单演示一下这个情况)

​ (5)当发生冲突时,HashMap采用链地址法处理冲突

​ (6)HashMap初始容量定为16,简单认为是8的话扩容阈值为6,阈值太小导致扩容频繁;而32的话可能空间利用率低。

jdk7中并发情况下的环形链表问题

​ 上面在说到resize方法的时候,我们也通过图示实例讲解了一个resize的过程,所以这里我们就不再演示单线程下面的执行流程了。我们首先记住resize方法中的几行核心代码

Entry<K,V> next = e.next;
//省略重新计算hash和index的两个过程...
e.next = newTable[i];
newTable[i] = e;
e = next;

​ resize方法中调用的transfer方法的主要几行代码就是上面的这四行,下来简单模拟一下假设两个线程thread1和thread2执行了resize的过程.

​ (1)resize之前,假设table长度为2,假设现在再添加一个entry4,就需要扩容了

​ (2)假设现在thread1执行到了 Entry<K,V> next = e.next;这行代码处,那么根据上面几行代码,我们简单做个注释

​ (3)然后由于线程调度轮到thread2执行,假设thread2执行完transfer方法(假设entry3和entry4在扩容后到了如下图所示的位置,这里我们主要关注entry1和entry2两个结点),那么得到的结果为

(4)此时thread1被调度继续执行,将entry1插入到新数组中去,然后e为Entry2,轮到下次循环时next由于Thread2的操作变为了Entry1

  • 先是执行 newTalbe[i] = e;在thread1执行时候,e指向的是entry1
  • 然后是e = next,导致了e指向了entry2(next指向的是entry2)
  • 而下一次循环的next = e.next,(即next=entry2.next=entry1这是thread2执行的结果)导致了next指向了entry1

如下图所示

​ (5)thread1继续执行,将entry2拿下来,放在newTable[1]这个桶的第一个位置,然后移动e和next

(6)e.next = newTable[1] 导致 entry1.next 指向了 entry2,也要注意,此时的entry2.next 已经指向了entry1(thread2执行的结果就是entry2->entry1,看上面的thread2执行完的示意图), 环形链表就这样出现了。

深入理解HashMap(jdk7)的更多相关文章

  1. 深入理解HashMap+ConcurrentHashMap的扩容策略

    前言 理解HashMap和ConcurrentHashMap的重点在于: (1)理解HashMap的数据结构的设计和实现思路 (2)在(1)的基础上,理解ConcurrentHashMap的并发安全的 ...

  2. Map 综述(一):彻头彻尾理解 HashMap

    转载自:https://blog.csdn.net/justloveyou_/article/details/62893086 摘要: HashMap是Map族中最为常用的一种,也是 Java Col ...

  3. 深入理解HashMap的扩容机制

    什么时候扩容: 网上总结的会有很多,但大多都总结的不够完整或者不够准确.大多数可能值说了满足我下面条件一的情况. 扩容必须满足两个条件: 1. 存放新值的时候当前已有元素的个数必须大于等于阈值 2. ...

  4. Hashmap jdk7 死循环

    如果理解的有问题,欢迎大家指正. https://www.cnblogs.com/webglcn/p/10587708.html jdk7的hashmap 由数组和链表组成,存在几个问题: 当key的 ...

  5. 深入理解HashMap

    转自:http://annegu.iteye.com/blog/539465 Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复习一下.网上关于hashmap的文章很多 ...

  6. 集合之深入理解HashMap

    Hashmap是一种非常常用的.应用广泛的数据类型 1.hashmap的数据结构 要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个 ...

  7. 深入理解HashMap上篇

    前言: HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化, ...

  8. 通过源码理解HashMap的并发问题

    最近在学习有关于Java的基础知识,在学习到HashMap的相关知识的时候,了解了HashMap的并发中会出现的问题,在此记录,加深理解(这篇文章是基于Java1.7的,主要是为了更加直观,更新版本的 ...

  9. 深入理解HashMap和CurrentHashMap

    原文链接:https://segmentfault.com/a/1190000015726870 前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据. 本篇 ...

随机推荐

  1. 有关Html页面节点的简单理解

    这是之前研究web前端的一点经验,主要针对刚入门还没怎么研究的朋友. 因为我发现我在用js,css参与过网站开发项目后仍然没有理解文本节点与普通节点的差别,所以记下来拿来分享一下. 先上结论:< ...

  2. ajax中error函数参数与返回值详解 200 300 400 500

    201-206:都表示服务器成功处理了请求的状态代码,说明网页可以正常访问. 200:(成功) 服务器已成功处理了请求.通常,这表示服务器提供了请求的网页. 201:(已创建) 请求成功且服务器已创建 ...

  3. 浅入深出Vue:数据渲染

    今天来正式开始 vue的学习,首当其冲的当然是数据的渲染.毕竟数据就是拿来看的,看看如果使用 vue来展示数据. 为什么渲染 俗话说 "人靠衣装马靠鞍", 那咱们的代码就是得靠 U ...

  4. Android 即时通讯开发小结(二)

    <Android 即时通讯开发小结>基于IM Andriod 开发的各种常见问题,结合网易云信即时通讯技术的实践,对IM 开发做一个全面的总结. 相关推荐阅读:. Android 即时通讯 ...

  5. 【java】SHA256加密工具

    SHA256: /** * 备用方案 SHA256加密 * @author zx */ public class SHA256Util { public static void main(String ...

  6. 学习kafka自己发生的几个小错误记录

    一.  The method iterator() is ambiguous ConsumerIterator<byte[],byte[]> it =stream.iterator(); ...

  7. java日期在今天的基础上加一个月。并计算时间相差天数

    Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.MONTH, 1); ...

  8. 56. Merge Interval

    56. Merge Interval 0. 参考文献 序号 文献 1 花花酱 LeetCode 56. Merge Intervals 2 [LeetCode] Merge Intervals 合并区 ...

  9. 谈谈 c# 对象初始化问题

    C#对象初始化 之前在学习过程中只是知道该如何初始化对象,但是却不明白为何要这么做,不这么做有什么问题. 现在就针对我最近遇到的问题(定义了全局字节数组没有初始化,然后在多线程里头使用,然后就一直报n ...

  10. nodejs进阶(1)——npm使用技巧和最佳实践

    nodejs进阶教程,小白绕道!!! npm使用技巧和最佳实践 前提:请确保安装了node.js npm的最佳实践 npm install是最常见的npm cli命令,但是它还有更多能力!接下来你会了 ...