Java hashmap的数据结构,开发的时候从来用不到那么深,MD,每个面试官都要问一遍。
别人恶心我的时候,我要比他更恶心才行。
放心,技术一般的面试官不可能看到我这个深度的。跟他聊聊 loadFactor,聊聊二进制 &运算,聊聊hashmap的resize(),就算不虐哭,也让他倒吸一口凉气。
接下来,技术一般的面试官就不敢问太深的问题了,因为他也不懂啊。
绝对原创,但是都是看的别人的帖子,结合 JDK1.7的源码,断点走出来的结果。
结合了数组结构(查询快)和链表结构(插入和删除快1)的特点。
第一层是数组 Entry(K,V) 的一个数组 table,根据key的hashcode值,对当前数组长度-1进行 &运算,得出该键值对 在数组中的存储位置。
然后再判断数组的该位置是否有值,如果该数组位置没有值(null),那么这个键值对的位置就是入住。如果该数组位置有值,那么老主人就作为新主人的一部分,新进来的键值对占据该位置, Entry current.next= oldEntry. 也就形成了链表的结构,上线找下线,下线下面可能还有下线也有可能没有
初始化的时候是16( 2的4次方),每次扩容都是 2的N次方
void addEntry(int hash, K key, V value, intbucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 *table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
为什么取2的N次方,因为在键值对插入的时候,会对index求hashcode值,然后将hashcode值和数组长度-1 进行 &运算
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash =hash(key);
int i = indexFor(hash, table.length);
for (Entry
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
static intindexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "lengthmust be a non-zero power of 2";
return h & (length-1);
}
就是把前后2个值转换为 2进制,相同位置上都为1 则为1,其他的都为0
例如左边图是 16长度数组,也就是 9&15 8&15 运算结果,一个1001(9 十进制) table[9]的位置, 一个1000(8 十进制)table[8]的位置
如果不是2的N次方,那么总数-1 转换为2进制的时候,肯定有一个位置上为0
例如如果数组长度为15,那么 N-1=14, 14转换为二进制就是 1110,
进行&运算的时候,因为1110与任何数进行&运算的结果都不可能是 ***1,所以例如 0001 也就是 table[1]这个位置上始终都不可能有键值对可以插入进去
所以数组的长度只能是2的N次方。
默认初始长度是16,扩容时机的判断2个,所以理论上长度 length=0.75size 到 size 之间。当loadFactor为0.75的时候。
因为当size达到 length的时候,一定会触发 size>=0.75*length 和table[index]!=null。也就是当size超过0.75*length的时候就有概率触发扩容,而且这种触发是随机的。
所以合理的 hashmap的长度应该是 4*size / 3 也就是 1.33*size这样就不可能触发 hashmap的重构。特别是数量比较多的时候,几万个键值对的时候,初始化 hashmap的时候设置长度,非常有意义。
当然也可以根据实际的需求设置 loadFactor,来设置适合业务规则的 hashmap。
如果内存富余,那么建议把loadFactor设置的小一点,但是要注意初始size的设置,如果不合适会导致频繁的 resize 严重影响插入的效率。
如果内存比较吃紧,就可以把loadFactor设置的大一些,但是loadFactor设置大的话,键值对以链表的形式存储的概率就提高,平均的查询时间变慢,但是对于插入而言,虽然没有直接的影响,但是loadFactor提高,
需要插入更多的数据才会触发 resize,这样某种程度上是提升了插入的效率
插入到某个有值的位置,挨个对比是否有 key值相同的对象
2.1)如果有key值一样(hashcode值相同,而且key==oldKey||key.equals(k)),则替换并返回老的key的value值
2.2)如果没有key值一样的,那么该位置会被最后一个进来的Entry 占据,并且Entry的next属性,指向之前第一个位置的Entry,也就是链表了,一个找一个
void addEntry(int hash, K key, V value, intbucketIndex) {
if ((size >=threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
初始化hashmap 2种构造函数,
public HashMap(int initialCapacity, float loadFactor) {
public HashMap(int initialCapacity) {
/**
* Constructs an empty HashMap with the specifiedinitial
* capacity and load factor.
*
* @param initialCapacity theinitial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor isnonpositive
*/
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;
threshold = initialCapacity;
init();
}
/**
* Constructs an empty HashMap with the specifiedinitial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initialcapacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
如果不修改 loadFactor的话(默认0.75),那么初始化的length应该为 1.34*size,比如1万个键值对,那么初始化的时候长度给13400比较合适,不会导致resize(),当然也要结合实际的内存情况做权衡
会直接放在数组的table[0]位置
直接返回null
如果该key已经有值了,那么则替换并返回老的值,
如果该key没有,则该key占据第一个位置,老的 键值对作为链表,存在于 currentEntry.next= oldEntry
判断该key是否已经存在有2个条件 &&,hashcode值相同并且 equals为true,一般实际开发不太可能出现这种情况,除非自己故意设置成这样。
会遍历之前所有的 Entry
计算新的 table.index
--- 如果该Entry的 next不为空,则next占据原位置,并且下一个处理这个next
---- 如果待插入的位置已经有Entry,则按照之前的规则,把老的Entry作为 next存在新的 Entry里面。
voidresize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = newEntry[newCapacity];
transfer(newTable,initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity *loadFactor, MAXIMUM_CAPACITY + 1);
}
voidtransfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry
while(null != e) {
Entry
if (rehash) {
e.hash = null == e.key ? 0: hash(e.key);
}
int i = indexFor(e.hash,newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}