HashMap jdk1.7原理探究

本文详细分析了Java 1.7中HashMap的数据结构,包括数组+单链表的实现,以及核心操作如put、get和remove的内部逻辑。讨论了哈希冲突解决、扩容策略和哈希码处理。还探讨了为何采用特定的数组下标计算方法,以及与Java 1.8的区别,如1.8引入红黑树等改进。
摘要由CSDN通过智能技术生成

目录

大纲:

1.数据结构

2.基本知识

3.源码

3.1属性

3.2 构造函数

3.3 PUT

3.4 GET

3.5 remove

4.几个问题

4.1 为什么直接采用hashCode() 处理的hashcode,作为存储数组table的下标位置

4.2 为什么采用哈希码与运算(数组长度-1) 计算数组下标

4.3 为什么在计算数组下标前,需要对哈希码进行二次处理:扰动处理

4.4 与jdk1.8中有什么区别

5.参考文献


大纲:

1.数据结构

2.基础知识

3.源码

4.存在的问题

HashMap结构,根据键的hashCode值存储数据,大多数情况能够直接定位到它的值,

1.数据结构

jdk1.7中 hashmap 采用的数据结构是数组+单链表;这种结构方式成为拉链法,是用来处理哈希冲突的方法之一:哈希冲突就是,存储两个不同的元素,这两个元素通过哈希函数得出的实际存储地址相同,导致插入的时候,发现数组的位置已经被占用;链地址的解决方案就是:hash值相同的元素,以链表的方式存储在数组中;数组是HashMap的主体,链表则是为了解决哈希冲突而存在的

 

HashMap的主干是一个Entry类型的数组。Entry也是HashMap的基本组成单元,每一个Entry都包含一个key-value键值对,

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;   //存储指向下一个Entry的引用,形成了解决hash冲突的单链表
        int hash; //对key的hashcode值进行hash运算后得到的值,存储在Entry,防止重复计算
​
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
 }

根据这个结构,当插入或者查找的时候,如果数组上不包含链表:即(table[i]==null || table[i].next==null);那么HashMap的插入和删除的复杂度都是O(1);而如果数组上存在链表的话,则可能需要遍历链表,复杂度就是O(n);

 

2.基本知识

以后补充

3.源码

3.1属性

HashMap的几个属性

public class HashMap<K,V>extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
{
​
    //默认初始容量(16)-必须是2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
​
    //最大容量,如果HashMap的构造函数隐士的指定了更高的容量,则使用该容量,同样必须是2的幂
    static final int MAXIMUM_CAPACITY = 1 << 30;
​
    // 构造函数未指定时使用的负载系数:(负载系数与HashMap的扩容有关;当当前容量size>(loadFactory*capacity)进行扩容)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
​
    //共享的空表实例,用来判断当前hashmap实例是否需要初始化;
    static final Entry<?,?>[] EMPTY_TABLE = {};
​
    // 数组table;根据需要调整大小,长度必须始终是2的幂;
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    //hashmap的大小:(键值对映射数=数组长度中的键值对+链表中的键值对)
    transient int size;
​
 
    //阈值:当size达到这个阈值时,hashmap会进行扩容;
    int threshold;
​
    //负载因子,代表了table的填充度有多少,默认是0.75;假设初始桶大小是16,负载因子是0.75;那么当hashmap的size到达了        16*0.75=12时,就会进行扩容,扩容是扩容到当前容量的2倍; resize(2 * table.length);
    final float loadFactor;
​
    // hashmap被改变的次数,由于HashMap的非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生了变化,就会抛出异常(ConcurrentModificationException)
    transient int modCount;
​
    //映射容量的默认阈值,超过该阈值时,可以使用替代哈希用于字符串键。由于字符串键的哈希代码计算能力较弱,替代哈希减少了冲突的发生率。可以通过定义系统属性{@code jdk.map.althashing.threshold}来覆盖该值。属性值{@code-1}强制始终使用替代哈希,而{@code-1}值确保从不使用替代哈希。
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

3.2 构造函数

构造函数有四个;可以发现在四个构造函数中,除了最后一个指定了map的构造器之外,其余均没有为数组table分配内存空间;(在执行put操作的时候才真正构建table数组)

1.仅接受初始化容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表table,

2、真正初始化哈希表(初始化存储数组table)是在第一次添加键值对时,即第一次调用put()时;

//创建一个空的hashMap,(指定初始容量以及负载因子)
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //HashMap的最大初始容量只能是MAXIMUM_CAPACITY,哪怕传入的初始容量>MAXIMUM_CAPACITY
    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(); //init在Hashmap中没有实际实现,其子类LinkedHashMap中有对应实现
}
​
   
//构造一个空的HashMap(指定初始化容量,默认负载因子0.75)
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
​
//创建一个空的hashMap,使用默认的初始化容量16,以及默认的负载因子0.75;
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//通过一个给定Map,创建一个同样映射的新的HashMap,这个hashmap,通过默认负载因子和初始化容量(Max(map/0.75),默认初始化容量);
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);
    //将传入的map全部元素逐个添加到HashMap中
    putAllForCreate(m);
}

3.3 PUT

public V put(K key, V value) {
    // 前面提过,构造函数中,并没有为哈希表(数组table)进行真正初始化,为table分配实际内存空间;第一次初始化时,threshold为initialCapacity,在构造函数中指定过
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //处理key为null的情况
    if (key == null)
        return putForNullKey(value);
    //通过key计算hash值,同时根据计算后的hash值确定该key在table里的索引i
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //遍历table[i]处的链表,确定链表中是否有与key相等的键值对;如果有,将链表中的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;
        }
    }
    //hashmap结构被改变次数加1
    modCount++;
    //走到这里,就意味着要么table[i]处链表为空,要么,就是遍历完之后没有找到对应key值,需要在table[i]上添加新的节点
    addEntry(hash, key, value, i);
    return null;
}

下面将逐一分析初始化函数inflateTable(threshold);处理空键函数putForNullkey(value)hash和索引计算函数hash(key) indexFor(hash,table.length)添加新的节点函数 addEntry(hash,key,value,i)以及后面的扩容函数resize(2*tableLength)

3.3.1 初始化inflateTable(threshold)

private void inflateTable(int toSize) {
    // 保证capacity一定是2的次幂;
    int capacity = roundUpToPowerOf2(toSize);
    //设置阈值,保证阈值不会超过最大容量
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

3.3.2 处理空键函数putForNullKey(value)

// 当key==null时,将该key-value的存储位置规定在数组table中的第一个位置,即table[0]
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++;
    addEntry(0, null, value, 0);
    return null;
}

3.3.3 hash和索引计算

1.hash

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
​
    h ^= k.hashCode();
​
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

2.indexFor

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

3.3.4 添加新的节点函数 addEntry(hash,key,value,i)

//在这里是线判断是否需要扩容,再创建节点
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);
}
//注意这里:1.7里面使用的是头插法;先用一个节点e指向table[bucketIndex],也就是链表原头节点。然后再把新节点插入链表头节点;最后再把新节点的next指向e;
void createEntry(int hash, K key, V value, int bucketIndex) {
    //保存table原位置的节点
    Entry<K,V> e = table[bucketIndex];
    //在table中bucketIndex的位置,新建一个Entry,并把key,value放在这个entry中,同时把entry.next指向table原位置的节点e
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

3.3.5 扩容 resize(2 * table.length)

void resize(int newCapacity) {
    //保存旧数组
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值。退出,不进行后续扩容。
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //创建新table:容量为 2*oldCapacity;
    Entry[] newTable = new Entry[newCapacity];
    //将旧数组上的数据(键值对) 转移到新table中,从而完成扩容 ❤❤
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //把新table引用到hashmap的table属性上;
    table = newTable;
    //设置新的阈值;不能超过系统默认最大容量
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//把为扩容前的旧table上的数据迁移到扩容后的新table;
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍历未扩容前的旧table:
    for (Entry<K,V> e : table) {
        //处理旧table上的每一个索引处的链表;
        while(null != e) {
            //因为链表是单链表,为了防止后续节点丢失,使用next保存当前节点的下一个节点;
            Entry<K,V> next = e.next;
            //如果要 重新计算哈希值,计算哈希值
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新计算每个元素的存储位置;
            int i = indexFor(e.hash, newCapacity);
            //下面两步就是1.7中hashmap扩容后可能出现逆序的原因;新表中的头节点之前是旧表的节点e的上一个节点;即
            // oldtableNode.next=e
            //旧表的节点e的next指向 新表中的头节点;
            e.next = newTable[i];
            //新表i处存储旧表的节点e
            newTable[i] = e;
            //继续遍历旧表的节点e的下一个节点;
            e = next;
        }
    }
}

3.4 GET

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);  // 通过key来获得entry

    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); //计算hash值
    for (Entry<K,V> e = table[indexFor(hash, table.length)]; //通过hash值得到key在table中的下标;遍历所在链表
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;//匹配到直接返回
    }
    return null;
}

3.5 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;
    }
    //得到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;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            size--;
            if (prev == e)  //如果第一次遍历正好遍历到了,这个时候e恰好还是=prev;因此直接让prev=e.next=next;
                table[i] = next;
            else
                prev.next = next; //不是第一次遍历了,这个时候prev.next=e; 直接prev.next=e.next=next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}

4.几个问题

4.1 为什么直接采用hashCode() 处理的hashcode,作为存储数组table的下标位置

答:hashcode算出来不一定在数据的大小范围内;计算下标通过 hash&(n-1) 确保映射到数组的范围内;

 

4.2 为什么采用哈希码与运算(数组长度-1) 计算数组下标

通过与运算,可以保证计算出来的数组下标一定位于 数组的范围内;同时由于数组的长度始终是2的次幂大小;这样可以保证length-1保证二进制时,低位全部为1.可以使得数组的索引index更加均匀

img

4.3 为什么在计算数组下标前,需要对哈希码进行二次处理:扰动处理

答:加大哈希码低位的随机性,使得分布更加均匀,从而提高对应数组存储下标位置的随机性&均匀性。最终减少hash冲突;

4.4 与jdk1.8中有什么区别

1.数据结构

版本JDK1.7JDK1.8
数据结构数组+链表数组+链表+红黑树
数组&链表节点的实现类Entry类Node类
红黑树的实现类/TreeNode类
核心参数:
主要参数相同:容量、加载因子、扩容阈值
JDK1.8中增加了红黑树的相关参数:
1.桶的树化阈值: 链表转成红黑树的阈值,在存储数据时,当链表长度>该值时,则将链表转换成红黑树。
2、桶的列表还原阈值:红黑树转为链表的阈值;
3、最小树形化容量阈值:当哈希表中的容量>该值时,才允许树形化链表;否则,若桶内元素太多,则直接扩容,而不是树形化

2.获取数据时

版本JDK1.7JDK1.8
初始化方式单独函数infalteTable()直接集成在扩容函数resize()中
hash值计算方式1.hashCode; 2扰动处理=9次扰动=4次位运算=5次异或运算1.hashCode; 2. 扰动处理 =2次扰动=1次位运算+1次异或运算
存放数据的规则数组+链表 1.无冲突时,存放数组 2.有冲突时,存放链表数组+链表+红黑树 1.无冲突时,存放数组 2.冲突&链表长度<8;存放到单链表 3.冲突&链表长度>8;存放到红黑树
插入数据的方式头插法 (先将原位置的数据移到后1位,再插入数据到该位置)尾插法 (直接插入到链表尾部)

3.扩容机制

版本JDK1.7JDK.18
扩容后存储位置的计算方式全部按照原来方法进行计算 (hashCode()->扰动处理->(h&length-1))按照扩容后的规律计算 (即扩容后的位置=原位置or原位置+旧容量)
转移数据的方式头插法尾插法
需插入数据的插入时机&位置重计算时机扩容后插入,单独计算扩容前插入、转移数据时同一计算

5.参考文献

  1. Carson带你学Java:手把手带你源码分析 HashMap 1.7_Carson带你学Android的博客-CSDN博客_hashmap1.7

  2. Java集合之一—HashMap_woshimaxiao1的博客-CSDN博客_hashmap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值