手撕HashMap底层源码

java 复制代码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    
    //默认初始化容量(必须是2的幂)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16
    //最大容量 
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空内容的数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
    
    //数据容器 - hash数组/hash表 - new Entry[16]{null,null,null,null,null,null,null...}
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    //元素个数
    transient int size;//0
    //阈值(容量*负载因子)
    int threshold;//24
    //负载因子
    final float loadFactor;//0.75
    //操作数
    transient int modCount;//0
    //hash种子数
    transient int hashSeed = 0;
    
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    //initialCapacity - 16
    //loadFactor - 0.75f
    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();
    }
    
    //交给子类去重写,实现子类的初始化过程
    void init() {
    }
    
    //key - null
    //value - "bbb"
    public V put(K key, V value) {
        //第一次添加时进入的判断
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        
        if (key == null)
            return putForNullKey(value);
        //获取key的hash值
        //hash - 20
        int hash = hash(key);
        //通过hash值计算出在数组中下标
        //i - 4
        int i = indexFor(hash, table.length);
        
        //hash冲突时才进入的循环
        //e - 0x003
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;//Entry中的key
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                //获取老的value
                V oldValue = e.value;//玩游戏
                //设置新的value
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回老的value
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    //v - "bbb"
    private V putForNullKey(V value) {
        //e - 0x004
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                //获取老的value
                V oldValue = e.value;
                //设置新的value
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回老的value
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
    
    //hash - 0
    //key - null
    //value - "aaa"
    //bucketIndex - 0
    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);
    }
    
    //newCapacity - 32
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        
        //oldCapacity - 16
        int oldCapacity = oldTable.length;
        
        //如果长度等于最大容量
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //将int最大值赋值给阈值,目的:减少扩容次数
            threshold = Integer.MAX_VALUE;
            return;
        }

        //创建新的hash表 - new Entry[32];
        Entry[] newTable = new Entry[newCapacity];
        
        //1.重新计算出hash种子数
        //2.将table里的数据全部赋值给newTable
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        
        //将newTable的地址赋值给table
        table = newTable;
        //重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    //该方法在多线程下会出现hash回环的情况
    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;
            }
        }
    }

    
    //hash - 0
    //key - null
    //value - "aaa"
    //bucketIndex - 0
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //e - null
        Entry<K,V> e = table[bucketIndex];
        
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    
    //h - 20
    //length - 16
    static int indexFor(int h, int length) {
       	//h        - 0000,0000,0000,0000,0000,0000,0001,0100
        //length-1 - 0000,0000,0000,0000,0000,0000,0000,1111
        //			 0000,0000,0000,0000,0000,0000,0000,0100
        
        return h & (length-1);
    }
    
   	//key - new Student("麻生希", '女', 26, "2308", "001")
    final int hash(Object k) {
        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);
    }
    
    //toSize - 16
    private void inflateTable(int toSize) {
        
        // 获取参数的向上的2的幂(16返回16,17返回32)
        //capacity - 16
        int capacity = roundUpToPowerOf2(toSize);
        //threshold - 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化hash表
        table = new Entry[capacity];//new Entry[16];
        //通过容量初识化hash种子数
        initHashSeedAsNeeded(capacity);
    }
    
    //number - 16
    private static int roundUpToPowerOf2(int number) {
        
        if(number >= MAXIMUM_CAPACITY){
            return MAXIMUM_CAPACITY;
        }else{
            if(number > 1){
                return Integer.highestOneBit((number - 1) << 1);
            }else{
                return 1;
            }
        }
        
       
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
    //映射关系类/节点类
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; ----------- 键
        V 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;
        }
        
        public final V setValue(V newValue) {
            V oldValue = value;//获取老的value
            value = newValue;//设置新的value
            return oldValue;//返回老的value
        }
    }
}
java 复制代码
HashMap<Student, String> map = new HashMap<>();
		
map.put(new Student("麻生希", '女', 26, "2308", "001"),"写代码");        
map.put(new Student("椎名空", '女', 23, "2308", "002"),"拍电影");        
map.put(new Student("水菜丽", '女', 29, "2308", "003"),"玩游戏"); 
map.put(new Student("水菜丽", '女', 30, "2308", "003"),"看书"); 
map.put(null, "aaa");
map.put(null, "bbb");
  1. JDK1.7版本的HashMap的数据结构是什么?

一维数组+单向链表

  1. HashMap默认的初始化容量是多少?

16

  1. HashMap的容量为什么必须是2的幂?

如果不是2的幂,计算下标时length-1,得到的二进制位数上有可能是0,0位上计算出的结果一定是0,会让下标分布不均匀

  1. HashMap的最大容量是多少?

1<<30

  1. HashMap的最大容量为什么是1<<30?

HashMap容量为int类型

int类型的最大取值范围是2的31次方-1

1<<30,相当于2的30次方,该数字为int类型取值范围内最大的2的幂

  1. HashMap默认的负载因子是多少?其作用是什么?

0.75f

容量*负载因子=阈值(扩容临界点)

  1. HashMap默认的负载因子为什么是0.75?

取得了时间和空间的平衡

负载因子过大,比如为1,数组必须装满才扩容,利用了空间,浪费了时间

负载因子过小,比如为0.1,数组装一点数据就扩容,浪费了空间,利用了时间

  1. 如何理解HashMap的hash桶?

hash桶指的是hash表中某个下标上的单向链表

  1. 如何理解HashMap的hash碰撞/冲突?如何解决?

多个元素之间的hash值一样,在HashMap的数组中的下标也是一样的。

注意:hash碰撞会让hashmap处理数据的效率降低

解决方案:重写hashCode(),尽可能避免多个对象的hash值一样

  1. HashMap存放null键的位置?

    下标为0的位置

  2. HashMap合适扩容?

    元素个数大于等于阈值 并且 当前下标上的元素不为null

  3. 如何理解HashMap的hash回环?

    多线程情况下,

    一个线程在遍历数据

    一个线程在添加数据,导致扩容,有可能形成闭环链表

  4. JDK1.7版本和JDK1.8版本的HashMap的区别

    JDK1.7版本的HashMap:一维数组+单向链表,头插法

    JDK1.8版本的HashMap:一维数组+单向链表+平衡二叉树,尾插法

  5. JDK1.8版本的HashMap为什么数据结构发生改变?

    一维数组+单向链表 ,数组长度大于64并且链表长度大于8,会改成一维数组+平衡二叉树

    因为数组长度大于64并且链表长度大于8时的数据量特别大,把单向链表变作为平衡二叉树,提高操作数据的效率

    注意:二叉树的节点小于7,就会从二叉树变回为单向链表

  6. JDK1.8版本的HashMap为什么链表长度大于8后变成二叉树

    泊松分布

相关推荐
程序媛小果5 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林11 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨25 分钟前
El表达式和JSTL
java·el
duration~1 小时前
Maven随笔
java·maven
zmgst1 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
九圣残炎3 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode