美文网首页
HashMap的技术细节

HashMap的技术细节

作者: Phoebe_Liu | 来源:发表于2018-10-22 19:04 被阅读0次

    关键词: java;面试;笔试;hashMap

    HashMap 的数据结构?基本原理?

    1. HashMap 底层是一个散列通,由数组+单向链表实现。HashMap 通过 put & get 方法存储和获取。
    2. HashMap在每个链表节点中储存键值对对象。
      HashMap是基于哈希表的Map接口的非同步实现。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。


      hashmap 散列桶

    HashMap两个重要的参数:初始容量、负载因子

    1. 容量表示哈希表中桶的数量 (table 数组的大小),初始容量是创建哈希表时桶的数量;
    2. 负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
    3. 对于使用 拉链法(下文会提到)的哈希表来说,查找一个元素的平均时间是 O(1+a),a 指的是链的长度,是一个常数。特别地,若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子为 0.75,这是时间和空间成本上一种折衷,一般情况下我们是无需修改的。

    hashmap的节点Node + 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;
            }
    }
    

    hash方法:

    总的来说分为三步骤:

    1. 调用hashCode()
    2. 计算hash=(h = key.hashCode()) ^ (h >>> 16): 如果不做异或,那么高16位信息可能丢失。有了异或,高16位于低16位信息混合后 都得到保留。
    3. 计算下标index=(n-1)&hash
      hash的计算规则:
     static final int hash(Object key) {
             int h;
             return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         }
    

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

    static int indexFor(int h, int length) {
            // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
            return h & (length-1); // 相当于计算余数
        }
    

    put函数的实现原理

    1. 当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为 bucketIndex。通过《Java 中的 ==, equals 与 hashCode 的区别与联系》 所述hashCode的协定可以知道,如果两个对象的hashCode不同,那么equals一定为 false;否则,如果其hashCode相同,equals也不一定为 true。所以,理论上,hashCode 可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals() 来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到
    2. 对key的hashCode()做hash,然后再计算(h = key.hashCode()) ^ (h >>> 16);
      高16bit不变,低16bit和高16bit做了一个异或
    3. 如果没碰撞直接放到bucket里;
    4. 如果碰撞了,以链表的形式存在buckets后;
    5. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD 在jdk1.8里 阈值是8),就把链表转换成红黑树;
    6. 如果节点已经存在就替换old value(保证key的唯一性)
    7. 如果bucket满了(超过load factor*current capacity),就要resize。
    public V put(K key, V value) {
        // 对key的hashCode()做hash
        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;
        // tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 计算index,并对null做处理
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 节点存在
            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 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
        // 超过load factor*current capacity,resize
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    get函数实现

    1. bucket里的第一个节点,直接命中;
    2. 如果有冲突,则通过key.equals(k)去查找对应的entry
      1. 若为树,则在树中通过key.equals(k)查找,O(logn);
      2. 若为链表,则在链表中通过key.equals(k)查找,O(n)。
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
     
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 直接命中
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 未命中
            if ((e = first.next) != null) {
                // 在树中get
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 在链表中get
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    resize

    1. 为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。
    2. loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
    3. 我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:
      image

    你知道get和put的原理吗?equals()和hashCode()的都有什么作用?

    通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

    如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

    如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法

    HashMap,LinkedHashMap,TreeMap 有什么区别?

    LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
    TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

    HashMap & TreeMap & LinkedHashMap 使用场景?

    HashMap:在 Map 中插入、删除和定位元素时;
    TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
    LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。

    HashMap 和 HashTable 有什么区别?

    1. HashMap 是线程不安全的,HashTable 是线程安全的;
    2. 由于线程安全,所以 HashTable 的效率比不上 HashMap;
    3. HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable 不允许;
    4. HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
    5. HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

    我们能否让HashMap同步?

    HashMap可以通过下面的语句进行同步:
    Map m = Collections.synchronizeMap(hashMap);

    相关文章

      网友评论

          本文标题:HashMap的技术细节

          本文链接:https://www.haomeiwen.com/subject/muzgzftx.html