java集合(4):HashMap源码分析(jdk1.8)

前言

  • Map接口虽然也是集合体系中的重要一个分支,但是Map接口并不继承自Collection,而是自成一派。
public interface Map<K,V>
  • Map集合存储键映射到值的对象。一个集合中不能包含重复的键,每个键最多只能映射到一个值。

  • Map 接口提供三种collection 视图(意思是3种迭代器),允许以键集、值集或键-值映射关系集的形式来迭代某个Map的内容。映射顺序 定义为迭代器在映射的 collection 视图上返回其元素的顺序。某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类。

  • Map接口最常用的两个实现类就是HashMap和TreeMap。前者以哈希表结构存储元素,后者以二叉树结构存储元素,而且Set接口下的HashSet底层结构就是HashMap,所以讲完HashMap再讲HashSet就很简单了。

正文

一,哈希表结构

  • 哈希表结构是数据结构中的重要概念之一,其原理不再赘述,本文重点介绍哈希表结构的代码体现。需要说明的一点是:jdk1.8以前的HashMap只使用了数组和链表的形式,这样可能导致数组某个位置上的链表过长,不利于查询。jdk1.8以后,如果某位置的链表超过一定长度就会转为红黑树,这样就有利于查询。

二,HashMap源码

1, 类名及常量

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

    private static final long serialVersionUID = 362498820763181265L;

    // Map集合初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // Map集合最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子,用于动态扩充数组容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 当链表上的节点数大于等于8时,链表转为树结构
    static final int TREEIFY_THRESHOLD = 8;

    // 当链表上的节点小于6时,树结构转链表
    static final int UNTREEIFY_THRESHOLD = 6;

    // 
    static final int MIN_TREEIFY_CAPACITY = 64;
}

2,成员变量

    // 存储元素的数组,每个元素都是Node型的,Node是一个静态内部类
    transient Node<K,V>[] table;

    // entrySet()方法返回的结果集
    transient Set<Map.Entry<K,V>> entrySet;

    // Map元素个数
    transient int size;

    // Map对象被修改的次数
    transient int modCount;

    // 增加数组容量的阈值
    int threshold;

    // 加载因子
    final float loadFactor;

3,Node类型元素的内部类形式

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

4,HashMap的4个构造方法

    public HashMap() {  // 1,无参构造方法,成员变量使用默认值
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

    public HashMap(int initialCapacity) { // 2,初始容量的构造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR); // 去调用2个参数的构造方法
    }

    public HashMap(int initialCapacity, float loadFactor) { // 3,指定容量和加载因子
        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;  

        // 调用计算容量的方法,返回大于initialCapacity的最小2的幂。
        this.threshold = tableSizeFor(initialCapacity);  
    }

    public HashMap(Map<? extends K, ? extends V> m) { // 4,接收一个map对象
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认加载因子
        putMapEntries(m, false); // 添加m到结构
    }

5,put()方法详解,该方法也是最重要的方法,下面是方法的执行流程图和源码分析

《java集合(4):HashMap源码分析(jdk1.8)》

执行过程:

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

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;

        // 如果table未初始化或长度为0,则进行初始化
        if ((tab = table) == null || (n = tab.length) == 0) 
            n = (tab = resize()).length;

        // 如果节点hash值对应的数组位置为空,直接赋值
        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))))
                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);

                        // 如果链表长度大于8,则将链表转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 如果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;  // map结构被修改次数自增
        if (++size > threshold) // 添加元素后,如果数组长度超过阈值,则扩容。
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5.1,扩容机制

  • 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

  • 数组的容量是2的次幂,也就是说每次扩展,长度扩展为原来的2倍。接着元素也要变换位置,那么,元素的位置要么是在原位置,要么是在原位置加上2的次幂的位置。所以,我们在扩充的时候,不需要再重新计算hash值,而是看原先的hash值新增的1bit是1还是0。如果是0,索引不变,元素待在原位;如果是1,新索引为“原索引+扩展长度”。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。

  • 扩容方法resize()
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;   // 当前数组赋值给oldTab
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;  // 当前阈值赋值给oldThr 
        int newCap, newThr = 0;  // 新数组容量,新阈值
        if (oldCap > 0) {

            // 如果超过最大容量,就不再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            // 如果老容量扩大2倍仍不超过最大值,则新容量为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 计算新的阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  // 创建新数组
        table = newTab;

        // 将每个元素移动到新的数组上
        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; 

                    // 如果e是树节点,则按照树结构处理该分支
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    // 如果e是链表节点,则按照链表结构处理该分支
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;

                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }

                            // 新索引(原索引+扩展容量)
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);

                        // 原索引放在老位置上
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }

                        // 新索引放在新位置上
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6,get()方法详解,get的过程就是遍历的过程。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;  // 调用下面的方法
    }


    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;  // tab是table的副本
        Node<K,V> first, e;  // first代表数组上链表的第一个元素
        int n; 
        K k; // k是first元素的key属性副本

        // 如果数组不为空,数组长度大于0,数组上链表第一个元素不为空,则进行遍历
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {

            // 如果参数的hash值和key与first的hash值和key相同,则返回first
            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;
    }

7,keySet()和entrySet()方法,

  • Map本身没有迭代器,若要迭代,需要将Map转化为Set集合,使用Set集合的迭代器进行迭代。如下图,这两个方法均返回一个Set集合对象,分别将key和(key,value)作为整体构成一个Set。

《java集合(4):HashMap源码分析(jdk1.8)》

方法源码:

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet(); // KetSet是个内部类,继承自Set体系,所以有迭代器方法
            keySet = ks;
        }
        return ks;
    }

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es; 

        // EntrySet也是内部类,同样继承自Set体系,有增强的迭代器方法
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

这两个方法常见用法:

package com.jimmy.map;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MapDemo1 {
    public static void main(String[] args) {

        Map<String, String> map = new HashMap<>();
        System.out.println(map.put("name1", "jimmy")); 
        System.out.println(map.put("name2", "jimmy")); 
        System.out.println(map.put("name3", "jimmy")); 

        Set<String> keySet = map.keySet(); // keySet()方法返回map集合中所有key的set集合
        for (String eachKey : keySet) {
            System.out.println(eachKey+"::"+map.get(eachKey));
        }

        System.out.println("------------------");
        // entrySet()方法返回map集合中(k,v)对的set集合
        Set<Entry<String, String>> entrySet = map.entrySet();
        for (Entry<String, String> eachMap : entrySet) {
            System.out.println(eachMap.getKey()+"--"+eachMap.getValue());
        }
    }
}

总结

map结构相对复杂,其实现类按照不同的数据结构(哈希表结构或者树结构)来存储元素,应该在学习了相应数据结构的基础上再学习相应的集合实现。

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/qq_32166627/article/details/72763737
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞