面试必考之HashMap源码分析与实现

以下是JDK1.8之前版本的源码简介
一、什么是HashMap?

Hash:散列将一个任意的长度通过某种(hash函数算法)算法转换成一个固定值。

Map:存储的集合、类似于地图X,Y坐标的存储
总结:通过hash出来的值,然后通过这个值定位到这个map,然后把这个value存储到这个map中 ~~ hashMap基本原理

二、HashMap形式?

key,value。例如 wukong 30 、电话薄 a-z字母等等

二、面试中问到的问题?

1.0 key值可以为空吗?

【回答】hashMap把Null当做一个key值来存储,原因看源码

 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //源码中判断了,如果key值未空的时候,没有返回错误信息,也是允许存储的
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        .............
}

2.0 HashMap和Hashtable的区别

【回答】HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。
HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。
Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。
Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。

3.0 如果Hash key重复了,那么value值会覆盖吗?

【回答】不会覆盖、简单解释:在Entry类中,有个Entry< K,V > next 实例变量;它是来存储hashKey冲突时,存放就的value值。不会覆盖。详细也是见源码

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
       ......
   }
 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))) {
                //这里实际上是先获取原来的value,保存老的备份,可以通过 xxx.get("xiaozheng").next.getValue()获取。
                V oldValue = e.value; // 假设原来的是30,传进来的是31 。 目前还是30
                //然后在把当前的value赋给它
                e.value = value;  // 31
                e.recordAccess(this);
                return oldValue; 
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

《面试必考之HashMap源码分析与实现》
4.0 HashMap什么时候做扩容?

【回答】在put的时候,HashMap集合的容量高于0.75的时候,进行扩容。而且扩容是偶数的,以双倍的形式向上扩容。具体也是看源码
《面试必考之HashMap源码分析与实现》

5.0 HashMap性能受什么影响?

【回答】HashMap主要是受 初始化容量跟加载因子。初始化容量:创建一个HashMap默认给与多大的容量的值。加载因子:HashMap在扩容之前最大可以达到的容量。具体原因的话,看下面的“|不足之处“就明白了

6.0 HashMap table的数据结构?

【回答】数组+链表。取两者的优点

四、源码分析
4.1 初始化参数介绍
        4.1.1 初始化容量

    /** * 初始化容量 1左移4位 6位 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

        4.1.2 最大容量

 /** * 最大容量 1左移30位 也就是 2的30次方。存储的HashMap的个数不能超过该最大容量 */
    static final int MAXIMUM_CAPACITY = 1 << 30;

        4.1.3 加载因子:这也就可以解释为什么上述我说:HashMap集合的容量高于0.75的时候,进行扩容。0.75不是我说的,而是在代码中定义好,0.75

    /* * 加载因子系数 * 可以理解成:在当前容量的四分之三的时候进行扩容。 */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

        4.1.2 其他初始化参数

 /* * 创建一个Entry对象 */
    static final Entry<?,?>[] EMPTY_TABLE = {};
    /** * 对table进行赋值 */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    transient int size;
    //扩容变量
    int threshold;
    //临时加载因子
    final float loadFactor;

    //修改标记
    transient int modCount;

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

4.2 HashMap构造方法
构造方法的话,我们只需要了解下述三个构造方法即可 – – 我们可以手动指定HashMap的初始化容量以及它的加载因子。这也是提高HashMap性能的一种方式。例如:如果你知道你的hashMap需要存储10万个map。那么一开始可以调大你的初始化容量。避免一开始16个集合,多次扩容,多次拷贝带来的时间、性能消耗

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

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

4.3 put方法分析
上述第一个问题跟第三个问题的答案就这下述代码里面

 public V put(K key, V value) {
        //判断table是否已经初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //如果key未空的话,调用存空key的方法,没有报错,也就是支持空key的情况
        if (key == null)
            return putForNullKey(value);
        //这也是hash的定义,散列,将key进行某种算法(hash算法),转化成一个固定的值。也就是map数据的下标
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        //for循环判断key的hash值是否是相等的,如果相等的话,就进行换位~~ 这里结合下面下面的图可以理解
        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))) {
                //这里获取原来旧的value
                V oldValue = e.value;
                //再把当前的value跟key对应,没有覆盖
                e.value = value;
                //存储旧value
                e.recordAccess(this);
                return oldValue;
            }
        }
        //修改标记+1
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
   //核心代码,if(),也就是当前容量达到0.75,就会执行resize()方法,进行扩容。这也是上述4.0问题的答案。
   //下面我们可以看一下resize的方法--扩容方法
   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);
    }
  void resize(int newCapacity) {
        //老的table数据
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建Entry对象数组
        Entry[] newTable = new Entry[newCapacity];
        //这个方法是干什么用的呢? -- 回答:赋值 ,
        //可见每次扩容的时候都必须把原来就的数据赋值到新的数组过去,这就产生很大的开销,这也就能够解释为什么加载因子和初始量影响着HashMap性能这个问题
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

4.4 get方法分析
比较简单,紧跟4.5一起看

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

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

4.5 entry对象介绍
Entry有next属性变量。专门用来处理key值出现重复的情况用的,详细解释看下图片

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        //默认情况之下,next=null,当出现一个key相同的情况之下,当前的value就会被替换,同时当前next-》会指向原来的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;
        }
        .......
  }

《面试必考之HashMap源码分析与实现》
4.6 赋值源码分析

   void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

五、不足之处
5.1 HashMap获取Map集合的时间复杂度?
【回答】:与key值是否重复有关系,一般情况下g(O)1。如果出现key值重复的话,那就另外计算。key值是否重复取决于我们的Hash算法
总结:时间复杂度:你的hash算法绝对了你的效率
5.2 从伸缩性的角度分析不足之处
【回答】每当hashMap扩容的时候需要重新去add entry对象,需要重新Hash。然后放入我们新的entry table数组里面。
如果你们的工作中你知道你的hashMap需要存多少值,几千或者几万的时候,最好就是指定它们的扩容大小,防止在put的时候进行再次扩容 多次扩容

    原文作者:xiaozhegaa
    原文地址: https://blog.csdn.net/xiaozhegaa/article/details/79491046
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞