JDK1.8源码(七)——java.util.HashMap 类,java面试题,java基础笔试题,BAT

C++实现 HashMap LinkedHashMap HashSet 源码 效率极高 图解 完全本人手写 带效率测试源码 C++HashMap源码。源码提供了LinkedHashMap以及HashSet。源码和std::unordered_map在读写性能、内存占用均相当,但能在高速查找的同时保证了容器内的键值对是有序的。此代码本人纯手写,希望大家多多支持。......... 阅读详情


写在最前面,我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家。扫码加微信好友进【程序员面试学习交流群】,免费领取。也欢迎各位一起在群里探讨技术。

 

本篇博客我们来介绍在 JDK1.8 中 HashMap 的源码实现,这也是最常用的一个集合。但是在介绍 HashMap 之前,我们先介绍什么是 Hash表。

 

1、哈希表

Hash表也称为散列表,也有直接译作哈希表,Hash表是一种根据关键字值(key - value)而直接进行访问的数据结构。也就是说它通过把关键码值映射到表中的一个位置来访问记录,以此来加快查找的速度。在链表、数组等数据结构中,查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。

比如对于前面我们讲解的 ArrayList 集合和 LinkedList ,如果我们要查找这两个集合中的某个元素,通常是通过遍历整个集合,需要O(N)的时间级。

如果是哈希表,它是通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表,只需要O(1)的时间级。

①、存放在哈希表中的数据是key-value 键值对,比如存放哈希表的数据为:

{Key1-Value1,Key2-Value2,Key3-Value3,Key4-Value4,Key5-Value5,Key6-Value6}

如果我们想查找是否存在键值对 Key3-Value3,首先通过 Key3 经过散列函数,得到值 k3,然后通过 k3 和散列表对应的值找到是 Value3。

②、当然也有可能存放哈希表的值只是 Value1,Value2,Value3这种类型:

{Value1,Value2,Value3,Value4,Value5,Value6}

这时候我们可以假设 Value1 是等于 Key1的,也就是{Value1-Value1,Value2-Value2,Value3-Value3,Value4-Value4,Value5-Value5,Value6-Value6}可以将 Value1经过散列函数转换成与散列表对应的值。

 

大家都用过汉语字典吧,汉语字典的优点是我们可以通过前面的拼音目录快速定位到所要查找的汉字。当给定我们某个汉字时,大脑会自动将汉字转换成拼音(如果我们认识,不认识可以通过偏旁部首),这个转换的过程我们可以看成是一个散列函数,之后在根据转换得到的拼音找到该字所在的页码,从而找到该汉字。

 

汉语字典是哈希表的典型实现,但是我们仔细思考,会发现这样几个问题?

①、为什么要有散列函数?

②、多个 key 通过散列函数会得到相同的值,这时候怎么办?

对于第一个问题,散列函数的存在能够帮助我们更快的确定key和value的映射关系,试想一下,如果没有汉字和拼音的转换规则(或者汉字和偏旁部首的),给你一个汉字,你该如何从字典中找到该汉字?我想除了遍历整部字典,你没有什么更好的办法。

对于第二个问题,多个 key 通过散列函数得到相同的值,这其实也是哈希表最大的问题——冲突。比如同音字汉字,我们得到的拼音就会是相同的,那么我们该如何在字典中存放同音字汉字呢?有两种做法:

第一种是开放地址法,当我们遇到冲突了,这时候通过另一种函数再计算一遍,得到相应的映射关系。比如对于汉语字典,一个字 “余”,拼音是“yu”,我们将其放在页码为567(假设在该位置),这时候又来了一个汉字“于”,拼音也是“yu”,那么这时候我们要是按照转换规则,也得将其放在页码为567的位置,但是我们发现这个页码已经被占用了,这时候怎么办?我们可以在通过另一种函数,得到的值加1。那么汉字"于"就会被放在576+1=577的位置。

第二种是链地址法,我们可以将字典的每一页都看成是一个子数组或者子链表,当遇到冲突了,直接往当前页码的子数组或者子链表里面填充即可。那么我们进行同音字查找的时候,可能需要遍历其子数组或者子链表。如下图所示:

对于开放地址法,可能会遇到二次冲突,三次冲突,所以需要良好的散列函数,分布的越均匀越好。对于链地址法,虽然不会造成二次冲突,但是如果一次冲突很多,那么会造成子数组或者子链表很长,那么我们查找所需遍历的时间也会很长。

关于哈希表的详细介绍,请点击这里。

2、什么是 HashMap?

听名字就知道,HashMap 是一个利用哈希表原理来存储元素的集合。遇到冲突时,HashMap 是采用的链地址法来解决,在 JDK1.7 中,HashMap 是由 数组+链表构成的。但是在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。下面我们来具体介绍在 JDK1.8 中 HashMap 是如何实现的。

3、HashMap定义

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,而且 key 和 value 都可以为 null。

 

1 public class HashMap<K,V> extends AbstractMap<K,V>

2     implements Map<K,V>, Cloneable, Serializable {

 

首先该类实现了一个 Map 接口,该接口定义了一组键值对映射通用的操作。储存一组成对的键-值对象,提供key(键)到value(值)的映射,Map中的key不要求有序,不允许重复。value同样不要求有序,但可以重复。但是我们发现该接口方法有很多,我们设计某个键值对的集合有时候并不像实现那么多方法,那该怎么办?

JDK 还为我们提供了一个抽象类 AbstractMap ,该抽象类继承 Map 接口,所以如果我们不想实现所有的 Map 接口方法,就可以选择继承抽象类 AbstractMap 。

但是我们发现 HashMap 类即继承了 AbstractMap 接口,也实现了 Map 接口,这样做难道不是多此一举?后面我们会讲的 LinkedHashSet 集合也有这样的写法。

毕竟 JDK 经过这么多年的发展维护,博主起初也是认为这样是有具体的作用的,后来找了很多资料,发现这其实完全没有任何作用,具体出处。

 

据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来了。

 

HashMap 集合还实现了 Cloneable 接口以及 Serializable 接口,分别用来进行对象克隆以及将对象进行序列化。

4、字段属性

 

 1     //序列化和反序列化时,通过该字段进行版本一致性验证

 2     private static final long serialVersionUID = 362498820763181265L;

 3     //默认 HashMap 集合初始容量为16(必须是 2 的倍数)

 4     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 5     //集合的最大容量,如果通过带参构造指定的最大容量超过此数,默认还是使用此数

 6     static final int MAXIMUM_CAPACITY = 1 << 30;

 7     //默认的填充因子

 8     static final float DEFAULT_LOAD_FACTOR = 0.75f;

 9     //当桶(bucket)上的结点数大于这个值时会转成红黑树(JDK1.8新增)

10     static final int TREEIFY_THRESHOLD = 8;

11     //当桶(bucket)上的节点数小于这个值时会转成链表(JDK1.8新增)

12     static final int UNTREEIFY_THRESHOLD = 6;

13     /**(JDK1.8新增)

14      * 当集合中的容量大于这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,

15      * 而不是树形化 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

16      */

17     static final int MIN_TREEIFY_CAPACITY = 64;

 

注意:后面三个字段是 JDK1.8 新增的,主要是用来进行红黑树和链表的互相转换。

 

 1     /**

 2      * 初始化使用,长度总是 2的幂

 3      */

 4     transient Node<K,V>[] table;

 5 

 6     /**

 7      * 保存缓存的entrySet()

 8      */

 9     transient Set<Map.Entry<K,V>> entrySet;

10 

11     /**

12      * 此映射中包含的键值映射的数量。(集合存储键值对的数量)

13      */

14     transient int size;

15 

16     /**

17      * 跟前面ArrayList和LinkedList集合中的字段modCount一样,记录集合被修改的次数

18      * 主要用于迭代器中的快速失败

19      */

20     transient int modCount;

21 

22     /**

23      * 调整大小的下一个大小值(容量*加载因子)。capacity * load factor

24      */

25     int threshold;

26 

27     /**

28      * 散列表的加载因子。

29      */

30     final float loadFactor;

 

下面我们重点介绍上面几个字段:

①、Node<K,V>[] table

我们说 HashMap 是由数组+链表+红黑树组成,这里的数组就是 table 字段。后面对其进行初始化长度默认是 DEFAULT_INITIAL_CAPACITY= 16。而且 JDK 声明数组的长度总是 2的n次方(一定是合数),为什么这里要求是合数,一般我们知道哈希算法为了避免冲突都要求长度是质数,这里要求是合数,下面在介绍 HashMap 的hashCode() 方法(散列函数),我们再进行讲解。

②、size

集合中存放key-value 的实时对数。

③、loadFactor

装载因子,是用来衡量 HashMap 满的程度,计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是 table 的长度length。

默认的负载因子0.75 是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子loadFactor 的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子 loadFactor 的值,这个值可以大于1。

④、threshold

计算公式:capacity * loadFactor。这个值是当前已占用数组长度的最大值。过这个数目就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍

5、构造函数

①、默认无参构造函数



 

    /**

     * 默认构造函数,初始化加载因子loadFactor = 0.75

     */

    public HashMap() {

        this.loadFactor = DEFAULT_LOAD_FACTOR; 

    }





View Code

无参构造器,初始化散列表的加载因子为0.75

②、指定初始容量的构造函数



 

 1     /**

 2      * 

 3      * @param initialCapacity 指定初始化容量

 4      * @param loadFactor 加载因子 0.75

 5      */

 6     public HashMap(int initialCapacity, float loadFactor) {

 7         //初始化容量不能小于 0 ,否则抛出异常

 8         if (initialCapacity < 0)

 9             throw new IllegalArgumentException("Illegal initial capacity: " +

10                                                initialCapacity);

11         //如果初始化容量大于2的30次方,则初始化容量都为2的30次方

12         if (initialCapacity > MAXIMUM_CAPACITY)

13             initialCapacity = MAXIMUM_CAPACITY;

14         //如果加载因子小于0,或者加载因子是一个非数值,抛出异常

15         if (loadFactor <= 0 || Float.isNaN(loadFactor))

16             throw new IllegalArgumentException("Illegal load factor: " +

17                                                loadFactor);

18         this.loadFactor = loadFactor;

19         this.threshold = tableSizeFor(initialCapacity);

20     }

21     // 返回大于等于initialCapacity的最小的二次幂数值。

22     // >>> 操作符表示无符号右移,高位取0。

23     // | 按位或运算

24     static final int tableSizeFor(int cap) {

25         int n = cap - 1;

26         n |= n >>> 1;

27         n |= n >>> 2;

28         n |= n >>> 4;

29         n |= n >>> 8;

30         n |= n >>> 16;

31         return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

32     }





View Code

6、确定哈希桶数组索引位置

前面我们讲解哈希表的时候,我们知道是用散列函数来确定索引的位置。散列函数设计的越好,使得元素分布的越均匀。HashMap 是数组+链表+红黑树的组合,我们希望在有限个数组位置时,尽量每个位置的元素只有一个,那么当我们用散列函数求得索引位置的时候,我们能马上知道对应位置的元素是不是我们想要的,而不是要进行链表的遍历或者红黑树的遍历,这会大大优化我们的查询效率。我们看 HashMap 中的哈希算法:

 

    static final int hash(Object key) {

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

    

    i = (table.length - 1) & hash;//这一步是在后面添加元素putVal()方法中进行位置的确定

 

主要分为三步:

①、取 hashCode 值: key.hashCode()

②、高位参与运算:h>>>16

③、取模运算:(n-1) & hash

这里获取 hashCode() 方法的值是变量,但是我们知道,对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(Object key) 所计算得到的 hash码 值总是相同的。

为了让数组元素分布均匀,我们首先想到的是把获得的 hash码对数组长度取模运算( hash%length),但是计算机都是二进制进行操作,取模运算相对开销还是很大的,那该如何优化呢?

HashMap 使用的方法很巧妙,它通过 hash & (table.length -1)来得到该对象的保存位,前面说过 HashMap 底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当 length 总是2的n次方时,hash & (length-1)运算等价于对 length 取模,也就是 hash%length,但是&比%具有更高的效率。比如 n % 32 = n & (32 -1)

这也解释了为什么要保证数组的长度总是2的n次方。

再就是在 JDK1.8 中还有个高位参与运算,hashCode() 得到的是一个32位 int 类型的值,通过hashCode()的高16位 异或 低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,n为table的长度:

7、添加元素



 

 1     //hash(key)就是上面讲的hash方法,对其进行了第一步和第二步处理

 2     public V put(K key, V value) {

 3         return putVal(hash(key), key, value, false, true);

 4     }

 5     /**

 6      * 

 7      * @param hash 索引的位置

 8      * @param key  键

 9      * @param value  值

10      * @param onlyIfAbsent true 表示不要更改现有值

11      * @param evict false表示table处于创建模式

12      * @return

13      */

14     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

15             boolean evict) {

16          Node<K,V>[] tab; Node<K,V> p; int n, i;

17          //如果table为null或者长度为0,则进行初始化

18          //resize()方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配,后面会详细讲解该方法

19          if ((tab = table) == null || (n = tab.length) == 0)

20              n = (tab = resize()).length;

21          //注意:这里用到了前面讲解获得key的hash码的第三步,取模运算,下面的if-else分别是 tab[i] 为null和不为null

22          if ((p = tab[i = (n - 1) & hash]) == null)

23              tab[i] = newNode(hash, key, value, null);//tab[i] 为null,直接将新的key-value插入到计算的索引i位置

24          else {//tab[i] 不为null,表示该位置已经有值了

25              Node<K,V> e; K k;

26              if (p.hash == hash &&

27                  ((k = p.key) == key || (key != null && key.equals(k))))

28                  e = p;//节点key已经有值了,直接用新值覆盖

29              //该链是红黑树

30              else if (p instanceof TreeNode)

31                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

32              //该链是链表

33              else {

34                  for (int binCount = 0; ; ++binCount) {

35                      if ((e = p.next) == null) {

36                          p.next = newNode(hash, key, value, null);

37                          //链表长度大于8,转换成红黑树

38                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

39                              treeifyBin(tab, hash);

40                          break;

41                      }

42                      //key已经存在直接覆盖value

43                      if (e.hash == hash &&

44                          ((k = e.key) == key || (key != null && key.equals(k))))

45                          break;

46                      p = e;

47                  }

48              }

49              if (e != null) { // existing mapping for key

50                  V oldValue = e.value;

51                  if (!onlyIfAbsent || oldValue == null)

52                      e.value = value;

53                  afterNodeAccess(e);

54                  return oldValue;

55              }

56          }

57          ++modCount;//用作修改和新增快速失败

58          if (++size > threshold)//超过最大容量,进行扩容

59              resize();

60          afterNodeInsertion(evict);

61          return null;

62     }





View Code

①、判断键值对数组 table 是否为空或为null,否则执行resize()进行扩容;

②、根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③、判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④、判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤、遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥、插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。

⑦、如果新插入的key不存在,则返回null,如果新插入的key存在,则返回原key对应的value值(注意新插入的value会覆盖原value值)

注意1:看第 58,59 行代码:

 

if (++size > threshold)//超过最大容量,进行扩容

    resize();

 

这里有个考点,我们知道 HashMap 是由数组+链表+红黑树(JDK1.8)组成,如果在添加元素时,发生冲突,会将冲突的数放在链表上,当链表长度超过8时,会自动转换成红黑树。

那么有如下问题:数组上有5个元素,而某个链表上有3个元素,问此HashMap的 size 是多大?

我们分析第58,59 行代码,很容易知道,只要是调用put() 方法添加元素,那么就会调用 ++size(这里有个例外是插入重复key的键值对,不会调用,但是重复key元素不会影响size),所以,上面的答案是 7。

注意2:看第 53 、 60 行代码:

 

 afterNodeAccess(e);

 afterNodeInsertion(evict);

 

这里调用的该方法,其实是调用了如下实现方法:

 

void afterNodeAccess(Node<K,V> p) { }

void afterNodeInsertion(boolean evict) { }

 

这都是一个空的方法实现,我们在这里可以不用管,但是在后面介绍 LinkedHashMap 会用到,LinkedHashMap 是继承的 HashMap,并且重写了该方法,后面我们会详细介绍。

8、扩容机制

扩容(resize),我们知道集合是由数组+链表+红黑树构成,向 HashMap 中插入元素时,如果HashMap 集合的元素已经大于了最大承载容量threshold(capacity * loadFactor),这里的threshold不是数组的最大长度。那么必须扩大数组的长度,Java中数组是无法自动扩容的,我们采用的方法是用一个更大的数组代替这个小的数组,就好比以前是用小桶装水,现在小桶装不下了,我们使用一个更大的桶。

JDK1.8融入了红黑树的机制,比较复杂,这里我们先介绍 JDK1.7的扩容源码,便于理解,然后在介绍JDK1.8的源码。



 

 1     //参数 newCapacity 为新数组的大小

 2     void resize(int newCapacity) {

 3         Entry[] oldTable = table;//引用扩容前的 Entry 数组

 4         int oldCapacity = oldTable.length;

 5         if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了

 6             threshold = Integer.MAX_VALUE;///修改阈值为int的最大值(2^31-1),这样以后就不会扩容了

 7             return;

 8         }

 9 

10         Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组

11         transfer(newTable, initHashSeedAsNeeded(newCapacity));//将数组元素转移到新数组里面

12         table = newTable;

13         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阈值

14     }

15     void transfer(Entry[] newTable, boolean rehash) {

16         int newCapacity = newTable.length;

17         for (Entry<K,V> e : table) {//遍历数组

18             while(null != e) {

19                 Entry<K,V> next = e.next;

20                 if (rehash) {

21                     e.hash = null == e.key ? 0 : hash(e.key);

22                 }

23                 int i = indexFor(e.hash, newCapacity);//重新计算每个元素在数组中的索引位置

24                 e.next = newTable[i];//标记下一个元素,添加是链表头添加

25                 newTable[i] = e;//将元素放在链上

26                 e = next;//访问下一个 Entry 链上的元素

27             }

28         }

29     }





View Code

通过方法我们可以看到,JDK1.7中首先是创建一个新的大容量数组,然后依次重新计算原集合所有元素的索引,然后重新赋值。如果数组某个位置发生了hash冲突,使用的是单链表的头插入方法,同一位置的新元素总是放在链表的头部,这样与原集合链表对比,扩容之后的可能就是倒序的链表了。

下面我们在看看JDK1.8的。



 

 1     final Node<K,V>[] resize() {

 2         Node<K,V>[] oldTab = table;

 3         int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组如果为null,则长度赋值0

 4         int oldThr = threshold;

 5         int newCap, newThr = 0;

 6         if (oldCap > 0) {//如果原数组长度大于0

 7             if (oldCap >= MAXIMUM_CAPACITY) {//数组大小如果已经大于等于最大值(2^30)

 8                 threshold = Integer.MAX_VALUE;//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了

 9                 return oldTab;

10             }

11             //原数组长度大于等于初始化长度16,并且原数组长度扩大1倍也小于2^30次方

12             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

13                      oldCap >= DEFAULT_INITIAL_CAPACITY)

14                 newThr = oldThr << 1; // 阀值扩大1倍

15         }

16         else if (oldThr > 0) //旧阀值大于0,则将新容量直接等于就阀值 

17             newCap = oldThr;

18         else {//阀值等于0,oldCap也等于0(集合未进行初始化)

19             newCap = DEFAULT_INITIAL_CAPACITY;//数组长度初始化为16

20             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阀值等于16*0.75=12

21         }

22         //计算新的阀值上限

23         if (newThr == 0) {

24             float ft = (float)newCap * loadFactor;

25             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

26                       (int)ft : Integer.MAX_VALUE);

27         }

28         threshold = newThr;

29         @SuppressWarnings({"rawtypes","unchecked"})

30             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

31         table = newTab;

32         if (oldTab != null) {

33             //把每个bucket都移动到新的buckets中

34             for (int j = 0; j < oldCap; ++j) {

35                 Node<K,V> e;

36                 if ((e = oldTab[j]) != null) {

37                     oldTab[j] = null;//元数据j位置置为null,便于垃圾回收

38                     if (e.next == null)//数组没有下一个引用(不是链表)

39                         newTab[e.hash & (newCap - 1)] = e;

40                     else if (e instanceof TreeNode)//红黑树

41                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

42                     else { // preserve order

43                         Node<K,V> loHead = null, loTail = null;

44                         Node<K,V> hiHead = null, hiTail = null;

45                         Node<K,V> next;

46                         do {

47                             next = e.next;

48                             //原索引

49                             if ((e.hash & oldCap) == 0) {

50                                 if (loTail == null)

51                                     loHead = e;

52                                 else

53                                     loTail.next = e;

54                                 loTail = e;

55                             }

56                             //原索引+oldCap

57                             else {

58                                 if (hiTail == null)

59                                     hiHead = e;

60                                 else

61                                     hiTail.next = e;

62                                 hiTail = e;

63                             }

64                         } while ((e = next) != null);

65                         //原索引放到bucket里

66                         if (loTail != null) {

67                             loTail.next = null;

68                             newTab[j] = loHead;

69                         }

70                         //原索引+oldCap放到bucket里

71                         if (hiTail != null) {

72                             hiTail.next = null;

73                             newTab[j + oldCap] = hiHead;

74                         }

75                     }

76                 }

77             }

78         }

79         return newTab;

80     }





View Code

该方法分为两部分,首先是计算新桶数组的容量 newCap 和新阀值 newThr,然后将原集合的元素重新映射到新集合中。

相比于JDK1.7,1.8使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。

9、删除元素

HashMap 删除元素首先是要找到 桶的位置,然后如果是链表,则进行链表遍历,找到需要删除的元素后,进行删除;如果是红黑树,也是进行树的遍历,找到元素删除后,进行平衡调节,注意,当红黑树的节点数小于 6 时,会转化成链表。



 

 1     public V remove(Object key) {

 2         Node<K,V> e;

 3         return (e = removeNode(hash(key), key, null, false, true)) == null ?

 4             null : e.value;

 5     }

 6     

 7     final Node<K,V> removeNode(int hash, Object key, Object value,

 8             boolean matchValue, boolean movable) {

 9         Node<K,V>[] tab; Node<K,V> p; int n, index;

10         //(n - 1) & hash找到桶的位置

11         if ((tab = table) != null && (n = tab.length) > 0 &&

12         (p = tab[index = (n - 1) & hash]) != null) {

13         Node<K,V> node = null, e; K k; V v;

14         //如果键的值与链表第一个节点相等,则将 node 指向该节点

15         if (p.hash == hash &&

16         ((k = p.key) == key || (key != null && key.equals(k))))

17         node = p;

18         //如果桶节点存在下一个节点

19         else if ((e = p.next) != null) {

20             //节点为红黑树

21         if (p instanceof TreeNode)

22          node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到需要删除的红黑树节点

23         else {

24          do {//遍历链表,找到待删除的节点

25              if (e.hash == hash &&

26                  ((k = e.key) == key ||

27                   (key != null && key.equals(k)))) {

28                  node = e;

29                  break;

30              }

31              p = e;

32          } while ((e = e.next) != null);

33         }

34         }

35         //删除节点,并进行调节红黑树平衡

36         if (node != null && (!matchValue || (v = node.value) == value ||

37                       (value != null && value.equals(v)))) {

38         if (node instanceof TreeNode)

39          ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

40         else if (node == p)

41          tab[index] = node.next;

42         else

43          p.next = node.next;

44         ++modCount;

45         --size;

46         afterNodeRemoval(node);

47         return node;

48         }

49         }

50         return null;

51     }





View Code

注意第 46 行代码

 

afterNodeRemoval(node);

 

这也是为实现 LinkedHashMap 做准备的,在这里和上面一样,是一个空方法实现,可以不用管。而在 LinkedHashMap 中进行了重写,用来维护删除节点后,链表的前后关系。

10、查找元素

①、通过 key 查找 value

首先通过 key 找到计算索引,找到桶位置,先检查第一个节点,如果是则返回,如果不是,则遍历其后面的链表或者红黑树。其余情况全部返回 null。



 

 1     public V get(Object key) {

 2         Node<K,V> e;

 3         return (e = getNode(hash(key), key)) == null ? null : e.value;

 4     }

 5     

 6     final Node<K,V> getNode(int hash, Object key) {

 7         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

 8         if ((tab = table) != null && (n = tab.length) > 0 &&

 9             (first = tab[(n - 1) & hash]) != null) {

10             //根据key计算的索引检查第一个索引

11             if (first.hash == hash && // always check first node

12                 ((k = first.key) == key || (key != null && key.equals(k))))

13                 return first;

14             //不是第一个节点

15             if ((e = first.next) != null) {

16                 if (first instanceof TreeNode)//遍历树查找元素

17                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);

18                 do {

19                     //遍历链表查找元素

20                     if (e.hash == hash &&

21                         ((k = e.key) == key || (key != null && key.equals(k))))

22                         return e;

23                 } while ((e = e.next) != null);

24             }

25         }

26         return null;

27     }





View Code

②、判断是否存在给定的 key 或者 value



 

 1     public boolean containsKey(Object key) {

 2         return getNode(hash(key), key) != null;

 3     }

 4     public boolean containsValue(Object value) {

 5         Node<K,V>[] tab; V v;

 6         if ((tab = table) != null && size > 0) {

 7             //遍历桶

 8             for (int i = 0; i < tab.length; ++i) {

 9                 //遍历桶中的每个节点元素

10                 for (Node<K,V> e = tab[i]; e != null; e = e.next) {

11                     if ((v = e.value) == value ||

12                         (value != null && value.equals(v)))

13                         return true;

14                 }

15             }

16         }

17         return false;

18     }





View Code

11、遍历元素

首先构造一个 HashMap 集合:

 

1 HashMap<String,Object> map = new HashMap<>();

2 map.put("A","1");

3 map.put("B","2");

4 map.put("C","3");

 

①、分别获取 key 集合和 value 集合。

 

1 //1、分别获取key和value的集合

2 for(String key : map.keySet()){

3     System.out.println(key);

4 }

5 for(Object value : map.values()){

6     System.out.println(value);

7 }

 

②、获取 key 集合,然后遍历key集合,根据key分别得到相应value

 

1 //2、获取key集合,然后遍历key,根据key得到 value

2 Set<String> keySet = map.keySet();

3 for(String str : keySet){

4     System.out.println(str+"-"+map.get(str));

5 }

 

③、得到 Entry 集合,然后遍历 Entry

 

1 //3、得到 Entry 集合,然后遍历 Entry

2 Set<Map.Entry<String,Object>> entrySet = map.entrySet();

3 for(Map.Entry<String,Object> entry : entrySet){

4     System.out.println(entry.getKey()+"-"+entry.getValue());

5 }

 

④、迭代

 

1 //4、迭代

2 Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();

3 while(iterator.hasNext()){

4     Map.Entry<String,Object> mapEntry = iterator.next();

5     System.out.println(mapEntry.getKey()+"-"+mapEntry.getValue());

6 }

 

基本上使用第三种方法是性能最好的,

第一种遍历方法在我们只需要 key 集合或者只需要 value 集合时使用;

第二种方法效率很低,不推荐使用;

第四种方法效率也挺好,关键是在遍历的过程中我们可以对集合中的元素进行删除。

12、总结

①、基于JDK1.8的HashMap是由数组+链表+红黑树组成,当链表长度超过 8 时会自动转换成红黑树,当红黑树节点个数小于 6 时,又会转化成链表。相对于早期版本的 JDK HashMap 实现,新增了红黑树作为底层数据结构,在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率。

②、允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。

③、非线程安全

④、无序(遍历HashMap得到元素的顺序不是按照插入的顺序)

参考文档:https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html#

http://www.importnew.com/20386.html

https://www.cnblogs.com/nullllun/p/8327664.html

 


转载:https://www.cnblogs.com/ysocean/p/8711071.html

推荐内容:
Java实习生面试题整理
从1.6W名面试者中收集的Java面试题精选汇总(内附知识脑图)
一个两年Java的面试总结
Java面试通关要点汇总集
最新Java校招面试题及答案
【Java】几道常见的秋招面试题
Java 最常见的 200+ 面试题汇总
Java集合必会14问(精选面试题整理)
JAVA工程师面试常见问题集锦
Java简历与面试

 

hashmap的C++实现 按照hashmap的基本原理用C++实现了简单的基本功能,复杂的实现参考C++库的源码,C++最新的标准库里已经有以下四种基于hashtable的容器: unordered_set (C++11) unordered_multiset (C++11) unordered_map (C++11) unordered_multimap (C++11)。 阅读详情

相关推荐

[C++] Hash_map 实现原理和源码[散列表][散列映射][泛式数据支持][STL]

Hash_map 作为非线性数据结构. 在阅读前,你需要确定你已经真正的理解了什么是指针. 并且在不看任何源码的情况下,通过原理实现出链表数据结构. 如果不太确定的话,建议以下文章: [C/C++]指针的原理和对指针的运用及理解(包括函数指针和多级指针) [C++]什么是引用,以及引用和指针的关系(对内存和指针运用的理解) [C++] LinkList:双向不循环链表的原理和实现(基础) 或者不想看长篇大论,可以直接滑动到最下面,copy源码. 源码内有注释和例子,复制粘贴就能用.如果可以,请点个免费的赞,

八宝咸鱼 1012

java.util.HashMap介绍

HashMap是基于哈希表实现的键值对映射,每个键值对是一个 Entry 对象,其中键是唯一的,但值可以重复。HashMap允许使用null作为键,但键只能有一个为null。同样,HashMap也允许使用null作为值。HashMap不保证映射表中的键值对顺序,不保证迭代顺序。// 创建一个 Map 实例// 添加键值对到映射表中// 获取指定键对应的值System.out.println("Bob 的年龄:" + age);// 判断映射表中是否包含指定键。

qq_27390023的博客 580

Java RSA工具库:零依赖解决密钥格式、填充算法与API简化难题

RSA是一种广泛使用的非对称加密算法,其核心原理基于大数分解的数学难题,通过公钥加密、私钥解密或私钥签名、公钥验签,实现数据加密和身份认证。在Java开发中,RSA的应用价值在于保障数据传输安全、实现数字签名和密钥交换。然而,实际工程实践中常遇到PEM、XML、PKCS#1/8等多种密钥格式解析困难,以及不同Java版本对OAEP、PSS等填充算法支持不一致的兼容性问题。针对这些痛点,一个设计良好的Java RSA工具库应能无缝处理密钥格式转换,并智能适配多种填充算法,从而简化API调用,提升开发效率与系统

weixin_33796205的博客 351

java util map_java.util.HashMap的简单介绍

1. java.util.HashMap的底层实现是数组+链表。2. 简介put(key, value)方法的执行过程:1)通过key值,使用散列算法计算出来一个hash值,用来确定该元素需要存储到数组中的哪个位置(index)。2)根据计算出来的位置(index),可以查看该位置是否被占用:2.1)如果位置(index)未被占用,将(key\value)封装成一个节点,保存到该位置。2.2)如果...

weixin_33905037的博客 1923

CVE-2016-8735 Tomcat JMX反序列化漏洞原理与复现指南

Java反序列化漏洞是Java应用安全领域的经典议题,其核心原理在于Java对象序列化与反序列化机制。当应用在反序列化不可信数据时,若未对反序列化的类进行严格过滤,攻击者可通过精心构造的恶意序列化对象,触发目标类库中存在的利用链(Gadget Chain),最终实现远程代码执行(RCE)。这种漏洞在Java中间件中尤为常见,具有极高的技术价值和现实威胁。Apache Tomcat作为广泛使用的Java Web应用服务器,其JMX远程管理功能若配置不当,便会成为此类漏洞的典型应用场景。本文聚焦于CVE-201

weixin_34080951的博客 303

flink大数据15天速成教程

不要追最新(如 1.19),也不要太旧(如 1.12)。

tian_jiangnan的博客 549

HashMap底层原理与性能调优:从哈希碰撞到红黑树治理

HashMap是Java中最基础也最易被低估的核心集合类,其本质是基于哈希表的键值映射结构,通过扰动函数、位运算索引和动态扩容机制实现平均O(1)时间复杂度。原理层面涉及哈希函数设计、数组+链表+红黑树的混合存储结构,以及JDK7到JDK8在死循环防护、树化阈值(8)、扩容算法(局部迁移)等关键演进。技术价值在于以空间换时间的工程权衡——如TreeNode复用降低内存抖动、CAS+synchronized提升并发吞吐。典型应用场景包括缓存实现、临时数据聚合与高频读写中间件;而当面临哈希碰撞退化、扩容阻塞或并

weixin_33888907的博客 514

深入解析MTK平台hang_detect机制:从原理到实战调试

本文深入解析了MTK平台hang_detect机制的工作原理与实战调试技巧。作为系统健康监测的核心机制,hang_detect通过多层级监控体系检测应用层和内核层异常,结合Kernel_exception等日志信息快速定位问题。文章详细介绍了典型问题案例、调试方法及预防措施,帮助开发者提升MTK平台系统稳定性调试能力。

purple的博客 1004

Java面试新趋势:从八股文到场景化深度考察的实战应对策略

在Java技术领域,面试考察正从传统的知识点背诵转向对技术原理深度理解和复杂场景解决能力的评估。理解技术原理是掌握Java并发编程、JVM性能调优、MySQL索引优化等核心概念的基础。这些原理知识能够帮助开发者在高并发、分布式系统等实际应用场景中,设计出高性能、高可用的解决方案,从而体现其技术价值。例如,深入理解JVM内存模型和垃圾回收机制,对于诊断线上OOM问题和进行系统调优至关重要;掌握MySQL的B+树索引原理和事务隔离级别,是设计千万级数据表架构和保证数据一致性的前提。当前,面试官更关注候选人能否将

weixin_34210740的博客 350

电磁仿真软件CST2020 下载链接及安装破·解教程笔记

众所周知,配环境是安装软件最头疼的事情。CST2020我调了一上午,网上现有的安装教程良莠不齐,很难有一篇文章能够全流程的讲述安装过程以及遇到的问题。正好今天我要给另一台电脑装软件,借此机会把安装过程记录下来,以供将来使用和他人借鉴。 首先提供下载地址: 百度网盘 链接:https://pan.baidu.com/s/1cDJZelufj-nliUfekMNpEA 提取码:8ihx 附: 根据二○○二年一月一日《计算机软件保护条例》规定:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软

weixin_42845306的博客 3万+

LinuxMint 22.1(Ubuntu24.04)下通过Wine完美运行同花顺远航版的实战指南

本文详细介绍了在LinuxMint 22.1(Ubuntu24.04)系统下通过Wine完美运行同花顺远航版的实战指南。从基础环境准备到深度优化,包括Wine环境配置、同花顺远航版安装、字体显示调优、任务栏兼容性修复等关键步骤,帮助Linux用户高效使用同花顺远航版进行股票交易和行情分析。

weixin_30919429的博客 571

Java工程师的思维坐标系:从八股文到工程能力构建

Java是一门融合语言规范、虚拟机机制与框架生态的系统性技术。理解其底层原理,需穿透语法糖直达字节码,把握JVM内存模型与线程安全本质,并贯通Spring容器生命周期与自动配置决策逻辑。技术价值在于将离散知识点组织为可迁移的工程反射弧——面对OOM、类加载冲突或事务失效等真实问题,能快速定位到JVM参数、JMM语义或Condition评估链等原始依据。典型应用场景包括高并发缓存设计、分布式事务兜底、线上故障根因分析等。本文聚焦Java工程师核心认知断层,以JVM底层契约、并发安全机制、Spring心智模型和

weixin_30824479的博客 293

机器学习预测股票收益(一)之随机森林模型

机器学习预测股票收益(一)前言一、导入库和数据二、处理数据以及计算特征变量三、使用随机森林回归预测股票收益1.构建训练集和测试集2.查看预测结果四、根据预测结果构建long-short投资组合1.定义投资组合函数2.定义绩效分析函数五、引入Fama-French Factor六、投资组合表现1.超额收益表现2.多因子回归总结 前言 本文将使用Python整理1927-2020年所有美国上市公司股票数据。根据历史收益以及交易量,使用随机森林,支持向量机以及神经网络等机器学习方法预测股票收益。最优结果构建的资产

weixin_47174968的博客 1万+

零基础学会flink,保姆级超详细教学

本文提供了一个三周学会Apache Flink的详细学习路线,从环境搭建到项目实战,适合零基础开发者快速掌握Flink流处理技术。 学习路线概览: 环境搭建(JDK11、Maven、IntelliJ IDEA和Flink集群) 基础入门(WordCount程序) 核心API学习(DataStream算子、时间窗口、状态管理) 高级功能(Table API/SQL、Kafka连接器) 综合项目实战(实时订单统计/用户行为分析) 第一天重点: 详细演示了开发环境配置(JDK11安装验证、Maven配置、IDEA

korry24的博客 288

Pikachu靶场环境搭建(Windows环境下安装详细教程)

Pikachu靶场是一个漏洞测试练习平台。其中包含了常见的web安全漏洞,如果你是一个渗透测试学习者没有靶场的练手,那么pikachu是一个很好的测试平台。

weixin_42857656的博客 1823

Windows下超详细安装Anaconda3以及jupyter notebook

1.下载Anaconda安装包 Anaconda官网网站:https://www.anaconda.com/products/individual下载安装包速度比较慢,国内小伙伴不建议尝试。 我们可以使用清华大学开源软件镜像站:清华大学开源软件镜像站:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/,请选择适合自己系统自行下载。 下载完成后如下图: 1.安装Anaconda安装包 1.打开安装包,点击Next 2.点击I Agre

m0_37957160的博客 3801

SSM框架电商系统毕业设计实战:从零构建农产品销售平台

在Java企业级开发领域,SSM(Spring+Spring MVC+MyBatis)框架组合因其清晰的层次结构和灵活的控制能力,成为构建Web应用的主流技术选型。其核心原理在于Spring的IoC容器管理对象生命周期,Spring MVC处理请求分发,MyBatis负责数据持久化,三者协同实现了高内聚、低耦合的架构设计。这种架构的价值在于提升了代码的可维护性和扩展性,尤其适用于电商、管理等需要处理复杂业务逻辑和数据交互的应用场景。本文聚焦于如何将这一技术体系应用于毕业设计实践,以农产品销售系统为例,深入剖

weixin_34015336的博客 455

PyCharm 怎么不升级直接使用Jupyter Notebook

在PyCharm 中需要升级到 PyCharm Professional 版本,才能直接使用 Jupyter Notebook。从截图来看,Jupyter Notebook 已经成功启动,并且正在运行。这将在默认网络浏览器中打开一个新的标签页,显示文件浏览器界面。可以在这里创建和编辑。文件添加到 PyCharm 项目中,并使用纯文本编辑功能进行修改。支持与外部工具集成,可以配置它以调用命令行工具或脚本来启动。,参数可以根据需要设置,工作目录设置为项目根目录。,你仍然可以在本地环境中安装和运行。

Davina_yu的博客 995

工作中使用到的单词(软件开发)

1.エイリアス   alias 别名 powershell使用 get-help 命令时,会得到与此命令 相关(别名)的命令 2.プロキシ proxy 代 理 3.プロモート   promote 促进,提升   开发的系统新功能上线发布,有时会使用到这个词,比如 STプロモート 还有使用リリース这个词的。比如STリリース。个人认为两者含义相同。 4.見逃し み................................................................

sun0322 6万+

手把手教你ensp仿真搭建校园网(含防火墙及无线网络)系列(二)校园网接入层交换机配置

本系列博客将带你从零开始,使用 华为 eNSP(Enterprise Network Simulation Platform) 仿真搭建一个完整的校园网络,涵盖 核心层、汇聚层、接入层、防火墙安全策略、无线AC+AP组网 等实战场景。无论你是网络初学者还是希望深化实战经验的工程师,都能通过本教程掌握企业级网络的规划、配置与排错技巧。

echo的博客 1219

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Android 随笔集 By tuliyuan

1.反射机制调用系统服务EthernetManager private void getNetInfo(Context context) { try { // Log.d(TAG, "dddd getNetInfo is " ); Method getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", new Cla

qq_18906227的博客 2280

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

【Java】HashMap的简单教程

HashMap是 Java 中最常用的数据结构之一,属于java.util包,主要用于以键值对(key-value)形式存储数据。

lklalmq的博客 1476

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Java的HashMap

如果所指定的 key 已经在 HashMap 中存在,返回和这个 key 值对应的 value, 如果所指定的 key 不在 HashMap 中存在,则返回 null。如果指定 key,返回指定键 key 关联的值,如果指定的 key 映射值为 null 或者该 key 并不存在于该 HashMap 中,此方法将返回null。如果插入的 key 对应的 value 已经存在,则执行 value 替换操作,返回旧的 value 值,如果不存在则执行插入,返回 null。

BASK2312的博客 3860

008_java.util.HashMap

java.util.HashMap概述 继承体系 HashMap继承自AbstractMap,并实现Map接口,提供了部分的map行为的默认实现,大部分方法依赖于map下的迭代器完成相应的操作。在1.8中,Map接口增加了很多流式操作相关的默认实现。 Map接口 Map接口内限定了Map容器语义: public interface Map<K,V> { /** map键值对数量 */ int size(); /** 判定是否为空 */ boolean isEm

吃饭睡觉打豆豆 909

[源码解析]HashMap

本篇文章是我解析Java-API的笔记,更多的是从方法着手,少对类的整体情况进行说明,故并不详尽,可供大家参考。 如果文中阐述不全或不对的,多多交流。

进步*于辰的博客 1万+

java.util包——HashMap类

java.util  类 HashMap&lt;K,V&gt; java.lang.Object java.util.AbstractMap&lt;K,V&gt; java.util.HashMap&lt;K,V&gt; 类型参数: K - 此映射所维护的键的类型 V - 所映射值的类型 所有已实现的接口: Serializable, Cloneable, Map&...

null 1万+
上一篇: Java枚举类型的用法,大厂 HR 如何面试
下一篇: Java并发之线程池ThreadPoolExecutor源码分析学习,java面试题,java高级笔试题
ccc_ccc8
博客等级 码龄8年 8粉丝 50原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值