HashMap源码分析

1、什么是HashMap?

       HashMap是Map集合的一个常用实现类,它是非线程安全的。它存储的是键值对(key-value),key必须唯一,不可重复,value可重复。它的底层实现是数组+链表。

2、HashMap的模型

在这里插入图片描述

       了解HashMap的基本模型后,下面从源码解析HashMap。

3、HashMap类的继承关系

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...}

在这里插入图片描述
       其中,实现Serializable接口是为了使类可持久化,不是本文关注的重点,这里不多加赘述,想了解详细可参考这位大佬的文章:java类中serialVersionUID的作用
       Cloneable接口不包含任何方法,实现Cloneable接口仅仅是用来指示Object类中的clone()方法可以用来合法的进行克隆,想了解详细可参考:Cloneable接口及clone()

4.Map接口

       然后接下来是我们关注的重点——Map接口,它包含的方法如下:

public interface Map<K,V> {
	int size();//返回Map容器的数据长度
	boolean isEmpty();//返回容器是否为空
	boolean containsKey(Object key);//验证Key是否存在
	boolean containsValue(Object value);//验证Value是否存在
	V get(Object key);//按Key获取键值对
	V put(K key, V value);//向容器添加键值对
	V remove(Object key);//按Key删除键值对
	void putAll(Map<? extends K, ? extends V> m);//复制其他容器的数据到此
	void clear();//清空容器
	Set<K> keySet();//返回所有key的集合
	Collection<V> values(); //返回所有values的集合
	Set<Map.Entry<K, V>> entrySet();//返回所有键值对
	interface Entry<K,V> {//内部接口
		......
	}
	boolean equals(Object o);//对象相等判断
	int hashCode();//hashCode值
	default V replace(K key, V value) {//键值对替换
	    V curValue;
	    if (((curValue = get(key)) != null) || containsKey(key)) {
	        curValue = put(key, value);
	    }
	    return curValue;
	}
	//链表节点
        static class Node<K,V> implements Map.Entry<K,V> {...}
        //红黑树节点
        static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
	......
}

       Map接口大致地定义了一个Map容器该有的基本方法及实现规范:它存储键值对(Key-Value)类型的数据,put()添加数据,get()获取数据,size()查看长度,remove()删除元素,clear()清空容器。

5.HashMap的默认常量

HashMap类实现了Map接口,在它的基础上添加了一些自己独有的特性。先来看看源码中写在最前面的几个常量(源码中在每一个常量前面都会用一大段的英文解释这个常量的作用,并不难理解):

private static final long serialVersionUID = 362498820763181265L;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

static final int TREEIFY_THRESHOLD = 8;

static final int UNTREEIFY_THRESHOLD = 6;

static final int MIN_TREEIFY_CAPACITY = 64;

第一个数据是:serialVersionUID = 362498820763181265L
       serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。

第二个数据是:DEFAULT_INITIAL_CAPACITY = 1 << 4默认初始容量)。
       当一个HashMap对象被创建时,如果没有在参数中指定它的大小,那它将被初始化为默认长度16。

第三个数据是:MAXIMUM_CAPACITY = 1 << 30最大容量)。
       一个HashMap容器的最大容量不能超过2^30(1,073,741,824‬)。

第四个数据是:DEFAULT_LOAD_FACTOR = 0.75f默认负载因子)。
       负载因子关乎到容器的扩容,HashMap的默认负载因子为0.75,即当容器中的数据数量达到容器容量的3/4时,容器将会进行扩容

第五个数据是:TREEIFY_THRESHOLD = 8树阈值)。
       当相同位置的键值对数量增加到这个值后,它们的存储结构将由链表转化成红黑树,默认该值为8。源码中注释“该值必须大于2,并且至少应为8,以符合树木移除中关于在收缩时转换回普通垃圾箱的假设”。

第六个数据是:UNTREEIFY_THRESHOLD = 6非树阈值)。
       当相同位置的键值对数量减少到这个值后,它们的存储结构将由红黑树重新转为链表,默认该值为6。

第七个数据是:MIN_TREEIFY_CAPACITY = 64最小树容量)。
       当HashMap容器的大小增加到这个值时,不管最大相同位置的键值对是否达到TREEIFY_THRESHOLD(树阈值),它的存储结构都将转化成红黑树

6.HashMap的构造方法

HashMap有四个构造方法:
第一个,参数为空,系统将默认负载因子传入。

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

第二个,容器大小(initialCapacity)+负载因子(loadFactor),系统根据用户自定义的容器大小和负载因子创建HashMap对象。

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);
}

第三个,容器大小(initialCapacity),系统调用第二个构造方法,根据用户定义的容器大小,再将默认负载因子传入。

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

第四个,Map对象,根据传入的Map对象复制一个HashMap对象。

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

选择上面的构造方法创建好容器后,就需要往容器中添加数据了,那么这些添加进去的数据是什么样的呢?

7.HashMap存储的数据

前面已经说了,HashMap存储键值对的数据,并且这些数据在默认情况下以链表的形式存储,当链表长度大于8,则转成红黑树
OK,我们先来看第一种,当数据以链表的形式存储时,它的每个节点是怎样的。

//链表数组,存储HashMap元素的地方
transient Node<K,V>[] table;//transient关键字:在序列化时,跳过该字段

从源码中可以看到,它将数据存储在一个Node<>数组中,这个Node其实就是一个链表。

//HashMap的内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
    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;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

我们从上往下来捋一捋这部分源码,它实现了Map接口的一个内部接口Entry(),然后每个链表节点对象都存储了四个值,分别是hash值(后面会讲),keyvalue和指向下一个节点的next
剩下的就是用来访问这个节点对象的get和set方法了,它还重写了hashCode()方法,用来给equals()方法做判断:当两个节点对象的key和value相等时,视为两个节点对象相等。

当节点数量达到阈值时,它将转成红黑树,以下是红黑树节点在HashMap中的定义

//HashMap的内部类TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // 红黑树父节点
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // 删除后需要取消链接
    boolean red;//是否为红
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
    //返回根节点
    final TreeNode<K,V> root(){...}
    //将红黑树的根节点移至链表首节点
    static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {...}
    //根据给的hash值和key在一颗红黑树中寻找节点
    final TreeNode<K,V> find(int h, Object k, Class<?> kc){...}
    //根据给的hash值和key在一颗红黑树中寻找节点
    final TreeNode<K,V> getTreeNode(int h, Object k) {...}
    //
    static int tieBreakOrder(Object a, Object b) {}
    //
    final void treeify(Node<K,V>[] tab) {...}
    //
    final Node<K,V> untreeify(HashMap<K,V> map) {...}
    //
    final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {...}
    //
    final void removeTreeNode(HashMap<K,V> map,Node<K,V>[] tab, boolean movable) {...}
    //
    final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {...}

首先,我们梳理一下TreeNode的继承关系。
在这里插入图片描述
TreeNode继承了Node,所以TreeNode不止是一个红黑树,也是一个链表,而在TreeNode 源码中还可以看到“TreeNode<K,V> prev;”用于指向前置节点,所以:TreeNode不只是一棵红黑树 ,也是一个双向链表。当容器中的节点数据从Node转成TreeNode后,它依然存储在Node<K,V>[] table中,其结构模型如下:

在这里插入图片描述
现在容器中的数据是怎样的已经确定下来了,接下来就应该确定数据在容器中的存放位置了。
HashMap通过 hash值+indexFor()方法 来确定数据的插入位置。

8.hash值的计算

关于hash值的计算,源码中如此写道:

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

这段代码并不难理解,稍稍简化一下就变成了

int hash(Object key){
    if(key==null)return 0;
    int h=key.hashCode();
    return h^(h>>>16);
}

所以hash值只跟对象的hashCode()有关,其中最最最重要的一个参数key.hashCode(),HashMap重写了hashCode()方法:

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

而hashCode(key)和hashCode(value)的来源为:

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}
public native int hashCode();

看到了吧,最终这个值还是由Object类的本地方法提供的,并不需要我们自己计算(网上有hashCode的计算公式)。每一个对象被创建出来,它们的hashCode()都是不一样的。

//既然hashCode()不用我们计算,那我们随便举个例子来计算一下上面的hash值,如 key.hashCode()=156.
0000 0000 1001 1100 //156的二进制
0000 0000 0000 0000 //>>>16:无符号右移16位,高位补0
0000 0000 1001 1100 //h^(h>>>16):^ 位异或运算,相同为0,不同为1

这样,我们就计算出了当key的hashCode为156时,它的hash值

hash=156

8.indexFor()方法计算插入位置

先来看看indeFor()方法的源码。

static int indexFor(int h, int length) {//h为hash值,length为容器大小
    return h & (length-1);//& 位与
}
//假设现在容器的大小为16,一个插入数据的hash值为156,则它的插入位置为:
 0000 0000 1001 1100 //156的二进制
 0000 0000 0000 1111 //16-1=15的二进制
 0000 0000 0000 1100 //156&15=12,则该数据的插入位置为数组下标12的位置

从上面这个length-1我们也可以看出一些问题:为什么HashMap的初始大小最好设置成2n(默认16)?
因为当这个length为2n时,length-1的二进制会变成 (0)n(1)m,这样它按位与多个不同的数时,可以极大地减少碰撞率
至此,HashMap的数据插入应该算分析完了,但当我在我的HashMap源代码文件中查找它时,却发现没有这个方法。
在这里插入图片描述
经过我的一番查找,我发现,原来indexFor()的计算被直接嵌入到调用它的方法去了。

//将链表转成红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index;//index变量即为要保存indexFor()的位置
    ...
    if (...)
        ...
    else if ((e = tab[index = (n - 1) & hash]) != null) {//indexFor()
        ...
    }
}

//删除链表节点
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    int n, index;//index为保存indexFor()的位置
    if (... &&
        (p = tab[index = (n - 1) & hash]) != null) {//indexFor()
        ......
    }
    return null;
}

//删除红黑树节点
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    ...
    int index = (n - 1) & hash;//indexFor()
    ......
}

9.HashMap的扩容机制

HashMap采用的是懒扩容,即每次在进行put操作时,它会检查容器中的数据量,当HashMap容器中的数据量达到当前容量*负载因子(0.75)时,进行扩容。

//插入操作put(),它调用了putVal()

transient Node<K,V>[] table;

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

//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;
    if ((tab = table) == null || (n = tab.length) == 0)//如果表为空或者表的大小为0
        n = (tab = resize()).length;//调用resize()为表初始化大小
        
    ......
    
    if (++size > threshold)//如果数据量超标
        resize();//扩容
    ...
    return null;
}

//实现初始化表或者扩容表
final Node<K,V>[] resize() {
    ......
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return oldTab;
       }
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
           newThr = oldThr << 1; // 新表扩容为原来的两倍
       }
    ......
    }
    ......
    return newTab;
}

总结,当HashMap在put()需要初始化容量或进行扩容时,会调用resize(),讲原表初始化或进行2倍扩容根据数据的hash值计算它在新表中的位置重新存放

10.总结

对HashMap的源码分析暂时就这么多了,只讲了类和方法的一部分关键代码,并没有对整个的代码进行全部分析,可能以后会补全吧…可能。我还还只是个即将毕业的新人,文中有什么疏漏的地方,还望各位大佬不吝指教,在下方评论区指出。

返回总目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值