JDK1.7 HashMap源码详尽剖析

在这里插入图片描述
在这里插入图片描述
先看类图结构:

HashMap

HashMap 实现了Map接口,扩展了AbstractMap抽象类

1.成员变量

// HashMap的默认初始容量,即hash表桶的初始个数,即数组初始长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// HashMap的最大容量,即hash表桶的最大个数,即数组最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;

// HashMap的默认负载因子
// threshold = 负载因子 * 容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 代表空数组
static final Entry<?,?>[] EMPTY_TABLE = {};

// HashMap真正用于存放元素的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

// hash表元素总个数
transient int size;

// 临界值
int threshold;

// 负载因子
final float loadFactor;

解释:

  • 在HashMap在Entry数组长度大于threshold时,如果put()元素时发送hash冲突,则会触发扩容,即将HashMap的Entry数组的capacity扩大为原来的两倍。
  • 默认初始容量一定是2的指数倍,就算指定的初始容量不是2的倍数,也会向上取到大于它的2的指数倍的数中的最小数(比如7就变成8,13就变成16…)。这是等会儿由于根据元素的hash计算放入元素的index时,运用了取模元算,同时想要提高计算机效率。

2.HashMap方法暂不罗列

HashMap内部类Entry

1.成员变量

	final K key;
	V value;
	Entry<K,V> next;
	int hash;

2.Entry方法暂不罗列


正片开始

一、新建HashMap
  1. Test.java (测试类)
//新建HashMap
HashMap map = new HashMap();   < — — — — — a.进去
Object obj = map.put("java", "1.7");
  1. HashMap
// 构造函数1
public HashMap() {
   // DEFAULT_INITIAL_CAPACITY 等于 1 << 4,也就是16
   // DEFAULT_LOAD_FACTOR 等于0.75f
   this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  < — — — — — b.进去
    }
  1. HashMap
// 构造函数2
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);
    // 简单地为loadFactor和threshold属性赋值
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    // 空方法,啥也不做
    init();
    }
二、调用put()方法
  1. Test.java (测试类)
//新建HashMap
HashMap map = new HashMap();   
Object obj = map.put("java", "1.7");  < — — — — — a.进去
  1. HashMap
public V put(K key, V value) {
	// 由于刚刚初始化,table == EMPTY_TABLE,进入if
    if (table == EMPTY_TABLE) {
    	// threshold == DEFAULT_INITIAL_CAPACITY == 16
        inflateTable(threshold);   < — — — — — b.进去
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    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);
    return null;
 }
  1. HashMap
// 初次扩容,参数toSize == 16
private void inflateTable(int toSize) {
    // 把传入参数向上取为2的指数倍最小的数
    int capacity = roundUpToPowerOf2(toSize);   < — — — — — c.进去

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
    }
  1. HashMap
// 该函数专门把传入的数变成其向上取的2的指数倍的最小的数
// 参数number == 16
private static int roundUpToPowerOf2(int number) {
        // 先判断【number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1)】
        // 有以下3中可能:
        // 1️⃣ 如果number小于MAXIMUM_CAPACITY且大于1,那就得到(number > 1)
        // 		1.2 即得到true; 所以返回Integer.highestOneBit((number - 1) << 1)
        // 		1.3 该表达数实现了向上取的2的指数倍的最小的数
        // 		1.4 number - 1是为了处理4、8、16这类已经是2的指数倍的数
        // 2️⃣ 如果number>= MAXIMUM_CAPACITY,那就得到MAXIMUM_CAPACITY
        // 		2.2 也会得到true;所以返回Integer.highestOneBit((number - 1) << 1) 同上
        // 3️⃣ 如果number<=1,那就得到(number > 1)
        // 		3.2 所以得到false;所以返回1
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;   < — — — — — d.返回
    }
  1. HashMap
private void inflateTable(int toSize) {
    int capacity = roundUpToPowerOf2(toSize);   
    // 现在capacity == 16
    // threshold = min(16*0.75,MAXIMUM_CAPACITY + 1)
    // threshold = 12
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 对数组table赋值,是一个容量为16的元素为Entry类型的数组
    table = new Entry[capacity];
    //不管这句
    initHashSeedAsNeeded(capacity);
    // 初次扩容完毕,返回
    }
  1. HashMap
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);   
    }
    // 如果key == null,那么执行putForNullKey。此处跳过
    // putForNullKey()方法很简单
    if (key == null)
        return putForNullKey(value);
    // 根据key计算该元素的hash
    int hash = hash(key);   < — — — — — e.进入
    int i = indexFor(hash, table.length);
    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);
    return null;
 }
  1. HashMap
// 传入的参数k就是key
// 此函数就是计算出key的hash值,保证充分“打散”
final int hash(Object k) {
	// h = hashSeed = 0;
    int h = hashSeed;
    // 如果h不等于0且key是String类型及其子类,此处跳过
    if (0 != h && k instanceof String) {
    	// 返回一种hash算法
        return sun.misc.Hashing.stringHash32((String) k);
    }

	// h 和 key的hashCode做按位异或
    h ^= k.hashCode();
    // h右移20位,以及h右移12位,相互异或
    h ^= (h >>> 20) ^ (h >>> 12);
    // h再右移,再异或,返回
    return h ^ (h >>> 7) ^ (h >>> 4);   < — — — — — f.返回
}
  1. HashMap
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);   
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);   
    // 根据元素的hash值,和数组的长度得到该元素的index
    int i = indexFor(hash, table.length);   < — — — — — g.进入
    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);
    return null;
 }
  1. HashMap
// 此函数用于得到元素的index
// 参数h是hash值,lenght是16
static int indexFor(int h, int length) {
    // h & (length-1) 等价于 h % length
    // 相当于取模运算,根据余数得到index
    // 由于之前规定了length必须是2的指数,所以length-1是全1的数,如1111
    // 这样取模运算就转化成了按位与,加快了运算效率
    return h & (length-1);     < — — — — — h.返回index
    }

补充:计算机本身不太擅长除法
在这里插入图片描述

  1. HashMap
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);   
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);   
    int i = indexFor(hash, table.length);  
    // 根据index,找到数组上那个桶,遍历以它为首的链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         // 如果遍历找到了和当前插入一样的元素(根据key判断),则更新它的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            // 返回即可
            return oldValue;
        }
    }

    modCount++;
    // 如果遍历了全hash表都没有找到相同元素,则new一个该元素的Entry加进去
    addEntry(hash, key, value, i);    < — — — — — i.进入
    return null;
 }
  1. HashMap
// 该函数把新建的Entry加入到hash表中
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入桶位置index
void addEntry(int hash, K key, V value, int bucketIndex) {
	// 如果此时hash表元素个数>=threshold,且发生了hash冲突。此处暂时跳过,稍后分析
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
	// 真正的执行把新建的Entry加入到hash表中
    createEntry(hash, key, value, bucketIndex);  < — — — — — j.进入
}
  1. HashMap
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入
void createEntry(int hash, K key, V value, int bucketIndex) {
	// 得到并取出数组现在index位置上的元素e
    Entry<K,V> e = table[bucketIndex];
    // 把数组index位置让给新元素Entry,该新元素Entry的next的指针指向刚刚的e
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // hash表元素个数+1
    size++;
    // 一路返回
    }
结束
三、HashMap的扩容
  1. HashMap
    上述put的过程中,第14步添加新Entry,假设此时满足了扩容条件,触发扩容,进入了if语句:
// 该函数把新建的Entry加入到hash表中
// 参数hash为hash值;key就是key,value就是value,bucketIndex就是插入桶位置index
void addEntry(int hash, K key, V value, int bucketIndex) {
	// 此时hash表元素个数>=threshold,且发生了hash冲突
    if ((size >= threshold) && (null != table[bucketIndex])) {
    	// 指向扩容函数,参数为原容量的两倍:32
        resize(2 * table.length);    < — — — — — a.进入
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);  
}
  1. HashMap
// 参数newCapacity = 32
void resize(int newCapacity) {
	// 用oldTable承接现在的hash表数组
    Entry[] oldTable = table;
    // 得到现在的容量
    int oldCapacity = oldTable.length;
    // 跳过
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

	// 创建新的hash表数组,数组长度为32
    Entry[] newTable = new Entry[newCapacity];
    // 新表换旧表,元素搬家
    transfer(newTable, initHashSeedAsNeeded(newCapacity));  < — — — — — b.进入 
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

解释:initHashSeedAsNeeded(newCapacity)暂不明白如何触发,这里不影响

  1. HashMap
// 该函数负责具体的扩容,元素的搬家
// 参数:新的空hash表,rehash标志
void transfer(Entry[] newTable, boolean rehash) {
	// newCapacity = 32
    int newCapacity = newTable.length;
    // 对于旧表里的每一个桶Entry元素,遍历
    for (Entry<K,V> e : table) {
    	// 当该桶不为空,遍历该桶下面的链表
        while(null != e) {
        	// 下一个Entry
            Entry<K,V> next = e.next;
            // 如果重hash
            if (rehash) {
            	// 如果有key值 == null,e.hash = 0
            	// 如果key值 != null,重hash运算:e.hash = hash(e.key)
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新计算该Entry元素的index
            int i = indexFor(e.hash, newCapacity);
            // 下面两步:该Entry加入到i桶的链表中,头插法
            e.next = newTable[i];
            newTable[i] = e;
            // 迭代下一步
            e = next;
        }
    }
    // 一路返回
}
扩容分析结束

简单说明问题:

  • JDK1.7的HashMap在方面容易出问题:在于他元素搬家是,可能会造成原来一个链上的元素顺序倒置,在多线程环境下扩容可能会倒置环形链表形成死循环。这时CPU100%
  • 由于可以构造大量的String的,使它们具有相同hashcode,从而大量的key在同一个桶内,hashmap退化成链表。早先的tomcat是用hashmap接受http线程的请求参数,此举会让tomcat性能急剧下降,形成DDoS攻击。

在这里插入图片描述
JDK1.7的HashMap存在问题

发布了17 篇原创文章 · 获赞 18 · 访问量 5558

猜你喜欢

转载自blog.csdn.net/Vincentqqqqqqq/article/details/105057716
今日推荐