技术信息

Java中HashMap原理分析

发布日期:2019-02-15      点击:
首先,HashMap中数据的存储是由数组与链表一起实现的。

数组是在内存中开辟一段连续的空间,因此,只要知道了数组首个元素的地址,在数组中寻址就会非常容易,其时间复杂度为O(1)。但是当要插入或删除数据时,时间复杂度就会变为O(n)。

链表是内存中一系列离散的空间,其插入和删除操作的内存复杂度为O(1),但是寻址操作的复杂度却是O(n)。那有没有一种方法可以结合两者的优点,即寻址,插入删除都快呢?这个方法就是HashMap。

首先,我们看看HashMap的拉链式实现方法。如下图:



HashMap中定义了一个Entry类的数组table:

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table;

其中table数组就是buckets,其中数组和链表的数据区保存的的就是一个Entry对象,Entry对象中保存的就是HashMap中大家所熟知的Key-Value对。


Entry类我们后面再详细解释,现在先来看看HashMap中两个重要的属性:capacity和load factor

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

capacity为buckets的容量,load factor是衡量buckets填满程度的比例。当buckets中entry数量大于capacity*loadfactor时就要把capacity扩充为原来的两倍。

由于篇幅有限,下面介绍HashMap的几个核心方法。

首先是存储的方法,即put方法。

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    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;
    }

从上面可以看出,put的主要步骤为一下几点:

1.先根据key计算出hash值,并用hash值计算出次元素应在数组中的哪个位置。

2.判断这个位置上是否为空,若为空,直接调用addEntry方法将元素加入,并且返回null

3.若不为空,将这个位置上的元素统一向这个位置所连的链表后方推一格,然后将要加入的元素放在链表头部(类似一个栈)。返回以前的链表头部元素。

下面看看put方法中的putForNullKey方法和addEntry方法,首先是putForNullKey方法:

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

由上可知,当key为null时,元素总是被放在数组下标为0的位置。下面看addEntry方法:

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    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);
    }
    /**
     * Like addEntry except that this version is used when creating entries
     * as part of Map construction or "pseudo-construction" (cloning,
     * deserialization).  This version needn't worry about resizing the table.
     *
     * Subclass overrides this to alter the behavior of HashMap(Map),
     * clone, and readObject.
     */
    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++;
    }

由上可知,此方法首先判断table数组的大小是不是应该扩大,当size大于等于阈值且当前要放入的位置不为null时,size扩大,重新计算应该放入的位置。否则直接调用createEntry方法,在table中加入一个Entry并且size++。

下面看看get方法:

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

        return null == entry ? null : entry.getValue();
    }
    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    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;
    }

get方法实际是调用了getEntry方法。首先计算 key 的 hashCode,找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。


在HashMap中有部分标有transient关键字,表明该数据不参与序列化。原因为:

1. HashMap 中的存储数据的数组数据成员中,数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义。所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。
2. 由于不同的虚拟机对于相同 hashCode 产生的 Code 值可能是不一样的,如果你使用默认的序列化,那么反序列化后,元素的位置和之前的是保持一致的,可是由于 hashCode 的值不一样了,那么定位函数 indexOf()返回的元素下标就会不同,这样不是我们所想要的结果.


HashMap中的Fail-Fast机制:

因为HashMap不是线程安全的,当时用迭代器的过程中有其他的线程改变了HashMap中的值将会抛出ConcurrentModificationException 。

例如,当我使用线程A去遍历HashMap时,线程B修改了HashMap的值就会抛出ConcurrentModificationException。

Fail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。

HashMap中有个属性modCount就是记录修改的次数,每次修改都会使modCount++。

日本a片网站,哪里有三级片看,不用不载立即能看的黄色网站,后入巨乳女邻居,日本女优做爱在线观看