hashMap源码分析JDK1.7

在分析完HashMapJDK1.8之后,今天依然从以下几个方面分析JDK1.7版本的HashMap的底层源码实现,虽然JDK1.7版本下又分几个不同的版本,但都大同小异。

继承结构、基本属性、默认值、构造函数、扩容机制、CRUD操作

1.继承结构分析

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

HashMap继承自AbstractMap父类,实现类Map接口、实现序列化、克隆

2.基本属性、默认值

/*HashMap中的主要参数 = 容量、加载因子、扩容阈值讲解前先对参数解释一下
 1. 容量(capacity): HashMap中数组的长度
 a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
 b. 初始容量 = 哈希表创建时的容量
*/

//默认的容量大小:16->哈希结构中数组的默认值大小
static final int DEFAULT_INITIAL_CAPACITY = 16;

//哈希结构中数组的最大容量上限为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认值:默认的加载因子 -> 怎么用?在哪里使用?为什么这样用?
 

static final float DEFAULT_LOAD_FACTOR = 0.75f;

//HashMap的存储结构,(transient:表示不进行序列化)
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry<K,V> implements Map.Entry<K,V> {
      
      final K key;
      
      V value;
      
      Entry<K,V> next;
      
      int hash;
    }
 
//存储entry节点的个数
transient int size;

//控制扩容的,扩容的阈值 ->在哪使用?怎么使用?
//当哈希表的大小>=扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
//扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
//扩容阈值 = 容量 x 加载因子
int threshold;

// 加载因子
final float loadFactor;

//hash种子
transient int hashSeed = 0;

//修改版本号
transient int modCount;
 

Entry结构存储信息包含:
 
   key:键值对的键
 
   value:键值对的值  

   next:在同一个哈希位置(索引)下的值通过next来连接   

   hash:和key相关的哈希值   key.hashcode

3.构造函数

//第一个构造方法:通过初始容量和状态因子构造HashMap,加载因子&容量=自己指定,其他三个构造方法都//会调用这个方法
public HashMap(int initialCapacity, float loadFactor) {
	//参数校验:初始容量不能小于0,否则抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
//如果自定义的初始容量大于默认的最大容量(1<<30=2^30)则默认最大容量赋值给传入的初始容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
//如果加载因子小于等于0或者加载因子不是数字,抛异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
	//当前加载因子=传入加载因子
        this.loadFactor = loadFactor;
	//扩容变量=传入初始容量
        threshold = initialCapacity;
        init();  //init方法在HashMap中没有实际实现,不过在其子类LinkedHashMap会有对应实现
    }

//第二个构造方法:指定“容量大小”的构造函数,加载因子=默认=0.75,容量=指定大小

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//第三个无参构造方法:装载因子为0.75,容量为16,构造HashMap
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

//第四个构造方法:通过其他map来初始化HashMap,容量通过传入的map的size来计算,加载因子为0.75,//容量为16
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);  //初始化HashMap底层的数据结构

        putAllForCreate(m);   //添加m中的元素
    }

4.CRUD操作

(1)HashMap

//hash方法
final int hash(Object k) {
//hash种子,在第一次put的时候会生成一次
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


 (2) get()方法分析

public V get(Object key) {
//如果key为空,则取table[0]上的值
        if (key == null)
            return getForNullKey();
//key不为空时,根据key获取value
        Entry<K,V> entry = getEntry(key);
//如果entry为null则返回null,若不为null则返回当前entry的value
        return null == entry ? null : entry.getValue();
    }

//get()方法中的getForNullKey()方法
//作用:当key=null时,则到以哈希表数组的第一个元素table[0]为头结点的链表中去

private V getForNullKey() {
//判断元素的个数为0返回null
        if (size == 0) {
            return null;
        }
//遍历table[0]为头结点的链表,寻找key==null对应的值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
	//否则返回null
        return null;
    }

//get()方法中的getEntry方法
//作用:key不为null时,去获得对应值

final Entry<K,V> getEntry(Object key) {
	//判断元素个数为0返回null
        if (size == 0) {
            return null;
        }
	//根据key获取hash值
        int hash = (key == null) ? 0 : hash(key);
	//根据hash值计算对应的数组下标
	//遍历以该数组下标的数组元素为头结点的链表所有节点,寻找该key对应的值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
//如果hash值和key相等,则返回对应的entry,通过equals()判断key是否相等
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
//否则返回空
        return null;
    }

总结get方法过程:

      判断key是否为null,特殊处理,在0号索引位置进行遍历,若存在key为null的节点,则直接返回节点Entry的value值
若key不等于null,则对key进行哈希,并找到在哈希表中的索引位置(indexFor)遍历该索引位置的节点,
判断key是否存在(hash,key),存在则直接返回value,不存在返回null。

思考一下重写equal方法的同时必须重写hashCode()方法?如果不重写会有什么问题呢?
重写前的equals方法和hashCode方法都可以用来比较两个对象的地址值是否相同,
不同的是,两个地址值不同的对象的hashCode可能相同,但是equals一定不同。

(3)Entry

//从成员变量HashMap.Entry<K,V>[] table,可以看出Entry是HashMap的一个静态内部类,实现了map.Entry接口,
//即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
//table变量实际就是Entry[],和int[]类似,就是一个数组,不过加了括号<K,V>,
//后面的HashMap的put、get操作都是基于table实现的,都是存储在entry中
static class Entry<K,V> implements Map.Entry<K,V> {//该内部类实现了map接口的内部类Entry
        final K key;    //被定义为final类型,不可改变key的值
        V value;	//映射的value值
        Entry<K,V> next;//在Entry内部类中又定义了一个Entry的成员变量,指向下一个元素的引用
        int hash;	//哈希值

        /**
         * Creates new entry.构造方法为Entry赋值
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
//将新的value赋值给当前类的value
//划重点:这里是老的entry赋值给新的entry的next,所以这里得出key的hash发生冲突后 不能说覆盖以前的值
//以前的值想要获取就在新的值得next里面,他们只是hash相同key不相同,只有hash相同时才会进行碰撞处理
//因为下标是hash和表容量计算出来的,没有发生碰撞时next为null
            value = v;
            next = n;
            key = k;
            hash = h;
        }
	// 返回 与 此项 对应的键
        public final K getKey() {
            return key;
        }
	
	 // 返回 与 此项 对应的值
        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

//equals()
    // 作用:判断2个Entry是否相等,必须key和value都相等,才返回true  
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }


        public final String toString() {
            return getKey() + "=" + getValue();
        }

    /** 
     * 当向HashMap中添加元素时,即调用put(k,v)时, 
     * 对已经在HashMap中k位置进行v的覆盖时,会调用此方法 
     * 此处没做任何处理 
     */      
        void recordAccess(HashMap<K,V> m) {
        }

        /** 
     * 当从HashMap中删除了一个Entry时,会调用该函数 
     * 此处没做任何处理 
     */  
        void recordRemoval(HashMap<K,V> m) {
        }
    }

(4)put方法分析

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
	//当哈希结构未初始化时(即table为空,第一次插入元素),
	//使用构造函数时设置的阈值(即初始容量)进行初始化数组table
            inflateTable(threshold);
        }
//key为null时特殊处理,将该键值对添加到table[0]中,该位置永远只有一个value,因为最多允许一个key为空
        if (key == null)	
            return putForNullKey(value);

	//key不为null,计算该key的hash值,然后通过hash值计算相应的下标
        int hash = hash(key);  //根据key求hash值
        int i = indexFor(hash, table.length);  //根据hash值求下标
//以上两步获取到当前元素要存储的哈希表的索引位置

//判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表逐个判断)
//当发生 Hash冲突时,为了保证 键key的唯一性,哈希表并不会马上在链表中插入新数据,
//而是先查找该 key是否已存在,若已存在,则替换即可
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { //遍历index处链表
            Object k;
	//如果有值,则判断是否存在相同key,若存在,当前value则覆盖已有值的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this); //一个空的预留方法,可以重写
                return oldValue;
            }
        }
//若key对应的键值对不存在,将key-value添加到table中,修改次数+1
        modCount++;
        addEntry(hash, key, value, i);  //否则添加新元素,添加到table[i]处,元素数量+1
        return null;
    }

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
//将初始值给定为大于等于当前初始值的最近的一个2的次方值
 ,
//即如果传入的容量大小是19,那么转化后的初始化容量大小为32
        int capacity = roundUpToPowerOf2(toSize);

//重新计算阈值 threshold=容量*加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

//使用计算后的初始容量初始化数组table
//即哈希表的容量大小=数组大小
        table = new Entry[capacity];   //用该容量初始化table
        initHashSeedAsNeeded(capacity);  //选择合适的Hash因子
    }

分析roundUpToPowerOf2
作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
private static int roundUpToPowerOf2(int number) {
// 若容量超过了最大值,初始化容量设置为最大值,否则设置为:>传入容量大小的最小的2的幂
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }



//当key为null时,做特殊处理,存放在0号索引位置上
private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
	//先查找table[0]中是否有值,若已经存在键为null的值,覆盖旧值
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
	//若不存在,即table[0]还是空,则插入此节点,修改次数+1
        modCount++;
        addEntry(0, null, value, 0);//新建一个Entry将它放入table[0]中
        return null;
    }


//根据hash值和table的容量计算出一个下标
static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
	return h & (length-1);
    }

//分析addEntry()
//作用:添加键值(Entry)到HashMap中

void addEntry(int hash, K key, V value, int bucketIndex) {
//先判断大小,若元素个数>=容量,且出现碰撞
        if ((size >= threshold) && (null != table[bucketIndex])) {、
	//,则进行扩容,即2倍扩容
            resize(2 * table.length);  //每次2倍扩容
//如果当前key不为null则取hash(key),否则hash为0
            hash = (null != key) ? hash(key) : 0;  //重新计算hash值
//获取一个下标覆盖原有下标
            bucketIndex = indexFor(hash, table.length); //重新计算key对应的table中的下标
        }
	//HashMap实际大小小于阈值,创建一个Entry,将key-value插入指定位置,bucketIndex是位置索引
        createEntry(hash, key, value, bucketIndex);
    }


//分析createEntey()
//若容量足够,创建一个新的数组元素Entry,并放到数组中
void createEntry(int hash, K key, V value, int bucketIndex) {
	//将table中该位置原来的Entry保存到e中
        Entry<K,V> e = table[bucketIndex];
	//在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对放到(链表)后1个结点中
	//将需插入的键值对放入到头结点中->从而形成链表
	//即在插入元素时,是在链表头插入的,
        table[bucketIndex] = new Entry<>(hash, key, value, e);
	//已使用数组位置+1
        size++;
    }
//进行头插,创建一个新的entry(jdk1.7版本采用的是头插法)
 Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
 }


//分析resize
//作用:当容量不足时(容量>阈值),则扩容(2倍扩容)
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引用到HashMap的table属性上
        table = newTable;
//计算新数组的扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


//哈希桶内的元素被逆序排列到新表中
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
//遍历旧数组得到每一个key,再根据新数组的长度重新计算下标存进去,如果是一个链表,则链表中的每个
//键值对也要重新hash计算索引
        for (Entry<K,V> e : table) {
//如果此位置上存放元素,则进行遍历,直到e==null,退出循环
            while(null != e) {
                Entry<K,V> next = e.next;
//当前元素总是直接存放在数组下标的位置上,而不是放在链表的最后
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

总结put的操作过程:
1.第一次put值的时候初始化表空间
2.key为空的时候会加入
3.会根据key生成哈希值
4.如果发生碰撞(hash值相等)的时候,会将新的值存放在新的entry里,老的值会存放在新值的entry.next里
5.正常情况下 修改次数+1,元素数量+1,新建一个Entry放入table[bucketIndex]中
6.bucketIndex是根据hash值和当前table长度计算出来的
7.如果遇到扩容,新容器是旧容器的2倍,新的容器将重新生成hash种子,老元素会赋值到新容器中,注意:高并发的时候取值可能为null,严重时会出现数组越界,死循环的问题,所以HashMap是线程不安全的

(5)remove()方法分析

//作用:删除键值对

public V remove(Object key) {
//移除key对应table[]中的entry
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

//移除key对应table[]中的entry具体操作
final Entry<K,V> removeEntryForKey(Object key) {
//元素个数为0返回null
        if (size == 0) {
            return null;
        }
//计算hash值,key==null和不等于null统一处理
        int hash = (key == null) ? 0 : hash(key);
//计算存储的数组下标位置
        int i = indexFor(hash, table.length);
//将当前下标的entry放在局部变量中
        Entry<K,V> prev = table[i];
//局部变量pre赋值给局部变量e
        Entry<K,V> e = prev;

//如果当前元素不为空
        while (e != null) {
//取出当前元素的next
            Entry<K,V> next = e.next;
            Object k;
//判断hash值和key值是否相等,注意碰撞只是hash值相同key不同
//该判断中key是否为null都可以判断
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {

                modCount++; //相等修改次数+1
                size--;	    //元素-1
	//若删除的是table数组中的元素(即链表的头结点)
	//则删除操作=将头结点的next引用存入table[i]中
                if (prev == e)
//将next的值覆盖当前下标的值,没有碰撞时这时候table[i]当前值为null
                    table[i] = next;
//否则将以table[i]为头结点的链表中,当前Entry的前一个Entry中的next设置为 当前Entry的next
                else
//碰撞情况下将next的值覆盖pre.next,也就是说这时候prev是有值的,移除的只是相同hashcode但key不同的碰撞体
                    prev.next = next;
                e.recordRemoval(this);
	//返回移除的entry
                return e;
            }
//上面我们知道了碰撞后的旧值在新值得next中,就是如果当前下标中有碰撞那么将e赋值给prev
            prev = e;
//e=e.next,如果不为空则继续移除
            e = next;

        }
//e为null直接返回
        return e;
    }


final Entry<K,V> removeMapping(Object o) {
        if (size == 0 || !(o instanceof Map.Entry))
            return null;

        Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
        Object key = entry.getKey();
        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;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

//清除hashmap 
public void clear() {
//修改+1
        modCount++;
//循环table将每个下标对应的值都替换成null
        Arrays.fill(table, null);
//元素个数归0
        size = 0;
    }

//循环table将每个下标对应的值都替换成null
  public static void fill(Object[] a, Object val) {
         for (int i = 0, len = a.length; i < len; i++)
             a[i] = val;
     }



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值