• HashMap常问面试题整理


    去面试时,hashmap总是被经常问的问题,下面总结了几道关于hashmap的问题。

    1、hashmap的主要参数都有哪些?

    2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

    3、hash计算规则是什么?

    4、说说hashmap的存取过程?

    5、说说hashmap如何处理碰撞的,或者说说它的扩容?

    解答:以1.7为例,也会掺杂一些1.8的不同点。

    1、

    1)桶(capacity)容量,即数组长度:DEFAULT_INITIAL_CAPACITY=1<<4;默认值为16

      即在不提供有参构造的时候,声明的hashmap的桶容量;

    2)MAXIMUM_CAPACITY = 1 << 30;

      极限容量,表示hashmap能承受的最大桶容量为2的30次方,超过这个容量将不再扩容,让hash碰撞起来吧!

    3)static final float DEFAULT_LOAD_FACTOR = 0.75f;

      负载因子(loadfactor,默认0.75),负载因子有个奇特的效果,表示当当前容量大于(size/)时,将进行hashmap的扩容,扩容一般为扩容为原来的两倍。

    4)int threshold;阈值

      阈值算法为capacity*loadfactory,大致当map中entry数量大于此阈值时进行扩容(1.8)

    5)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;(默认为空{})

      核心的数据结构,即所谓的数组+链表的部分。

    2、hashmap的数据结构是什么样子的?自己如何实现一个hashmap?

      主要数据结构即为数组+链表。

      在hashmap中的主要表现形式为一个table,类型为Entry<K,V>[] table

      首先是一个Entry型的数组,Entry为hashmap的内部类:

      

    1 static class Entry<K,V> implements Map.Entry<K,V> {
    2         final K key;
    3         V value;
    4         Entry<K,V> next;
    5         int hash;
    6 }

      在这里可以看到,在Entry类中存在next,所以,它又是链表的形式。

      这就是hashmap的主要数据结构。

    3、hash的计算规则,这又要看源码了:

      

    1 static final int hash(Object key) {
    2         int h;
    3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    4     }

      这是1.8的源码,1.7太复杂但原理是一致的,简单说这就是个“扰动函数”,最终的目的是让散列分布地更加均匀。

      算法就是拿存储key的hashcode值先右移16位,再与hashcode值进行亦或操作,即不求进位只求按位相加的值:盗图:

      

      最后是如何获得,本key在table中的位置呢?本身应该是取得了hash进行磨除取余运算,但是,源码:

    1 static int indexFor(int h, int length) {
    2         // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    3         return h & (length-1);
    4     }

      为什么又做了个与运算求得位置呢?简单说,它的意义和取余一致。

      不信可以自己算一下。

      首先说,他利用了table的长度肯定是2的整数次幂的原理,假设当前length为16,2的4次方

      而与&运算,又是只求进位运算,比如1111&110001结果为000001

      只求进位运算(&),保证算出的结果一定在table的length之内,最大为1111。

      故而,它的运算结果与价值等同于取余运算,并且即使不管hash值有多大都可以算出结果,并且在length之内。

      并且,这种类型的运算,能够更加的节约计算机资源,少了加(计算机所有运算都是加运行)运算过程,更加地节省资源。

    4、hashmap的存取过程

      源码1.7:

     1 /**
     2 *往hashmap中放数据
     3 */
     4 public V put(K key, V value) {
     5         if (table == EMPTY_TABLE) {
     6             inflateTable(threshold);//判断如果为空table,先对table进行构造
     7             //构造通过前面的几个参数
     8         }
     9         //首先判断key是否为null,为null也可以存
    10         //这里需要记住,null的key一定放在table的0号位置
    11         if (key == null)
    12             return putForNullKey(value);
    13         //算出key的hash值
    14         int hash = hash(key);
    15         //根据hash值算出在table中的位置
    16         int i = indexFor(hash, table.length);
    17         //放入KV,遍历链表,如果位置上存在相同key,进行替换value为新的,且将替换的旧的value返回
    18         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    19             Object k;
    20             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    21                 V oldValue = e.value;
    22                 e.value = value;
    23                 e.recordAccess(this);
    24                 return oldValue;
    25             }
    26         }
    27         modCount++;
    28         //增加一个entry,有两种情况,1、如果此位置存在entry,将此位置变为插入的entry,且将插入entry的next节点变为原来的entry;2、如果此位置不存在entry则直接插入新的entry
    29         addEntry(hash, key, value, i);
    30         return null;
    31     }

    取数据:

     1 //根据key获得一个entry
     2 public V get(Object key) {
     3         //如果key为null,获取0号位的切key为null的值
     4         if (key == null)
     5             return getForNullKey();
     6         //如果不是,获取entry,在下面方法
     7         Entry<K,V> entry = getEntry(key);
     8         //合法性判断
     9         return null == entry ? null : entry.getValue();
    10     }
    11 //获取一个key不为null的entry
    12 final Entry<K,V> getEntry(Object key) {
    13         //如果table为null,则返回null
    14         if (size == 0) {
    15             return null;
    16         }
    17         //计算hash值
    18         int hash = (key == null) ? 0 : hash(key);
    19         //根据hash值获得table的下标,遍历链表,寻找key,找到则返回
    20         for (Entry<K,V> e = table[indexFor(hash, table.length)];
    21              e != null;
    22              e = e.next) {
    23             Object k;
    24             if (e.hash == hash &&
    25                 ((k = e.key) == key || (key != null && key.equals(k))))
    26                 return e;
    27         }
    28         return null;
    29     }

     5.扩容和碰撞

      先说碰撞吧,由于hashmap在存值的时候并不是直接使用的key的hashcode,而是通过扰动函数算出了一个新的hash值,这个计算出的hash值可以明显的减少碰撞。

      还有一种解决碰撞的方式就是扩容,扩容其实很好理解,就是将原来桶的容量扩为原来的两倍。这样争取散列的均匀,比如:

      原来桶的长度为16,hash值为1和17的entry将会都在桶的0号位上,这样就出现了碰撞,而当桶扩容为原来的2倍时,hash值为1和17的entry分别在1和17号位上,整号岔开了碰撞。

      下面说说何时扩容,扩容都做了什么。

      1.7中,在put元素的过程中,判断table不为空、切新增的元素的key不与原来的重合之后,进行新增一个entry的逻辑。

    1 void addEntry(int hash, K key, V value, int bucketIndex) {
    2         if ((size >= threshold) && (null != table[bucketIndex])) {
    3             resize(2 * table.length);
    4             hash = (null != key) ? hash(key) : 0;
    5             bucketIndex = indexFor(hash, table.length);
    6         }
    7         createEntry(hash, key, value, bucketIndex);
    8     }

      由源代码可知,在新增元素时,会先判断:

      1)当前的entry数量是否大于或者等于阈值(loadfactory*capacity);

      2)判断当前table的位置是否存在entry。

      经上两个条件联合判定,才会进行数组的扩容工作,最后扩容完成才会去创建新的entry。

      而扩容的方法即为:resize()看代码

      

     1 void resize(int newCapacity) {
     2         //拿到原table对象
     3         Entry[] oldTable = table;
     4         //计算原table的桶长度
     5         int oldCapacity = oldTable.length;
     6         //先判定,当前容量是否已经是最大容量了(2的30次方)
     7         if (oldCapacity == MAXIMUM_CAPACITY) {
     8             //假如达到了,将阈值设为int的最大值2的31次方减1,返回
     9             threshold = Integer.MAX_VALUE;
    10             return;
    11         }
    12         //创建新的table对象
    13         Entry[] newTable = new Entry[newCapacity];
    14         //将旧的table放入新的table中
    15         transfer(newTable, initHashSeedAsNeeded(newCapacity));
    16         //赋值新table
    17         table = newTable;
    18         //计算新的阈值
    19         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    20     }
    21 //具体的扩容过程
    22 void transfer(Entry[] newTable, boolean rehash) {
    23         int newCapacity = newTable.length;
    24         //遍历原table,重新散列
    25         for (Entry<K,V> e : table) {
    26             while(null != e) {
    27                 Entry<K,V> next = e.next;
    28                 if (rehash) {
    29                     e.hash = null == e.key ? 0 : hash(e.key);
    30                 }
    31                 int i = indexFor(e.hash, newCapacity);
    32                 e.next = newTable[i];
    33                 newTable[i] = e;
    34                 e = next;
    35             }
    36         }
    37     }

    至此,扩容就说完了。。。

  • 相关阅读:
    win7 重装 docker 启动后无法启动错误解决
    ASP.NET MVC 播放远程服务器上的MP3文件
    ubuntu+mono+PetaPoco+Oracle+.net 程序部署
    .NET Core 2.0 问题杂记
    博客园挂了吗?
    Content-Type: application/vnd.ms-excel">
    Storm
    Razor语法
    类型后面加问号 int?
    Apache vue site configuration
  • 原文地址:https://www.cnblogs.com/qfxydtk/p/8734784.html
Copyright © 2020-2023  润新知