45

HashMap漫谈(1)

 5 years ago
source link: https://bryantchang.github.io/2018/08/10/java-hashmap/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

HashMap原理解析–JDK1.7

今天无意间看Spring Core的源码,里面有一个HashSet,手一滑点进了源码查看,发现HashSet是用HashMap实现的。瞬间想到了当时准备面试时的场景。背了那么多Java Collection的概念,竟然都没有仔细看过任何一个类的源码。今天索性就从HashMap开刀仔细看看这些基本数据结构的实现。

从jdk1.7到1.8,HashMap的数据组织结构经历了比较彻底的转变,这篇文章主要介绍JDK1.7的实现

基本数据结构

在JDK1.7中,HashMap的基本构成是通过邻接表的方式实现的,大致结构如下图所示

F7biQ3Z.png!web

我们来看一下其中的数据结构

Entry

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return 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();
    }

    /**
     * This method is invoked whenever the value in an entry is
     * overwritten by an invocation of put(k,v) for a key k that's already
     * in the HashMap.
     */
    void recordAccess(HashMap<K,V> m) {
    }

    /**
     * This method is invoked whenever the entry is
     * removed from the table.
     */
    void recordRemoval(HashMap<K,V> m) {
    }
}

这个Entry<K,V>实现了Map.Entry这个内部接口,这个类中有下面几个方法

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

相比与这个接口,HashMap中的Entry主要是添加了Entry next使其具有了链表的特性,接着我们看一些关键的方法。

构造方法

一般我们使用如下方法创建HashMap

Map<String, String> map = new HashMap();

我们直接看HashMap()构造方法

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

其中的两个常量DEFAULT_INITIAL_CAPACITY=1 << 4;(16)DEFAULT_LOAD_FACTOR=0.75这个常量的含义我们在后面详细说明。

这个构造方法调用了同名的构造方法

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

put(key, value)

1、如果bucket数组为空,调用inflateTable方法完成初始化;
2、判断待插入key是否为null:
 key == null成立,调用putForNullKey方法完成数据插入,由此可以看出,HashMap的key是可以为null的;
 key == null不成立,跳转到步骤3;
3、计算带插入key的hashCode;
4、根据hashCode按位与计算出所在bucket数组中的位置i;
5、遍历挂在bucket中位置i下的Entry链表,如果当前key已存在,更新它所对应的oldValue为value,并返回oldValue,否则,跳转到步骤6;
6、将key-value插入对应bucket中位置i下的Entry链表中,返回null。
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;
}

我们可以看到,在这个方法中,首先会初始化邻接表的各个Hash桶,通过调用tableinflateTable(threshold)方法

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

其中主要的步骤如下:

  • roundUpToPowerOf2(toSize) — 获取大于等于toSize的2的幂次方的最小整数
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
  • 初始化Entry数组table
  • initHashSeedAsNeeded — 初始化hash的seed,每次capacity(如调用resize方法)改变时触发
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

回到put方法,随后将key做hash,然后根据这个hash值找到具体的table的索引值

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

对于jdk,用了一个很巧的方法完成了取余的操作。值得学习(^v^)。

当找到所在的Entry数组的下标后,会遍历这个由这个下标所在的Entry为头的链表,在这个链表下链接的都是hash完之后值相同的key,在遍历这个链表时,如果key是相同的,那么直接替换掉这个key对应的value,将原先的value返回。如果没有,则会创建新的Entry,然后头插入这个链表。那么我们就来看看添加新Entry的过程:

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

这个方法中有一个比较重要的判断条件,就是是否需要扩容,如果当前的size已经达到阈值并且这个表格最后一个下标已经没有空的链表位置,那么就需要扩容,这里的扩容大小为原来大小的两倍,resize函数中其实和我们最早大学学习数据结构时教材上的执行没有两样,代码如下,总体思路如下:创建一个两倍大小的table,并将原来的table放在了新的table中,同时设置一些属性信息,对于其中有一个loadFactor,这里的目的是为了能够尽可能减少冲突的概率,增加查找效率,随后通过initHashSeedAsNeeded重新设置hashseed,最后创建新的Entry。

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

这里面有一个特殊的case,就是key为null的情况,这样的情况会直接连接到Entry[0]的链表中,通过插入的逻辑可以知道只能插入一个key为空的value,如果多个,只有最后一个有效。

get(key)

我们来看get方法,直观来想,是put方法的逆过程,即:

1、拿到key的hash值
2、找到这个hash对应的Entry
3、遍历这个Entry为头对应的链表,直到找到对应的元素
4、如果没找到,返回null

具体的代码如下:

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

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

我们看基本和我们上面的预期基本相同,这里面有个getEntry方法,其思路就是根据key的hash找下标遍历链表的过程,代码如下,具体的和put方法类似,这里不再赘述。

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

还有一个方法,containsKey也是基于get方法实现,有兴趣的大家可以自行看代码。

remove

这个方法也和get,put方法类似,不同的就是删除Entry的时候,这个就是链表中删除特定节点的操作,具体代码如下:

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

HashMap迭代

最后,我们就要看Map的遍历了,常见的Map遍历有如下几种

1、获取keySet,迭代key获取value
2、获取entrySet,直接迭代,获取key和value

我们简单看一下其实现:

  • KeySet
public Set<K> keySet() {
    Set<K> ks = keySet;
    return (ks != null ? ks : (keySet = new KeySet()));
}

private final class KeySet extends AbstractSet<K> {
    public Iterator<K> iterator() {
        return newKeyIterator();
    }
    public int size() {
        return size;
    }
    public boolean contains(Object o) {
        return containsKey(o);
    }
    public boolean remove(Object o) {
        return HashMap.this.removeEntryForKey(o) != null;
    }
    public void clear() {
        HashMap.this.clear();
    }
}
  • entrySet
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());
}

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

这里面都是用了一个相同的实现机制,就是通过内部类调用该内部类所在的外部对象,这样实现的目的是为了,当外部对象发生变化时,这个内部类对象会发生变化,同时,内部类对象改变时也会改变外部对象的结果,而如果使用副本时,外部对象的修改对内部类对象没有影响,返回的值不确定。

这就是JDK1.7中HashMap的实现,下篇文章中,我将介绍JDK1.8中的相关变更。

谢谢你请我吃糖果


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK