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

代码片段是一个自定义的HashMap类,它扩展了AbstractMap类并实现了Map接口。HashMap是一种常用的数据结构,它使用键值对存储和检索数据。

在这个类中, <K,V> 表示泛型参数,它允许您在创建HashMap对象时指定键和值的类型。AbstractMap类提供了一些默认的实现,而Map接口定义了操作HashMap的方法。

通过扩展AbstractMap类和实现Map接口,您的HashMap类将继承和实现一些基本的方法和功能.

//默认容量 - 容量必须的2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
	//最大容量 -- int类型取值范围内最大的2的幂的数字
    static final int MAXIMUM_CAPACITY = 1 << 30;//1073741824
	//默认的负载因子
    //1 --》 16*1=16(阈值) 	 -- 扩容数组慢(利用了空间,牺牲时间)
    //0.1 --》 16*0.1=1(阈值) -- 扩容数组快(利用了时间,牺牲了空间)
    //0.75 --> 16*0.75=12(阈值) -- 装12个数据就扩容(取得时间和空间的平衡)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//空内容的数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
	//数组容器 -- hash表、hash数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//{}
	//元素个数(映射关系的个数)
    transient int size;//0
	//阈值(容量*负载因子)
    int threshold;//16
	//负载因子
    final float loadFactor;//0.75
	//外部操作数
    transient int modCount;//0
    //hash种子数
    transient int hashSeed = 0;//0
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

这段代码看起来是一个构造函数,它创建了一个HashMap对象。这个构造函数使用了两个默认参数:DEFAULT_INITIAL_CAPACITYDEFAULT_LOAD_FACTOR

//initialCapacity - 16
    //loadFactor - 0.75
    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))//NaN - Not a Number
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
    
    //toSize - 16
    private void inflateTable(int toSize) {
        //计算容量(获取toSize的二的幂的数字) -- 16
        int capacity = roundUpToPowerOf2(toSize);

        //计算阈值 - 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化容器数组 -- new Entry[16]
        table = new Entry[capacity];
        //计算hash种子数
        initHashSeedAsNeeded(capacity);
    }
    
    //number - 16
    private static int roundUpToPowerOf2(int number) {
		//Integer.highestOneBit(数字) -- 保留最高位的1,其余都是0
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY//用于限制结果不能超过2的30次方
                : (number > 1) ? Integer.highestOneBit((number-1) << 1) : 1;
    }			//使用了Java内置的方法Integer.highestOneBit()来找到一个数字的最高位的1。通过将该数字					减1,然后将结果与原数字相或,并将结果左移1位,我们可以得到最接近原数字的2的幂次方。
    
    private V putForNullKey(V value) {
        //下标为0的位置有Entry对象,就意味着hash碰撞了
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {//如果将HashMap的只能设置为0,那么下表为0的key就不为空,这样就会将空的值存在第一个。
                //获取老的value
                V oldValue = e.value;
                //替换value
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回老的value
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//把数据添加到Entry对象中,Entry对象添加table中
        return null;
    }
    
    final int hash(Object k) {
        int h = hashSeed;
        //判断key是否是String,如果是,就计算hash值(hashSeed去参与计算hash值的工作)
        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);
    }
    
    static int indexFor(int h, int length) {
        //长度必须的2的幂,是因为要让元素散列均匀
        //为了保证在进行哈希计算时,能够更有效地利用位运算来确定元素在新的数组中的位置。

		/**HashMap内部使用哈希函数将键映射到数组的索引位置。当HashMap的容量达到负载因子(load factor)			所定义的阈值时,就会触发扩容操作。扩容操作的目的是为了减少哈希冲突,提高HashMap的性能。
		   在扩容过程中,HashMap会创建一个更大的数组,然后将原数组中的元素重新分配到新数组中。为了确定元			素在新数组中的位置,HashMap使用元素的哈希码(hash code)与新数组的长度进行位运算,计算出元素			 在新数组中的索引位置。
		   当newsize不是2的幂时,进行位运算时无法保证结果的正确性。例如,如果newsize是一个奇数,那么在进            行位运算时,无法保证元素在新数组中的位置分布均匀,可能会导致哈希冲突增加,降低HashMap的性能。
           因此,为了保证位运算的正确性和性能的最优化,HashMap在扩容时要求newsize必须是2的幂。这样可以通            过位运算来高效地计算元素在新数组中的位置,减少哈希冲突的发生,提高HashMap的效率和性能。
       */
        //如果长度不为2的幂,会增加同一个下标上有多个元素的几率(hash碰撞),导致效率降低
        return h & (length-1);
    }
    
    //key - new Student("李孙浩", '男', 22, "2301", "003")
    //value - "美食"
    public V put(K key, V value) {
        //第一次添加元素时,进入的判断
        if (table == EMPTY_TABLE) {
            //初始化数据(阈值、hash种子、数组)
            inflateTable(threshold);
        }
        //判断key是否是null,如果是null,就将数据添加至数组下标为0的位置
        if (key == null)
            return putForNullKey(value);
        //获取key的hash值
        int hash = hash(key);
        //利用hash值计算在数组中的下标
        int i = indexFor(hash, table.length);
        //判断下标上是否有Entry对象
        //进入该判断,就意味着hash碰撞(竟可能的避免 -- 重写key的hashCode和equals)
        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))) {
               	//获取老的value
                V oldValue = e.value;
                //替换value值
                e.value = value;
                e.recordAccess(this);
                //返回老的value
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    //hash - 20
    //key - new Student("张浩天", '男', 23, "2301", "002")
    //value - "打篮球"
    //bucketIndex - 4
    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);
    }

		 //newCapacity - 32
   		 void resize(int newCapacity) {
        //获取老的table
        Entry[] oldTable = table;
        //获取老的长度 -- 16
        int oldCapacity = oldTable.length;
        //判断老的长度是否等于数组的最大值,如果等于数组的最大值,不能再扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //将int的最大值赋值给阈值,意味永远都无法扩容
            threshold = Integer.MAX_VALUE;
            return;
        }

        //创建新的数组
        Entry[] newTable = new Entry[newCapacity];
        //扩容 - 1.重新计算hashSeed 2.将老的数组中的数据复制到新数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //将新数组的地址赋值给老的数组的引用
        table = newTable;
        //重新计算阈值 threshold - 24
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    void transfer(Entry[] newTable, boolean rehash) {
        //newCapacity - 32
        int newCapacity = newTable.length;
        //遍历老的数组
        
        for (Entry<K,V> e : table) {
            //e - null
            while(null != e) {
                //next - null
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算Entry对象在新数组中的下标
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    
    //hash - 20
    //key - new Student("张浩天", '男', 23, "2301", "002")
    //value - "打篮球"
    //bucketIndex - 4
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    
    //节点类/映射关系类
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; ------ key
        V value; ---------- value
        Entry<K,V> next; -- 下一个节点的引用地址
        int hash; --------- key的hash值
            
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }
    
}
  1. 构造函数 HashMap(int initialCapacity, float loadFactor):接受初始容量和负载因子作为参数,并进行参数合法性检查。如果初始容量小于0,抛出IllegalArgumentException异常;如果初始容量大于最大容量MAXIMUM_CAPACITY,则将初始容量设置为最大容量;如果负载因子小于等于0或者是NaN(不是一个数字),则抛出IllegalArgumentException异常。然后,将负载因子和阈值设置为参数值,并调用init()方法进行初始化。
  2. inflateTable(int toSize)方法:根据给定的大小 toSize 扩展哈希表的容量。首先,通过调用roundUpToPowerOf2(int number)方法计算容量(将toSize 转为大于等于它的最小的2的幂次数)。然后,计算阈值,即容量乘以负载因子和最大容量加1的较小值。接下来,初始化哈希表数组 table 为指定容量大小,并调用initHashSeedAsNeeded(int capacity)方法计算哈希种子数。
  3. roundUpToPowerOf2(int number)方法:将给定的数字 number 转为大于等于它的最小的2的幂次数。如果 number 大于等于最大容量 MAXIMUM_CAPACITY,则返回最大容量;否则,如果 number 大于1,使用 Integer.highestOneBit((number-1) << 1) 方法计算最小的2的幂次数;如果 number 等于1,则返回1。
  4. putForNullKey(V value)方法:将键为null的映射关系添加到哈希表中。首先,遍历数组下标为0的位置,查找是否已经存在键为null的映射关系。如果存在,替换其值并返回旧值;如果不存在,调用addEntry(int hash, K key, V value, int bucketIndex)方法将新的映射关系添加到数组下标为0的位置。
  5. hash(Object k)方法:计算给定键 k 的哈希值。首先,将哈希种子数赋值给 h。如果哈希种子数不为0且键 k 是一个字符串类型,调用 sun.misc.Hashing.stringHash32((String) k) 方法计算字符串的哈希值。否则,将 k.hashCode() 的结果与 h 异或,并对结果进行一系列位运算操作,最后返回结果。
  6. indexFor(int h, int length)方法:根据给定的哈希值 h 和数组长度 length,计算哈希值在数组中的下标。这里要求数组长度必须是2的幂次数,这样可以保证元素在数组中分布均匀,减少哈希冲突的几率。
  7. put(K key, V value)方法:将给定的键值对添加到哈希表中。首先判断哈希表是否为空,如果为空,则调用inflateTable(int toSize)方法进行初始化。然后,判断键是否为null,如果是null,则调用putForNullKey(V value)方法将映射关系添加到数组下标为0的位置。如果键不为null,计算键的哈希值和在数组中的下标。接着,遍历数组对应下标位置的链表,查找是否已经存在相同的键。如果存在,替换其值并返回旧值;如果不存在,调用addEntry(int hash, K key, V value, int bucketIndex)方法将新的映射关系添加到链表中。
  8. addEntry(int hash, K key, V value, int bucketIndex)方法:将给定的哈希值、键、值和在数组中的下标添加到哈希表中。首先判断是否需要进行扩容,如果当前元素个数大于等于阈值且对应下标位置已经有元素存在,则调用resize(int newCapacity)方法进行扩容。然后,重新计算哈希值和在数组中的下标,并调用createEntry(int hash, K key, V value, int bucketIndex)方法创建新的节点并添加到链表中。
  9. resize方法:
  • newCapacity:新的容量大小。
  • 首先获取旧的table数组,并获取旧的容量大小。
  • 然后判断旧的容量是否等于最大容量,如果是,则将阈值设为最大整数值,表示无法再进行扩容。
  • 创建新的table数组,长度为新的容量大小。
  • 调用transfer方法将旧的table数组中的元素转移到新的table数组中。
  • 将新的table数组赋值给table成员变量。
  • 重新计算阈值。

10.transfer方法:

  • newTable:新的table数组。
  • rehash:是否需要重新计算hash值。
  • 首先获取新的容量大小。
  • 遍历旧的table数组中的每个元素。
  • 对于每个元素,首先将其下一个元素赋值给next变量。
  • 如果需要重新计算hash值,则将元素的hash值设为0或使用hash方法重新计算。
  • 计算元素在新的table数组中的下标。
  • 将元素的下一个元素设为新的table数组中该下标位置的元素。
  • 将元素插入新的table数组中该下标位置。
  • 将元素设为next变量的值,继续下一个元素的处理。

在resize方法中,首先判断旧的容量是否等于最大容量,如果是,则表示HashMap已经达到了最大容量,无法再进行扩容。然后创建新的table数组,长度为新的容量大小。接着调用transfer方法将旧的table数组中的元素转移到新的table数组中,同时根据需要重新计算hash值。最后重新计算阈值。

在transfer方法中,首先获取新的容量大小。然后遍历旧的table数组中的每个元素,对于每个元素,首先将其下一个元素赋值给next变量。如果需要重新计算hash值,则将元素的hash值设为0或使用hash方法重新计算。接着计算元素在新的table数组中的下标,并将元素插入新的table数组中该下标位置。最后将元素设为next变量的值,继续下一个元素的处理。这样就完成了

11.createEntry(int hash, K key, V value, int bucketIndex)方法:创建一个新的节点,并将其添加到链表的头部。首先,获取链表头部的节点 e。然后,创建一个新的节点,并将其赋值给链表头部。最后,增加元素个数 size。//头插法

  1. 节点类 Entry:表示哈希表中的映射关系。每个节点包含键 key、值 value、下一个节点的引用 next 和键的哈希值 hash。节点通过链表形式连接在一起,用于解决哈希冲突。
HashMap<Student, String> map = new HashMap<>();
	map.put(new Student("小梦", '男', 21, "2301", "001"), "唱跳);
	map.put(new Student("兴旺", '男', 23, "2301", "002"), "跑步");
	map.put(new Student("喜儿", '男', 22, "2301", "003"), "听歌");
	
	map.put(new Student("喜儿", '男', 22, "2301", "003"), "美食");
	
	map.put(null, "玩游戏");
	map.put(null, "写代码");

创建了一个HashMap对象map,键的类型是Student,值的类型是String。然后,使用put方法向map中添加了五个键值对。

  1. 第一个键值对的键是一个Student对象,值是字符串"品茗"。
  2. 第二个键值对的键是一个不同的Student对象,值是字符串"打篮球"。
  3. 第三个键值对的键是又一个不同的Student对象,值是字符串"听歌"。
  4. 第四个键值对的键是与第三个键值对的键相同的Student对象,值是字符串"美食"。这将会替换之前的值。
  5. 第五个键值对的键是null,值是字符串"玩游戏"。
  6. 第六个键值对的键是null,值是字符串"写代码"。这将会替换之前的值。

请注意,HashMap允许键为null,并且在这种情况下,null键的值可以被替换。

存储内存图

深度理解HashMap底层源码_HashMap

学习HashMap的过程:

1. 创建对象的过程(注意:属性的初始化)

2. 添加数据的过程(注意:添加的步骤、hash碰撞)

hash碰撞

当两个key通过hashCod计算相同时(其实hashCode是随机产生的,是有可能hashCode相同),则发生了hash冲突,开放定址法、再哈希法、链地址法、建立公共溢出区

解决hash碰撞

用链表。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next,说白就是比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上

3.扩容的过程(注意:hash回环)

Hash回环

(也称Hash死循环)出现原因:多线程下,线程1不断添加新的数据导致HashMap扩容(数据的迁移) 线程2:不断地遍历,因此在多线程下要避免这个问题。

注意:

1、JDK1.7版本,HashMap的数据结构是什么? 答:单向链表+ 数组 2、HashMap中数组的默认初始化容量是多少? 答初始容量为(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR)为16,必须是2的幂。 3、什么叫作Hash桶? 答:数组中的单向链表 4、HashMap的数组长度为什么必须是2的幂? 答:计算元素存在数组中下标的算法:hash值&数组长度-1,如果数组长度不是2的幂,减1过后而精致的某一位可能出现0,导致数组某个位置永远存不到数据 5、HashMap的默认负载因子是多少,作用是什么? 答:负载因子:0.75 作用:数组长度-负载因子=阈值(扩容条件) 6、HashMap的默认负载因子(loadFactor)为什么是 0.75? 答:取得了时间与空间的平衡。 假设负载因子过大,导入数组装满后才扩容,牺牲时间,利用空间。 假设负载因子过小,导致数组装载较少内容就扩容,牺牲空间,利用时间 7、HashMap数组最大长度是多少?为什么是这么多? 答:最大长度为:1<<30,因为数组长度必须是2的幂并且HashMap数组最大长度的变量为int类型为了不超出界限,所以1<<30 8、什么叫作Hash碰撞? 答:两个对象的hash值一样,导致在数组中的下标一样 9、HashMap何时扩容? 元素个数>=阈值,并且存入数据的位置不等于null 10、HashMap的扩容机制是什么? 答:是原来的2倍 11、HashMap存入null键的位置? 答:hash数组下标为0的位置。 12、什么叫作hash回环? 答:Hash回环(也称Hash死循环)出现原因:多线程下,线程1不断添加新的数据导致HashMap扩容(数据的迁移) 线程2:不断地遍历,因此在多线程下要避免这个问题。 13、JDK1.7版本与JDK1.8版本的HashMap的区别? 答:JDK1.7:数组+链表(头插法),通过散列算法获取hash值 JKD1.8:数组+链表+红黑树(尾插法),通过低16位^高16位让hash值更加散列 14、JDK1.8版本HashMap为什么添加红黑树的数据结构? 答:因为链表查询慢,红黑树查询快 15、JDK1.8版本什么时候由 数组+链表变成数组+红黑树 答:当链表长度>8的时候并且数组长度>64,从数组+链表变成数组+红黑树 16、JDK1.8版本为什么链表长度大于8时,变成数组+链表+红黑树 答:因为泊松分布(统计概率学),当红黑树里的数据小于6时,又会将数组+红黑树变成数组+链表