HashMap jdk1.7 源码分析 -- 作者哇塞大嘴好帅

HashMap源码分析 – 作者哇塞大嘴好帅

JDK1.7 HashMap源码分析

主方法

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
}

​ 首先点进去new HashMap<>();

    /**
     * 默认初始容量-必须为2的幂。
     * 默认数组容量为16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    /**
     * 在构造函数中未指定时使用的负载因子。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
  * 使用默认的初始容量构造一个空的HashMap
  *(16)和默认负载系数(0.75)。
  */
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

看到这个无参构造,调用了自身的有参构造

HashMap有参构造

    /**
    	最大容量,如果隐式指定更高的值,
    	由两个带参数的构造函数组成。
    	必须是两个<= 1 << 30的幂
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 设置下一次的大小值  算法(容器 * 加载因子).
     * @serial
     */
    int threshold;

public HashMap(int initialCapacity, float loadFactor) {
    //判断默认长度是否小于0 如果小于0就抛异常初始容器长度不合法
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //判断当前容量是否大于最大的容量,如果大于了就取最大的值
    if (initialCapacity > MAXIMUM_CAPACITY)
        //容器大小赋值为:最大的值
        initialCapacity = MAXIMUM_CAPACITY;
    //加载因子小于0	  	 ||  是不是数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
	/**
        找到一个大于等于传递进来的容器大小的2次幂数
    	接受传递进来的容器大小,然后去设置容器大小,且容器必须是2的幂
    	capacity如果2次幂还小于传递进来的容器大小,那么就接着2次幂
   		
        1------ 0000 0001
        2------ 0000 0010
        4------ 0000 0100
        16----- 0001 0000
        .....
        
        如果你传参为0那么就为1 因为capacity为1
    **/
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
	
    //将加载因子复制给Hashmap的属性
    this.loadFactor = loadFactor;
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //完成数组的初始化
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //接下来去调用init() 在HashMap中他什么也不做
    init();
}

这个方法就做了容器的初始化

put方法
public V put(K key, V value) {
    //判断这个键是否为空 如果等于空就会进行初始化
    //通过这行代码可以看到 hashmap的key值是可以等于null的
    if (key == null)
        //将数据存放在数组第0个位置
        return putForNullKey(value);
    //给key算出一个hash值 		
    int hash = hash(key);
    //拿出hash值 还有数组的长度 得到一个数组下标
    int i = indexFor(hash, table.length);
    /**
    	该循环做的事情就是,找到指定下标的链表,然后遍历链表,然后判断新插入的Hashmap的键值是否相同,如果相同就把原来的值返回,把新值覆盖。
    
    	Entry<K,V> e = table[i]; 获取到某个数组的下标的链表
    	e != null				 如果不等于空就继续,如果等于空就代表这个链表探索玩了
    	e = e.next				 链表的下一个
    **/
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判断 hash值是否相等		判断key是否相当		
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            //先把原来的value拿出来
            V oldValue = e.value;
            //然后再把新的value覆盖进去
            e.value = value;
            //在HashMap不会有效果
            e.recordAccess(this);
            //当找到相同把原来的值返回
            return oldValue;
        }
    }
	//修改次数
    modCount++;
    //		hash值 键名字 值   数组下标
    addEntry(hash, key, value, i);
 	//如果没有找到相同的key值那么就返回null
    return null;
}

addEntry方法

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //进行扩容
        resize(2 * table.length);
        //计算出hash值
        hash = (null != key) ? hash(key) : 0;
        //计算出下标
        bucketIndex = indexFor(hash, table.length);
    }
	//添加
    createEntry(hash, key, value, bucketIndex);
}

createEntry方法

/**
 *	做了将一个数组存到hashmap里面去
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    //新创一个Entry对象 把他赋值到table[bucketIndex]的前面
    Entry<K,V> e = table[bucketIndex];    //获取key值的下标
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

hash方法

final int hayihsh(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
	
    //首先h会异或hashCode
    h ^= k.hashCode();

    /*
    	hash值算法
    	h =     h 异或 (h二进制向右移动20位 异或 h二进制向又移动12位)
    	然后 h = h 异或 (h二进制又移动12位) 异或 h二进制又移动4位
    */
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

indexFor方法

static int indexFor(int h, int length) {
    /*
     *	hash值 & 15
     *  假设hash值二进制是 0101 0101
     *  leng-1       是  0000 1111
     *  那么他们的结果就是  0000 0101
     */
    return h & (length-1);
}

putForNullKey() 方法

private V putForNullKey(V value) {
    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++;
    //这里他直接写死了 hash值为0  数组下标为0
    addEntry(0, null, value, 0);
    retu
remove方法

remove()方法

public V remove(Object key) {    Entry<K,V> e = removeEntryForKey(key);    return (e == null ? null : e.value);}

removeEntryForKey方法

final Entry<K,V> removeEntryForKey(Object key) {
    //获取hash值
    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;
        //判断key是否相等
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            //操作次数++
            modCount++;
            //容量减1
            size--;
            //把为1的元素删除掉
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}
扩容

​ 扩容的目的,是为了让链表变短,而不是单纯的移植下链表和扩容下数组

addEntry方法

void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果当前存放的容量超过了预值
    //预值算法  table.length*加载因子 并且当前 当前数组下标不等于null
    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);
}

resize方法

void resize(int newCapacity) {
    //记录下老数据
    Entry[] oldTable = table;
    //记录老数组的容量
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	//新生成的一个数组  比如我们之前的数组容量是4,那么就变成了8
    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    //创建一个新的数组
    transfer(newTable, rehash);
    //把新的数组赋值给旧的数组,这样旧的数组旧变成新的数组了
    table = newTable;
    //计算出新的预值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer方法

void transfer(Entry[] newTable, boolean rehash) {
    //拿到新的数组大小
    int newCapacity = newTable.length;
    //遍历旧的的数组,并且把旧的数组的值赋值到新的数组
    for (Entry<K,V> e : table) {
        /**
        *	如果当前链表不等于空 那么就将旧链表的内容利用头插入一个一个的移植到新链表
        **/
        while(null != e) {
            //next = 链表的下一个
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //通过hash值计算出新的数组的下标
            /*
            			     hash值       二进制		运算符
            	假设hash值为:   69       0100 0101		  &
            	  旧数组长度:   15       0000 0101
                 数组下标结果:   5        0000 0101

			  第一种可能	      
                假设hash值为:   69      0100 0100		  &
                新的数组长度 :   31	   0001 1111
                 数组下标结果:   5       0000 0101
            
             第二种可能
             	假设hash值为:   85      0101 0101        &
             	新的数组长度:    31      0000 1111
 				数组下标结果:    21      0001 0101      
            
			  通过以上两个例子,可以得出,将旧数组的链表的移植到扩容后的新数组下标中可能存在2个位置,一个位			 置是旧数组所在的下标,或者 旧数组的所存在的下标 + 旧数组的长度。
            */
            int i = indexFor(e.hash, newCapacity);
            /**
            *	采用了头插法
            **/
            //将旧的数组元素指定到新的数组指定下标
            e.next = newTable[i];
            //将e赋值给新数组计算出来的指定下标
            newTable[i] = e;
            //然后在将e指向旧链表的下一个
            e = next;
        }
    }
}

1.7 HashMap 多线程安全隐患

jdk7里面多线程扩容的时候可能会导致循环列表的出现

​ 出现此问题的原因,扩容的时候使用的头插法导致的

解决方案:控制预值。 预值算法 = 数组容量*加载因子

​ 假设2个对象同时调用put方法,当执行到

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        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;
        }
    }
}
void resize(int newCapacity) {
    //记录下老数据
    Entry[] oldTable = table;
    //记录老数组的容量
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	//新生成的一个数组  比如我们之前的数组容量是4,那么就变成了8
    Entry[] newTable = new Entry[newCapacity];
    // useAltHashing 由在创建HashMap静态类赋值。
    boolean oldAltHashing = useAltHashing;
    /**
     *	isBooted() 默认值为false
     *  newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 为 true
     **/
    useAltHashing |= 
      sun.misc.VM.isBooted()&&(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    /**
    	如果 表达式一真一假  rehash为真
   		认为当我的数组的容量超过某一个值的时候就去生成一个hash总值	
  	**/
    boolean rehash = oldAltHashing ^ useAltHashing;
    //传递一个新的数组
    transfer(newTable, rehash);
    //把新的数组赋值给旧的数组,这样旧的数组旧变成新的数组了
    table = newTable;
    //计算出新的预值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

HaspMap静态方法

  static {
        //判断虚拟机有没有jdk.map.althashing.threshold环境变量的值
        String altThreshold = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction(
                "jdk.map.althashing.threshold"));

        int threshold;
        try {
            /**
              如果 altThreshold 不等于空 
            	就取虚拟机设置的值
           		如果虚拟机里没值就取默认的值
            **/
            threshold = (null != altThreshold)
                    ? Integer.parseInt(altThreshold)
                    : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

            // 如果等于-1 就拿Integer.MAX_VALUE
            if (threshold == -1) {
                threshold = Integer.MAX_VALUE;
            }

            if (threshold < 0) {
                throw new IllegalArgumentException("value must be positive integer.");
            }
        } catch(IllegalArgumentException failed) {
            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
        }
        ALTERNATIVE_HASHING_THRESHOLD = threshold;

        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
                HashMap.class.getDeclaredField("hashSeed"));
        } catch (NoSuchFieldException | SecurityException e) {
            throw new Error("Failed to record hashSeed offset", e);
        }
    }
}

程序运行前

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SNH1ZO6-1629305825824)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530203637685.png)]

第一个线程执行后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BuwBo5cn-1629305825827)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530204123865.png)]

​ 这时候我们会发现第一个存在了这三个元素,第二个线程确没有存在,

第二个线程执行

第一次循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weShw561-1629305825829)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211012812.png)]

第二次循环

​ Entry<K,V> next = e.next;操作后的图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7HzuTUI-1629305825831)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211107117.png)]

第二次循环执行后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jeD3Ghve-1629305825832)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530210332361.png)]

第三次循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLuLhLZx-1629305825833)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211954817.png)]

当执行到 e = next; 这时候e == null 就会结束循环。

​ 当执行到table = newTable;

第一个线程没有问题。

当第二个线程赋值就会出现问题[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sHZOlve-1629305825834)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530211903929.png)]因为他们已经成了一个循环链表

HashMap常犯错误
ON.1
    public static void main(String[] args) {
        HashMap map  = new HashMap();
        map.put("2","2");
        map.put("1","1");
        Iterator iterator = map.keySet().iterator();
        while (iterator.hasNext())
        {
            String key = (String) iterator.next();
            if (key.equals(key))
            {
                map.remove(key);
            }
        }
    }
}

报错信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yLEvGBF-1629305825834)(C:\Users\Y5128\AppData\Roaming\Typora\typora-user-images\image-20200530234510883.png)]

为什么会有异常?

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

​ 如果我们**key.equals(“1”)**那么就不会有异常,因为哪个时候 modCount = 2. expectedModCount = 2;

modCount 默认值为0 两次put方法 一个remove方法这时候他的值为3 expectedModCount = 2,

private final class KeyIterator extends HashIterator<K> {
    public K next() {
        return nextEntry().getKey();
    }
}

KeyIterator 继承了 HashIterator就会执行他的构造类

        HashIterator() {
            //赋值给他操作次数
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

​ 因为以上的操作HashMap会认为不是安全的做法他会利用它的快速报错的机制

正确的做法

public static void main(String[] args) {
    HashMap map  = new HashMap();
    map.put("2","1");
    map.put("1","1");
    map.put("3","1");
    Iterator iterator = map.keySet().iterator();
    while (iterator.hasNext())
    {
        String key = (String) iterator.next();
        if (key.equals("3"))
        {
            iterator.remove();
        }
    }
}
public void remove() {
    if (current == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    Object k = current.key;
    current = null;
    HashMap.this.removeEntryForKey(k);
    expectedModCount = modCount;
}

这个方法的区别就是他调用玩删除会重新赋值下

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哇塞大嘴好帅(DaZuiZui)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值