Java 7源码分析第10篇 - Map集合的实现

本文深入剖析了Java中HashMap的内部实现机制,包括其数据结构、存储原理、哈希算法及冲突解决方式等。此外,还详细介绍了HashMap的主要方法,如put()、get()等。

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

可能我们经常在实际应用或面试中注重的是各个集合的特点,比如,Set集合不能存储重复元素,不能保持插入和大小顺序,Map集合存储key-value对等等。但是,如果你想学好Java,或者目标更高一点,想要精通Java,那仅掌握这些是远远不够的。其实集合中还有需多的知道需要我们去掌握和了解。

我们首先来看Map集合的实现而不是Set集合,因为Map和Set集合实现上非常的相似,Set中许多方法都是通过调用Map中的方法来实现功能的。为什么呢?因为Map可以说就是一个Set,Set集合不重复且无序、Map中的key也有这个特性。当我们将Map中的value看为key的附属时,获取到的所有key就可以组成一个Set集合。

来看一下两者实现的类图:

Set 集合框架图


Map集合框架图

先来看一下Map接口,源代码如下:

public interface Map<K,V> {
    // Query Operations
    int size();
    boolean isEmpty();

    boolean containsKey(Object key);
    boolean containsValue(Object value);

    V get(Object key);

    // Modification Operations
    V put(K key, V value);
    V remove(Object key);

    // Bulk Operations
    /*
       The behavior of this operation is undefined if the
       specified map is modified while the operation is in progress.
     */
    void putAll(Map<? extends K, ? extends V> m);
    void clear();


    // Views
    Set<K> keySet();// 由于Map集合的key不能重复,key之间无顺序,所以Map集合中的所有key就可以组成一个Set集合
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();

    interface Entry<K,V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
    }
    // Comparison and hashing
    boolean equals(Object o);
    int hashCode();

}

接口中有一个values方法,通过调用这个方法就可以返回Map集合中所有的value值;有一个keySet()方法,调用后可以得到所有Map中的 key值;调用entrySet()方法得到所有的Map中key-value对,以Set集合的形式存储。为了能够更好的表示这个key-value值,接口中还定义了一个Entry<K,V>接口,并且在这个接口中定义了一些操作key和value的方法。

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

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

    static final float DEFAULT_LOAD_FACTOR = 0.75f;//指定负载因子

    // The table, resized as necessary. Length MUST Always be a power of two.
    // 使用Entry数组来存储Key-Value,类似于ArrayList用Object[]来存储集合元素
    transient Entry[] table;
    transient int size;
    // The next size value at which to resize (capacity * load factor).
    // HashMap所能容的mapping的极限
    int threshold;
    /*
      * 负载因子:
    */
    final float loadFactor;
    transient int modCount;
}
如上定义了一些重要的变量,其中的loadFactor是负载因子,增大值时可以减少Hash表(也就是Entry数组)所占用的内存空间,但会增加查询数据时时间的开销,而查询是最频繁的操作,减小值时会提高数据查询的性能,但是会增大Hash表所占用的内存空间,所以一般是默认的0.75。
threshold表示HashMap所能容纳的key-value对极限,如果存储的size数大于了threshold,则需要扩容了。
hashmap提供了几个构造函数,如下:
// 健HashMap的实际容量肯定大于等于initialCapacity,当这个值恰好为2的n次方时正好等于
    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);
        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)  // 计算出大于initialCapacity的最小的2的n次方值
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);//设置容量极限
        table = new Entry[capacity];
        init();
    }

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

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }
由第一个构造函数可以看出,其实际的capacity一般是大于我们指定的initialCapacity,除非initialCapacity正好是2的n次方值。接着就来说一下HashMap的实现原理吧。在这个类中开始处理定义了一个transient Entry[] 数组,这个Entry的实现如下:
private static class Entry<K,V> implements Map.Entry<K,V> {
        int hash;
        K key;
        V value;
        Entry<K,V> next;
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        protected Object clone() {
            return new Entry<>(hash, key, value,(next==null ? null : (Entry<K,V>) next.clone()));
        }
        // Map.Entry Ops
        public K getKey() {
            return key;
        }
        public V getValue() {
            return value;
        }
        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        public int hashCode() {
            return hash ^ (value==null ? 0 : value.hashCode());
        }
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

了解了key-value存储的基本结构后,就可以考虑如何存储的问题了。HashMap顾名思义就是使用哈希表来存储的,哈希表为解决冲突,采用了开放地址法和链地址法来解决问题。Java中HashMap采用了链地址法。
链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被hash后,得到数组下标,把数据放在对应下标元素的链表上。
当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
HashMap<String , Double> map = new HashMap<String , Double>(); 
 map.put("语文" , 80.0); 
 map.put("数学" , 89.0); 
 map.put("英语" , 78.2); 
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。
当程序执行 map.put("语文" , 80.0); 时,系统将调用"语文"的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:

// 当向HashMap中添加mapping时,由key的hashCode值决定Entry对象的存储位置,当两个 key的hashCode相同时,
// 通过equals()方法比较,返回false产生Entry链,true时采用覆盖行为
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:并且会调用 indexFor() 方法来计算该对象应该保存在 table 数组的哪个索引,源代码如下:
static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
  static int indexFor(int h, int length) {
    return h & (length-1);
  }

length是table.length,所以数组的长度总是 2 的 n 次方,通过h&(length-1)计算后,保证计算得到的索引值位于 table 数组的索引之内,计算的函数并不总是这样的,好的计算函数应该还会让索引值尽量平均分布到数组中。

当调用put()方法向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false),而且新添加的 Entry 位于 Entry 链的头部,这是通过调用addEntry()方法来完成的,源码如下:

void addEntry(int hash, K key, V value, int bucketIndex){ 
    Entry<K,V> e = table[bucketIndex]; // 获取指定 bucketIndex 索引处的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    // 如果 Map 中的 key-value 对的数量超过了极限
    if (size++ >= threshold) 
            resize(2 * table.length); 	 //  把 table 对象的长度扩充到 2 倍

} 

程序总是将新添加的 Entry 对象放入 table数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象, e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

接下来看一下HashMap是如何读取的。 get()方法的源代码如下:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        // 搜索该Entry链的下一个Entry,有多个Entry链时必须顺序遍历,降低了索引的速度
        //  如果Entry链过长,说明发生“Hash”冲突比较频繁,需要采用新的算法或增大空间
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 	 
               Object k;
               if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
         }
         return null;
    }

当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry 时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后循环遍历查找 hash值相同,key值相同的value。

下面来看一下HashMap是如何实现如下的三个方法的,源代码如下:

 public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
    public Collection<V> values() {
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()));
    }
    public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }

    private Set<Map.Entry<K,V>> entrySet0() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }
分别得到了KeySet、Values和EntrySet私有类的实例,那么他们是怎么从HashMap中取出这些值的呢?其实这里会涉及到非常多的类和方法,大概框架如下所示:



如上类中最重要的就是HashEntry类的实现,如下:

private abstract class HashIterator<E> implements Iterator<E> {
        Entry<K,V> next;        // next entry to return
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null);// 将index指向第一个table不为null的位置
            }
        }
        public final boolean hasNext() {
            return next != null;
        }
        final Entry<K,V> nextEntry() {// 遍历Entry链
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null);
            }
            current = e;
            return e;
        }
        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }

    }

和ArrayList一样,在获取到HashMap的Iterator对象后,就不可以使用ArrayList进行添加或删除的操作了,否则会出现异常。看一下几个重要的变量,如图示。





























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值