hashmap put过程_面试八股文之一:HashMap源码分析

推荐学习

  • 闭关28天,奉上[Java一线大厂高岗面试题解析合集],备战金九银十
  • 死磕「并发编程」100天,全靠阿里大牛的这份最全「高并发套餐」

java面试的高频面试点HashMap

面试官:说一下HashMap的底层数据结构?
面试官:说一下HashMap的扩容机制?
面试官:说一下HashMap是不是线程安全的?从而引出ConcurrentHashMap。
面试官:说一下HashMap的put的过程是怎么样的?
面试官:把你知道的都告诉我。

0e2a0dbb9dc23c7a3347894dfe441c78.png

jdk1.7HashMap

在jdk1.7中hashmap采用的是数据+链表的数据结构。看下面的这段代码。

 @Test    public void hashMapTest(){        HashMap hashMap = new HashMap<>();        hashMap.put("1","张三");        hashMap.put("2","李四");        System.out.println(hashMap.get("1"));        System.out.println(hashMap.get("2"));    }

在HashMap中对应的是key,value这种形式,其中实际的key在HashMap中并不是实际我们输入的1,或者2这样。value也并不是仅仅使用Object或者是泛型。首先看一下HashMap的属性。

 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 = {}; transient Entry[] table = (Entry[]) EMPTY_TABLE; int threshold;//阈值 final float loadFactor;//可以传入的加载因子 transient int modCount;

在以上属性中,有一个table,是Entry类型的数组,这个Entry相当于实际存储的值,也就是在entry中存放了key、value、hash、以及Entry成员变量。所以Entry可以形成一个链表。下面是Entry的属性和方法。

 static class Entry implements Map.Entry {        final K key;        V value;        Entry next;        int hash;        Entry(int h, K k, V v, Entry n) {            value = v;            next = n;            key = k;            hash = h;        }        public final K getKey() {            return key;        }        public final V getValue() {            return value;        }        public final V setValue(V newValue) {            V oldValue = value;            value = newValue;            return oldValue;        }        public final boolean equals(Object o) {            if (!(o instanceof Map.Entry))                return false;            Map.Entry e = (Map.Entry)o;            Object k1 = getKey();            Object k2 = e.getKey();            if (k1 == k2 || (k1 != null && k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();                if (v1 == v2 || (v1 != null && v1.equals(v2)))                    return true;            }            return false;        }        public final int hashCode() {            return java.util.Objects.hashCode(getKey()) ^ java.util.Objects.hashCode(getValue());        }        public final String toString() {            return getKey() + "=" + getValue();        }        void recordAccess(HashMap m) {        }        void recordRemoval(HashMap m) {        }    }

通过以下一些属性,我们可以大致得出,HashMap存储数据的形式。首先计算kay的hash值,然后通过key的hash值计算出数组下标,因为是hash算法,那么就会发生hash冲突,所以链表就是解决hash冲突的。具体的图形大致如下。

85de39f76031ebb785bbbcc3b44ad89e.png

大致对HashMap的存储结构有了一定的了解,看一下HashMap的扩容以及put的具体过程。
首先构造函数可以传递数组的默认大小和加载因子,如果没有传递使用默认的加载因子与容量大小。0.75 和16,由此我们可以得出一个阈值12。如果传递了初始值,则按照传递数据计算。事实上我们并不是很明白这个阈值,和加载因子到底有什么用,那么接下来在put中我们会看到。

 public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    public HashMap() {        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    }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))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        this.loadFactor = loadFactor;        threshold = initialCapacity;        init();    }

HashMap的put过程

 public V put(K key, V value) {        if (table == EMPTY_TABLE) {            inflateTable(threshold);        }        if (key == null)            return putForNullKey(value);        int hash = hash(key);//计算key的hash值        int i = indexFor(hash, table.length);//得出所在的下标        for (Entry 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;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }

很显然,我们会先执行inflateTable(threshold);这个方法,然后我们判断key是不是为null的,如果为null,执行处理null的逻辑,否则执行hash(key),得出key的hash值,然后计算indexFor(),然后会有一个for循环,这个for循环是遍历链表,然后会有一个判断逻辑,发现如果有key相等,我们返回老的值,也就是新值会被覆盖。如果不存在,那么modCount++,执行 addEntry() 方法。
首先看看inflateTable(threshold)这个方法,这个方法又会调用roundUpToPowerOf2这个方法。包含highestOneBit

private void inflateTable(int toSize) {        // Find a power of 2 >= toSize        int capacity = roundUpToPowerOf2(toSize);//计算数组容量        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        table = new Entry[capacity];        initHashSeedAsNeeded(capacity);    }    private static int roundUpToPowerOf2(int number) {        // assert number >= 0 : "number must be non-negative";        return number >= MAXIMUM_CAPACITY                ? MAXIMUM_CAPACITY                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;    }    public static int highestOneBit(int i) {        // HD, Figure 3-1        i |= (i >>  1);        i |= (i >>  2);        i |= (i >>  4);        i |= (i >>  8);        i |= (i >> 16);        return i - (i >>> 1);    }    highestOneBit(6)    011000110111001101110111-0011 = 0100 = 4

这两个方法和起来的作用其实就是计算数组的容量并且必须使他的容量为2的幂次方。假设我们传递的数组容量大小4,(4-1)*2 = 6 ,求出小于6的2的幂次方,也就是4。这个我们得出数组的容量大小总是2的幂次,为什么总是要2的幂次?hash 函数与indexFor函数,与hashcode做异或运算,在进行多次移位异或为的是让hash更加散列。数组下标的计算为什么是将hash值与数组容量减一做与运算。这里用与运算和取模运算达到的效果是一致的,如果与数组容量取模运算的话,同样可以达到下标取值范围为0-15。而与运算同样可以达到这种效果。并且效率更高。

 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);    }    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);    }    如果初始16 减一15    0000 0000 1011 1101    0000 0000 0000 1111    0000 0000 0000 1101

遍历链表后如果key没有重复,则进行addEntry。在addEntry中出现了resize(),resize的条件是
(size >= threshold) && (null != table[bucketIndex]) 当前数组元素个数超过阈值时要进行扩容,这个时候就可以看出阈值的设定以及加载因子的设定还是重要的。然后执行头插插入,put过程结束。
扩容的具体实现看transfer 会将原先数组的值重新进行一次hash运算,重新复制到新数组。扩容的效率相对较低的。

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);    }    void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry[] newTable = new Entry[newCapacity];        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 e : table) {            while(null != e) {                Entry 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;            }        }    }    void createEntry(int hash, K key, V value, int bucketIndex) {        Entry e = table[bucketIndex];        table[bucketIndex] = new Entry<>(hash, key, value, e);//实现头插并下移        size++;    }

jdk1.8HashMap

在jdk1.8中,对HashMap进行了优化。数据结构改成了数组加链表加红黑树的结构。对于二叉树,他的插入删除查询的效率都很高,红黑树是一种特殊的二叉查找树。二叉查找树在特定的情况下会退化成链表这种结构,例如数据都是有序的。通过红黑树的规则,使红黑树在破坏规则的情况下改变节点的颜色以及左旋和右旋维持自身的平衡,使之左右高度差不超过1。
在jdk1.8中多了两个属性,TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD。当节点数大于8的时候会转换成红黑树,当节点数小于6的时候会转换成链表。

    static final int TREEIFY_THRESHOLD = 8;    static final int UNTREEIFY_THRESHOLD = 6;
static final class TreeNode extends LinkedHashMap.Entry {        TreeNode parent;  // red-black tree links        TreeNode left;        TreeNode right;        TreeNode prev;    // needed to unlink next upon deletion        boolean red;        TreeNode(int hash, K key, V val, Node next) {            super(hash, key, val, next);        }        /**         * Returns root of tree containing this node.         */        final TreeNode root() {            for (TreeNode r = this, p;;) {                if ((p = r.parent) == null)                    return r;                r = p;            }        }
static  TreeNode balanceInsertion(TreeNode root,                                                    TreeNode x) {            x.red = true;            for (TreeNode xp, xpp, xppl, xppr;;) {                if ((xp = x.parent) == null) {                    x.red = false;                    return x;                }                else if (!xp.red || (xpp = xp.parent) == null)                    return root;                if (xp == (xppl = xpp.left)) {                    if ((xppr = xpp.right) != null && xppr.red) {                        xppr.red = false;                        xp.red = false;                        xpp.red = true;                        x = xpp;                    }                    else {                        if (x == xp.right) {                            root = rotateLeft(root, x = xp);//左旋                            xpp = (xp = x.parent) == null ? null : xp.parent;                        }                        if (xp != null) {                            xp.red = false;                            if (xpp != null) {                                xpp.red = true;                                root = rotateRight(root, xpp);//右旋                            }                        }                    }                }                else {                    if (xppl != null && xppl.red) {                        xppl.red = false;                        xp.red = false;                        xpp.red = true;                        x = xpp;                    }                    else {                        if (x == xp.left) {                            root = rotateRight(root, x = xp);右旋                            xpp = (xp = x.parent) == null ? null : xp.parent;                        }                        if (xp != null) {                            xp.red = false;                            if (xpp != null) {                                xpp.red = true;                                root = rotateLeft(root, xpp);//左旋                            }                        }                    }                }            }        }

作者: @小码哥

原文链接:https://blog.csdn.net/qq_42581175/article/details/108156506

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值