Map集合的存储机制,源码分析以及冲突解决

HashMap的存储与实现

      

       我们如果要保存一组对象,用我们之前学过的知识,会使用对象数组,但鉴于数组的局限性,数组长度一经定义就不能改变,所以我们使用链表、队列等数据结构操作,但是很麻烦。类集框架就是一个动态的数组,但不受数组长度的限制。 



HashMap允许key值为空,(在方法containsValue(Object value):如果指定值key==null,并且在键值对中有value为null时,也返回true)但是Hashtable不允许,否则会报“NullPointer Expection”异常。 

一、HashMap键值对的实现 

       HashMap是Map接口的实现子类,用于存放一对值,即类中的每一个元素都是以Key---->Value的形式存储。我们知道,在Java集合框架中,无论是将一个对象存放在数组中,还是队列中,其实并不是把这个对象存入其中,而是将对象的引用存入数组或者队列中。在HashMap中,数据的存储同样如此,我们通过调用put(K key,V value)方法存储键值对,方法如下:

Java代码  收藏代码

  1. /** 
  2.      * 存储关联的键值对 
  3.      * @param key:键 
  4.      * @param value:值 
  5.      * @return 
  6.      */  
  7.      public V put(K key, V value) {  
  8.          //当键值为null时,调用putForNullKey(value)的方法存储,  
  9.          //在该方法中调用recordAccess(HashMap<K,V> m)的方法处理  
  10.             if (key == null)  
  11.                 return putForNullKey(value);  
  12.             //根据key的KeyCode,计算hashCode  
  13.             int hash = hash(key.hashCode());  
  14.             //调用indexFor方法,返回hash在对应table中的索引(Entry[] table)  
  15.             int i = indexFor(hash, table.length);  
  16.             //当i索引处的Entry不为null时,遍历下一个元素  
  17.             for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  18.                 Object k;  
  19.                 //如果遍历到的hash值等于根据Key值计算出的hash值并且  
  20.                 //key值与需要放入的key值相等时,存放与key对应的value值  
  21.                 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  22.                     //覆盖oldValue的值  
  23.                     V oldValue = e.value;  
  24.                     e.value = value;  
  25.                     e.recordAccess(this);  
  26.                     return oldValue;  
  27.                 }  
  28.             }  
  29.               
  30.             modCount++;  
  31.           //当i索引处的Entry为null时,将指定的key、value、hash条目放入到指定的桶i中  
  32.             //如果现有HashMap的大小大于容量*负载因子时,resize(2 * table.length);  
  33.             addEntry(hash, key, value, i);  
  34.             return null;  
  35.         }  



       在上面的put(K key,V value)方法中可知,当要存储Key---->Value对时,实际上是存储在一个Entry的对象e中,程序通过key计算出Entry对象的存储位置。换句话说,Key---->Value的对应关系是通过key----Entry----value这个过程实现的,所以就有我们表面上知道的key存在哪里,value就存在哪里。在Map接口中,有一个Entry接口,该接口用于处理key和value的set()和get()方法,所以在Map中存储数据,实际上是将Key---->value的数据存储在Map.Entry接口的实例中,再在Map集合中插入Map.Entry的实例化对象,如图示: 

 

二、HashMap的存储机制 

         HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 
 
HashMap有四种方法: 
        HashMap():初始容量16,默认加载因子0.75 
        HashMap(int initialCapacity):自定义初始容量 
        HashMap(int initialCapacity,float loadFactor):自定义初始容量和加载因子 
        HashMap(Map<? extends K,? extends V> m) 
        这四个构造方法其实都受两个参数的影响:容量和加载因子。容量是哈希表中桶的数量,初始容量为16。加载因子是对哈希表的容量在自动增加resize()之前所达到尺度的描述。当哈希表中的条目数超过threshold(=Capacity*loadFactor) 的值时,要对哈希表进行rehash操作。 
        默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 
三、HashMap的冲突处理问题 

        由于哈希函数是一个压缩映象,因此在一班情况下,很容易产生“冲突”现象,即key1 ≠ key2,而f(key1)=f(key2)。而且,由于关键字的集合比较大,这种冲突是不可避免的,所以必须采取合理的解决方案,找出尽量少产生冲突的哈希函数和处理冲突的方法。对于哈希函数的构造,通常有直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法等。而这里重点讲述处理冲突的两种方法。 
       1、 开放地址法 
        开放地址法是对那些发生冲突的记录,用hi=(h(key)+di)mod n方法再次确定Hash地址。 
        n:为哈希表长; 
       di:为增量序列,其取法有以下三种: 
        1)线性探测再散列      di= c * i  
        2)二次探测再散列      di = 12, -12, 22, -22, …, 
        3) 随机探测再散列      di是一组伪随机数列 或者 di=i×H2(key) (又称双散列函数探测) 
例如表长为11的哈希表中已填有关键字为17,60,29的记录,H(key)=key  MOD  11,现有第4个记录,其关键字为38 


       H(38)=38 MOD 11=5    冲突 
       H1=(5+1) MOD 11=6    冲突 
       H2=(5+2) MOD 11=7    冲突 
       H3=(5+3) MOD 11=8    不冲突 
对于其他增量序列的方法也是如此计算。 

        2、链地址法 
        将所有哈希地址相同的记录都链接在同一链表中图形类似于图2。也就是说,当HashMap中的每一个bucket里只有一个Entry,不发生冲突时,Hashmap是一个数组,根据索引可以迅速找到Entry,但是,当发生冲突时,单个的bucket里存储的是一个Entry链,系统必须按顺序遍历每个Entry,直到找到为止。为了减少数据的遍历,冲突的元素都是直接插入到第一个Entry后面的,所以,最早放入bucket中的Entry,位于Entry链中的最末端。这从put(K key,V value)中也可以看出,在同一个bucket存储Entry链的情况下,新放入的Entry总是位于bucket中。 

四、HashMap元素的输出 

        对于Map接口来说,其本身是不能直接使用迭代(Iteraor)进行输出的,因为Map接口的中的每个位置存放的是一对值(key---->value),而Iterator中每次只能找到一个值,如果要通过迭代的方法进行输出,主要分为以下几步: 
        1、将Map接口的实例通过Set<Entry<K,V>> entrySet();方法变为Set接口对象; 
        2、通过Set接口实例为Iterator实例化 
        3、通过Iterator迭代输出,输出的每个内容都是Map.Entry的对象 
        4、通过Map.Entry进行key---value的分离 
具体代码实现如下:

Java代码  收藏代码

  1. /实例化HashMap对象  
  2.         HashMap<String,String> hashMap=new HashMap<String,String>();  
  3.         //1、将Map接口变为Set接口  
  4.         Set<Map.Entry<String,String>> set=hashMap.entrySet();  
  5.         //2、实例化Iterator接口  
  6.         Iterator it=set.iterator();  
  7.         while(it.hasNext()){  
  8.             //3、得到存储在HashMap中的Entry对象  
  9.             Map.Entry<String,String> me=(Entry<String, String>) it.next();  
  10.             //4、通过Entry得到key和value  
  11.             System.out.println("Key="+me.getKey()+"Value="+me.getValue());  
  12.         }  


        上面的Map的输出过程,entrySet()主要是返回此映射所包含的映射关系的 Set 视图,在HashMap中,还有一个keySet()方法用于返回此映射中所包含的键的 Set 视图,步骤都是一样的。根据key可以通过get(key)方法找到对应的value。如果存储的key值不是系统类,而是自定义的类,则需要注意以下两点: 
1)必须存储自定义类的实例化对象,如果使用匿名对象,就找不到对应值。 
例如,key值是一个Student的类型

Java代码  收藏代码

  1. HashMap<Student,String> map=new HashMap<Student,String>();  
  2.         map.put(new Student("1608100201","Jony"), "CSU");  
  3.         System.out.println(map.get(stu));  



这段代码是无法找到对应的value值的,会输出null;正确的代码应该是下面的写法,才能找到value值,因为在设置和取得的过程中,都使用的是Student的实例化对象,地址没有变化。

Java代码  收藏代码

  1. //实例化一个学生对象  
  2.         Student stu=new Student("1608100201","Jony");  
  3.         HashMap<Student,String> map=new HashMap<Student,String>();  
  4.         map.put(stu, "CSU");  
  5.         System.out.println(map.get(stu));  



        2)覆写equals()和hashCode()方法。我们在使用时,要想明确的知道其中一个key的引用地址,就得依靠这两个方法。

Java代码  收藏代码

  1. public class Student {  
  2.     //学生的学好属性  
  3.     public static String ID;  
  4.     //学生的姓名属性  
  5.     private String name;  
  6.     /* 
  7.      * 重载构造方法 
  8.      */  
  9.     public Student(String ID,String name){  
  10.         this.ID=ID;  
  11.         this.name=name;  
  12.     }  
  13.       
  14.     /** 
  15.      * 覆写equals()方法 
  16.      */  
  17.     public boolean equals(Object obj) {  
  18.         //判断地址是否相等  
  19.         if(this==obj){  
  20.             return true;  
  21.         }  
  22.         //传递进来用于比较的对象不是本类的对象  
  23.          if (!(obj instanceof Student))  
  24.              return false;  
  25.          //向下转型  
  26.         Student stu = (Student)obj;  
  27.         //比较属性内容是否相等  
  28.          if (this.ID.equals(stu.ID)&&this.name.equals(stu.name)) {  
  29.              return true;  
  30.          }  
  31.          return false;  
  32.     }  
  33.     /** 
  34.      * 覆写hashCode()方法 
  35.      */  
  36.     public int hashCode() {  
  37.         return this.ID.hashCode();  
  38.     }  
  39. }  
  40.  
    • 大小: 25 KB
    • 大小: 17.3 KB
    • 大小: 7.4 KB
    • 大小: 50.3 KB
  • 大小: 25 KB
  • 大小: 17.3 KB
  • 大小: 7.4 KB
  • 大小: 50.3 KB

五、HashMap源码分析及1.6,1.7,1.8之间的区别(重点)

hashMap源码特别是1.8加入了红黑树,导致源码的难易程度大大升级

1.简介

HashMap最早出现在JDK1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,是非线程安全类,在多线程环境下可能会存在问题。

2.HashMap各个版本之间的变化

jdk1.8版本的HashMap相比于1.7的变化:

  • Entry结构变成了Node结构, hash变量加上了final声明 ,即不可以进行rehash了
  • 插入节点的方式从 头插法 变成了 尾插法
  • 引入了 红黑树
  • tableSizeFor方法、hash算法等等

jdk1.7版本的HashMap相比于1.6的变化:

  • 加入了jdk.map.althashing.threshold这个jdk的参数用来控制是否在扩容时使用String类型的新hash算法。
  • 把1.6的构造方法中对表的初始化挪到了put方法中。
  • 1.6中的tranfer方法对旧表的节点进行置null操作(存在多线程问题),1.7中去掉了。

2.HashMa1.8源码重点梳理

HashMap作为最常使用的集合之一;JDK1.7之前,有很大的争议,一方面是数据量变大之后的查询效率问题,还有就是线程安全问题。

HashMap1.8版本的源码分析:

jdk1.8版本的HashMap相比于1.7的变化:

  • Entry结构变成了Node结构, hash变量加上了final声明 ,即不可以进行rehash了
  • 插入节点的方式从 头插法 变成了 尾插法
  • 引入了 红黑树
  • tableSizeFor方法、hash算法等等

面试必会之HashMap源码分析

存储的基本结构为数组+链表,jdk1.8中加入了红黑树,具体转换情况下面介绍:

1)jdk1.8版本HashMap成员属性如下:

 1 /**
 2  * 默认初始容量16(必须是2的幂次方)--1.8把数组的初始化(空构造函数)放在了put中
 3  */
 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 5 
 6 /**
 7  * 最大容量,2的30次方
 8  */
 9 static final int MAXIMUM_CAPACITY = 1 << 30;
10 
11 /**
12  * 默认加载因子,用来计算threshold(threshold--扩容的阈值大小)
13  */
14 static final float DEFAULT_LOAD_FACTOR = 0.75f;
15 
16 /**
17  * 链表转成树的阈值,
      当桶中链表长度大于8并且数组长度小于MIN_TREEIFY_CAPACITY时转成树,
     (将所有的节点转换成树形节点,并且构造成双链表 为treeify 转换成红黑树准备。)
      否则进行扩容
18    threshold = capacity * loadFactor
19  */
20 static final int TREEIFY_THRESHOLD = 8;
21 
22 /**
23  * 进行resize操作时,若桶中数量少于6则从树转成链表
24  */
25 static final int UNTREEIFY_THRESHOLD = 6;
26 
27 /**
28  * 桶中结构转化为红黑树对应的table的最小值
29 
30  当需要将解决 hash 冲突的链表转变为红黑树时,
31  需要判断下此时数组容量,
32  若是由于数组容量太小(小于 MIN_TREEIFY_CAPACITY )
33  导致的 hash 冲突太多,则不进行链表转变为红黑树操作,
34  转为利用 resize() 函数对 hashMap 扩容
35  */
36 static final int MIN_TREEIFY_CAPACITY = 64;
37 /**
38  保存Node<K,V>节点的数组
39  该表在首次使用时初始化,并根据需要调整大小。 分配时,
40  长度始终是2的幂。
41  */
42 transient Node<K,V>[] table;
43 
44 /**
45  * 存放具体元素的集
46  */
47 transient Set<Map.Entry<K,V>> entrySet;
48 
49 /**
50  * 记录 hashMap 当前存储的元素的数量
51  */
52 transient int size;
53 
54 /**
55  * 每次更改map结构的计数器
56  */
57 transient int modCount;
58 
59 /**
60  * 临界值(阈值) 当实际大小(容量*填充因子)超过临界值时,会进行扩容
61  */
62 int threshold;
63 
64 /**
65  * 负载因子(哈希表的加载因子,一般情况下会设置为DEFAULT_LOAD_FACTOR)
66  */
67 final float loadFactor;

LoadFactor负载因子解释:

 HashMap中负载因子是个很重要的参数,反应了 HashMap 桶数组的使用情况。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。

详解:

      当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间

      相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。

       一般情况下,我们用默认值就可以了。大多数情况下0.75在时间跟空间代价上达到了平衡所以不建议修改

2)jdk1.8版本的数据存储结构

Node  静态内部类:

      static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

 TreeNode  红黑树数据结构:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
...

 3)1.8版本的HashMap构造函数

4种(重要点:初始化用户自定义的加载银子loadFactor)

  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;
        this.threshold = tableSizeFor(initialCapacity);
    }

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

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

   //参数为Map的构造方法,先计算需要的容量大小,然后调用putVal方法插入节点
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

4)jdk1.8HashMap的几个重要方法详解:

重点:存放put(),取值get(),移除remove(),扩容resize(),以及两个内部方法(tableSizeFor()与简化的

hash算法)

①put(K key,V value)

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
 }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
      //容量初始化,当table为空,调用扩容方法
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n - 1) & hash确定元素存放在桶中的下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
           //比较桶中第一个元素(数组结点)的hash值与key值
           //若相等,则将e指向该键值对,否则需要判断桶中是链表还是红黑树
            if (p.hash = hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //若为聊表,则需要遍历链表,并统计链表结构,判断节点数量是否达到转红黑树阈值
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                       //在聊表尾部插入新结点
                        p.next = newNode(hash, key, value, null);
                       //节点数量达到阈值,转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
               //若未到达链表尾部,需判断此结点key值与待插入元素的key值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //此时e的位置为要插入元素的位置,判断要插入的键值对是否存在于HashMap中
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//LinkedHashMap重写使用,会根据accessOrder(若为false则按照插入顺序(默认),否则按照访问顺序true--最少访问的靠前,最新访问的靠后)该方法一般在key值相同更新结点时调用
                return oldValue;
            }
        }
        ++modCount;
       //若当前HashMap容量超过阈值时,需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//LinkedHashMap重写使用
        return null;
    }

get(Object key)方法

  public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
  getNode方法很简单,(n - 1) & hash计算key值对应的table下标,找到链表,先判断头节点,然 
  后循环查找,如果头节点是树节点,调用树节点的getTreeNode方法
  final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

resize() 扩容方法 

  final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {  //旧表已经初始化
            if (oldCap >= MAXIMUM_CAPACITY) { //1.达到上限,不再扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }//2.若旧表容量>=16,并且*2还小于上限,扩容2倍(新表与新阈值)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }//旧表未初始化且旧阈值>0,则新表容量为阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults   //旧表未初始化且旧阈值=0  此时都为默认值(16,0.75)
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
//若旧表中存在数据,则涉及到表中值的迁移
 /**
   * 如果旧表里有值,需要把旧表里的值重新计算放到新表里
   * hash & (oldCap*2-1)计算新表中的位置,只可能得到两种结果(把新表分成两个小表)
   * hash & (oldCap-1)代表放在前面的表里hash & (oldCap-1) + oldCap 放在后面的表里
   * hash & oldCap == 0 就是第一种结果, !=0 就是第二种结果
 */
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)//若数组中结点处只有一个元素,则直接赋值
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//结点处为树节点
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order  //否则为链表结构
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

remove(Object key) 方法

  @Override
  public boolean remove(Object key, Object value) {
      return removeNode(hash(key), key, value, true, true) != null;
 }

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

 ⑤tableSizeFor(int cap)方法

该方法被调用三次

 该方法目的:用位运算   找到大于或等于输入参数的最近的2的整数次幂的数。比如10,则返回16。

具体详解参看:https://www.cnblogs.com/loading4/p/6239441.html

⑥1.8版本的hash算法

1.8版本hash算法进行了简化,直接把高16位移下来进行或运算
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

1.7版本hash算法:
   final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= 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);
    }
1.6版本的hash算法:
static int hash(int h) {
        // 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);
    }

HashMap疑问和进阶:

1.Hash()作用:
      它的目的是让“1”变的均匀一点,散列的本意就是要尽量均匀分

当计算出来的hash函数h和hashMap的length做了&运算后,会得到[0,length-1]其中的一个值,而散列的均匀也会使这个值分布的均匀,从而达到HashMap高效的一点(当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,简单而效率高)

hash对一个对象的hashCode进行重新计算,而IndexFor生成这个对象的index。

2.hash值重新计算目的:

        防止质量低下的hashCode()函数实现。在hashMap数组长度中长度是初始长度的2倍。通过右移造成地位的数据尽量的不同。

3.jdk1.8的HashMap为什么使用红黑树,而不用平衡二叉树?

        插入效率比平衡二叉树高,查询效率比普通二叉树高。所以选择性能相对折中的红黑树。

4.HashMap为什么不直接使用对象的原始hash值呢?

       通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。

5.既然红黑树那么好,为什么不直接使用红黑树,而是当链表数量超过8个时才转?

因为红黑树需要进行左旋,右旋操作, 而单链表不需要。

以下都是单链表与红黑树结构对比。

     如果元素小于8个,查询成本高,新增成本低。如果元素大于8个,查询成本低,新增成本高。

      至于为什么选数字8,是大佬折中衡量的结果-.-,就像loadFactor默认值0.75一样。

关于HashMap源码各个版本的对比参考下文:

https://blog.csdn.net/boom_man/article/details/78286610

https://www.codercto.com/a/12460.html


 

猜你喜欢

转载自blog.csdn.net/happyAliceYu/article/details/52556461
今日推荐