Java集合 HashMap底层实现详解

在前面几篇文章中,我们也已经学习了关于List、Set的常用集合,今天学习最常用的Map集合:HashMap。
在学习HashMap之时,首先应该清楚明白:
HashMap的工作原理: HashMap基于hashing原理,通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用对象的hashCode()方法来计算hashCode,然后找到bucket位置来存储对象(其实存储的格式依然是K-V格式,后面会介绍)。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象会以链表的方式存储在每一个bucket位置上。

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前要弄清楚它们之间的分别。主要的区别:线程安全性,同步(synchronization),以及速度。

  • HashMap几乎可以等价于Hashtable,除了HashMap是线程非安全的,Hashtable是线程安全的,HashMap可以接收null的键和值,而Hashtable不可以。
  • Hashtable是线程安全的,多个线程可以共享一个Hashtable,而如果没有正确同步的话,多个线程是不能共享HashMap的。Java5提供了ConcurrentHashMap,它正是HashTable的替代,比HashTable的扩展性更好。
  • 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器(fail-fast机制是Java集合中的一种错误机制,即当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。),而HashTable的enumerator迭代器不是fail-fast。所以当其他线程改变了HashMap的结构(增加或删除),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出该异常。但这并不是一个一定发生的行为,要看JVM。
  • 由于Hashtable是线程安全的,所以单线程环境下它比HashMap要慢。如果你不需要进行同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。

注意:

  • Fail-safe和iterator迭代器相关。如果某个集合创建了Iterator或者ListItreator,然后将其他的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其他线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。
  • 结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。

HashMap的基本属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16 
static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75,即当空间使用了75%就进行扩容操作
static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默认数组
transient int size;     //HashMap中元素的数量
int threshold;          //判断是否需要调整HashMap的容量

结构存储示意图:
Java集合 HashMap底层实现详解_第1张图片主要方法解析:

**添加方法 **

 public V put(K key, V value) {
        if (table == EMPTY_TABLE) { //是否初始化
            inflateTable(threshold);
        }
        if (key == null) //放置在0号位置
            return putForNullKey(value);
        int hash = hash(key); //计算hash值
        int i = indexFor(hash, table.length);  //计算在Entry[]中的存储位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i); //添加到Map中
        return null;
}

在添加方法中,添加键值对时,首先对table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。然后进行key是否为null的判断,如果key==null,将元素放在Entry[]的0号位置。计算在Entry[]中的存储位置,判断是否有相同的key,如果存在,即用新的value替换原有的value,并将旧值返回。如果key不存在于HashMap中,程序继续执行,将key-value插入到Map中。

获取方法 get

public V get(Object key) {
     if (key == null)
         //返回table[0] 的value值
         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;
     }

     int hash = (key == null) ? 0 : hash(key);
     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;
}

在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值

删除操作 remove

public V remove(Object key) {
     Entry<K,V> e = removeEntryForKey(key);
     return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
     if (size == 0) {
         return null;
     }
     int hash = (key == null) ? 0 : hash(key);
     int i = indexFor(hash, table.length);
     Entry<K,V> prev = table[i];
     Entry<K,V> e = prev;

     while (e != null) {
         Entry<K,V> next = e.next;
         Object k;
         if (e.hash == hash &&
             ((k = e.key) == key || (key != null && key.equals(k)))) {
             modCount++;
             size--;
             if (prev == e)
                 table[i] = next;
             else
                 prev.next = next;
             e.recordRemoval(this);
             return e;
         }
         prev = e;
         e = next;
    }

    return e;
}

删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则开始遍历列表。定义了三个Entry引用,分别为pre, e ,next。 在循环遍历的过程中,首先判断pre 和 e 是否相等,若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。若形成了pre -> e -> next 的连接关系,判断e的key是否和指定的key 相等,若相等则让pre -> next ,e 失去引用。

是否能让HashMap同步?

HashMap时可以通过手动进行同步,Map map = Collections.synchronizeMap(new HashMap());
HashMap和HashSet的区别是经常问到的问题,两者都是Collection框架的一部分,它们让我们能够使用对象的集合。Collection框架有自己的接口实现,主要分为Set接口,List接口和Queue接口。Set接口不允许对象有重复的值,List接口允许有重复的,Queue的龚总原理是FIFO。

什么是HashMap

HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null,HashMap是非Synchronized的,但Collection框架提供方法能保证HashMap同步,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。

HashMap和HashSet的区别
HashMap HashSet
HashMap实现了Map接口 HashSet实现了Set接口
HashMap存储键值对 HashSet仅仅存储对象
使用put()方法将元素方法map中 使用add()方法将元素放入set中
HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap比较快,因为使用唯一的键来获取对象 HashSet较HashMap来说比较慢
HashMap死锁形成原理

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用线程安全的ConcurrentHashMap。

要理解HashMap死锁形成的原理,我们要对HashMap的resize里的transfer过程有所了解,transfer过程是将旧数组中的元素复制到新数组中,在Java 8之前,复制过程会导致链表倒置,这也是形成死锁的重要原因(Java 8中已经不会倒置)。死锁会在这种情况产生:两个线程同时往HashMap里放Entry,同时HashMap正好需要扩容,如果一个线程已经完成了transfer过程,而另一个线程不知道,并且又要进行transfer的时候,死锁就会形成。 在形成链表换以后再对HashMap进行Get操作时,就会形成死循环。

在Java 8中对这里进行了优化,链表复制到新数组时并不会倒置,不会因为多个线程put导致死循环,但是还有很多弊端,比如数据丢失等,因此多线程情况下还是建议使用ConcurrentHashMap。

总结:
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做一个Entry。这些Entry分散存储在一个数组当中,这个数组就是HashMap的主干。因为table数组的长度是有限的,再好的hash函数也会出现index冲突的情况,所以我们用链表来解决这个问题,table数组的每一个元素不只是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过Next指针指向下一个Entry节点。当新来的Entry映射到冲突数组位置时,只需要插入对应的链表即可。

需要注意的是:新来的Entry节点插入链表时,会插在链表的头部,因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
所以在JDK1.8之后,采用的方式:数组 + 链表 + 红黑树 的存储方式
Java集合 HashMap底层实现详解_第2张图片

【参考】1、 https://xinxingastro.github.io/2018/05/11/Java/HashMap底层实现原理/
2、https://blog.csdn.net/suifeng629/article/details/82179996

你可能感兴趣的:(Java面试)