美文网首页
HashMap实现原理

HashMap实现原理

作者: Rxlywz | 来源:发表于2017-05-05 13:02 被阅读0次

本篇博客仅为本人了解HashMap原理过程中,查阅多篇博客后,为了加强记忆,写下此篇,在此对多篇博客多有借鉴

HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap数据结构

HashMap中数据的存储是由数组与链表一起实现的。HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

数组

数组是在内存中开辟一段连续的空间,因此占用内存严重,故空间复杂的很大。我们只要知道数组首个元素的地址,在数组中寻址就会非常容易,其时间复杂度为O(1)。但是当要插入或删除数据时,时间复杂度就会变为O(n)。数组的特点是:寻址容易,插入和删除困难;

链表

链表在内存的存储区间是离散的,其插入和删除操作的内存复杂度为O(1),但是寻址操作的复杂度却是O(n)。链表的特点是:寻址困难,插入和删除容易。

image.png

从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

HashMap原理

HashMap类有一个叫做Entry的内部类。这个Entry类包含了key-value作为实例变量。 每当往hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。Entry具体存在table的那个位置是 根据key的hash值来决定。

/**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

HashMap存取实现

存储

 /**
     * 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) {
//HashMap允许存放null键值
//当key为null是,调用putForNullKey()方法,将value插入到数组的第一个位置,即角标为0的位置
        if (key == null)
            return putForNullKey(value);
//计算key的hash值
        int hash = hash(key);
//搜索hash值对应的指定数组的索引
        int i = indexFor(hash, table.length);
//如果i处的索引处Entry不为null,遍历e元素的下一个元素
        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;
            }
        }
//如果i出索引Entry为null,表明此处没有Entry
        modCount++;
//将key和value添加到i处索引
        addEntry(hash, key, value, i);
        return null;
    }

从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hash值,得到这个元素在数组中的位置(即下标),然后再在该索引上的单向链表进行循环遍历用equals比较key是否存在,如果存在则用新的value覆盖原值,如果不存在,则插入链表的头部,这与后面的多线程安全相关。

putForNullKey(V value)方法

/**
     * Offloaded version of put for null keys
     */
    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;
    }

addEntry(int hash, K key, V value, int bucketIndex) 方法

根据计算出的hash值,将key-value对放在数组table的i索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:

/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The next size value at which to resize (capacity * load factor).一般是大于0.75,开始扩容,double
     * @serial
     */
    int threshold;
/**
     * 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);
    }

createEntry(int hash, K key, V value, int 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++;
    }

读取

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
//如果key为null,调用getForNullkey()方法,如果数组0角标对应的Entry不为null,遍历e元素的下一个元素
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

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

从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hash值,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

getForNullKey()方法

/**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

getEntry(Object key)方法

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

总结

HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash值来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash值找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

山外山

HashMap的两个重要属性是容量capacity和装载因子loadfactor,默认值分别为16和0.75,当容器中的元素个数大于 capacity*loadfactor = 12时,容器会进行扩容resize 为2n,在初始化Hashmap时可以对着两个值进行修改,负载因子0.75被证明为是性能比较好的取值,通常不会修改,那么只有初始容量capacity会导致频繁的扩容行为,这是非常耗费资源的操作,所以,如果事先能估算出容器所要存储的元素数量,最好在初始化时修改默认容量capacity,以防止频繁的resize操作影响性能。

java8对hashmap做了优化 ,底层有两种实现方法,一种是数组和链表,一种是数组和红黑树,hsahmap会根据数据量选择存储结构
if (binCount >= TREEIFY_THRESHOLD - 1)
当符合这个条件的时候,把链表变成treemap,这样查找效率从o(n)变成了o(log n)

相关文章

网友评论

      本文标题:HashMap实现原理

      本文链接:https://www.haomeiwen.com/subject/ahbjtxtx.html