美文网首页
深入分析HashMap

深入分析HashMap

作者: Franck_ | 来源:发表于2018-07-07 16:59 被阅读6次

基于JDK 1.8.0。

简介:

HashMap的底层是通过一个邻接表来实现的。就是,将每个单链表的头结点保存在 一个数组里面。既然用到了数组,数组是不可变的。所以就需要动态扩容。

特点:

  • 底层实现:邻接表。可以理解成数组链表。
  • 线程安全:线程不安全。
  • 扩容:需要扩容。
  • 是否可以存放null:可以使用null作key
  • 是否有序:
  • 效率:直接通过函数计算出value的位置,效率非常高。
  • 是否可以重复:一个key只能对应1个value。重复的key会覆盖掉前面的value。

源码分析:

HashMap基础的属性如下:

首先有3个常量。

     //默认的容量16 。
     static final int DEFAULT_INITIAL_CAPACITY  = 16 
     //最大容量1073741824。左移运算,1转换成二进制然后,左移30位。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认负载系数。当构造函数没有指定负载系数的时候就会使用。
    static final float DEFAULT_LOAD_FACTOR = 0.75f;



接下来是4个变量:

    //Entry是HashMap的静态内部类。就是用Entry对象来存储K-V的。这个数组的每个元素又是一个单链表。
    transient Entry<?,?>[] table;
    //map的大小,map保存的键值对个数。
    transient int size;
    //临界值,用来标志是否需要扩容,如果键值对个数超过这个临界值,将会先扩容。
    int threshold;
    //负载系数,用来衡量HashMap的容量是否需要扩容的程度。用来计算临界值的。
    final float loadFactor;
构造函数:

HashMap有4个构造函数,有1个需要传初始容量和负载系数的函数是主要的函数,另外3个都是调用这个函数的。

  1. 需要传入初始容量和构造因子的构造函数。
   /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        //判断初始容量是否合法,如果小于0,则抛出非常参数异常。
        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);

        //强制设定,容量为2的倍数。
        //位移运算每左移1位,容量就增加2倍
        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        //设置负载系数
        this.loadFactor = loadFactor;

        //计算并且设置好临界值。临界值取 容量*负载系数 和 最大容量+1 小的那个。
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

        //新建数组,数组大小=容量
        table = new Entry[capacity];
        init();
    }



2.用一个Map作为构造函数的参数,先调用构造函数,然后调用putAllForCreate方法将map复制到table属性里面。需要计算出初始容量: (map的长度 / 默认的负载系数)+1 ,如果比默认的长度(16)小的话,就取默认的长度。
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}

剩下的两个构造函数,一个是不用传任何参数, 然后调用2个参数的构造函数,创默认的初始化容量和默认的负载系数当做参数传入。另一个是需要传入初始化容量,加上默认的负载系数,这两个作为参数调用2个参数的构造函数。


在分析添加方法之前,有5个方法需要了解和分析的。分别是:

  • hash() 计算出hash值
  • indexFor() 用于计算新的entry元素在table的存放位置;
  • resize() 对HashMap进行扩容;
  • transfer() 将旧的table元素转移到新的table元素中;
  • addEntry() 添加键值对;
/**
     * Retrieve object hash code and applies a supplemental hash function to the
     * result hash, which defends against poor quality hash functions.  This is
     * critical because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits.
     */
    final int hash(Object k) {
        //如果k是字符串类型,则直接返回字符串对象的hash32()方法的结果。
        if (k instanceof String) {
            return ((String) k).hash32();
        }

        int  h = hashSeed ^ k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        //只是简单地将h和长度-1 进行与运算
        return h & (length-1);
    }



/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        //保存当前的table。
        Entry<?,?>[] oldTable = table;
        //获取当前的table的长度。
        int oldCapacity = oldTable.length;
        
        //如果旧的容量已经是最大值了,就不能再扩容了,只能设置临界值为最大值。
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        
        //新建一个新的entry数组,容量为新的容量,然后将就的数组转移到新的数组里面。
        Entry<?,?>[] newTable = new Entry<?,?>[newCapacity];
        transfer(newTable);
        //设置当前的table为新的table
        table = newTable;

        //重新计算临界值。
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }



/**
     * Transfers all entries from current table to newTable.
     */
    @SuppressWarnings("unchecked")
    void transfer(Entry<?,?>[] newTable) {
        //将当前的entry数组保存在src里面
        Entry<?,?>[] src = table;

        //保存新的容量
        int newCapacity = newTable.length;

        //用新的table替换旧的table的元素。
        for (int j = 0; j < src.length; j++ ) {
            //获取当前table的第j个元素
            Entry<K,V> e = (Entry<K,V>) src[j];

            
            while(null != e) {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = (Entry<K,V>) newTable[i];
                newTable[i] = e;
                e = next;
            }
        }

        //清空table,方便GC。
        Arrays.fill(table, null);
    }



添加键值对。这个方法主要是做扩容判断。真正执行扩容的方法是resize(),执行将键值对放进table的是createEntry()方法。
/**
     * 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) {
        //如果大小大于等于临界值,并且目标存储位置不为空,则扩容并重新计算hash值和目标存储位置。
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //进行扩容。
            resize(2 * table.length);

            //重新计算hash值
            hash = (null != key) ? hash(key) : 0;

            //重新计算存储的位置索引
            bucketIndex = indexFor(hash, table.length);
        }
        //创建一个entry并且加入到table里面
        createEntry(hash, key, value, bucketIndex);
}



添加元素

添加键值对的方法有5个。公开的只有2个,添加单个键值对的put(),和添加一个map的putAll()。其余的都是私有的,提供给公开方法调用的,分别是使用null
作为key的putForNullKey()方法。添加的时候直接创建键值对的putForCreate()。添加多个键值对的时候直接创建键值对的putAllForCreate()。

1 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) {
        
        //如果key为null。则调用放入nullkey的方法并返回。
        if (key == null)
            return putForNullKey(value);

        //根据key计算出hash值。
        int hash = hash(key);
        
        //利用hash值和table的长度,算出位置i。
        int i = indexFor(hash, table.length);
        @SuppressWarnings("unchecked")
        
        //获取table第i个元素的值。
        Entry<K,V> e = (Entry<K,V>)table[i];
        
        //判断key是否重复,如果重复了,就覆盖value并返回旧的value。
        for(; e != null; e = e.next) {
            Object k;
            //如果e的hash值和key计算出来的hash值相等,并且key一致,则覆盖e的value并返回原先的value。
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //这个方法什么也不做,应该是预留方法。
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;//修改次数加1 。
        //增加entry。将在这个方法里面进行扩容。
        addEntry(hash, key, value, i);
        return null;
    }

  1. putAll()方法用map做参数,将一个map添加到当前的map。
 /**
     * Copies all of the mappings from the specified map to this map.
     * These mappings will replace any mappings that this map had for
     * any of the keys currently in the specified map.
     *
     * @param m mappings to be stored in this map
     * @throws NullPointerException if the specified map is null
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        //获取将要添加的键值对的个数,如果是0,则直接返回。
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;

        /*
         * Expand the map if the map if the number of mappings to be added
         * is greater than or equal to threshold.  This is conservative; the
         * obvious condition is (m.size() + size) >= threshold, but this
         * condition could result in a map with twice the appropriate capacity,
         * if the keys to be added overlap with the keys already in this map.
         * By using the conservative calculation, we subject ourself
         * to at most one extra resize.
         */
        //判断是否需要扩容,如果需要,则进行扩容。
        if (numKeysToBeAdded > threshold) {
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1;
            if (newCapacity > table.length)
                resize(newCapacity);
        }
        
        //循环调用put方法,将键值对一个一个put进去。
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

  1. 私有的putForNullKey() 用null作为key。添加一个键值对。
 /**
     * Offloaded version of put for null keys
     */
    //用null为key的value放进table数组的首位,如果首位已经有值,则连接为首结点。
    //如果已经有null为key的键值对,则覆盖旧的value。返回旧的value。
    private V putForNullKey(V value) {
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)table[0];
        for(; 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;
    }

  1. 私有的putForCreate() 。 快速添加一个元素,不用判断扩容。
 /**
     * This method is used instead of put by constructors and
     * pseudoconstructors (clone, readObject).  It does not resize the table,
     * check for comodification, etc.  It calls createEntry rather than
     * addEntry.
     */
    private void putForCreate(K key, V value) {
        //结算出hash值,和存储索引。
        int hash = null == key ? 0 : hash(key);
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        //判断key是否有重复,如果重复则覆盖旧值并返回。
        for (@SuppressWarnings("unchecked")
             Entry<?,V> e = (Entry<?,V>)table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        //创建一个键值对。不判断扩容。
        createEntry(hash, key, value, i);
    }

  1. 私有的 putAllForCreate(); 快速添加多个键值对的版本。只是简单地循环调用putForCreate() 方法。
  private void putAllForCreate(Map<? extends K, ? extends V> 
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }


删除元素

删除元素有1个公开的方法。只有删除单个元素的remove(Object key); 另外2个包访问权限的方法分别是:removeEntryForKey(), 实际上是提供给remove方法调用的。实际的删除元素的方法; removeMapping() 删除一个键值对,传入的参数类型,实际上必须是Map的内部类Entry。 删除元素只能单个删除,不能多个删除。



根据key删除单个键值对remove()。 只是简单的调用removeEntryForKey()方法。removeEntryForKey()方法是包权限的,不公开的。

  /**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @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 remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }


/**
     * Removes and returns the entry associated with the specified key
     * in the HashMap.  Returns null if the HashMap contains no mapping
     * for this key.
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        //根据key计算出hash值和存储索引 i
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);


        //循环找出对应的结点,然后删除结点,最后size-1.
        @SuppressWarnings("unchecked")
            Entry<K,V> prev = (Entry<K,V>)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;
    }



获取值

根据key获取值也只有1个get() 方法。get() 方法只是简单地调用了getEntry() 方法。

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

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


//getEntry方法的实现思路也是计算出hash值和存储索引,然后根据索引来循环找出key和hash值相等的value,如果有则返回,如果没有则返回null。
/**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    @SuppressWarnings("unchecked")
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<?,?> 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 (Entry<K,V>)e;
        }
        return null;
    }




总结

HashMap没有实现同步,所以不是线程安全的。
扩容是按照2的倍数扩容的。负载系数默认是0.75, 临界值=容量*负载系数,如果当前大小超过了临界值,则进行扩容;扩容需要复制旧的键值对到新的table里面去,需要损耗一定的性能;如果在能预估map的容量的情况下,尽量给HashMap一个初始的容量。初始容量=目标容量/0.75 + 1。

相关文章

  • Java集合框架—HashMap—源码研读-2

    前言: 本篇是HashMap系列的第二篇,上一篇:Java集合框架—HashMap—源码深入分析1 我们主要讲解了...

  • 深入分析HashMap

    基于JDK 1.8.0。 简介: HashMap的底层是通过一个邻接表来实现的。就是,将每个单链表的头结点保存在 ...

  • HashMap了解一下

    前言 HashMap HashMap类继承图 HashMap属性 HashMap构造函数HashMap(int i...

  • HashMap源码

    eg: HashMap hashMap = new HashMap<>(); hashMap.put(1,"QG...

  • Javaweb深入分析

    . 深入分析

  • 2018-03-12

    HashMap in Java HashMap in Redis HashMap in Golang

  • HashMap源码理解

    HashMap理解 HashMap定义 HashMap实现机制 HashMap与HashTable的主要区别 关键...

  • 【16】 hashmap

    hashmap 1.7 和1.8的区别 hashmap全家桶 hashmap 源码解析 hashmap hashm...

  • HashMap源码分析

    HashMap数据结构 HashMap数据结构.png HashMap继承图 HashMap-class.jpg ...

  • HashMap剖析

    Java集合:HashMap源码剖析 一、HashMap概述 二、HashMap的数据结构 三、HashMap源码...

网友评论

      本文标题:深入分析HashMap

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