手撕源码----jdk 8.0 HashMap源码1

在我们手撕jdk 8.0 HashMap源码之前需要知道源码中这几个常量的意义:

 * DEFAULT_INITIAL_CAPACITY : HashMap 的默认容量 : 16
 * DEFAULT_LOAD_FACTOR :      HashMap的默认加载因子 : 0.75
 * threshold :                扩容的临界值 = 容量 * 填充因子 : 16 *0.75 = 12
 * TREEIFY_THRESHOLD :        Bucket中链表长度大于该默认值,转化为红黑树 : 8
 * MIN_TERRIFY_CAPACITY :     桶(Bucket)中的Node被树化时最小的hash表容量 : 64

HashMap 底层用了 数组 + 链表 + 红黑树实现。

其中数组table : Node<K,V> 存放的是链表的头结点(非空)

Node是HashMap的内部类,有如下字段:

hash;    //哈希值
key;     //
value;   //
next;    //next指针,类型是Node<K,V>

Node是链表的节点元素,这里是jdk8,采用的尾插法。

Map接口其实还定义了Entry接口,里面主要是getValue()和getKey()方法,获得Map容器中的键值对元素。

我们先来看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);
    }

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

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

HashMap主要就这三个构造函数

第一个构造函数主要就是给填充因子和数组长度赋值

第二个构造函数是只有一个参数,就是给数组长度赋值。

第三个是无参构造函数,填充因子和数组长度全部是默认值。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

这个方法则是用来实现putAll()和HashMap的构造函数

如果当前的内部数组是null(用于构造函数),我们观察传入的Map

float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);

主要看这段代码

为什么要算 s / loadFactor?

因为这里用于构造函数,一开始的构造函数里的内置数组就是Null,而根据公式:

threshold = capacity * loadFactor

所以:

capacity = (float) threshold / loadFactor

这里其实是根据传入的map对象反推我们现在造的HashMap对象的capacity

下面就进行判断,如果我们反推出来的capacity大于限定数组最大长度就改为最大长度,否则不变。

如果这个capacity大于threshold(扩容阈值)就根据t重新确定扩容阈值。

而下面那段代码:

 else if (s > threshold)
                resize();

则是我们调用putAll()要执行的代码,如果传入的长度大于扩容阈值,那么就要更新HashMap的capacity和threshold。

最后调用:

for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }

把传入的Map添加进容器,这里还会调用putVal方法,我们后面再分析。

下面看get(Object key)方法:

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

这里它调用了getNode方法,根据key的哈希值和key对象找到Node节点,如果没找到返回Null,否则返回key对应的value值

那么getNode方法是怎么样的呢:

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) {
            if (first.hash == hash && // always check first node
                ((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;
    }

我们首先看这一段代码:

(tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null)

这里首先是要做赋值,把当前HashMap对象内置的table赋给局部的tab数组,tab的长度赋给n,根据哈希算法找到我们要找的key的头结点在数组中的位置。

并且还要进行判断,防止你这个数组是空的然后再引用产生空指针异常了,赋值并且判断后就进入查找了。

if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

如果头节点的hash值和我们要找的key的哈希值相等并且key equals k 那么就说明找到了,返回头结点,没找到接着往下找。

if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);

如果当前这个不是链表结构而是红黑树,就调用红黑树的查找方式查找key。

do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }

经过刚才的红黑数判断后,说明这应该是链表结构,就根据链表结构使用do - while 遍历链表

根据hash值和equals()配套查找与k相同的key,找到了返回Node节点,没找到返回null。

那么这个方法我们就看完了。

再看下一个方法:boolean containsKey(Object key) 根据key查找是否有这样的key的键值对

    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

这里也是要使用我们上面的getNode方法,没找到说明没有,返回false,找到了说明有返回true。

我们再来看一个常用方法: put(K key,K value) 方法

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

这个方法主要调用了putVal方法,我们看看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)
            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;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                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
                            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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

好家伙,这个方法太长了,我们一段一段研究。

首先还是来介绍一下参数:

参数:
hash – 哈希值
key – 键
value – 值
onlyIfAbsent – 如果是true,不改变节点的value值
evict – 如果是false,说明正处于创建模式
Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

首先定义几个变量,tab : Node[]  ,   p 是当前的节点,n是数组长度。

这段代码主要判断是不是出于创建模式,如果是创建模式就创建tab数组并且重新赋给n值。

 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

如果根据哈希值没有在数组中找到这个键值对,就可以在哈希值对应的数组位置新建一个节点。

否则,如果找到的话:

 Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

引用e指向头节点.

else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

如果当前这个不是链表而是红黑树就根据红黑树的方法添加进去。

否则,这个就是链表,我们就根据链表的方法去查找:

else {
                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
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }

根据binCount来计数,e始终指向p的下一个节点,p用来遍历。

如果e指向的节点是空,说明这条链表没有和key相同的键,就可以插入了,并且如果当前计数(节点个数)大于TREEIFY_THRESHOLD(数组中链表长度的默认最大值),就要进行一次判断要不要把这条链表转化为红黑树存储。

如果我们在遍历这条链表的时候发现这条链表有和key相同的值了,就跳出循环。

if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }

如果e不为空:存在一个节点其key与我们传入的key相同,就要根据onlyIfAbsent判断要不要更改这个节点的value值,并且返回旧的value值。

 ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

如果e为空说明我们是插入进去的,不要忘了长度以及修改次数都要+1,并且进行一次容量判断。

下面来看看常用的putAll(Map map) 方法,把Map容器中的元素插入到当前HashMap容器

public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

这就要调用我们之前介绍的putMapEntries()方法,并且这不是创建模式。

 

未完待续......

posted @ 2021-09-09 15:36  Apak陈柏宇  阅读(63)  评论(0编辑  收藏  举报