手撕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后变成二叉树

    泊松分布

相关推荐
北执南念13 分钟前
项目代码生成工具
java
中国lanwp19 分钟前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
苹果酱05671 小时前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
Java致死1 小时前
单例设计模式
java·单例模式·设计模式
mit6.8241 小时前
[Lc_week] 447 | 155 | Q1 | hash | pair {}调用
算法·leetcode·哈希算法·散列表
胡子发芽1 小时前
请详细解释Java中的线程池(ThreadPoolExecutor)的工作原理,并说明如何自定义线程池的拒绝策略
java
沫夕残雪1 小时前
Tomcat的安装与配置
java·tomcat
胡子发芽2 小时前
请解释Java中的NIO(New I/O)与传统I/O的区别,并说明NIO中的关键组件及其作用
java
柚个朵朵2 小时前
IDEA中使用Git
java·git·spring
jerry6092 小时前
优先队列、堆笔记(算法第四版)
java·笔记·算法