Java源码分析HashMap源码分析

Posted 流动的城市

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java源码分析HashMap源码分析相关的知识,希望对你有一定的参考价值。

类的定义

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

属于AbstractMap子类,具有clone和序列化的属性

  1. 实现了Map接口,这样HashMap支持所有的Map操作,并且允许NULL Value和NULL Key.HashMap和HashTable几乎是相同的,区别在于两点。一点是HashMap是允许null的key和value一点是HashMap不是线程安全的,但是HashTable是线程安全的
  2. 由于采用hash方法确定对象在HashMap集合中的位置,所以get和put的操作时间复杂度都是常量级别的。迭代器迭代的效率是和HashMap集合本身的大小以及HashMap集合中装入的对象数目有关,所以如果迭代效率要求较高的话,最好不要设置装在因子很小或者容量很大。
  3. 影响HashMap性能的两个因素是装在因子load-factor和容量capacity.装载因子描述了HashMap可以装的多满,而容量表示最大可以装入的对象数。当实际装入的对象数目超过了装载因子和容量的乘积的时候就会再哈希,容量加倍
  4. 通常情况下,装载因子设置为0.75可以获得较好的时空效率。如果设置为更高的值,会因为冲突过多导致查找和添加速度变慢。所以在初始化的时候需要考虑大致装入多少对象以及装载因子设置多少比较合适。例如装入很多的对象的时候最好一开始就设置较大的capacity避免后面不断的再哈希和自增
  5. HashMap不是线程安全的,所以在多线程环境下还是需要用户去考虑线程安全问题。通常通过对象锁的形式解决,也可以使用Collections工具类实现Map m = Collections.synchronizedMap(new HashMap(...));,在处理线程安全问题的时候最好在初始化的时候做,避免潜在的不安全因素
  6. HashMap同样存在着fail-fast现象,如果迭代器创建之后还有修改HashMap结构的操作(增加或者删除数据等)都会抛出ConcurrentModificationException异常,如前面的分析中一样,此时迭代器就会出现fail-fast现象

几个重要的成员变量

static final int DEFAULT_INITIAL_CAPACITY = 16; // The default initial capacity - MUST be a power of two.
static final int MAXIMUM_CAPACITY = 1 << 30; // MUST be a power of two 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry<K,V>[] table; // The table, resized as necessary. Length MUST Always be a power of two.
transient int size;

默认大小是16,默认装载因子是0.75.如注释中所说的,默认的初始容量和最大容量以及扩容后的容量都必须是2的幂,总结一下就是无论任何时候,HashMap的容量都必须是2的幂

构造函数

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)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();

从上述的while循环中可以看出即使给定的初始容量打下不是2的幂,也会通过移位运算找到第一个大于initialCapacity的2的整数倍的值作为初始容量。最后的init();是每一个构造函数或者伪构造函数(clone() 反序列化等)都会调用的方法,实际上是一个空方法,是为子类提供的一个初始化链接。

public HashMap() 
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);


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


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);

上述的三个构造比较简单,第三个采用默认的0.75装载因子以及刚好可以装入参数中map中数据的容量进行map的初始化

重点是hash()函数

final int hash(Object k) 
    int h = 0;
    if (useAltHashing) 
        if (k instanceof String) 
            return sun.misc.Hashing.stringHash32((String) k);
        
        h = hashSeed;
    

    h ^= k.hashCode();

    // 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);

空的key总是映射到index为0的位置。注意HashMap中hash的求法是在对象本身的hashCode()结果的基础上做了一个补充,这样主要是为了避免所装入的对象的key的hashCode()函数写的太烂导致hash不均匀。

获取hash下标

static int indexFor(int h, int length) 
    return h & (length-1);

这是一个技巧,因为前面其实说过length无论何时都是为为偶数的,而偶数位是0,那么做与运算,必然总是为0。所以这里做了一个减一操作之后相与,使值分布的更加均匀避免值都集中到偶数位。此外这和数据结构中所说明的取模运算不同,这里直接使用与运算代替了模运算,另外length-1也保证了所有的值最终都是在length范围内,也就不会超过table的下标范围。

取值

public V get(Object key) 
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();


final Entry<K,V> getEntry(Object key) 
    int hash = (key == null) ? 0 : hash(key);
    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 != null && key.equals(k))))
            return e;
    
    return null;


private V getForNullKey() 
    for (Entry<K,V> e = table[0]; e != null; e = e.next) 
        if (e.key == null)
            return e.value;
    
    return null;

根据key来get值的时候首先判断key 是否为空,如果是空的key,那么调用getForNullKey()来查找,前面说过HashMap实现是通过数组实现的,但是每个数组下标对应的实质上是一个链表。getForNullKey()的查找过程很直接,直接取table[0]这个链表(前面也提到过,null key对应的数据都存储在table[0],但是并不是table[0]中只存放null key 对应的键值对),然后遍历这个链表,如果找到key为空的一个键值对,返回其值。
如果不是空key,那么getEntry()方法进行查找,首先根据hash值确定所在链表,然后遍历该链表即可,如果没有找到,就返回null。

添加操作

public V put(K key, V value) 
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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,那么新建并加入,否则替换旧的value。返回值如果为null既可能是因为旧的value是null也可能是HashMap中不存在加入的key

private V putForNullKey(V value) 
    for (Entry<K,V> e = table[0]; e != null; e = e.next) 
        if (e.key == null) 
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        
    
    modCount++;
    addEntry(0, null, value, 0);
    return null;

从putForNullKey()的添加过程也可以看出,实际的添加null key的时候是直接添加到table[0],同时在添加的过程中如果发现已经有Null key了,那么就替换,也就是说null key只有一个

调整大小

void resize(int newCapacity) 
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) 
        threshold = Integer.MAX_VALUE;
        return;
    

    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);


void transfer(Entry[] newTable, boolean rehash) 
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) 
        while(null != e) 
            Entry<K,V> next = e.next;
            if (rehash) 
                e.hash = null == e.key ? 0 : hash(e.key);
            
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        
    
   

将已有内容重新放入到一个新的数组中,该方法是在存储空间不足的时候自动调用的。如果当前容量已经达到MAXIMUM_CAPACITY,那么不会发生扩容操作。由于该操作需要重新分配一个更大的数组,而且重新进行hash映射,也就是transfer中的操作,将旧的数组中的数据重新计算一下key的hash值,所以还是比较影响效率的

删除

public V remove(Object key) 
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);


final Entry<K,V> removeEntryForKey(Object key) 
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) 
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) 
            modCount++;
            size--;
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        
        prev = e;
        e = next;
    

    return e;

删除key对应的键值对,如果不存在就返回null。首先根据key的hash值获取在数组table中的下标,然后记录前驱和后继,进行遍历,如果遍历过程中遇到了指定Key对应的键值对,那么删除。删除过程本质是一个单链表的删除

清空HashMap内容

public void clear() 
    modCount++;
    Entry[] tab = table;
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    size = 0;

这里实现比较简单,直接截断slot的连接即可,至于键值对Entry与Entry之前的连接没有置空。

是否包含指定值

public boolean containsValue(Object value) 
    if (value == null)
        return containsNullValue();

    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;

如果有一个或者多个key映射到给定的value就返回true否则返回false。注意这里是一个双重循环,第一重是遍历table[0-length],第二重是遍历table[i]所对应的链表

看看键值对的映射实体

static class Entry<K,V> implements Map.Entry<K,V> 
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    ......
    public final boolean equals(Object o) 
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) 
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            
            return false;
        
    .....

这是一个内部类,HashMap的实体即键值对,可以看到成员变量中有一个指向下一节点的指针next。其中判断两个实体是否相等的标准也比较直接,首先判断key是否相等,如果相等则继续判断value是否相等。

添加实体到HashMap

void addEntry(int hash, K key, V value, int bucketIndex) 
    if ((size >= threshold) && (null != table[bucketIndex])) 
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    

    createEntry(hash, key, value, bucketIndex);


void createEntry(int hash, K key, V value, int bucketIndex) 
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;

这里就涉及到了扩容,可以从第三行代码中看出,容量不足的时候就扩充为原来的2倍。

迭代器

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
    ......

其实迭代器的实现在Collection的实现类中都是差不多的,只要看一个就可以了。这里同样记录了修改次数expectedModCount,也就是迭代器创建之后不能再迭代过程中发生modeCount++这样的操作,否则出现fail-fast

序列化和反序列化-保存和读取HashMap的状态

private void writeObject(java.io.ObjectOutputStream s)
    throws IOException

    Iterator<Map.Entry<K,V>> i =
        (size > 0) ? entrySet0().iterator() : null;

    // Write out the threshold, loadfactor, and any hidden stuff
    s.defaultWriteObject();

    // Write out number of buckets
    s.writeInt(table.length);

    // Write out size (number of Mappings)
    s.writeInt(size);

    // Write out keys and values (alternating)
    if (size > 0) 
        for(Map.Entry<K,V> e : entrySet0()) 
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
        
    


private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException

    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                           loadFactor);

    // set hashSeed (can only happen after VM boot)
    Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
            sun.misc.Hashing.randomHashSeed(this));

    // Read in number of buckets and allocate the bucket array;
    s.readInt(); // ignored

    // Read number of mappings
    int mappings = s.readInt();
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                           mappings);

    int initialCapacity = (int) Math.min(
            // capacity chosen by number of mappings
            // and desired load (if >= 0.25)
            mappings * Math.min(1 / loadFactor, 4.0f),
            // we have limits...
            HashMap.MAXIMUM_CAPACITY);
    int capacity = 1;
    // find smallest power of two which holds all mappings
    while (capacity < initialCapacity) 
        capacity <<= 1;
    

    table = new Entry[capacity];
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

    init();  // Give subclass a chance to do its thing.

    // Read the keys and values, and put the mappings in the HashMap
    for (int i=0; i<mappings; i++) 
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        putForCreate(key, value);
    

注意反序列化过程是比较复杂的,相当于是读取基本数据(大小,装载因子,容量等数据)并检验之后,根据这些数据新建了一个Map并将从流中读取的键值对重新映射到HashMap中

以上是关于Java源码分析HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章

HashMap源码分析

Java HashMap源码分析

Java源码分析HashMap源码分析

Java源码分析HashMap源码分析

[java源码解析]对HashMap源码的分析

Java 集合源码分析HashMap