转载

HashMap漫谈(1)

HashMap原理解析–JDK1.7

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

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

基本数据结构

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

HashMap漫谈(1)

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

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中的相关变更。

谢谢你请我吃糖果

原文  https://bryantchang.github.io/2018/08/10/java-hashmap/
正文到此结束
Loading...