HashMap详细解析,高频面试题解答,源码阅读

HashMap 详解

下面我首先抛出以下问题,让我们带着这些问题开始解析 HashMap:

  1. JDK8 中 HashMap 有哪些改动?
  2. JDK8 为什么要使用红黑树?
  3. 为什么重写对象的 Equals 方法,要重写 HashCode方法,跟 HashMap有关系吗?
  4. HashMap 是线程安全的吗? 遇到过 ConcurrentModificationException 异常吗?为什么会出现,如何解决?
  5. 在使用 HashMap 过程中,我们应该注意什么?
  6. 手写HashMap?
  7. 你知道 HashMap 的工作原理吗?
  8. HashMap 中能 PUT 两个相同的 key 吗?为什么能或为什么不能?
  9. HashMap 中的键值可以为 NULL?能简单说一下原理吗?
  10. HashMap 扩容机制是怎么样的, JDK 7与 JDK 8 有什么不同吗?

原先是打算写一篇介绍,但是发现了一篇博客写的非常棒,我就不重复造轮子了。给出链接Java 8系列之重新认识HashMap

结合博客,再给出文章开始给出的问题的解答

JDK 8 中 HashMap 有哪些改动?

HashMap 在原有的基础上增加了*红黑树 *结构,当链表长度大于8会转换为红黑树进行处理,当结点数目小于 6 则会还原成链表。

JDK 8 为什么要使用红黑树?

首先红黑树遍历的时间复杂度为 O(logn),长度为8 时,平均查找长度是 3 ,而此时链表的平均查找长度是 4,所以转换为树在遍历、查找时性能会得到提升。
不过这个答案并不严谨,选择数字 8 的原因如下文所述。

0: 0.60653066 
1: 0.30326533 
2: 0.07581633 
3: 0.01263606 
4: 0.00157952 
5: 0.00015795 
6: 0.00001316 
7: 0.00000094 
8: 0.00000006 

理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布,具体可以查看泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。

为什么重写对象的 Equals 方法,要重写 HashCode方法,跟 HashMap有关系吗?

Equals 方法是用于比较对象是否相等,通过源码可以得知, HashMap 中结点的比较是根据 key 和 value 是否同时相等判断的。

 public final boolean equals(Object o) {
     if (o == this)
     return true;
     if (o instanceof Person) {
         Person e = (Person)o;
         if (Objects.equals(id, e.getId()) &&
         Objects.equals(name, e.getName()))
         return true;
     }
     return false;
 }

假设我们现在只重写 Equals 方法,而不重写 HashCode方法

 public final boolean equals(Object o) {
     if (o == this)
     return true;
     if (o instanceof Person) {
         Person e = (Person)o;
         if (Objects.equals(id, e.getId()))
         return true;
     }
     return false;
 }

那么这种情况下,按照代码而言应该是,只要 Person 对象的 id 相等,则默认他们是相等的,但是当我们这么操作

Person p1 = new Person("1", "zhangsan");
Person p2 = new Person("2", "lisi");
// 此时 p1 和 p2 是相等的
System.out.println(p1.equals(p2));
HashMap map = new HashMap();
map.put(p1, "test");
System.out.println(map.get(p2));

按照道理来说这里应该是可以获取到 test ,然而事实上返回的是 null,因为在 HashMap 在进行 put() 和 get() 取出时都是通过 hash() 方法来获取对象的 hash 值,确定数组的下标。当重写了 hash 方法就可以确定 hash 值相同。

HashMap 是线程安全的吗? 遇到过 ConcurrentModificationException 异常吗?为什么会出现,如何解决?

不是线程安全的,需要线程安全可以采用 ConcurrentHashMap 。
ConcurrentModificationException 异常时由于 modCount 和 exceptModCount 不同,这里使用的是 fastfial 机制,出现错误直接抛出异常。而 iterater 中的 remove() 方法则可以保证不出现异常。

在使用 HashMap 过程中,我们应该注意什么?

使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。

如果能够确定大小,在初始化的时候一定要给出,避免不必要的扩容操作。

手写HashMap?

public class MyHashMap<K,V> {

    // 数组的长度    默认数组长度是 16
    private static final int length = 8;
    // HashMap 的大下
    private int size;
    private Entry<K, V>[] table;

    // 1. 实现构造方法
    public MyHashMap() {
        table = new Entry[length];
    }

    public V put(K key, V value) {
        int hash = key.hashCode();
        int index = hash % length;
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            // 如果存在相同的 key 直接将原先的覆盖
            if (entry.key.equals(key)) {
                V oldValue = entry.value;
                entry.value = value;
                return oldValue;
            }
            entry = entry.next;
        }
        // 结点中并没有
        addEntry(key, value, index);
        return value;
    }

    private void addEntry(K key, V value, int index) {
        size++;
        table[index] = new Entry<>(key, value, table[index]);
    }

    public int size() {
        return size;
    }

    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % length;
        if (table[index] == null) {
            return null;
        }
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.key.equals(key)) {
                return entry.value;
            }
        }
        return null;
    }

    public void clear() {
        // 不为空则全部赋给 null, 将 size 修改为 o
        Entry<K, V>[] newTable;
        if ((newTable = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < newTable.length; ++i) {
                newTable[i] = null;
            }
        }
    }

    class Entry<K, V>{
        private K key;
        private V value;
        private Entry<K, V> next;

        public Entry(K key, V value, Entry<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
}

你知道 HashMap 的工作原理吗?

这个题目相对比较宽泛,怎么回答都可以。主要考察的是个人对于 HashMap 掌握的深度。

HashMap 中能 PUT 两个相同的 key 吗?为什么能或为什么不能?

可以,put 两个相同的 key 的时候,会将之前 put 的值替换掉。

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;

HashMap 中的键值可以为 NULL?能简单说一下原理吗?

key 是可以为 null 的,HashMap 插入 值时会先计算 key 的 hash 值,我们可以看到当 key 为 null 时会返回 0, 也就是说位于数组的下标 0 处。

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

HashMap 扩容机制是怎么样的, JDK 7与 JDK 8 有什么不同吗?

这个问题我也理解的不是很清楚,有兴趣的可以好好研究一下给出的参考博客。

参考链接:
Java 8系列之重新认识HashMap

HashMap, ConcurrentHashMap 原理及源码,一次性讲清楚!

高并发编程:HashMap 深入解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章