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

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

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

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

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


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

1. 设计结构

 

结合了数组结构(查询快)和链表结构(插入和删除快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);

    }

 

什么是 &运算?


就是把前后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)插入的位置已经有值了


      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;

            }

        }

    }

 

 

 


你可能感兴趣的:(JAVA学习)