【3月6日】源码阅读日记(1):HashMap

本文深入剖析了Java中的HashMap数据结构,探讨了其内部实现机制,包括哈希算法、节点存储及扩展策略等内容,并对比了与Hashtable的主要区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

首先,致敬下几位authors

@author  Doug Lea
@author  Josh Bloch
@author  Arthur van Hoff
@author  Neal Gafter

HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。

1. HashMap概述

HashMap是基于哈希表的 Map 接口的实现,以key-value对的形式存储数据。

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

通过源码,我们知道,HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
- HashMap 的实现不是同步的,这意味着它不是线程安全的。
- 它的key、value都可以为null
上述两点是HashMap和Hashtable的最重要的区别。此外,HashMap中的映射不是有序的。

可以使用工具类Collections中的方法:Map m = Collections.synchronizedMap(new HashMap(…));来获取一个并发的hashmap。当遍历HashMap时,有另一个Thread试图修改hashmap,会立即终止迭代并抛出 ConcurrentModificationException ,即所谓的fail-fast策略。

初始容量和加载因子

在HashMap中,有两个参数会影响其性能::“初始容量(initialCapacity)” 和 “加载因子(loadFactor)”。


容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

通过下面的源码可以看出,初始容量必须是2的平方,默认为16,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。


    /**
     * Increases the capacity of and internally reorganizes this
     * hashtable, in order to accommodate and access its entries more
     * efficiently.  This method is called automatically when the
     * number of keys in the hashtable exceeds this hashtable's capacity
     * and load factor.
     * 增加散列表的容量并在内部重新组织,以便更有效地容纳和访问条目。
     * 当散列表中的键数超过散列表的容量和加载因子的乘积时,将自动调用此方法。
     * 使得 :newCapacity = (oldCapacity << 1) + 1
     */

    @SuppressWarnings("unchecked")
    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }
HashMap的构造函数
  • 1 参数:初始容量+加载因子
   /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
       //a. 初始容量小于零 非法
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //b.初始容量大于 MAXIMUM_CAPACITY,置为 MAXIMUM_CAPACITY。
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 加载因子小于零或者为null,非法
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
  • 2 参数 : 初始容量
/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        // 加载因子为默认值
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 3 参数:无
  /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 4 参数:子Map
    这里面用到了putMapEntries()方法,该方法用于重置Map的key-value。
/**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
HashMap的API
void                 clear()
Object               clone()
boolean              containsKey(Object key)
boolean              containsValue(Object value)
Set<Entry<K, V>>     entrySet()
V                    get(Object key)
boolean              isEmpty()
Set<K>               keySet()
V                    put(K key, V value)
void                 putAll(Map<? extends K, ? extends V> map)
V                    remove(Object key)
int                  size()
Collection<V>        values()

2.源码解析

2.1 实现原理

在HashMap的实际实现时,其底层为bucket的数组(bucket=bin)。

为让Node分布更均匀,不至于扎堆集中到同一个bin中,通过key.hashCode()经位运算得一个h值,在利用此h值来计算数组下标index。据此index,在数组中定位到bucket,然后在bucket中进行查找或插入。bucket有list和tree两种形式:list式的node更小,但是可能导致bucket很深,遍历list时更耗时;treenode更大,但其能有效降低bucket的深度,能够更加快速的遍历bucket。在实际使用时,只有当map比较大时,才会采用tree式的bucket,以空间换时间。
//几个重要属性
transient Node<K,V>[] table;     //bucket的数组,即底层list的数组  
transient int size;              //<key, value>对的数量,调用size()方法返回的就是这个量  
transient int modCount;          //记录在迭代时,map被修改的次数,据此在并发环境下报告异常,用来实现fail-fast机制的。
2.2 hash的原理

HashMap最大的优点就是快。


首先看一下这个hash是如何出来的,上源码:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

以上也就解释了为什么的HashMap是允许null的。且对于key来说,如果是null,返回的hash就是0,否则就返回 (h = key.hashCode()) ^ (h >>> 16)。
此过程调用了key.hashCode()方法,hashCode()是超类Object中定义的方法,返回对象的hash值,对此方法的常规协定有:

  • 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

此方法不同的类有具体的实现,以String中的为例,看看其hashCode()方法的说明:


String 对象的哈希码根据以下公式计算:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。(空字符串的哈希值为 0。)选择31是因为它是一个奇素数,但是它已经成为了传统的选择。总体上来说,使用31的优势在于:

  • 31是一个素数,相乘得到的结果更具有唯一性;
  • 31还是一个奇数,因为偶数在乘法溢出时,信息会丢失,因为与2相乘等价于位移运算;
  • 31还具备一个很好的性能,就是可以用位移和减法代替乘法运算:31*i == (i<<5)-i。

具体的实现如下:

// String 类的hashCode()
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

那么底层上是如何保证相比于其他的数据储存方式,HashMap的速度更快的呢?这里以HashMap的的put()方法进行切入。下面上源码。

// put方法调用的final V putVal()
public V put(K key, V value) {
    // 调用hash()方法计算出对应的hash指,再调用putVal
    return putVal(hash(key), key, value, false, true);
}


/**
 * 实现Map.put()的相关方法
 *
 * @param hash  key的hash值
 * @param key 键
 * @param value 需要put的值
 * @param onlyIfAbsent 为真,不改变已有的值
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 * 为方便理解,理清如下量:
 * tab :  transient Node<K,V>[] table, 数组
 * n: map的长度
 * i: (n - 1) & hash计算出的索引
 * p = tab[i]:在该索引上的元素
 * k = p.key: 该索引元素的key
 * e = p.next
 */
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为空或者table的长度为零,resize()一下,这里的resize()涉及到一系列策略,这里就不谈了。
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

// (n - 1) & hash 并运算,计算其在数组中的索引
// 如果该索引为null,则直接添加value
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    Node<K,V> e; K k;
    // 如果p的hash和key 均与 要put的相同,直接e=p
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    // 否则,如果p为TreeNode
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
        for (int binCount = 0; ; ++binCount) {
            // p.next == 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;
            }
            // 
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;
            p = e;
        }
    }
    // e不为null
    if (e != null) { // existing mapping for key
        V oldValue = e.value;
        // onlyIfAbsent为false 或者 e.value ==null 的时候,更新e的value;否则,不更新
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
}
++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;
}

我们特别关注一下在put()中的(n - 1) & hash,这个按位与的算法其实就是h%length求余,一般什么情况下利用该算法,典型的分组,例如怎么将100个数分组16组中,应用非常广泛。。为什么不直接用 hash 对 数组长度取模?因为除法运算效率低

2.3 Node
 /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
     // 利用静态内部类Node来封装<key-value>对数据,同时用next属性来构成list结构的bucket 
    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; }


        // 基于位运算的hashCode()
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

        // 判断两个对象相等 当且仅当两个Entry的key和value完全相等
        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;
        }
    }

从中,我们可以看出 Node 实际上就是一个单向链表。这也是为什么我们说HashMap是通过拉链法解决哈希冲突的。
Node 实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。

一些方法的实现
  • clear()

clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。

public void clear() {
    modCount++;
    Entry[] tab = table;
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    size = 0;
}
  • containsKey()和get()

containsKey() 的作用是判断HashMap是否包含key。两者都调用了getNode()。

public boolean containsKey(Object key) {  
    return getNode(hash(key), key) != null;                //使用h值来定位bucket  
}  
public V get(Object key) {  
    Node<K,V> e;  
    return (e = getNode(hash(key), key)) == null ? null : e.value;  
}  

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


final Node<K,V> getNode(int hash, Object key) {            //被get与contains调用的工具方法,输入的hash不是key.hashCode,而是上面提到的h值  
    Node<K,V>[] tab;   
    Node<K,V> first, e;   
    int n; K k;  
                                                           //index = (table.length-1) & h ,定位到bucket  
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {  

        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))  //若这个bin中第一个元素即为所找就直接返回  
            return first;  

        if ((e = first.next) != null) {                                   //否则,就得遍历这个bucket来查找  
            if (first instanceof TreeNode)  
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);     //bin为tree式的  

            do {                                                          //bin为list式的  
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))  
                    return e;                                             //在遍历bucket时得不断调用equals()方法  
            } while ((e = e.next) != null);                                 
        }  

    }  
    return null;  
}  
  • containValue()

基本也是把Value分为null和非nul进行讨论,即 if ((v = e.value) == value ||
(value != null && value.equals(v)))

  public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值