作者:ShadowJava
链接:https://juejin.im/post/5f172a0ff265da22d6512350
来源:掘金

HashMap为什么经常被面试官问到,但是经常被面试官问趴下怎么办?

哈,原理我不知道?笑话!

不就是一个hash加一个map嘛,多简单啊? 答:利用key的hashCode重新hash计算出当前对象的元素在数组中的下标,存储到数组里面就行了,底层就是数组嘛! 然后面试官说了句:好的,我知道了,回去听消息吧!

这种答复并不是面试官想要看到的,想要听到程序员有自己的理解和分析优化!

客官,来,1.7源码!

/**
*继承AbstractMap,并重写Map接口
**/
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializablestatic final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 位运算,1左移4位=16static final int MAXIMUM_CAPACITY = 1 << 30;//位运算,左移30位=1073741824static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子0.75transient Entry<K,V>[] table;//entry数组 entry又是个单链表结构transient int size;//hashmap长度int threshold;//capacity * load factor 阈值比如16*0.75 = 12void resize(int newCapacity);//扩容方法static int indexFor(int h, int length) {return h & (length-1);//计算index方法
}void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);//头插法解决hash冲突size++;
}

通过源码可以知道,HashMap的默认初始化长度为1 << 4=16,最大容量为1<<30=1073741824. 可以看出1.7是采用数组加单链表的结构,Entry是一个继承Map.Entry的单链表结构。size就是hashmap的长度,threshold就是插入扩容的阈值。 从reSize方法可以看出,1.7是先进行扩容再插入数据的。

别着急,1.7是怎么进行扩容再插入的嘛!

容量初始值为16,当插入数据数量大于阈值(capacity * load factor=12)并发生hash冲突时就进行扩容。每次扩容都是2的n次幂,这样好处就是利于位运算,并且length-1的二进制最后一位为1,减少了hash碰撞。且插入时是通过int i = indexFor(e.hash, newCapacity);重新计算的,即hash & (length-1),并重新设置阈值(newCapacity * loadFactor)。

void resize(int newCapacity) {//扩容方法:resize(2 * table.length),当容量不足时(容量 > 阈值),则扩容(扩到2倍)Entry[] oldTable = table;//复制旧的哈希表int oldCapacity = oldTable.length;//旧的容量if (oldCapacity == MAXIMUM_CAPACITY) //容量如果等于最大值,则不再扩容threshold = Integer.MAX_VALUE;return;}Entry[] newTable = new Entry[newCapacity];boolean oldAltHashing = useAltHashing;useAltHashing |= sun.misc.VM.isBooted() &&(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);boolean rehash = oldAltHashing ^ useAltHashing;transfer(newTable, rehash);//转移数据到新哈希表里table = newTable;//完成扩容threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//重新设置阈值
}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) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);//重新计算index值e.next = newTable[i];newTable[i] = e;//头部插入e = next;}}
}

还有1.8吗,太难了。挺住,坚持坚持就看完了!

客官,还有1.8呢

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializablestatic final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//Bins are converted to trees when adding an element to a bin with at least this many nodes.
static final int TREEIFY_THRESHOLD = 8;//当桶的节点数量大于该阈值时转换为红黑树
static final int UNTREEIFY_THRESHOLD = 6;//当桶的节点数小于该阈值时会转换成链表,前提是当前是红黑树结构
static final int MIN_TREEIFY_CAPACITY = 64;//桶的节点数大于该容量时也会转换成红黑树
static class Node<K,V> implements Map.Entry<K,V> ;//换成节点了
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>;//树节点
transient Node<K,V>[] table;//数组加单链表,后期会转红黑树
transient Set<Map.Entry<K,V>> entrySet;//数据的另一种存储方式,主要用于迭代功能
transient int size;//hashmap元素数量
transient int modCount;//该map的修改次数

从源码可以看出,初始容量还是16,负载因子还是0.75,只是多了关于红黑树的知识点。1.8采用数组加单链表加红黑树的数据结构,扩容还是扩2的n次幂。

hash方法:

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

通过判断key值是否为空,如果空就为0,不为空就异或运算。

存值方法put

public V put(K key, V value) {//存值return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)//(length-1)&hash计算出索引值,判断当前哈希桶是否为空tab[i] = newNode(hash, key, value, null);//空就赋值给当前桶else {//该哈希桶不为空,会发生hash冲突,以下为发生hash冲突几种情况Node<K,V> e; K k;//第一种情况,当前的hash值相同,且key值也相同,e = p表示首节点。if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//第二种情况,如果当前桶为树节点实例且不是首节点,即红黑树节点。是树节点则在红黑树中添加else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//第三种,不为首节点,不为红黑树节点,则为链表节点for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//桶下个节点为空,则直接插入,尾插法p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//节点数大于等于7,则转换红黑树结构(从0开始到7)break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//插入节点与next节点重复,跳出循环p = e;}}if (e != null) { //在节点中存在重复的值,直接覆盖旧值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//修改次数加一if (++size > threshold)resize();//容量增一,判断是否需要扩容afterNodeInsertion(evict);//添加成功return null;
}

总的来说就是put的时候会进行判断当前桶是否为空,空就直接赋值。不为空且hash相同,则存在冲突,此时需要处理冲突的三种情况: ①当前桶不为空,且key值相同,则直接覆盖。 ②当前桶不为空,则桶节点不为首节点,且为红黑树节点,则在红黑树中添加。(节点数>=TREEIFY_THRESHOLD) ③当前桶为不为空,不为首节点,不为红黑树节点,则为链表节点。(节点数<TREEIFY_THRESHOLD)添加时如果大于等于7(从0到7),则需要转为红黑树节点。

插入值过后就需要判断容量问题,此时就需要reSize()方法,从上述的源码中if (++size > threshold)就知道JDK1.8是先插入再进行判断扩容的。当链表深度大于8时,会自动扩容为红黑树结构,时间复杂度从O(n)转到O(logn)

final Node<K,V>[] resize() {//扩容方法Node<K,V>[] oldTab = table;//之前旧的哈希表int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;//old的表的阈值int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {//容量大于最大值threshold = Integer.MAX_VALUE;//此时阈值为整数最大值return oldTab;//最大限度 不能扩容了}//左移一位,新的哈希表扩容两倍,且要小于最大容量;且旧容量要比默认初始容量大else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold 阈值也要扩大两倍}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//初始化的阈值else {               //容量为0,阈值就为容量*负载因子newCap = DEFAULT_INITIAL_CAPACITY;//默认初始容量16newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阈值16*0.75=12}if (newThr == 0) {//如果初始化容量小于16时,没有阈值。因为构造函数可以自己设初始容量float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;//赋值给阈值@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//扩容后将旧的哈希表赋值到新的哈希表中if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {//当前哈希桶节点为eoldTab[j] = null;if (e.next == null)//没有下个节点newTab[e.hash & (newCap - 1)] = e;//重新计算index,放入新的哈希表中else if (e instanceof TreeNode)//有next节点,且为树节点,将此树转移到新的cap中((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve order 链表情况,将链表插入到新的哈希表节点中Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;//下一个节点if ((e.hash & oldCap) == 0) {//当前桶节点为首节点if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;//返回新的哈希表
}

总的来讲,插入数据后扩容过程分为以下几个步骤:

①当前容量是否超过最大容量,超过则不扩容,直接返回。且阈值(threshold)为整数的最大值 ②当前容量小于最大容量且大于初始容量。容量扩大两倍,阈值也扩大两倍

扩容后就将旧的表数据转移到新的表,利用hash&length-1计算index值,判断当前桶节点属于链表节点还是红黑树节点,再依次移入。

public V remove(Object key) {Node<K,V> e;//判断删除的节点是否为空,不为空就返回该节点值return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;//获取当前的节点else if ((e = p.next) != null) {//要删除的节点if (p instanceof TreeNode)//下一个节点为树节点node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//链表节点,遍历找到节点do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//遍历找到节点if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)//如果删除的是红黑树节点((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)//普通节点tab[index] = node.next;else//链表节点,修改next值p.next = node.next;++modCount;//修改次数加一--size;//元素减一afterNodeRemoval(node);return node;}}return null;//表示没有该节点,没找到
}

区别加总结

相同点: 计算index都是hash&length-1 不同点: ①JDK1.7时采用的是数组加单链表的数据结构,JDK1.8采用的是数组加单链表加红黑树的数据结构 ②1.7是先扩容再插入,1.8是先插入再扩容 ③解决冲突时,1.7是头插法的纵向延伸,但会容易逆序环形链表死循环问题;1.8是加入了红黑树,采用尾插法,能够避免出现逆序死循环的问题。

HashMap有哪些不足,怎么解决和优化

hashmap具有键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化的特性,即

为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

HashMap 中的 key若 Object类型, 则需实现哪些方法?

部分内容借鉴网址:http://www.jianshu.com/p/8324a3457… 确实写的好,佩服!

Hashmap确实还有很多可以深挖的知识点,这也是我第一次认真的写一篇文章,在我这么写了几千字的面子上就点个小心心呗,以后我会隔几天写一篇技术栈的文章并且同步到github上!有什么写的不好的地方和不足,欢迎大家指出!

隔壁写的实在是太好了,导致我有点力不从心,就这样吧!

树结构有什么好处_什么啊?面试官还在问HashMap了,老知识点了啊相关推荐

  1. java游戏面试_作为一个面试官,我会问初级java工程师哪些问题?

    初级java工程师多数是刚毕业或者工作1,2年的新人.对于新人,面试中基础问题会问道很多,因为先要考察这个人的基础. 关于基础类的题目,我在面试初级java工程师的时候一般会问下面两大类问题,每类5个 ...

  2. 二面面试官一般都会问什么?

    在找工作的过程中,面试是不可避免的一环.对于面试者来说,准备充分是必须的,但往往也会面对各种各样的问题,其中就包括"二面面试官一般都会问什么?"这个问题.下面,小编将为大家介绍二面 ...

  3. qt定时器是阻塞的吗_吊打面试官 | 面试官:TCP真的可靠吗

    点击蓝字关注我哦 以下是本期干货视频视频后还附有文字版本哦 ▼<面试官:TCP真的可靠吗>▼ ps:请在WiFi环境下打开,如果有钱任性请随意 TCP真的可靠吗 面试官经常会问的一个问题是 ...

  4. 面试java你最擅长什么_面试官最喜欢问的10道Java面试题

    1.Java的HashMap是如何工作的? HashMap是一个针对数据结构的键值,每个键都会有相应的值,关键是识别这样的值. HashMap 基于 hashing 原理,我们通过 put ()和 g ...

  5. 阿里的面试官都喜欢问哪些问题?

    作者:徐刘根 | 公众号:Java之间 金九银十是招聘的旺季,小编在这里也给大家整理了一套阿里面试官最喜欢问的问题或者出场率较高的面试题,助校招或者社招路上的你一臂之力! 首先我们需要明白一个事实,招 ...

  6. 为什么面试官总喜欢问String?

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者:tan日拱一兵 来源:公众号「日拱一兵」 关于 Java String,这是面试的基础 ...

  7. 面试官上来就问:Java 进程中有哪些组件会占用内存?

    本文的内容来自 StackOverflow 的一个问答:Java using much more memory than heap size (or size correctly Docker mem ...

  8. 安卓application_阿里面试官刁钻连问:安卓 UID的分配、查看及相关知识

    面试难度还行,但是如果不好好复习,有几个点还真是答不上来. uid机制 uid:Android系统中uid用于标识一个应用程序,uid在应用安装时被分配,并且在应用存在于手机上期间,都不会改变.一个应 ...

  9. 面试官最爱问的并发问题

    转载自  面试官最爱问的并发问题 在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(J ...

最新文章

  1. linux进程的环境变量,LINUX系统学习一(进程、MMU,环境变量、getenv、fork、getpid/ge...
  2. 什么是php工作流,什么是工作流?
  3. leetcode 222 Count Complete Tree Nodes
  4. java mongodb drivers 2升级到3_JAVA从头开始一基础梳理(3-2)
  5. bug的一生:如何体现测试专业度?
  6. 最大化/最小化/关闭窗口代码集锦
  7. Python菜鸟入门:day06元组与字典
  8. Maven模块聚合与继承
  9. 51单片机按键控制数码管0~9_0.39寸2位数码管厂家
  10. mysql cnf和ini区别_再谈mysql的配置文件my.ini or my.cnf (之一) .
  11. 提升自我的42个实用技巧
  12. 网络号,网络标识,广播地址,有效主机范围计算
  13. React Context源码是怎么实现的呢
  14. 属兔2013年蛇年运程
  15. Leetcode 92反转链表
  16. 服务器里怎么给网站刷排名,细数刷排名软件的原理与实现过程
  17. Hackbar 免费下载+使用指南
  18. 我,阿里的程序员,工作5年,揭秘阿里员工的真实收入和工作经历
  19. http://www.yesky.com/424/163424.shtml http://www.yesky.com/424/163424.shtml
  20. Twitter开发者账号申请被拒【推特开发者文档系列6】——Twitter IDs(snowflake)

热门文章

  1. IRNet:弱监督实例分割 | 步步为营,隔山打牛
  2. 厉害了!写代码、搜问题,全部都在「终端」完成!如此编程神器,是时候入手了...
  3. 打造计数君!谷歌提出RepNet:可自动计数视频重复片段 | CVPR 2020
  4. 我敢打赌!你见过的所有程序员都是这样的!!!
  5. Mysql高级调优篇——第五章:Sql调优在面试中深度剖析
  6. 笔记精选(返回点赞总数和挑选笔记数量)
  7. 线性回归api深度介绍
  8. 什么是“单播”“组播”和“多播”
  9. Python翻转字典,以及解决IndentationError: expected an indented block报错!
  10. 素数和_只愿与一人十指紧扣_新浪博客