手撕HashMap底层源码+面试题

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    
    //默认初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16
    //hash数组最大长度
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空内容的数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
    //hash数组/表
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//new Entry[16];
    //元素个数
    transient int size;//3
	//阈值
    int threshold;//12
    //负载因子
    final float loadFactor;//0.75f
    //操作数
    transient int modCount;//3
    //种子数
    transient int hashSeed = 0;
    
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_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();
    }
    
    // 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值(hashCode() + 散列算法)
        int hash = hash(key);
        //通过hash值计算出在数组中的下标 i-2
        int i = indexFor(hash, table.length);
        //e -- 李林的Entry对象
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断是否是同一个key的标准
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                //oldValue - 吃火锅
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                
                //返回被替换的值
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    // value - "bbb"
    private V putForNullKey(V value) {
        //e - 0x004
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                //oldValue - aaa
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                //返回被替换的值
                return oldValue;
            }
        }
        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);
            //重新计算key的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;
        //如果原数组的长度等于数组的最大值(1<<30)
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //将int类型的最大值赋值给阈值
            threshold = Integer.MAX_VALUE;
            //直接结束该方法
            return;
        }
	
        Entry[] newTable = new Entry[newCapacity];
        //transfer的作用:
        //1.将原数组的数据全部迁移到新数组中
        //2.重新计算hash种子数
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //将新数组的地址赋值给原数组的引用
        table = newTable;
        //重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    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 - 2345672
    //length - 16
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    
    // k - new Student("水菜", '女', 25, "2204", "001")
    final int hash(Object k) {
        //获取hash种子数
        int h = hashSeed;
        //判断k如果是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);
    }
    
    //toSize - 16
    private void inflateTable(int toSize) {
        
        //roundUpToPowerOf2()传入int值,返回大于或等于参数的2的幂的数字
        //capacity - 16
        int capacity = roundUpToPowerOf2(toSize);

        //threshold - 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
		//初始化hash数组
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
    
    private static int roundUpToPowerOf2(int number) {
        //Integer.highestOneBit(数字)只保留该数字二进制最高位的1,其余的1变作为0
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
    //根据长度初始化hash种子数
    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    
    //HashMap的映射关系类/节点类
    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;
        }
    }
}


HashMap<Student, String> map = new HashMap<>();
		
map.put(new Student("水菜", '女', 25, "2204", "001"), "拍黄瓜");
map.put(new Student("天使", '女', 21, "2204", "002"), "吹唢呐");
map.put(new Student("李林", '男', 22, "2204", "003"), "吃火锅");
map.put(new Student("李林", '男', 22, "2204", "003"), "写代码");
map.put(null, "aaa");
map.put(null, "bbb");
  1. JDK1.7版本的HashMap底层数据结果是什么?

一维数组 + 单向链表

  1. 什么是Hash桶?

单向链表

  1. 什么是hash碰撞?

多个key的hash值相同,在数组中的下标也是相同的,就会判断两个对象是否相同,这个过程叫做hash碰撞。

hash碰撞应该要避免,因为一维数组查询快,而单向链表查询慢,hash碰撞后如果有单向链表的存在,势必会影响HashMap的查询效率

  1. HashMap底层一维数组的初始化长度为多少?

1<<4 --> 16

  1. HashMap底层一维数组的长度为什么必须是2的幂?

计算元素在数组中的下标的代码是:h & (length-1)

长度不是2的幂,长度-1结果的二进制表示位数上有可能出现0,&的结果位数上就一定是0,导致元素在数组中的分布不均匀

  1. HashMap的一维数组的最大长度是什么?

1 << 30; —> 1073741824

  1. HashMap的一维数组的最大长度为什么是1<<30?

最大长度的类型是int

1 << 30 是int取值范围里最大的2的幂的数字

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

默认的负载因子是0.75f

作用:数组长度*负载因子得到阈值,元素个数达到阈值后就扩容

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

取得了时间和空间的平衡

如果负载因子过小,会导致装载一点点数据就扩容,利用时间,牺牲空间

如果负载因子过大,会导致装载满了才扩容,利用空间,牺牲了时间

  1. HashMap存放null键null值的位置?

    hash数组下标为0的位置

  2. HashMap的扩容机制?

    元素个数大于等于阈值并且添加元素的下标位置不等于null,才会扩容

    扩容的容量是原来的2倍

  3. 什么叫做hash回环?

    多线程的情况下

    线程1不断的添加元素,导致扩容

    线程2不断的遍历

    线程1扩容期间,单向链表的下一个节点位置有闭环,线程2遍历有出现hash回环的问题

    经验:使用HashMap出现hash回环问题,不是HashMap的错误,是程序员应该背的锅,因为HashMap明确表示该实现不是一个线程安全的集合,你在多线程下使用出现的问题应该是你去负责。多线程下应该使用ConcurrentHashMap

  4. HashMap使用的注意事项?

    1.不能在多线程下使用

    2.key的hashCode不要写死了,不然会出现hash碰撞

  5. JDK1.7和1.8中HashMap的区别?

    JDK1.7:

    ​ 一维数组+单向链表

    ​ 计算hash值:hashCode() + 散列算法

    ​ 头插法

    JDK1.8:

    ​ 一维数组+单向链表+平衡二叉树(提高查询效率)

    ​ 计算hash值:高16 ^ 低16位

    ​ 尾插法

  6. JDK1.8HashMap什么是从一维数组+单向链表 变为 一维数组+平衡二叉树

    数组的长度大于64,并且单向链表的长度大于8,就会从一维数组+单向链表 变为 一维数组+平衡二叉树

    平衡二叉树小于6时,有会从一维数组+平衡二叉树 变为 一维数组+单向链表

  7. JDK1.8HashMap 为什么单向链表大于8后变为平衡二叉树

    因为泊松分布

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

real_fxyyyyyy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值