Java集合学习总结-----HashMap源码剖析

JDK1.8的HashMap源码刨析

JDK1.8HashMap底层由桶数组+链表+红黑树三部分组成。红黑树是高度平衡的搜素二叉树,加红黑树的目的是为了提高查询效率,因为链表的查询时间复杂度O(n),而红黑树的查询时间复杂度为O(logN).。当N很大时,大大提高查询速率。

JDK1.8HashMap结构图:
在这里插入图片描述

主要属性

//当不指定容量时,默认初始化容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//树化的条件:1,链表结点个数大于8
static final int TREEIFY_THRESHOLD = 8;

//红黑树转链表:结点个数少于6时
static final int UNTREEIFY_THRESHOLD = 6;

// 树化的条件2:桶数组长度大于64
static final int MIN_TREEIFY_CAPACITY = 64;

// 内部链表的节点类  (静态内部类)
static class Node<K,V> implements Map.Entry<K,V>{
	final int hash;   //哈希值
	final K key;  
	V value;
	Node<K,V> next;
}

// 红黑树的结点类,继承了链表的结点类
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;  
 }     
        
 //桶数组
transient Node<K,V>[] table;

//扩容阈值,桶数组大于该值就会扩容 
int threshold;

// hash表中有效元素的个数
transient int size;

主要构造方法

无参构造

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

初始容量默认16,负载因子默认0.75。此时的桶数组仍然为null,当第一次put的时候才初始化。

指定容量的构造方法

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

该构造方法调用了下面的构造方法,参数为自定义的容量和默认的负载因子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))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

解析:首先进行参数的正确性判断,首先容量不能小于0,否则抛出异常,容量大于最大容量时,就默认使用最大容量即2的30次方。负载因子不能小于0,否则也抛出异常。Float.isNaN()方法判断一个数是否是一个合法的浮点数,如1.0 / 0.0,sqrt(-1)的结果都是非法的浮点数。桶数组未初始化之前,threshold 就是桶数组的长度,HashMap中桶数组的长度必须是2的幂,tableSizeFor方法就是这个功能。

参数为一个Map集合的改造方法
'待续。。。。。

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

解析:该构造方法也是采用默认的负载因子0.75.

构造方法总结

HashMap采用懒加载策略,在初始化阶段是不会马上就开辟空间,创建桶数组的。

为什么数组长度必须是2的幂

散列方法源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode())(h >>> 16);
}

首先判断key是否为null,当key为null的时候,默认hash值为0。>>>该符号为绝对右移,即不论正数还是负数向右移动16位之后,高位都是补0.。因子当key不为null的时候,将计算的hashCode的高16位和低16位异或的结果作为低16位,高16位保持不变,因为0和任何数异或得任何数。通过高16位和低16位相结合使得每一个key的hash码都更加独一无二,可以更好的避免hash碰撞。

桶下标得计算方法:

不管是增加,删除,查找都需要通过key定位到桶数组得具体下标。因此这一步是个很重要的操作

i = (n - 1) & hash

其中n表示数组长度,hash是通过key算的hash码。因为n是2得幂,所以(n - 1) & hash与hash % n 是等价的,但却有两个优点:

  • 位运算速度比取模运算快
  • 当hash为负数时,也可以找到正确的桶下标。

这就是数组长度必须是2的幂的主要原因,还有一个原因是一定程度上可以避免hash碰撞。

举例说明:
当n为2的幂如16 的时候,减一e为15,二进制形式01111:
在这里插入图片描述
当n不是2的幂假设为10,减一之后二进制形式01001:
在这里插入图片描述
这便是 为什么数组长度必须是2的幂d的原因。

主要的普通方法

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

将一个key ,value 对加入HashMap中。核心方法是里面的putVal方法。代码很长。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断数组数组y有没有被初始化,或者初始化过但长度为0
    if ((tab = table) == null || (n = tab.length) == 0)
        // 进行扩容
        n = (tab = resize()).length;
        // 计算对应索引,若该索引处没有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
       // 直接存放在数组中。
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // hash相等,key值也相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 直接替换(value替换)
            e = p;
         // key不相等,且已经树化,走红黑树的put
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { //走链表的put
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                   // 尾插
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                     // 结点个数大于8,可能被树化
                        treeifyBin(tab, hash);
                    break;
                }
                // 在链表中发现hash相等,key值也相等,替换即可
                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;
    // 判断是否需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

put步骤要点:

  • 首先判断桶数组是否初始化,没初始化或者自定义容量为0时,则进行resize()扩容。
  • 根据key计算hash值,再通过hash值计算桶中相应的下标。若该下标位置没有元素,则将这个元素添加进数组的该下标处即可。
  • 若该下标位置处已有元素,判断数组中的元素和要插入的元素的两个的key值是否相等(hashCode和equals方法判断),相等则用新的覆盖数组中的这个即可
  • 不相等,判断桶数组该索引处的结点是否为红黑树结点类型,如果是,走红黑树的put
  • 若桶数组该索引处的结点不是树节点类型,则走链表的put。
  • 判断链表长度是否大于8,大于8 且数组长度大于64后会树化,否则只是扩容。
  • 遍历链表,若发现某个结点的key与要插入的key相等的则替换。若一直不相等val则将要插入的元素插入在链表尾部。
  • 插入成功后判断有效元素个数是否大于threshold,即扩容阈值,超过则进行扩容。
get方法

通过key值查找相应的value。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

先通过key找到这个key-value对结点,找到则返回该节点的value,否则返回null。

源码:

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断桶数组是否已经初始化
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //根据key算下标,该下标处的第一个结点的key与要查找的key相等
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
           // 直接返回这个结点
            return first;
            // 在红黑树中找
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
               // 在链表中找
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

get步骤要点:

  • 首先判断桶数组是否初始化,未初始化或者长度为0,直接返回null。
  • 通过key算hash值,通过hash值算桶下标。
  • 若桶中该下标处的结点的key与传入的key相等,说明找到了,直接返回这个结点就ok了。
  • 不相等且该索引处结点是红黑树类型,就在红黑树中找。
  • 否则就在链表中找。
  • 找到返回,找不到返回null。
resize()扩容方法

代码太长了,不贴了。
当hashmap中的元素个数超过数组长度 * loadFactor时,就会扩容。

resize步骤要点:

  • 判断当前hashmap桶数组的长度是否大于0,小于0的话进行初始化操作。

  • 大于0,且已经超出容量的最大值 ,则将阈值提升为int的最大值,桶数组并不扩容。

  • 否则的话,容量翻倍,通过左移一位达到这个目的。

扩容之后,将通过rehash将旧表中的元素全部搬移到新表中。

猜你喜欢

转载自blog.csdn.net/weixin_43213517/article/details/89457795