java hash 面试_虐哭java面试官--聊一聊hashmap

Java hashmap的数据结构,开发的时候从来用不到那么深,MD,每个面试官都要问一遍。

别人恶心我的时候,我要比他更恶心才行。

放心,技术一般的面试官不可能看到我这个深度的。跟他聊聊 loadFactor,聊聊二进制 &运算,聊聊hashmap的resize(),就算不虐哭,也让他倒吸一口凉气。

接下来,技术一般的面试官就不敢问太深的问题了,因为他也不懂啊。

绝对原创,但是都是看的别人的帖子,结合 JDK1.7的源码,断点走出来的结果。

1. 设计结构

441526.html

结合了数组结构(查询快)和链表结构(插入和删除快1)的特点。

第一层是数组  Entry(K,V) 的一个数组  table,根据key的hashcode值,对当前数组长度-1进行 &运算,得出该键值对 在数组中的存储位置。

然后再判断数组的该位置是否有值,如果该数组位置没有值(null),那么这个键值对的位置就是入住。如果该数组位置有值,那么老主人就作为新主人的一部分,新进来的键值对占据该位置,  Entry  current.next= oldEntry. 也就形成了链表的结构,上线找下线,下线下面可能还有下线也有可能没有

2. 数组的长度

数组的长度规则:

初始化的时候是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 e = table[i]; e != null; e = e.next) {

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

}

什么是 &运算?

441526.html

就是把前后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的设置

当然也可以根据实际的需求设置 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);

}

初始化hashmap length的设置

如果不修改 loadFactor的话(默认0.75),那么初始化的length应该为 1.34*size,比如1万个键值对,那么初始化的时候长度给13400比较合适,不会导致resize(),当然也要结合实际的内存情况做权衡

3. 数据的put

3.1)key=null

会直接放在数组的table[0]位置

3.2)插入的位置没有值

直接返回null

3.3)插入的位置已经有值了

441526.html

3.31)遍历该key是否存在

如果该key已经有值了,那么则替换并返回老的值,

如果该key没有,则该key占据第一个位置,老的 键值对作为链表,存在于  currentEntry.next= oldEntry

判断该key是否已经存在有2个条件 &&,hashcode值相同并且 equals为true,一般实际开发不太可能出现这种情况,除非自己故意设置成这样。

4. hashmap的重构 resize()

会遍历之前所有的 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 e : table) {

while(null != e) {

Entry next = e.next;

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;

}

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值