Java 集合 - HashMap

Posted

tags:

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

一、源码解析

(1). 属性

 

// 默认初始化空间 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大空间 = 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 一个空的 Entry 数组
static final Entry<?,?>[] EMPTY_TABLE = {};

// HashMap 是数组 + 链表实现的,如下 table
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

transient int size;

// 极限容量
int threshold;

// 加载因子
final float loadFactor;

 

(2). 构造方法

 

public HashMap(int initialCapacity, float loadFactor):new hashmap,自定义初始化容量和加载因子

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;
    threshold = initialCapacity;
    init(); // 空方法
}

 

// 构造方法。自定义初始化因子,加载因子为 0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 构造方法。创建一个初始化容量为 16 和加载因子为 0.75 的 HashMap
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

 

public HashMap(Map<? extends K, ? extends V> m):创建一个包含了一个 map 的 hashmap

public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    inflateTable(threshold); // 为 map 添加元素之前先扩充空间

    putAllForCreate(m);
}

1). new hashmap,使用默认加载因子,根据所加入的 map 元素个数来计算应该需要多大的初始化容量。

2). inflateTable 数组初始化,new 出一个 table 数组,数组的长度为 2的 n次方数。(下面介绍)

3). putAllForCreate 往里面塞一个 map(下面介绍)

 

// 1. toSize 是初始化容量,roundUpToPowerOf2 方法得到一个最小的比 toSize 大的 2 的 n次方数,即实际初始化容量。比如:传进来的自定义容量是 15,经过 roundUpToPowerOf2 方法处理之后就会得到实际初始化容量 16
// 2. threshold 是极限容量,等于实际初始化容量 * 加载因子 0.75
// 3. new 数组
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}


// 遍历 map,将 map 中的元素依次存入 hashmap
private void putAllForCreate(Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}     

// 1. 根据 key 算出 hash 码
// 2. 根据 hash 码定位到应该存储在哪条链表中,得到数组的下标
// 3. 遍历链表如果找到了同样 key 的元素,跳过;如果找不到同样 key 的元素,调 createEntry 方法将该元素插入到这条链表的链首
private void putForCreate(K key, V value) {
    int hash = null == key ? 0 : 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 != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }

    createEntry(hash, key, value, i);
}

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++;
}

 

(3). Entry 类

 

// 单向链表结构
static class Entry<K,V> implements Map.Entry<K,V> {
    // 四个元素,key、value、next指针、hash 码
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    // 构造方法。创建一个 Entry 节点
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    // 设置新 value,返回值为旧 value 值
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // 判断两个节点是否相等
    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;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
    
    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }
}

hashmap 的元素其实就是一个个 Entry 类。

hashmap 维护了一个 table 数组,而数组中的元素就是 Entry。每个 Entry 拥有一个指向另一个 Entry 的引用 next。所以 hashmap 的存储结构简单来说就是多条链表的集合。

 

(4). hash

 

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

    h ^= k.hashCode();
    
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

得到 hash 码(一个数学计算,不必关注)。
只要 hashcode() 返回值一样,得到的 hash 码也一定一样。

 

(5). indexFor

 

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

这个方法可以 根据 hash 码和数组长度 定位这个元素应该存储在哪一条链表中,得到数组下标。

根据构造方法中可知,实际初始化容量是一个 2 的 n 次方数,所以 length 就是一个 2的 次方数,经过计算:

h=5, length=16, h & length - 1 得到 5;

h=6, length=16, h & length - 1 得到 6;

……

h=15, length=16, h & length - 1 得到 15

h=16, length=16, h & length - 1 得到 0

h=17, length=16, h & length - 1 得到 1

......

h &(table.length -1) 计算得到的索引值总是在 table 数组的索引值之内。

数组在进行构建的时候,使用这个方法得到元素的存储位置,从而能保证元素的均匀分布。

 

(6). resize

 

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];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    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;
        }
    }
}

扩充容量一定会重新构造 map 的存储结构。

1). 创建一个新的数组,长度为 newCapacity

2). 遍历旧的数组和链表,根据 hash 码和新数组的长度重新定位数组下标 index

3). 将该元素存储在数组 index 位置的链表中,且新存入的元素总是在链首。

 

使用链表存储为什么要对数组进行扩容呢 ?

扩容后 hashmap 的结构更优,效率更高。

 

那么何时会需要 resize 呢 ?

分两种情况:

1). 使用 put 方法逐个向 hashmap 中添加元素的时候,若 map 中的元素个数大于等于极限容量,就扩容为原来的两倍。(下面会有 put 方法)

2). 使用 putAll 方法向 hashmap 中添加一个集合的时候,经过一系列计算来判断是否需要扩容。(下面会有 putAll 方法)

 

需要注意的是,resize 要遍历旧数组和链表,并且为每个 key 生成索等,所以是很耗时的,所以要尽量减少 resize 操作。

比如,在向 hashmap 中增加一个 map 的时候,有人可能会想到遍历 map 然后逐个元素 put 到 hashmap 中,put 的时候逐个判断是否需要扩容。这样就太耗费时间了。

所幸源码中的方法无疑是很聪明的,在知道了 map 的大小之后,经过计算得出需要扩容的新容量,进行一次性扩容。

 

(7). put

 

public V put(K key, V value):增加新的键值对或根据 key 替换掉旧的 value

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    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;
}

1). map 为空的话,进行数组初始化

2). 若 key == null,putForNullKey(下面介绍)

3). 若 key != null

  先求出 hash 码

  根据 hash 码判断该键值对应该存储在哪条链表中

  得到该链表的链首 Entry 进行遍历寻找,如果找到了 key 值相同的节点,就给该节点赋新值并且返回旧值

  如果找不到 key 值相同的节点,就在该链表增加一个节点,同时新增节点为链首,返回 null。

  (其实和 key == null 的原理是一样的,只不过此种情况下需要先确定存储位置,剩下了就基本一样了)

 

// 为 key == null 的节点赋值(得到 table[0] 的链表遍历寻找,若能找到这个节点,则赋新值,返回旧值;若找不到,则为链表新增元素<null,value>,且该节点为链首,返回 null)
// 由此可见,如果存在 key == null 的元素,一定存在于 table[0] 的链表中
// 再想一下原因,key == null 得到的 hash 码为 0,hash * (length -1) = 0,所以应该存储在 table[0] 的链表中
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;
}

// 当元素个数 >= 极限容量  并且 Entry 数组 bucketIndex 位置的元素 != null 的时候。1. 容量扩充为原来的两倍 2. rehash 得到存储位置并存储
// 否则直接存储
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);
}

 

public void putAll(Map<? extends K, ? extends V> m):为 hashmap 增加一个 map

public void putAll(Map<? extends K, ? extends V> m) {
    int numKeysToBeAdded = m.size();
    if (numKeysToBeAdded == 0)
        return;

    if (table == EMPTY_TABLE) {
        inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
    }

    // 判断是否需要扩容
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); // 得到 m 需要的目标容量
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
        int newCapacity = table.length;                                // 数组现有的容量
        while (newCapacity < targetCapacity)                           // 若数组现有容量 < 存储 m 需要的目标容量,数组现有容量扩大为两倍,循环比较下去
            newCapacity <<= 1;                                         
        if (newCapacity > table.length)                                // 若前面 newCapacity 有增大,就为数组扩容为 newCapacity。最后扩充容量之后足以存储 putAll 之后的数组了
            resize(newCapacity);
    }

    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
}

1). m 元素个数为0,不用操作了

2). 如果 table 为空,就得先初始化了

3). 判断是否需要扩容(不必深究)

4). 遍历 m,一个一个 put

 

(8). get

 

public V get(Object key)

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

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

1). 若 key == null,就会调 getForNullKey,若得到就会返回 value,找不到就会返回 null

2). 若 key != null,就会找该 Entry,若得到就会返回 value,找不到就会返回 null

 

相关方法如下:

// 1. 数组长度为0,找不到,返回 null
// 2. 遍历数组和链表进行寻找,找得到就返回 value,找不到就返回 null
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

// 根据 key 值寻找 Entry
// 1. 数组长度为0,找不到,返回 null
// 2. 根据 key 值得到 hash 码,再根据 hash 码得到应该存储在数组的那个位置
// 3. 在数组的该位置从链首开始遍历寻找,找得到就返回 value,找不到就返回 null
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

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

 

(9). remove

 

public V remove(Object key):根据 key 删除元素

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

final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    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;
}

1). 如果元素个数为0,就直接返回 null

2). 元素个数不为 0,根据 key 得到 hash 码,根据 hash 码定位数组下标

3). 得到数组在该位置的 Entry,遍历如果找得到这个 key 就删除该元素并且返回该元素,找不到返回 null

 

final Entry<K,V> removeMapping(Object o) {
    if (size == 0 || !(o instanceof Map.Entry))
        return null;

    Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
    Object key = entry.getKey();
    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;
        if (e.hash == hash && e.equals(entry)) {
            modCount++;
            size--;
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}

 

public void clear():清空 hashmap

public void clear() {
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}

 

(10). 其他方法

 

// 判断是否包含 value
// 遍历数组和链表进行寻找。
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;
}
private boolean containsNullValue() {
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (e.value == null)
                return true;
    return false;
}   
    
    
    
// 得到元素个数
public int size() {
    return size;
}

// 判断是否为空
public boolean isEmpty() {
    return size == 0;
}

// 判断是否包含 key
public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

// 影子克隆,没有克隆元素
public Object clone() {
    HashMap<K,V> result = null;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // assert false;
    }
    if (result.table != EMPTY_TABLE) {
        result.inflateTable(Math.min((int) Math.min(size * Math.min(1 / loadFactor, 4.0f),HashMap.MAXIMUM_CAPACITY),table.length));
    }
    result.entrySet = null;
    result.modCount = 0;
    result.size = 0;
    result.init();
    result.putAllForCreate(this);

    return result;
}

 

二、总结

说说源码解析中没有提到的

(1). 一些特性:

1). 不保证顺序,并且不保证顺序永远不变,因为 resize 后会重构数据结构

2). 支持空的 key 和 空的 value

(2). fast-fail 机制:原理同 ArrayList

 

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

Java集合 -- HashSet 和 HashMap

Java集合系列五HashMap解析

Java集合详解4:HashMap和HashTable

Java并发多线程编程——集合类线程不安全之HashMap的示例及解决方案

Java入门系列之集合HashMap源码分析(十四)

Java 集合框架:HashMap