HashMap的源码分析

来源:互联网 发布:淘宝卖家如何上货 编辑:程序博客网 时间:2024/06/05 17:11

上篇综述了一下Java的集合类,

本篇主要是看一下HashMap的源码,看下HashMap到底是个什么东西。


1.成员变量简要说明

  /**     * 最小容量     */    private static final int MINIMUM_CAPACITY = 4;    /**     * 最大容量     */    private static final int MAXIMUM_CAPACITY = 1 << 30;    /**     *初始值     */    private static final Map.Entry[] EMPTY_TABLE            = new HashMapEntry[MINIMUM_CAPACITY >>> 1];    /**     * 默认加载因子     */    static final float DEFAULT_LOAD_FACTOR = .75F;    /**     * 这个才是HashMap的真实内容     */    transient HashMapEntry<K, V>[] table;    /**     * HashMap允许Key为Null,Key为Null时就存在这里     */    transient HashMapEntry<K, V> entryForNullKey;    transient int size;    /**     * 修改次数,和线程安全有关     */    transient int modCount;    /**     * 阈值, 阈值 =  容量 * 加载因子     */    private transient int threshold;    private transient Set<K> keySet;//存了所有的Key    private transient Set<Map.Entry<K, V>> entrySet; //存了所有的<KEY, VALUE>    private transient Collection<V> values; //存了所有的Value

HashMap的值是一个数组, HashMapEntry[] table。

这个数组的单元是HashMapEntry,实际上是一个单向链表,结构如下

static class HashMapEntry<K, V> implements Entry<K, V> {        final K key;        V value;        final int hash;        HashMapEntry<K, V> next;
典型的链表,有数据域(key、value、hash)和一个指针next。
也就是说,HashMap实际上就是个单向链表组成的数组。


modCount和Fail-Fast机制

每次对HashMap进行修改的时候都会增加这个值


在迭代器遍历的时候,一旦发现modCount的值被改变了,就会 throw new ConcurrentModificationException()。

这就是Fail-Fast机制。

 private abstract class HashIterator {        int nextIndex;        HashMapEntry<K, V> nextEntry = entryForNullKey;        HashMapEntry<K, V> lastEntryReturned;        int expectedModCount = modCount;        HashIterator() {            if (nextEntry == null) {                HashMapEntry<K, V>[] tab = table;                HashMapEntry<K, V> next = null;                while (next == null && nextIndex < tab.length) {                    next = tab[nextIndex++];                }                nextEntry = next;            }        }        public boolean hasNext() {            return nextEntry != null;        }        HashMapEntry<K, V> nextEntry() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            if (nextEntry == null)                throw new NoSuchElementException();            HashMapEntry<K, V> entryToReturn = nextEntry;            HashMapEntry<K, V>[] tab = table;            HashMapEntry<K, V> next = entryToReturn.next;            while (next == null && nextIndex < tab.length) {                next = tab[nextIndex++];            }            nextEntry = next;            return lastEntryReturned = entryToReturn;        }        public void remove() {            if (lastEntryReturned == null)                throw new IllegalStateException();            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            HashMap.this.remove(lastEntryReturned.key);            lastEntryReturned = null;            expectedModCount = modCount;        }    }


2.方法说明

扩容

先要知道一个公式,加载因子、阈值、容量的关系: 阈值 = 容量 * 加载因子,

HashMap的size超过阈值的时候,会对HashMap进行扩容。

if (size++ > threshold) {            tab = doubleCapacity();            index = hash & (tab.length - 1);        }
扩容代码如下
    private HashMapEntry<K, V>[] doubleCapacity() {        HashMapEntry<K, V>[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) { //达到最大容量就不进行扩容了            return oldTable;        }        int newCapacity = oldCapacity * 2;//每次扩容大小都是原来的2倍        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);        if (size == 0) { //size为0的时候不需要进行rehash操作            return newTable;        }        /**         * 扩容后需要进行rehash操作         */        for (int j = 0; j < oldCapacity; j++) {            HashMapEntry<K, V> e = oldTable[j];            if (e == null) {                continue;            }            int highBit = e.hash & oldCapacity;            HashMapEntry<K, V> broken = null;            newTable[j | highBit] = e;            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {                int nextHighBit = n.hash & oldCapacity;                if (nextHighBit != highBit) {                    if (broken == null)                        newTable[j | nextHighBit] = n;                    else                        broken.next = n;                    broken = e;                    highBit = nextHighBit;                }            }            if (broken != null)                broken.next = null;        }        return newTable;    }

get()、put()方法

取值的复杂度,以前校招的时候好像不少这种题目,那时只是死背O(1),但是真的是O(1)吗?

 public V get(Object key) {        if (key == null) {            HashMapEntry<K, V> e = entryForNullKey;            return e == null ? null : e.value;        }        int hash = Collections.secondaryHash(key);        HashMapEntry<K, V>[] tab = table;        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];                e != null; e = e.next) {            K eKey = e.key;            if (eKey == key || (e.hash == hash && key.equals(eKey))) {                return e.value;            }        }        return null;    }

get()方法的步骤是这样的:

(1) key == null的情况,直接从entryForNullKey中取值。O(1)

(2) key != null的情况,

     第一步。算hash值。

     第二步。根据hash值从数组中取出链表,遍历链表。

遍历链表的时间复杂度是O(n),那么说HashMap的取值复杂度O(1)只有在 n = 1的时候满足了。

实际上,这个n不为1时也是不大的,基本上是接近O(1)的。所以说HashMap的取值复杂度是接近O(1)的。

为什么n不为1时一般页是不大的,可以从put()方法中找到原因。


put()方法

 public V put(K key, V value) {        if (key == null) {            return putValueForNullKey(value);        }        int hash = Collections.secondaryHash(key);        HashMapEntry<K, V>[] tab = table;        int index = hash & (tab.length - 1);        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {            if (e.hash == hash && key.equals(e.key)) {                preModify(e);                V oldValue = e.value;                e.value = value;                return oldValue;            }        }        // No entry for (non-null) key is present; create one        modCount++;        if (size++ > threshold) {            tab = doubleCapacity();            index = hash & (tab.length - 1);        }        addNewEntry(key, value, hash, index);        return null;    }
和get()是相近的

(1) key == null, 直接往entryForNullKey里放值。

(2) key != null, 算hash值,取链表遍历。

                  a.对应的key有value,则覆盖;

                  b.对应的key没value, 超过阈值则扩容,最后要调用addNewEntry

void addNewEntry(K key, V value, int hash, int index) {        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);    }
从这里可以看出,如果HashMap足够分散的话(table的Size足够大),那么几乎每一次add都会在table上单独占用一个位置。

每次HashMap的大小超过阈值时,都会Double一下容量,再进行rehash操作,因此只要没达到容量上限,hashmap是足够分散的。

这就解释了为什么get()的时间复杂度是趋于O(1)。

remove

remove操作也是相似的,找到链表后和链表的删除节点是相同的。

实际上,似乎知道了HashMap的结构后,所有的操作不用看代码心中都是清楚的,都是一个套路。

containsKey和containsValue(时间复杂度)

这个也是我曾经碰到过的面试题,先说答案,containsKey的复杂度是趋于O(1),containsValue的复杂度是趋于O(n)。

先看containsKey

 @Override public boolean containsKey(Object key) {        if (key == null) {            return entryForNullKey != null;        }        int hash = Collections.secondaryHash(key);        HashMapEntry<K, V>[] tab = table;        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];                e != null; e = e.next) {            K eKey = e.key;            if (eKey == key || (e.hash == hash && key.equals(eKey))) {                return true;            }        }        return false;    }
是不是和get很像,算出hash值根据hash值取链表遍历!


containsValue

@Override public boolean containsValue(Object value) {        HashMapEntry[] tab = table;        int len = tab.length;        if (value == null) {            for (int i = 0; i < len; i++) {                for (HashMapEntry e = tab[i]; e != null; e = e.next) {                    if (e.value == null) {                        return true;                    }                }            }            return entryForNullKey != null && entryForNullKey.value == null;        }        // value is non-null        for (int i = 0; i < len; i++) {            for (HashMapEntry e = tab[i]; e != null; e = e.next) {                if (value.equals(e.value)) {                    return true;                }            }        }        return entryForNullKey != null && value.equals(entryForNullKey.value);    }
这个只能遍历数组和链表了,所以是接近O(n)。

enteySet、keySet、values解惑

实际上,这三者都没有写入操作,或者说三者的写入操作都是在put()里进行的。
因为这三者根本没有独立分配空间,他们只是对table的不同内容的代理。
以KeySet为例:
private final class KeySet extends AbstractSet<K> {        public Iterator<K> iterator() {            return newKeyIterator();        }        public int size() {            return size;        }        public boolean isEmpty() {            return size == 0;        }        public boolean contains(Object o) {            return containsKey(o);        }        public boolean remove(Object o) {            int oldSize = size;            HashMap.this.remove(o);            return size != oldSize;        }        public void clear() {            HashMap.this.clear();        }    }
对KeySet的操作实际上就是对table的操作。

原创粉丝点击