文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

怎么理解Java1.7中的HashMap源码

2023-06-25 13:57

关注

本篇内容主要讲解“怎么理解Java1.7中的HashMap源码”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么理解Java1.7中的HashMap源码”吧!

存储结构

内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶容量取模运算结果相同的 Entry。

怎么理解Java1.7中的HashMap源码

啊啊

transient Entry[] table;  //位桶数组  static class Entry<K,V> implements Map.Entry<K,V> {    final K key;  // 键    V value;  // 值    Entry<K,V> next; // next指针    int hash;  //hashCode()方法计算出的hash值            Entry(int h, K k, V v, Entry<K,V> n) {          value = v;          next = n;          key = k;          hash = h;      }        // 返回 与 此项 对应的键    public final K getKey() {          return key;      }      // 返回 与 此项 对应的值    public final V getValue() {          return value;      }        public final V setValue(V newValue) {          V oldValue = value;          value = newValue;          return oldValue;      }                public final boolean equals(Object o) {          if (!(o instanceof Map.Entry))              return false;          Map.Entry e = (Map.Entry)o;          Object k1 = getKey();          Object k2 = e.getKey();          if (k1 == k2 || (k1 != null && k1.equals(k2))) {              Object v1 = getValue();              Object v2 = e.getValue();              if (v1 == v2 || (v1 != null && v1.equals(v2)))                  return true;          }          return false;      }               public final int hashCode() {         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());      }        public final String toString() {          return getKey() + "=" + getValue();      }              void recordAccess(HashMap<K,V> m) {      }              void recordRemoval(HashMap<K,V> m) {      } }

属性成员

// 1. 容量(capacity): HashMap中数组的长度// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)// b. 初始容量 = 哈希表创建时的容量  // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  // 最大容量 =  2的30次方(若传入的容量过大,将被最大值替换)  static final int MAXIMUM_CAPACITY = 1 << 30;// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)  // 实际加载因子  final float loadFactor;  // 默认加载因子 = 0.75  static final float DEFAULT_LOAD_FACTOR = 0.75f;// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量) // a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数// b. 扩容阈值 = 容量 x 加载因子  int threshold;    //默认的threshold值      static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;// 4. 其他 // 存储数据的Entry类型 数组,长度 = 2的幂 // HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表  transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  static final Entry<?,?>[] EMPTY_TABLE = {};   // HashMap的大小,即 HashMap中存储的键值对的数量  transient int size;

构造函数:

      public HashMap() {             // 传入默认的容量(16)和负载因子(0.75)        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);     }        public HashMap(int initialCapacity) {                // 传入指定的容量,和默认的负载因子        this(initialCapacity, DEFAULT_LOAD_FACTOR);            }        public HashMap(int initialCapacity, float loadFactor) {        // HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量        //如果大于最大容量,还是赋值为1 << 30        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        // 设置 加载因子        this.loadFactor = loadFactor;                // 设置 扩容阈值 = 初始容量        // 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算        threshold = initialCapacity;           init(); // 一个空方法用于未来的子对象扩展    }        public HashMap(Map<? extends K, ? extends V> m) {        // 设置容量大小 & 加载因子 = 默认        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);        // 该方法用于初始化 数组 & 阈值,下面会详细说明        inflateTable(threshold);        // 将传入的子Map中的全部元素逐个添加到HashMap中        putAllForCreate(m);    }}

hash方法

hash(Object k):计算key的hash值

该函数在JDK 1.7 和 1.8 中的实现不同,但原理(扰动函数)一样使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)

           // a. 根据键值key计算hash值 ->> 分析1        int hash = hash(key);        // b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2        int i = indexFor(hash, table.length);     // JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)     static final int hash(int h) {        h ^= k.hashCode();         h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);     }      // JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)      // 1. 取hashCode值: h = key.hashCode()      //  2. 高位参与低位的运算:h ^ (h >>> 16)        static final int hash(Object key) {           int h;            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);            // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null                  // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null            // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制     }         static int indexFor(int h, int length) {            return h & (length-1);           // 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)}

Map中添加数据

put方法

put(int hash, K key, V value, int bucketIndex):向HashMap添加数据(成对存放 key - value)

怎么理解Java1.7中的HashMap源码

流程图

怎么理解Java1.7中的HashMap源码

源码

    map.put("name", "huangkaiyu");   map.put("age", 21);    public V put(K key, V value)// 1.如果哈希表未初始化(即 table为空)         if (table == EMPTY_TABLE) {         // 则使用构造函数传入的阈值(即初始容量) 初始化数组table          inflateTable(threshold);     }          // 2. 判断key是否为空值null// 若key == null,则将该键值对放在table [0](本质:key = Null时,hash值 = 0,故存放到table[0]中)        if (key == null)            return putForNullKey(value); //若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)//计算hash值        int hash = hash(key);       //传入hash值和table长度算出index        int i = indexFor(hash, table.length);        // 3. 遍历table[indexFor]对应的链表判断该key对应的值是否已存在        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;    //若该key已存在(即 key-value已存在 ),则用替换原来的值            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue; //并返回旧的value            }        }//改动计数器+1        modCount++;// 若该key不存在,则将“key-value”添加到table中        addEntry(hash, key, value, i);        return null;    }

inflateTable方法

inflateTable(int toSize):初始化数组(table)、扩容阈值(threshold

注意:

真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()

         if (table == EMPTY_TABLE) {          //此处传入的是构造函数时设置的阈值(即初始容量),不是真正的扩容阈值        inflateTable(threshold);     }        private void inflateTable(int toSize) {          // 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂(传入18转化得32)    int capacity = roundUpToPowerOf2(toSize);       // 2. 重新计算阈值 threshold = 容量 * 加载因子(之前存的是容量)    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);      // 3.传入容量初始化位桶数组table(作为数组长度)    table = new Entry[capacity];           initHashSeedAsNeeded(capacity);  }           private static int roundUpToPowerOf2(int number) {            //若超过了最大值,则设置为最大值;否则,设置为大于传入容量大小的最小的2的次幂       return number >= MAXIMUM_CAPACITY  ?             MAXIMUM_CAPACITY  : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;

putForNullKey方法

putForNullKey(V value):当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]

         if (key == null)           return putForNullKey(value);          private V putForNullKey(V value) {               for (Entry<K,V> e = table[0]; e != null; e = e.next) {           //如果链表结点key为空          if (e.key == null) {            //保存旧值            V oldValue = e.value;            //链表赋新值            e.value = value;              e.recordAccess(this);               //返回旧值            return oldValue;          }      }      //改动次数+1    modCount++;      // 若没有key==null的键,那么调用addEntry()将其加入链表    addEntry(0, null, value, 0);               // a. addEntry()的第1个参数hash值传入0(当key = null时,也有hash值 = 0,所以HashMap的key 可为null)    // c. 对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null    // d. 此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,    return null;  }

addEntry方法

addEntry(int hash, K key, V value, int bucketIndex):添加键值对(Entry )到 HashMap中

        void addEntry(int hash, K key, V value, int bucketIndex) {                      // 1. 插入前先判断是否需要扩容                    // 如果元素个数>=扩容阈值 并且 对应数组下标不为空          if ((size >= threshold) && (null != table[bucketIndex])) {                         //扩容2倍            resize(2 * table.length);              // 重新计算Key对应的hash值               hash = (null != key) ? hash(key) : 0;           // 重新计算该Key对应的hash值的存储数组下标位置             bucketIndex = indexFor(hash, table.length);      }      //如果不需要扩容,则创建1个新的Entry并放入到数组中    createEntry(hash, key, value, bucketIndex);  }

createEntry方法

createEntry(int hash, K key, V value, int bucketIndex)

   void createEntry(int hash, K key, V value, int bucketIndex) {     // 1. 把table中该位置原来的Entry保存      Entry<K,V> e = table[bucketIndex];    // 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表    // 即 在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突)    table[bucketIndex] = new Entry<>(hash, key, value, e);      // 3. 哈希表的键值对数量计数增加    size++;  }

扩容方法

resize方法

resize(int newCapacity):扩容为原来两倍

在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况

设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 = 线程不安全

     void resize(int newCapacity) {          // 1. 保存旧数组(old table)     Entry[] oldTable = table;      // 2. 保存旧容量(数组长度)    int oldCapacity = oldTable.length;     // 3. 若旧容量已经是系统默认最大容量了,那么将扩容阈值设置成整型的最大值,退出     if (oldCapacity == MAXIMUM_CAPACITY) {          threshold = Integer.MAX_VALUE;          return;     }      // 4. 根据新容量(2倍容量)新建1个数组,即新table      Entry[] newTable = new Entry[newCapacity];      // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容    transfer(newTable);     // 6. 新数组table引用到HashMap的table属性上    table = newTable;      // 7. 重新设置阈值      threshold = (int)(newCapacity * loadFactor); }

transfer方法

transfer(Entry[] newTable):

 void transfer(Entry[] newTable) {      // 1. src指向原table      Entry[] src = table;       // 2. 获取新数组的大小                  int newCapacity = newTable.length;      // 3. 通过遍历旧table,将键值对转移到新table上      for (int j = 0; j < src.length; j++) {                   // 创建辅助entry指向旧数组中的元素            Entry<K,V> e = src[j];                    if (e != null) {              // 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)              src[j] = null;   //开始遍历              do {                   //创建辅助指针next(因是单链表,故要保存下1个结点,否则转移后链表会断开)                 Entry<K,V> next = e.next;                  // 重新计算每个元素的存储位置                 int i = indexFor(e.hash, newCapacity);                                   //头插法插入                 e.next = newTable[i];                  newTable[i] = e;                                   // e跳到下一个entry                 e = next;                          } while (e != null);             // 循环直到遍历完数组上的所有数据元素         }     } }

从HashMap中获取数据

get方法

public V get(Object key):根据键key,向HashMap获取对应的值

   public V get(Object key) {      // 1. 当key == null时,则到table[0]为头结点的链表去检索    if (key == null)          return getForNullKey();    // 2. 当key ≠ null时,去获得对应值     Entry<K,V> entry = getEntry(key);      return null == entry ? null : entry.getValue();  }    private V getForNullKey() {      if (size == 0) {          return null;      }      // 遍历以table[0]为头结点的链表,寻找 key==null 对应的值    for (Entry<K,V> e = table[0]; e != null; e = e.next) {          // 从table[0]中取key==null的value值         if (e.key == null)              return e.value;     }      return null;  }      final Entry<K,V> getEntry(Object key) {      //如果元素个数为空返回null    if (size == 0) {          return null;      }      // 1. 计算key对应的hash值    int hash = (key == null) ? 0 : hash(key);      // 2. 根据hash值计算出对应的数组下标    // 3. 遍历对应index的数组元素为头结点的链表,检索键值对    for (Entry<K,V> e = table[indexFor(hash, table.length)];  e != null;  e = e.next) {          Object k;          // 若 hash值 & key 相等,则证明该Entry = 我们要的键值对        // 通过==或者equals()判断key是否相等        if (e.hash == hash &&              ((k = e.key) == key || (key != null && key.equals(k))))              return e;      }      return null;  }

从HashMap中删除数据

remove方法

remove(Object key):删除该键值对

public V remove(Object key) {      Entry<K,V> e = removeEntryForKey(key);      return (e == null ? null : e.value);  }    final Entry<K,V> removeEntryForKey(Object key) {      if (size == 0) {          return null;      }      // 1. 计算hash值    int hash = (key == null) ? 0 : hash(key);      // 2. 计算存储的数组下标位置    int i = indexFor(hash, table.length);          //prev记录要删除entry的前一个entry    Entry<K,V> prev = table[i];          //e记录要删除的entry    Entry<K,V> e = prev;        while (e != null) {          //辅助指针,指向下一个entry        Entry<K,V> next = e.next;          Object k;          //如果key相等        if (e.hash == hash &&              ((k = e.key) == key || (key != null && key.equals(k)))) {              modCount++; //改动次数+1                      size--;   //元素个数-1                        // 若删除的是链表的头结点             if (prev == e)                // 则将头结点的next引用存入table[i]中                table[i] = next;            //否则 将当前结点的前1个结点中的next指向当前结点的下一个结点(直接越过当前Entry)            else                  prev.next = next;               e.recordRemoval(this);              return e;          }                //prev指向当前结点        prev = e;          //e指向下一个结点        e = next;      }      //遍历结束e为null,表示没找到返回null    return e;  }

对HashMap的其他操作

HashMap除了核心的put()get()函数,还有以下主要使用的函数方法

void clear();清除哈希表中的所有键值对
int size();返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty();判断HashMap是否为空;size == 0时 表示为 空
void putAll(Map<? extends K, ? extends V> m);将指定Map中的键值对 复制到 此Map中
boolean containsKey(Object key);判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value);判断是否存在该值的键值对;是 则返回true

源码

  public boolean isEmpty() {      return size == 0;  }     public int size() {      return size;  }    public void clear() {    //改动次数+1    modCount++;      //全部元素设空    Arrays.fill(table, null);    //元素个数清0    size = 0;}       public void putAll(Map<? extends K, ? extends V> m) {      // 1. 统计需复制多少个键值对      int numKeysToBeAdded = m.size();      if (numKeysToBeAdded == 0)          return;     // 2. 若table还没初始化,先用刚刚统计的复制数去初始化table      if (table == EMPTY_TABLE) {          inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));      }        // 3. 若需复制的数目 > 阈值,则需先扩容     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);      }      // 4. 开始复制(实际上不断调用Put函数插入)      for (Map.Entry<? extends K, ? extends V> e : m.entrySet())          put(e.getKey(), e.getValue());}       public boolean containsKey(Object key) {      return getEntry(key) != null; }     public boolean containsValue(Object value) {      // 若value为空,则调用containsNullValue()      if (value == null)        return containsNullValue();          // 若value不为空,则遍历链表中的每个Entry,通过equals()比较values 判断是否存在    Entry[] tab = table;    for (int i = 0; i < tab.length ; i++)          for (Entry e = tab[i] ; e != null ; e = e.next)              if (value.equals(e.value))                 return true;//返回true      return false;  }    // 判断是否有空值 private boolean containsNullValue() {      Entry[] tab = table;      for (int i = 0; i < tab.length ; i++)          for (Entry e = tab[i] ; e != null ; e = e.next)              if (e.value == null)                return true;      return false;  }

1.7和1.8版本区别

JDK 1.8 的优化目的主要是:减少 Hash冲突 & 提高哈希表的存、取效率

数据结构

版本存储结构数组&链表结点实现类红黑树的实现类初始化方式
JDK1.7数组+链表Entry类无红黑树单独函数:inflateTable()
JDK1.8数组+链表+红黑树Node类TreeNode类直接集成在扩容函数:resize()中

hash值计算方式

版本hash值计算方式
JDK1.71.hashcode计算
JDK1.8按照扩容后的规律计算(扩容后的位置=原位置 or 原位置 +旧容量)

扩容机制

版本重hash计算位置
JDK1.71.Object.hashCode计算
2. 9次扰动处理 =4次位运算+5次异或运算
JDK1.81.Object.hashCode计算
2. 2次扰动处理 =1次位运算+1次异或运算

到此,相信大家对“怎么理解Java1.7中的HashMap源码”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯