面试系列 - HashMap

HashMap 详解

简介

HashMap 是一种Map集合,他的数据结构是key_value的形式。

底层数据结构

HashMap的底层数据结构是List + 链表(java7 , java8之后当链表对长度超过8 & list的长度大于64 之后就会转化为红黑树)。
每个节点的对象是(key + value + next) 构成的

存取原理

存:
对key做hash处理。然后根据hash处理得出来的index找到对应的index 如果对应的index != null 就遍历链表,存在即覆盖,否则就添加
取:

public V get(Object key) {
    
    
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }
final Entry<K,V> getEntry(Object key) {
    
    
            
        if (size == 0) {
    
    
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
    
    
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }  

取的时候 如果重写equles方法,改变equals规则,也要重写hashCode方法。不然equals是true,但是按照之前的hashCode方法就可能会返回false。

Hash的公式

index = hash(HashCode(key)) &(Length - 1)
对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀

获取:
根据key处理得出来的index找到相应的元素,如果是链表,就遍历然后做数据处理

java7和java8的区别

相同hash-index的值插入方式不一样
1.7是头插法,1.8是尾插法
因为HashMap是线程不安全的,使用头插法会造成环
扩容是是按照链表头->尾进行扩容的,如果头的对象->next指针的线程没有重定向就停止了,那么此时下一个线程执行 对象->next->next 指向对象。这样就会形成环。
当1.8改成尾插法之后就不会出香这个问题

1.8还更新了链表的存储方式。当链表value>8 & list.size>64的时候 链表会转换为红黑树。当条件不满足的时候,红黑树会再变为链表

为什么会线程不安全

进行插入操作的时候不是原子性的,没有加锁会导致问题。
1.7 环 + 两次get到的值不一样
1.8 put or get不同步

默认初始化大小?为啥是这么多?为啥大小都是2的幂?

16 2进制是1111
反观长度16或者其他2的幂,length - 1的值是所有二进制位全为1,这种情况下,index的结果等同于hashcode后几位的值
只要输入的hashcode本身分布均匀,hash算法的结果就是均匀的
2的幂等 和 hash值 & 都是hash的后几位 如果hash有规律,那么最后的index也有规律。是均匀的

HashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。我们也能看到这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换),个人理解。

所以,HashMap的默认长度为16,是为了降低hash碰撞的几率

HashMap的扩容方式?负载因子是多少?为什是这么多?

底层List在扩容的时候是按照2的幂次方来进行扩容的,扩容之后,原来的key-value会重新计算hash-index。

官方给出的解释是 0.75
原因:0.75的时候刚好可以在利用率和hash碰撞率之间达到一个最高的平衡

刚好分布均匀 太小会提升碰撞几率 太小会降低数组的利用率

有什么线程安全的类代替么?

ConcurrentHashMap
volatile实现线程安全
1.8线程安全实现:

//会发现源码中没有一处加了锁
public V get(Object key) {
    
    
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //计算hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
    
    //读取首节点的Node元素
        if ((eh = e.hash) == h) {
    
     //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
    
    //既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
 }

用volatile修饰的Node 实现线程安全

static class Node<K,V> implements Map.Entry<K,V> {
    
    
    final int hash;
    final K key;
    //可以看到这些都用了volatile修饰
    volatile V val;
    volatile Node<K,V> next;

    Node(int hash, K key, V val, Node<K,V> next) {
    
    
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    public final K getKey()       {
    
     return key; }
    public final V getValue()     {
    
     return val; }
    public final int hashCode()   {
    
     return key.hashCode() ^ val.hashCode(); }
    public final String toString(){
    
     return key + "=" + val; }
    public final V setValue(V value) {
    
    
        throw new UnsupportedOperationException();
    }

    public final boolean equals(Object o) {
    
    
        Object k, v, u; Map.Entry<?,?> e;
        return ((o instanceof Map.Entry) &&
                (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                (v = e.getValue()) != null &&
                (k == key || k.equals(key)) &&
                (v == (u = val) || v.equals(u)));
    }

    /**
     * Virtualized support for map.get(); overridden in subclasses.
     */
    Node<K,V> find(int h, Object k) {
    
    
        Node<K,V> e = this;
        if (k != null) {
    
    
            do {
    
    
                K ek;
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
            } while ((e = e.next) != null);
        }
        return null;
    }
}

HashMap是怎么处理hash碰撞的?

1.7 链表
1.8 链表 + 红黑树

猜你喜欢

转载自blog.csdn.net/automal/article/details/107581553