面试(一)-HashMap

简介: HashMap

一、前言


     其实这一面来的挺突然,也是意想不到的,这个要起源于BOSS直聘,很巧,其实也算是一种缘分吧,谢谢BOSS那个哥们,还是那句话来滨江我请你吃饭,身怀感激你总会遇到帮助你的人,只是这个我没有想到,我转Java也没多久,很多东西也没有搞清楚,没想到菜鸟会给我这个电话,内心是震惊的,但是也感谢给我这个机会让我真正认清自己,谢谢菜鸟,那我现在将这份迟到答卷交一交吧。


二、试卷


     1、HashMap和Hashtable区别在哪?

     2、Synchronized是怎么实现同步的?

     3、Spring IOC的启动流程?另外还说一下BeanFactory和ApplicationContext的区别?

     4、拦截器和过滤器的区别?

     5、单点登录?

     6、RabbitMQ怎么保证消息到达的?

     这6个问题其实我感觉就第5个问题回答的比较好,剩下都是似懂非懂,这可能是对于我的自满的一个警钟,还是那句话深挖基础,来年再战,接下来我们来用来深耕一下这些问题,给自己再交一份满意的答卷。

     第一个问题我们从基层结构、对null值的处理、数据结构以及线程安全这4个方面结合源码来谈一下:

    1.继承结构

     Map总体继承:

     1005447-20180317134226987-559353133.png

     HashMap的继承结构:

     1005447-20180317134315440-54134849.png

     HashTable的继承结构:

     1005447-20180317134418817-886753252.png

     对比下实现,不同点主要在基类上继承上,对比下暴露出的API主要是Hashtable多elements和contains方法,elemets主要是返回所有的value的值,为空的时候返回空枚举对象,而contains方法和containsValue方法都是查询是否包含value的值,剩下方法基本上都是相同。

     2.对null的处理

     Hashtable对null的处理,如果发现为null会报空引用异常;

public synchronized V put(K key, V value) {
        // value为空引用异常
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        //key计算的时候同样为空引用异常
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        addEntry(hash, key, value, index);
        return null;
}

 

  HashMap对Null的处理以及添加元素的整个流程:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
        //key为null则默认hashcode为0
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断一下是否为第一次插入,如果是第一次插入则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
       //根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//对应的节点存在
            Node<K,V> e; K k;
           //判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
           //判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
            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);
                        //如果个数大于8则转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果key值存在覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //找到或者新建key和hashcode相等的键值进行插入
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //onlyIfAbsent为false或旧值为null时,允许替换旧值
                //这个地方有点不明,明明啥时候都为false
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  1005447-20180317145441243-757652974.png

 3.数据结构

     1005447-20180317150233699-104761821.png

      总体上两者的数据结构都是通过数组+链表实现,但是JDK1.8以后当链表的长度超过8以后会更改为红黑树,都是通过继承Entry数组实现哈希表;

//Hashtable节点定义    
private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }
        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }
        // Map.Entry Ops
        public K getKey() {
            return key;
        }
        public V getValue() {
            return value;
        }
        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }
//HashMap节点定义  
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

      初始化Hashtable默认初始化的长度为11,加载因子为0.75;HashMap默认为16,加载因子为0.75

      扩容Hashtable为2n+1,HashMap为2倍,另外HashMap如果为整数的最大值则就不会扩容,这块是JDK8实现的别的还请自己去查看一下;

      另外就是算法问题,JDK8中实现高位运算的算法,相比Hashtable的去模算法来具有更高的效率;具体看下https://tech.meituan.com/java-hashmap.html

      扩容算法也有必要看下;

//Hashtable默认构造函数 
public Hashtable() {
        this(11, 0.75f);
 }
//Hashtable扩容
 protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
        // overflow-conscious code
        //位运算相当于2n+1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }
//HashMap扩容
 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;
            }
            //新容量调整旧容量的2倍
            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;
        //初始化默认为16
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 计算新的resize上限
        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;
    }

    4.线程安全性

     主要针对这块说一说,我就是死在这里的,首先明确的是Hashtable肯定是线程安全的,因为使用Synchronized修饰,所以内部调用的时候是线程安全的;接下主要说下HashMap,这个其实面试官感觉我能回答上来,还好几次问我你确定嘛,我说我确定,哈哈当时也真是,我没说出死循环这个点来,接下来我们分析下为什么会照成死循环?同样上面那个美团技术博客也是有这部分讲解的,这部分JDK8确实写的很麻烦,我们使用JDK7来看下怎么照成的,站到巨人的肩膀上,消化为自己的;

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        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);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}

    以上为主要代码,接下来我们来谈下照成死循环的过程,下面这段代码是主要的罪魁祸首:

    1005447-20180318132509126-721167549.png

     首先来看下单线程下运行状况,代码如下,过程如图:

     1.扩容的时候首先遍历数组中的元素;

     2.对链表的每一个节点进行遍历,next指向下一个要移动元素,将e转向转向Hash表的头部,使用头插入元素;

     3.循环到链表为null结束这次循环;

     4.最终等数组循环完毕,完成HashMap扩容;

 HashMap<Integer,String> map = new HashMap<>(2,0.75f);
        map.put(5, "C");
        map.put(7, "B");
        map.put(3, "A");

1005447-20180318132828597-318065220.png  

思考一个问题,当多线程操作做时候,这块不是原子操作,所以肯定会出现问题,假设这样一种情况,代码如下:

HashMap<Integer,String> map = new HashMap<>(2,0.75f);
        map.put(5, "C");
        new Thread("Thread1") {
            public void run() {
                map.put(7, "B");
                System.out.println(map);
            };
        }.start();
        new Thread("Thread2") {
            public void run() {
                map.put(3, "A");
                        System.out.println(map);
            };
        }.start();
        map.put(11,"D");

1005447-20180318144334831-480962773.png

      当线程1和线程2同时进入put方法,进入transfer()该方法时候,线程1执行到"Entry<k,v> next = e.next"然后被挂起,线程2开始执行resize()方法,最终形成如下:这部分可以使用多线程调试进行模拟,不会的请参考:http://blog.csdn.net/kevindai007/article/details/71412324

     

       这个时候线程1被唤醒,执行步骤如下:

       1.执行e.next = newTable[i],这个时候 key(3)的 next 指向了线程1的新 Hash 表,所以e.next=null;

       2.执行newTable[i]=e,e = next,导致e指向了key(7);

       3.下一次循环的next = e.next导致了next指向了key(3)。

       4.e.next = newTable[i] 导致 key(3).next 指向了 key(7),环形链表就这样出现了。

     

         1005447-20180318143156220-683105798.png 

        另外还有一种情况,也就是常见的更新丢失问题的,当2个线程同时插入到数组同一个位置的时候,线程A也写入,线程B也写入,则会照成B写入的覆盖A写入的,照成更新丢失,执行如下代码,这种情况不是很容易出现,多执行几次代码会发现,截图如下:

HashMap<String,String> map=new HashMap<>();
        //线程1
        Thread t1 = new Thread(){
            public void run() {
                for(int i=0; i<25; i++){
                    map.put(String.valueOf(i), String.valueOf(i));
                }
            }
        };
        //线程2
        Thread t2 = new Thread(){
            public void run() {
                for(int i=25; i<50; i++){
                    map.put(String.valueOf(i), String.valueOf(i));
                }
            }
        };
        t1.start();
        t2.start();
        Thread.currentThread().sleep(1000);
        //System.out.print(map.values());
        for(int i=0; i<50; i++){
            //如果key和value不同,说明在两个线程put的过程中出现异常。
            if(!String.valueOf(i).equals(map.get(String.valueOf(i)))){
                System.err.println(String.valueOf(i) + ":" + map.get(String.valueOf(i)));
            }
        }

1005447-20180318152116862-1415577337.png

三、结束语


      这几个问题要好好深耕一下的话,估计1篇或者几篇是写不完的,我会慢慢写,请大家耐心等待吧,先进行一个预告,下一篇我会再把第一题发散一下:HashMap和ConcurrentHashMap对比,上面有不明白的地方可以找我,QQ群:438836709

相关文章
|
15天前
|
存储 缓存 安全
面试题-HashMap底层原理与HashTable的区别
字节跳动面试题-HashMap底层原理与HashTable的区别
34 0
|
7月前
|
存储 Java
每日一道面试题之谈一谈HashMap和HashSet的区别
每日一道面试题之谈一谈HashMap和HashSet的区别
|
7月前
|
存储 索引 容器
每日一道面试题之介绍一下HashMap~
每日一道面试题之介绍一下HashMap~
|
8月前
|
存储 算法 安全
拜托,面试请不要再问我HashMap了
拜托,面试请不要再问我HashMap了
38 0
|
7月前
|
搜索推荐 Java API
一道Java集合排序题,HashMap排序,面试必备
一道Java集合排序题,HashMap排序,面试必备
|
9天前
|
Python
2024年Python最新刷爆全网的动态条形图,原来5行Python代码就能实现!,2024年最新Python面试必问的HashMap
2024年Python最新刷爆全网的动态条形图,原来5行Python代码就能实现!,2024年最新Python面试必问的HashMap
2024年Python最新刷爆全网的动态条形图,原来5行Python代码就能实现!,2024年最新Python面试必问的HashMap
|
15天前
|
存储 算法 Java
如果面试也能这样说HashMap,那么就不会有那么多遗憾!(中)
如果面试也能这样说HashMap,那么就不会有那么多遗憾!
32 0
|
15天前
|
Java API
面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
面试官上来就让手撕HashMap的7种遍历方式,当场愣住,最后只写出了3种
23 1
|
15天前
|
存储 算法 Java
耗时3天写完的HashMap万字解析,争取一篇文章讲透它,面试官看了都直点头!
耗时3天写完的HashMap万字解析,争取一篇文章讲透它,面试官看了都直点头!
53 3
|
7月前
|
设计模式 算法 Java
面试官:JDK1.8 HashMap扩容rehash算法是如何优化的?
本文跟大家聊一聊一个常见的面试题,那就是JDK1.8 HashMap扩容rehash算法是如何优化的?