java开发进阶

Java之HashMap实现原理

2017-03-04  本文已影响0人  dotaer_shashen

目录
1.HashMap的数据储存方式
---- HashMap中的部分结构源码
2.HashMap put元素时的 hash 碰撞
---- 第一种情况
---- 第二种情况
---- 第三种情况
3.put函数的源码与扩容
4.HashMap的扩容阈值验证
---- 验证代码示例一
---- 验证代码示例二

Java之HashMap实现原理(jdk11)

1.HashMap的数据储存方式

HashMap采取 数组+ 链表 + 红黑树 的存储方式来实现,亦即数组(散列桶)中的每一个元素都是链表;

HashMap中的部分结构源码

// 储存链表结构的初始化数组
transient Node<K,V>[] table;
// 链表结构类 Node 
static class Node<K,V> implements Map.Entry<K,V> {
    // 链表元素通过 key 值计算出来的 hash值
    final int hash;
    final K key;
    V value;
    // 链表指针
    Node<K,V> next;
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    //......
}

// 红黑树结构类TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    // 父节点
    TreeNode<K,V> parent;  // red-black tree links
    // 左子节点
    TreeNode<K,V> left;
    // 右子节点
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    // 红色还是黑色
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    // ......
}

2.HashMap put元素时的 hash 碰撞

第一种情况

通过 key 元素的 hashCode() 方法计算出 hash 值, 再通过 hash 值得到索引, 并且索引处没有元素则将新元素直接加入; 如下图:

第一种情况.png

第二种情况:

通过 key 元素的 hashCode() 方法计算出 hash 值, 再通过 hash 值得到索引, 索引处有元素, 则再调用 key 元素的 equals() 方法, 检测链表上有么有相等的 key 值, 如果有直接覆盖; 如下图:

第二种.png

第三种情况:

通过 key 元素的 hashCode() 方法计算出 hash 值, 再通过 hash 值得到索引, 索引处有元素, 则再调用 key 元素的 equals() 方法, 检测链表上有没有相等的 key 值; 如果没有, 则检测链表上已有元素的个数:

第三种1.png 第三种2.png

3.put函数的源码与扩容

// 链表转为红黑树时的判断值
static final int TREEIFY_THRESHOLD = 8;

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 为局部数组变量 tab 赋值并检测是否为空 
    // n为tab数组长度
    // i是所存元素经过计算所得到的 tab 数组下标
    // p是tab[i]上的元素
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 第一种情况p为null,也就是tab[i]位置没有元素,可以直接存入数据
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            // 第二种情况p不为null,也就是tab[i]上已有元素,但是计算出来的key值相同,所以直接覆盖
            e = p;
        else if (p instanceof TreeNode)
            // 第三种情况
            // tab[i]上已经是红黑树结构类型了 所以添到红黑树上
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // 将新元素添加到tab[i]的链表尾
                    p.next = newNode(hash, key, value, null);
                    // 判断 tab[i] 上的链表是否应该转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 转为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 判断是否超过了HashMap的扩容值
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

treeifyBin(tab, hash) 与 resize() 函数

在put函数中会在将链表转为红黑树时, 调用这个函数; 其部分源代码如下:

static final int MIN_TREEIFY_CAPACITY = 64;

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 当第一次发生元素链表转成红黑树操作时,如果数组长度小于 MIN_TREEIFY_CAPACITY
    // 会自动调用 resize() 扩容;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
       // ...... 省略红黑树操作
    }
}

final Node<K,V>[] resize() {
    // 扩容之后会 reHash 重新分配所存储的元素,这样旧的链表就会打散,链表上的元素个数就会减少;
    // 如果还是有红黑树结构, 此时直接找到 TreeNode 的类型判断位置
    // ......
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 重新分配之后的已经确定这里是链表转为红黑树
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    // ......
                }
            }
        }
    }
    return newTab;
}

static final int UNTREEIFY_THRESHOLD = 6;
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    // ......
    
    // 下面函数是对红黑树再分配的操作,当元素个数大于 UNTREEIFY_THRESHOLD 时
    // 就会将链表转为红黑树存储这个值是 6;
    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

其实搞不懂为啥设计的这么复杂, 红黑树就算了, 又是8 又是6的, 先吐槽一下;
大体是这个样子的:

4.HashMap的扩容阈值验证

HashMap的扩容阈值是由变量 loadFactorthreshold 决定的; 前者是负载因子, 后者等于负载因子 x 数组容量; loadFactor 的默认值是 0.75 , 这个值也可以在 new HashMap对象时手动传入 (与 HashMap 性能有关);

4.1 验证代码示例一

HashMap 并没有对外提供获取 HashMap 对象数组容量的函数, 可以用反射的方式获取capacity()函数来拿到初始数组 table[]的大小;

示例代码如下:

public class HashMapTest {
    public static void main(String[] args){
        // 指定初始容量16 来创建一个HashMap
        HashMap hashMap = new HashMap(16);
        // 获取HashMap整个类
        Class<?> mapType = hashMap.getClass();
        // 获取指定方法,因为HashMap没有容量这个属性,但是capacity方法会返回容量值
        Method capacity = mapType.getDeclaredMethod("capacity");
        // 设置目标方法为可访问
        capacity.setAccessible(true);
        
        for (int i = 0; i < 14; i++) {
            hashMap.put(i, i);
            // 动态监测HashMap的容量与元素数量
            System.out.println("容量:" + capacity.invoke(hashMap) + ","
                               + "元素数量:" + hashMap.size());
        }
    }
}

打印结果

容量:16,元素数量:1
容量:16,元素数量:2
容量:16,元素数量:3
容量:16,元素数量:4
容量:16,元素数量:5
容量:16,元素数量:6
容量:16,元素数量:7
容量:16,元素数量:8
容量:16,元素数量:9
容量:16,元素数量:10
容量:16,元素数量:11
容量:16,元素数量:12
容量:32,元素数量:13
容量:32,元素数量:14

4.1 验证代码示例二

将 threshold 主动设置为0.5;

public class HashMapTest {
    public static void main(String[] args){
        // 指定初始容量16 来创建一个HashMap
        HashMap hashMap = new HashMap(16,0.5f);
        // 获取HashMap整个类
        Class<?> mapType = hashMap.getClass();
        // 获取指定方法,因为HashMap没有容量这个属性,但是capacity方法会返回容量值
        Method capacity = mapType.getDeclaredMethod("capacity");
        // 设置目标方法为可访问
        capacity.setAccessible(true);
        
        for (int i = 0; i < 14; i++) {
            hashMap.put(i, i);
            // 动态监测HashMap的容量与元素数量
            System.out.println("容量:" + capacity.invoke(hashMap) + ","
                               + "元素数量:" + hashMap.size());
        }
    }
}

打印结果

容量:16,元素数量:1
容量:16,元素数量:2
容量:16,元素数量:3
容量:16,元素数量:4
容量:16,元素数量:5
容量:16,元素数量:6
容量:16,元素数量:7
容量:16,元素数量:8
容量:32,元素数量:9
容量:32,元素数量:10
容量:32,元素数量:11
容量:32,元素数量:12
容量:32,元素数量:13
容量:32,元素数量:14

可以看到示例一是元素数量是 12 的时候开始扩容, 示例二是元素数量是 8 的时候就开始了扩容;

上一篇下一篇

猜你喜欢

热点阅读