# 2019面试难点第一弹(Map)

1.关于map

首先,map是用来存储键值对(以前我一直有一个认知误区,数组里存的是key,然后value挂在key后面,形成链表),所以map的基本单位是Entity,那么Entity是从哪儿来的呢?

interface Entry<K,V> 

这是map的内部接口,也就是说我们用的put(key,value)会被内部封装成一个Entity存储。,至于为什么不能有重复的key是因为hashcode()算法。下面是map的常见实现类:

linkedHashMap:记录了插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯 定是先插入的,也可以在构造时带参数,按照访问次序排序。
TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

上面这些话是从别的地方引用的,相信大多数人和我一样:看不懂,记不住,不过没关系,我把他们的特点总结了:

  1. Treemap有序,hashmap快
  2. hashmap线程不安全,hashtable线程安全(不过它太low了,下面我会解释)
  3. hashmap的键,值都可以为null

上面三句话只要谈到map基本是必问的,答不出来基本上就可以凉凉了

2.关于Hashmap

hashmap的考察点很多,但是基本绕不开3个点,插入扩容线程安全

3.hashmap的插入你知道嘛?

58e67eae921e4b431782c07444af824e_r.jpg

我看过很多讲解,这是从美团的技术博客中拿下来的一张图,可以说非常完美的解释了hashmap的存储,结合源码,来一步一步看(调用put方法其实就是调用putVal(),这个方法可以说是HashMap存储的精髓):

/**
 * Implements Map.put and related methods.
 *
 * @param hash                     key的hash值
 * @param key                      key的真实值
 * @param value                    value的真实值
 * @param onlyIfAbsent             如果为true,就不修改已经存在的value(默认为false)
 * @param evict                    如果是false,就创建table(默认传true)
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //1.tab为空则创建(也就是说在第一次调用put()的时候它才创建table)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //2.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
        //如果不是那么就在table[i]后面挂的链表中找
    else {
        Node<K,V> e; K k;
         //3.如果key存在,那么直接覆盖value(在table中判断)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断该节点是链表还是红黑树
        else if (p instanceof TreeNode)
            //如果是红黑树是就调用putTreeVal()
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 5.如果是链表
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //链表长度大于8转换为红黑树进行处理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // key已经存在直接覆盖value(在链表中判断)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
     // 6.超过最大容量 就扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

总结:

hashmap的存主要分为6步:

  1. 判断table是否为null
  2. 根据key的hash值看table中是否存在相同的hash值,如果有就在链表里找,如果没有就新建一个节点在table里
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,如果不一样就遍历链表,遍历完链表以后执行插入逻辑(jdk1.7以前使用头插,jdk1.8使用尾插)
  4. 判断该节点是红黑树还是链表,如果是红黑树跳转至putTreeVal()处理,否则,按链表处理
  5. 遍历链表并记录链表长度,如果链表长度大于8,就树化。
  6. 查看存在的键值对是否数量过多,如果过多就扩容

4.你了解hashmap的扩容机制嘛?

扩容这个过程,我看网上很少有讲扩容源码的,大部分是讲扩容的方法,这里总结一下扩容的方法:

一个关于扩容的小故事:

那时候我还在学C语言,给我讲哈希的陈老师问我们:“hash有一个致命的缺点就是hash冲突,如果hash冲突过多了怎么办?”

“扩容”

”那么扩容怎么扩?“

“......”

"把所有的数据拿出来,重新hash一遍"

“怎么会有这么煞笔的算法”

这段简短的对话在后期解决了我的很多疑惑。

hash扩容的关键就是“怎么扩”和“什么时候扩”

  1. 关于“怎么扩“

    把hash的数据全部拿出来,重新hash一遍。

    但是这个前提下我们可以优化整个过程,这里非常敬佩写hashmap的大神,真的牛批,难怪都爱考1.8的hashmap,这个hashmap的写法是真的是相当的优秀,简直让人......(此处略却10000字赞叹只留下一句“卧槽”),跪舔结束,来看下真正的王者编码

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        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 threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            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) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((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;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.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;
    }
    
  1. 什么时候扩?

    hashmap扩容是因为hash冲突过高,而hash冲突过高在HashMap中有一个默认的负载因子(0.75),当数组的大小占到了总大小的0.75就会触发扩容机制。在jdk1.8以后规定了hashmap的扩容大小为每次扩到一倍,也就是*2(具体为什么要扩大两倍,是因为一个非常巧妙地扩容算法,这里不多)。(这里的两次循环可以证明,扩容机制是把所有的Entry都拿了出来重新的放了一遍)

总结:

在jdk1.7以前,hashmap的建造方式是:Entity+数组+链表,jdk1.8建造方式:Node+数组+红黑树

一个小问题:

为什么要用红黑树来替代链表?

首先如果链表的长度超过8了,就会被树化。选用红黑树是因为分析一下它的竞争对手

  1. 二叉搜索树,存在最坏情况,导致搜索效率变低
  2. AVL树,需要满足左右高度条件,使得插入麻烦
  3. 中庸选择红黑树,红黑树是低配的AVL树

5.hashmap是线程安全的嘛?如果想要线程安全的hashmap怎么办?

hashmap是线程不安全的类,如果想要线程安全的可以使用hashtable,但是hashtable的key和value是不能为null的,还可以使用Collections工具类制造一个synchronizedHashMap接口,或者使用ConcurrentHashMap来获得一个线程安全的hashmap。hashmap线程不安全主要因为:在插入的时候,如果不加锁,两个线程同时进行扩容的时候会形成一个环形链表,造成死锁,所以如果想要线程安全加锁的话,就加在put方法上就好了。

6.concurrentHashMap了解过嘛?

了解过,concurrentHashMap是线程安全的HashMap类,在jdk1.7以前它采用的是:segment分段锁来保证线程安全的,在jdk1.8以后它采用CAS算法来保证线程安全(下一章讲“线程锁”会提到)。这里主要讲segment,segment的源码如下

static class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    final float loadFactor;
    Segment(float lf) { this.loadFactor = lf; }
}

首先segment是ReentratLock的实现类(下一章会讲ReentratLock)。这里主要讲一个过程:

  1. 先算出插入数据的hash值
  2. 根据此hash值计算出线程要拿的segment锁
  3. 拿到锁以后对锁内的数据进行操作,用完释放锁

总结

线程安全实在是太重要了,所以下一节细讲。其实考察的点也就三个:存储结构,插入方式,线程安全,其中主要的变化是jdk1.7到jdk1.8的变化:

jdk1.7:数组+链表+segment

jdk1.8:数组+链表+红黑树+CAS

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容