源码解析jdk7.0-HashMap的底层实现原理

源码解析jdk7.0-HashMap底层实现原理


一、HashMap的概述

HashMap是Map接口的实现类,键值对存储(基于哈希表的映射:根据指定的键,可以获取对应

的值),并允许null作为键和值,线程不安全,即方法为非同步方法。

二、HashMap的存储结构

  1. Java编程语言中,最基本的两种结构:数组和链表(引用模拟指针),所有的数据结构都可以用这两种基本结构进行构建。数组的特点:寻址容易,插入和删除难;而链表的特点是:寻址困难,插入和删除容易。

  2. 综合数组和链表两者的特点,HashMap(直译为散列表,音译为哈希表)采用数组+链表的存储方式。

    底层结构是一个数组(默认长度为16),而数组元素是一个单向的链表,每一个数组存储的元素代表的是每一个链表的头结点,结构如下: [外链图片转存失败(img-bzWwPDWs-1568894178648)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1568889420594.png)]

三、HashMap内部实现原理机制(源码解析)

  1. HashMap的基本元素

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable{
          
          
        /*
        	默认的初始容量为:16
        	1 << 4 代表将1左移4位:2^4 = 16
        */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        //最大容量为2^30 = 1024*1024*1024 = 1073741824
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        /* 默认的负载因子
        	负载因子表示一个散列空间的使用程度。
        	当向集合容器中添加元素的时候,会判断当前容器的个数:
        	如果当前容器的个数 > 阈(yu)值:即底层数组长度*负载因子
        */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    	// 空表(空数组)
        static final Entry<?,?>[] EMPTY_TABLE = {
          
          };
    
        /**
           以Entry<K,V>为元素的数组,也就是上图HashMap的纵向的长链数组,
           起长度必须为2的n次幂
         */
        transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    
        // Map集合中键值对的个数    
        transient int size;
    
        /* 扩容的临界值(阈值),或者所能容纳的key-value对的极限。
           当size>threshold的时候就会扩容
        */
        int threshold;
    
        // 加载因子
        final float loadFactor;
        
        // 记录对集合修改的次数
        transient int modCount;
    

    (1) DEFAULT_INITIAL_CAPACITY 和 MAXIMUM_CAPACITY

    ​ 通过HashMap的无参数的构造方法创建一个HashMap时,系统会默认使用默认的数组

    ​ 长度为:DEFAULT_INITIAL_CAPACITY(16),但是如果指定长度时,容量不能超过

    ​ MAXIMUM_CAPACITY(2^30)

    (2) DEFAULT_LOAD_FACTOR 和 loadFactor

    ​ 创建HashMap对象时。可以指定负载因子(loadFactor),如果通过无参数的构造方法

    ​ 创建HashMap对象时,则默认的负载因子DEFAULT_LOAD_FACTOR 为 0.75

    ​ 注意:负载因子表示一个散列表的空间使用程度,如果负载因子越大则代表散列表的

    ​ 填装度越高,即能容纳的元素越多,元素多,链表相对会长,所以索引的效率

    ​ 会降低;如果负载因子越小,则代表散列表的填充度越稀疏,此时对空间造成

    ​ 浪费,此时索引效率相对较高。所以负载因子需要合理设置其大小。

    (3) size 和 threshold

    ​ size代表HashMap集合中存储Entry<K,V>的个数(即键值对的个数);

    ​ threshold代表扩容的临界值,即阈(yu)值,来源:底层数组长度 * 负载因子

    ​ 注意: threshold的初始数据和底层数组长度相同,在第一次调用put方法时,将

    ​ threshold的值重新赋值为:capacity(数组长度) * loadFactor(加载因子)

    (4) Entry<K,V>[] table

    ​ Entry<K,V>[] table是HashMap的一个重要组成部分,一个Entry中包含了一个键值对的

    ​ 内容,详见以下源码:

    // 此静态内部类和内部的属性和方法都是包级访问权限
    static class Entry<K,V> implements Map.Entry<K,V> {
          
          
    	final K key;
        V value;
        Entry<K,V> next;
        int hash;
        Entry(int h, K k, V v, Entry<K,V> n) {
          
          
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        public final K getKey() {
          
          
           return key;
        }
        public final V getValue() {
          
          
            return value;
        }
       ....

    从源码中可以看到Entry<K,V>是HashMap类中的一个静态内部类,它既是HashMap底层数

    组的组成元素,又是每一个单向链表的组成元素,并且它包含了元素的key和value,以及链

    表所需要的指向下一个节点的地址区next,存储的具体结构如下:
    在这里插入图片描述

  2. HashMap中的构造方法:

    //可以指定数组长度(容量)和负载因子
    public HashMap(int initialCapacity, float loadFactor) {
          
          
    	if (initialCapacity < 0)
        	throw new IllegalArgumentException(
                    "Illegal initial capacity: "+initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
        	initialCapacity = MAXIMUM_CAPACITY;
         if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " 
                                            + loadFactor);
    
         this.loadFactor = loadFactor;
         threshold = initialCapacity;
         init();
    }
    // 指定数组长度(容量),负载因子为默认的0.75
    public HashMap(int initialCapacity) {
          
          
         this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    // 无参数的构造:都采用默认数据(数组长度为16;负载因子为0.75)
    public HashMap() {
          
          
         this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    // 基于一个Map创建一个新的HashMap对象
    public HashMap(Map<? extends K, ? extends V> m) {
          
          
    	this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);
        putAllForCreate(m);
    }
    
  3. HashMap中存储:put方法的实现

    public V put(K key, V value) {
          
          
        /*
          首次添加,才初始化Entry数组;
          创建HashMap对象时,并没有完成数组的初始化
        */
        if (table == EMPTY_TABLE) {
          
          
            inflateTable(threshold);
        }
        /*
        	HashMap是允许null作为键和值,如果键为null,
        	则调用 putForNullKey方法,将null存放在数组
        	下标0位置上,具体看putForNullKey方法的实现    
        */
        if (key == null)
            return putForNullKey(value);
        
        // 如果键不为null,获取hash值
        int hash = hash(key);
        
        // 根据hash值和数组长度进行运算获取,对应的存储下标
        int i = indexFor(hash, table.length);
        
        /*
           如果对应下标位置上Entry不为null,则表示此位置已经存在,则
           查看当前数组下标和对应的链表中是否存在和存储的key相同的元素,
           存在,则新value覆盖原有value,原有value作为返回值返回。
        */
        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++;
        /* 
           如果当前数组下标位置上Entry为null,或是不存在相同的key,
           则在对应数组下标位置上存储该 键值对(Entry)
        */
        addEntry(hash, key, value, i);
        return null;
    }
    

    putForNullKey方法的实现源码如下:

    // 键为null的存储方法:本类中的访问权限
    private V putForNullKey(V value) {
          
          
        /* 将key=null的键值对存储在数组下标0位置上,
           如果0位置上已经有元素,则遍历数组0下标及对应链表内容,
           查看是否有键为null,
           如果存在key为null的数据,则新值覆盖旧值
        */
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
          
          
            if (e.key == null) {
          
          
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        /* 如果数组0下标对应的所有元素都没有key为null的键,
           则将key为null的数据存储在数组0下标的第一个位置
        */
        addEntry(0, null, value, 0);
        return null;
    }
    

    解析:从put方法的源码中可以看出:当调用HashMap的put方法往集合中添加元素时,先根据key的hashCode计算hash码值,根据hash码值和底层数组的长度获取该键值对在数组中存储的下标:

    (1) **如果数组该下标位置上已经存储了其他元素:**先检测要存储的元素和已经存储元素

    ​ 的键是否有相同的,如果有相同的,则新值覆盖旧值,被覆盖的旧值作为返回值进

    ​ 行返回;如果没有相同的键(对象类型用equals比较的),则数组的这个下标位置将

    ​ 采用数组+链表的形式进行存放,但是需要注意的是:新加入元素放在链表的头部,

    ​ 最早加入的放在链表的尾部。

    (2) 如果数组该下标位置上没有存储其他元素:直接将键值对存储在数组下标位置上即

    ​ 可。

    但是如果key为null,将此键值对存储在数组的0下标位置上,如果0下标上没有元素直

    接存储该键值对,但是如果0下标上已经存在键值对,由于HashMap是要求键不可以

    重复,所以先判断数组0下标及对应的链表中是否存在key为null的Entry(键值对),如果

    存在,则新值覆盖旧值,被覆盖的原有数组作为返回值进行返回,如果不存在,将此键

    值对存储在数组的0下标位置上。


    应用层解析:根据put方法的分析,当HashMap中存储键值对时,仅仅考虑key,完全不考虑value,只是根据key来计算并决定每一个键值对(Entry)存储的位置。如果保证键的不重复性,需要让内容相同的键有一个相同的存储位置,这样会让for循环的过程中if条件成立(此过程中需要调用equals方法),则才能让重复的键对应的值,新值替换旧值;但是如果每存储一个键值对时,都获取相同的存储下标,这样数组的同一个下标对应的链表就会很长,并且每一次存储都需要调用equals方法具体比较键的内容是否相同,则会降低存储的效率,所以为了提高效率,尽可能满足内容不同的键给定一个不同的存储下标,这样可以尽量让HashMap中的元素分布均匀即每一个位置上尽可能一个元素。

    所以如果自定类型的元素作为HashMap的键时,需要覆盖hashCode方法和equals方法:

    (1) 覆盖hashCode方法的原则:

    ​ a. 必须保证内容相同的元素返回相同的哈希码值

    ​ b. 为了提高效率,尽可能做到内容不同的元素返回不同的哈希码值

    (2) equals方法:内容相同的对象返回true。

  4. HashMap的读取:get方法的实现

    public V get(Object key) {
          
          
        //如果键为null,则调用getForNullKey方法
        if (key == null)
            return getForNullKey();
        // 获取键对应的Entry
        Entry<K,V> entry = getEntry(key);
    	// 通过Entry中getValue方法,获取键对应的value
        return null == entry ? null : entry.getValue();
    }
    
    // 被调getForNullKey方法:
    private V getForNullKey() {
          
          
        //如果HashMap中为空,则返回null
        if (size == 0) {
          
          
            return null;
        }
        /*根据put方法讲解,如果key为null,则将此键值对存储
          在数组的0下标位置上,所以get方法时,如果指定的键
          为null,则遍历数值的0下标及对应链表中的内容,找到
          key为null,将对应的值返回
        */
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
          
          
            if (e.key == null)
                return e.value;
        }
        // 如果在数组0下标中不存在键为null的元素,则直接返回null
        return null;
    }
    
    // 被调getEntry方法
    final Entry<K,V> getEntry(Object key) {
          
          
        // 集合为空,则直接返回null
        if (size == 0) {
          
          
            return null;
        }
    	// 获取键对应的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 根据hash值计算对应的下标,通过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;
        }
        //如果对应下标没有相同的key,则返回null
        return null;
    }
    

    解析:从HashMap通过get方法获取元素时,首先计算key的hash码值,找到数组中对应的下标,然后 通过equals方法获取key对应的的Entry,再通过Entry中getValue方法获取键对应的value。如果数组的同一个下标的链表越长,循环次数就会越多,查询的效率相对会低;所以为了尽可能让HashMap存储时,尽可能均匀。

  5. HashMap的扩容:resize方法

    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 addEntry(int hash, K key, V value, int bucketIndex) {
          
          
        if ((size >= threshold) && (null != table[bucketIndex])) {
          
          
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
    
        createEntry(hash, key, value, bucketIndex);
    }
    

    解析:当HashMap中元素逐步增多时,键值对存储时,获取的下标冲突的几率也逐步增高,因为数组的长度是固定的。所以为了提高效率,需要对HashMap的底层数组进行扩容,而在数组扩容之后,消耗性能的操作为:原数组中的数据必须根据每一个元素的key和新数组的长度获取新的存储位置,并进行存储.


    问题:HashMap什么时候进行扩容?

    解析:当HashMap中的元素个数超过threshold(阈值:即数组长度*加载因子[loadFactor])时,需要将数组扩展为 2 * 数组长度,即扩大一倍。

四、Fail-Fast机制(快速失败)

  1. 在使用迭代器遍历集合过程中,对集合中对象的内容进行了修改(增、删、改)会抛出java.util.ConcurrentModificationException(并发修改异常),这就是所谓的Fail-Fast策略。

  2. 实现原理:在HashMap类中定义了modCount属性,记录了对HashMap的修改次数,只要对HashMap中的内容进行修改,都会让modCount增加;在迭代器初始化时 ,会将modCount的值赋值给 expectedModCount ,每次迭代(nextNode操作)时,都会先判断modCount和expectedModCount 的值是否相等,如果不相等则代表HashMap集合被操作,就会抛出java.util.ConcurrentModificationException异常。

    注意:modCount声明为volatile,保证线程之间修改的可见性。

  3. 源码如下:

    abstract class HashIterator {
          
          
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
    
        HashIterator() {
          
          
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) {
          
           // advance to first entry
                do{
          
          }while(index<t.length&&(next=t[index++])==null);
            }
        }
    
        public final boolean hasNext() {
          
          
            return next != null;
        }
    
        final Node<K,V> nextNode() {
          
          
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next=(current=e).next)==null&&(t=table)!=null){
          
          
                do{
          
          }while(index<t.length&&(next=t[index++])==null);
            }
            return e;
        }
    
        public final void remove() {
          
          
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
    

    解析:在HashMap的API中指出:java.util 包中所有集合的迭代器都是Fail-Fast(快速失败)机制,在迭代器创建之后,如果想对元素内容进行修改,则可以通过迭代器本身的remove方法,其他任何修改方式,迭代器都将会抛出java.util.ConcurrentModificationException异常。

    注意:迭代器的Fail-Fast机制不能完全确保,一般来说:存在非同步的并发修改时,不能做出十分保证,快速失败机制尽最大的可能抛出java.util.ConcurrentModificationException,因此开发时不要依赖于此异常进行编写程序,只能作为程序的错误检测。

猜你喜欢

转载自blog.csdn.net/Java_lover_zpark/article/details/101034083