Java 集合框架_HashMap源码解析

今天终于分析HashMap的源码,其实它的主要算法在我的Java 集合框架_HashMap JDK1.8新算法这篇文章中详细说明了。HashMap集合是通过哈希表储存数据的,关于哈希表,请阅读这篇文章数据结构_哈希表(Java)。

一. 主要成员属性

   // 默认初始容量 16,必须是2的幂数。 即只能是 16 , 32 , 64 等等
    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;

   // 用来储存链表的数组
    transient Node[] table;

    // 缓存entrySet集合,避免每次调用entrySet()方法,都要重新生成Set集合
    transient Set> entrySet;

    // HashMap集合元素数量
    transient int size;

    // 记录集合修改次数,在多线程情况下,用于判断集合是否被修改
    transient int modCount;

    // HashMap集合阈值,集合元素数量超过这个值,那么就要扩充数组大小
    int threshold;

    // 负载因子
    final float loadFactor;

二. 静态内部类Node

   static class Node implements Map.Entry {
        // 这个值是通过static final int hash(Object key)静态方法得到的,它和key的哈希值有关
        final int hash;
        // 键值对中的key值
        final K key;
        // 键值对中的value值
        V value;
        // 指向下个Node的引用,这样才能形成一个链表
        Node next;

        Node(int hash, K key, V value, Node 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; }

        // 将key和value的哈希值异或,得到一个新的哈希值。
        // 而且它保证的了相同key和value的Node,它们得到的哈希值是一样的。
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        // 替换键值对中的value值,并返回被替换的值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        // key和value都相等的两个Node,它们是一样的。
        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接口,HashMap储存的键值对元素就是Node类的实例对象,它有四个属性:

  1. hash: 这个值是通过static final int hash(Object key)静态方法得到的,它和key的哈希值有关。通过这个值进行哈希化,得到对应下标值。
  2. key:键值对中的key值
  3. value:键值对中的value值
  4. next:指向下个Node的引用,这样才能形成一个链表

三. 构造函数

1. 空参构造

   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; defaulted
    }

注意:空参构造只设置了loadFactor值,没有设置threshold值

2. 设置初始化大小和负载的构造函数

   public HashMap(int initialCapacity) {
        this(initialCapacity, 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;
        // tableSizeFor返回的是数组的长度大小,但为什么将它赋值给阈值threshold,
        // 那是因为创建数组不是在初始化的时候,所以threshold这里只是暂存数组长度,等创建完数组后,会改变这个值的
        this.threshold = tableSizeFor(initialCapacity);
    }

通过tableSizeFor(initialCapacity)方法,保证数组长度是2的幂数,因为数组还没有创建,threshold表示数组长度

3. 通过Map集合创建的构造函数

    public HashMap(Map m) {
        // 设置默认负载因子
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

调用putMapEntries方法,将m集合中数据存入HashMap集合中

三. 重要方法

3.1 添加键值对元素

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

    // 向HashMap中存放键值对元素
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        // table为null或者table数组长度是0,都要去创建新数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // [i = (n - 1) & hash]表示哈希化得到的下标值,如果为null,表示要创建一个新列表,存放到对应位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            // 如果这个条件相等,表示要插入的键值对元素key值在Map集合中已存在,那么就替换value值,e就表示已存在的键值对元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果是红黑树节点,就调用它的存放键值对的方法
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 遍历整个链表,binCount用来记录链表中键值对数量,用来判断是否将链表转成红黑树
                for (int binCount = 0; ; ++binCount) {
                    // 为null,表示已经到了链表尾,就直接插入这个键值对
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 将链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果发现key键一样,那么就直接跳出循环,执行value值覆盖操作
                    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;
                // 当原来的oldValue为null,或者onlyIfAbsent的值为false,那么就进行覆盖
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 用于LinkedHashMap回调的,在HashMap中是空实现
                afterNodeAccess(e);
                // 直接返回,因为没有新添加值
                return oldValue;
            }
        }
        ++modCount;
        // 超过阈值,那么调用resize()进行数组扩容
        if (++size > threshold)
            resize();
        // 用于LinkedHashMap回调的,在HashMap中是空实现
        afterNodeInsertion(evict);
        return null;
    }

向HashMap集合中添加元素是通过putVal方法实现的。

方法参数说明:hash、key、value创建键值对元素需要,onlyIfAbsent表示是否阻止覆盖键值对元素的value值,evict用于afterNodeInsertion方法,主要用在LinkedHashMap中。

putVal方法的主要流程:

  1. table为null或者table数组长度是0,那么通过resize()方法创建新数组
  2. 利用hash值通过(n - 1) & hash公式获取哈希化后的下标值,然后判断该位置是否有链表,如果没有,通过newNode方法创建新的键值对元素,存放到该位置
  3. 如果链表不为null,那么就是查找链表中有相同的hash值和key值的键值对元素。
  4. 查看表头的键值对元素是否符合条件。如果符合条件,就用e记录表头元素
  5. 如果不符合,判断表头元素是否是红黑树节点元素,如果是,那么调用红黑树的putTreeVal方法存放键值对元素
  6. 如果还不是,那么遍历整个链表,binCount用来记录链表中键值对数量,用来判断是否将链表转成红黑树。
  7. e != null表示没有添加新的键值对元素,而是找到相同key的键值对元素,通过onlyIfAbsent值,来判断是否要进行value值的覆盖。
  8. 如果e == null表示添加新的键值对元素,那么就要将size值自增,然后判断是否超过阈值threshold,如果是,那么通过resize()进行数组扩容。

3.2 添加一个Map集合

    public void putAll(Map m) {
        putMapEntries(m, true);
    }

   final void putMapEntries(Map m, boolean evict) {
        // 获取集合m键值对元素的数量
        int s = m.size();
        if (s > 0) {
            // 如果table为null,表示数组还没有创建,这个时候threshold表示的是数组长度,而不是阈值,
            // 所以这里进行了不同处理
            if (table == null) {
                // s表示要存储键值对元素的数量。所以除以负载因子就得到所需要的数组长度大小
                float ft = ((float)s / loadFactor) + 1.0F;
                // 防止超过数组最大长度
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 如果所需要数组的长度比原先设置的大,就要重新计算数组长度
                // 不直接将t赋值给threshold,是因为Map集合数组长度必须是2的幂数。
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 如果table已存在,这个时候threshold表示阈值,所以s大于threshold时,就要调用resize方法进行数组扩容
            else if (s > threshold)
                resize();
            // 遍历集合m中所有的键值对元素,然后调用putVal方法,将键值对存放到本集合中
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

通过putMapEntries方法添加集合中全部元素,方法流程:

  1. 先获取m集合的键值对元素大小s
  2. 判断table是否为null,如果为null,那么通过s来计算数组的长度,因为数组还没有创建,这时threshold就表示数组长度,而不是阈值
  3. 如果数组table已经存在,那么就判断m集合元素大小是否超过阈值threshold,如果是,那么就要通过resize()方法进行数组扩容
  4. 遍历集合m中所有的键值对元素,然后调用putVal方法,将键值对存放到本集合中

3.3 获取键值对元素

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

    // 通过hash和key值获取对应的键值对Node,hash的值是通过hash(key)方法得到的
    final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        // tab表示哈希表,n表示哈希表数组长度,first表示对应下标链表头元素。
        // (n - 1) & hash得到哈希化后的下标值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 通过hash,已经key的equals方法来检查是否是相同的key。
            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)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;
    }

通过key值获取键值对元素,主要流程:

  1. 通过hash(key)获取hash值
  2. 通过(n - 1) & hash得到哈希化后的下标值,进而得到对应位置的链表,如果为null,表示没有找到。
  3. 先判断表头元素的hash值和key值是否与参数中的相同,如果相同就返回表头元素。
  4. 如果不同,那么判断下一个元素是不是红黑树节点,如果是,就通过红黑树获取对应的元素
    5, 否则,就遍历链表寻找对应的键值对元素。

3.4 删除元素

   public V remove(Object key) {
        Node e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    // 删除HashMap集合中的元素
    final Node removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node[] tab; Node p; int n, index;
        // tab表示哈希表,n表示哈希表数组长度,index表示哈希化下标值,p表示链表头元素
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node node = null, e; K k; V v;
            // 寻找对应hash和key的元素Node
            // 1.先比较表头元素,如果相等就是它,如果不相等,就获取表头下一个元素
            // 2.判断元素e是不是红黑树节点,如果是那么通过红黑树的getTreeNode方法获取对应节点元素
            // 3.如果不是,就遍历链表,找到相同元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 找到要删除的元素node。如果matchValue为true,那么还要比较元素node的value值与传入的value是否相同
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 如果node是红黑树节点,那么调用红黑树的removeTreeNode方法,删除节点
                if (node instanceof TreeNode)
                    ((TreeNode)node).removeTreeNode(this, tab, movable);
                // node == p相等,表示元素node是表头元素,那么就要重新赋值表头
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

通过removeNode方法删除集合中元素。

方法中参数说明:hash、key用来查找要删除的键值对元素,value、matchValue用来判断找到的键值对元素要不要删除,movable用于红黑树删除节点时需要

方法流程:

  1. 首先通过hash与key查找出对应的键值对元素,这部分代码与getNode方法几乎一样
  2. 如果node不为null,且matchValue为false或者传入的value值与node元素的value相同,那么就要删除该元素
  3. 如果node是红黑树节点,那么调用红黑树的removeTreeNode方法,删除节点
  4. node == p相等,表示元素node是表头元素,那么就要重新赋值表头
  5. node != p,那么p.next = node.next,将node节点从链表中移除
  6. 最后将size自减

四. HashMap的迭代器

因为HashMap可以返回key值的Set集合,value值的Collection集合,以及键值对的Set集合,它们是以键值对集合为主。

4.1 HashIterator:迭代器抽象父类

   abstract class HashIterator {
        Node next;        // 下一个键值对元素
        Node current;     // 当前键值对元素
        int expectedModCount;  // 用于检查fast-fail异常的
        int index;             // 用来记录哈希表下标位置

        HashIterator() {
            expectedModCount = modCount;
            Node[] t = table;
            current = next = null;
            index = 0;
            // 如果哈希表数组已经创建了,并且哈希表中有值,那么要寻找第一个元素
            if (t != null && size > 0) {
                // 从数组下标0开始寻找链表,并将链表头赋值给next
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node nextNode() {
            Node[] t;
            // 将next赋值给e,用于返回
            Node e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            // 如果e.next为null,表示这个链表已经到结尾了,那么就要寻找下一个链表
            if ((next = (current = e).next) == null && (t = table) != null) {
                // 寻找下一个链表,并将链表头赋值给next
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            // 当前遍历到元素current
            Node p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            // 调用removeNode方法删除当前元素
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

注意:因为HashMap是通过哈希表储存的,那么就表示数组有的下标位置是空值,所以遍历哈希表,就要找到数组中所有的链表。而在HashIterator就通过很好的方式找到链表。如下:
do {} while (index < t.length && (next = t[index++]) == null);
当next不为null时,表示找到链表了,跳出循环。index++从0开始自增,表示会遍历完整个数组。

4.2 KeyIterator、ValueIterator和EntryIterator

  final class KeyIterator extends HashIterator
        implements Iterator {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator> {
        public final Map.Entry next() { return nextNode(); }
    }

4.3 KeySet类

  public Set keySet() {
        Set ks = keySet;
         // 延迟创建keySet
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

    final class KeySet extends AbstractSet {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        // 返回KeyIterator迭代器
        public final Iterator iterator()     { return new KeyIterator(); }
        // 调用HashMap的containsKey方法
        public final boolean contains(Object o) { return containsKey(o); }
        // 调用HashMap的removeNode方法
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        // 返回Spliterator
        public final Spliterator spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        // 遍历整个哈希表每个元素,调用action方法
        public final void forEach(Consumer action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                // 遍历整个数组
                for (int i = 0; i < tab.length; ++i) {
                    // 遍历链表
                    for (Node e = tab[i]; e != null; e = e.next)
                        // 调用action,传入key值
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

4.4 Values类

   public Collection values() {
        Collection vs = values;
        // 延时创建values
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
    }    

    final class Values extends AbstractCollection {
        public final int size()                 { return size; }
        // 调用HashMap对应的clear方法
        public final void clear()               { HashMap.this.clear(); }
        // 返回ValueIterator迭代器
        public final Iterator iterator()     { return new ValueIterator(); }
        // 调用HashMap的containsValue方法
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator spliterator() {
            return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        // 遍历整个哈希表每个元素,调用action方法
        public final void forEach(Consumer action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                // 遍历整个数组
                for (int i = 0; i < tab.length; ++i) {
                    // 遍历链表
                    for (Node e = tab[i]; e != null; e = e.next)
                        // 调用action,传入value值
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

4.5 EntrySet类

   public Set> entrySet() {
        Set> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet> {
        public final int size()                 { return size; }
        // 调用HashMap对应的clear方法
        public final void clear()               { HashMap.this.clear(); }
        // 返回EntryIterator迭代器
        public final Iterator> iterator() {
            return new EntryIterator();
        }
        // 是否包含o对象
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            // 将o转成Map.Entry对象
            Map.Entry e = (Map.Entry) o;
            // 得到key值
            Object key = e.getKey();
            // 通过的HashMap的getNode方法,获取对应的键值对元素candidate。
            Node candidate = getNode(hash(key), key);
            // 返回是否包含
            return candidate != null && candidate.equals(e);
        }
        // 调用HashMap的removeNode方法移除键值对元素
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry e = (Map.Entry) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer> action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

总结:

HashMap是通过哈希表来储存元素的。

一. 快速哈希化

HashMap是通过两个方面实现快速哈希化的问题:

  1. 通过static final int tableSizeFor(int cap)方法,保证数组的长度是2的幂数(即2^n)。
  2. 通过(n - 1) & hash快速得到哈希化的下标值。其中n表示数组的长度即2的幂数,hash并不是key的hashCode值,而是(h = key.hashCode()) ^ (h >>> 16)计算后的值(防止高位丢失问题)

二. 数组扩容

当HashMap的元素容量超过阈值threshold,就要进行数组扩容了。最重要的操作就是将老数组中的值存放到新数组中。

因为数组长度是2的幂数2n,而每次扩容都是2倍即新数组长度是2(n+1)。
那么通过(length - 1) & hash获取哈希化的下标值,就只有两个情况,要么是原来的值,要么是原来的值加上老数组的长度。
那么老数组的链表就可能被分成两个链表,分别放在原来下标位置或者原来下标加上老数组的长度的位置。
在这里有详细解释

三. 主要方法

  1. 添加或者替换元素: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
  2. 通过key值获取键值对元素:final Node getNode(int hash, Object key)
  3. 删除键值对元素: final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)

你可能感兴趣的:(Java 集合框架_HashMap源码解析)