前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >手撕HashMap

手撕HashMap

作者头像
东边的大西瓜
发布于 2022-05-05 03:39:06
发布于 2022-05-05 03:39:06
22400
代码可运行
举报
文章被收录于专栏:吃着西瓜学Java吃着西瓜学Java
运行总次数:0
代码可运行

❝在JDK7中HashMap的底层数据结构为:数组+链表 在JDK8中HashMap的底层数据更改为:数组+链表+红黑树 本文以JDK8源码为例! ❞

在我扒拉那么多大厂面试题目后,发现HashMap的出现频率是非常高的,当然也会拿出一些类似的进行对比解析,比如HashTable、ConCurrentHashMap、LinkedHashMap,不着急我们后面慢慢聊它们。在讲HashMap前我们先简单回忆一下Hash的相关内容。

哈希算法

  • 直接定值法:取关键字或者关键字的某个线性函数为哈希地址,即h(key) = key或h(key) = a*key+b;
  • 平方取值法:先求出关键字的平方值,然后取出中间几位作为哈希地址。
  • 折叠法:将关键字分隔成位数相同的几部分,然后取这几部分的叠加和作为哈希地址。
  • 随机数法:选择一个随机函数,取关键字的随机函数值作为Hash地址,通常用于关键字长度不同的场合。
  • 除留余数法:取关键字某个不大于Hash表长m的数p除后所得的余数为哈希地址,即h(key)=key%p。p值的的选择很重要,如果p选不好容易产生同义词。由经验可得:p的长度最好为一个不大于表长m的质数、或者不包含小于20的质因数的合数。
  • Java中的HashCode:在java中Object类提供了一个HashCode方法,而每个类对于其重写的方案也不相同,这里我们介绍Integer和String类中的hashCode方法:

「Integer类,返回其本身value:」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public static int hashCode(int value) {
        return value;
    } 

「String类: h(key) = s[0] * 31^(n-1) + s[1] * 31^(n-2) + ... + s[n-1]」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public int hashCode() {
      int h = hash;
      if (h == 0 && value.length > 0) {
          char val[] = value;

          for (int i = 0; i < value.length; i++) {
              h = 31 * h + val[i];
          }
          hash = h;
      }
      return h;
    }

哈希表

❝给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。在哈希表中进行添加,删除,查找等操作,性能非常高。我们知道哈希表的主干是数组,所以在不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。 ❞

哈希冲突

我们上面介绍了几种计算哈希地址的哈希算法,但是不可避免的是会出现两个不同的元素通过一个哈希函数,会得到相同的哈希地址。具体情况就是,我们在对某个元素进行hash运算,得到一个哈希地址,在进行插入操作的时候,发现该哈希地址已存在别的元素,这就是哈希冲突,也叫做哈希碰撞。

「哈希表和HashMap」哈希表是一种逻辑数据结构,HashMap是Java中的一种数据类型(结构类型集合),它通过代码实现了哈希表这种数据结构,并在此结构上定义了一系列操作。

HashMap的一些属性

「我们先来了解一下HashMap中源码的一些基本属性:」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  // 默认容量,默认为16,必须是2的幂

  static final int DEFAULT_INITIAL_CAPACITY = 1<< 4;

  // 最大容量,值是2^30

  static final int MAXIMUM_CAPACITY = 1<< 30

  // 负载因子,默认的负载因子是0.75

  static final float DEFAULT_LOAD_FACTOR = 0.75f;

  // 解决冲突的数据结构由链表转换成树的阈值,默认为8

  static final int TREEIFY_THRESHOLD = 8;

  // 解决冲突的数据结构由树转换成链表的阈值,默认为6

  static final int UNTREEIFY_THRESHOLD = 6;

  /* 当桶中的bin被树化时最小的hash表容量。

   *  如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作。

   *  这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。

   */
  static final int MIN_TREEIFY_CAPACITY = 64;

  //table数组,大小在resize()中初始化
  transient Node<K,V>[] table;

   //元素个数
  transient int size;

  //容量阈值(元素个数大于等于该值时会自动扩容)
  int threshold;

「table数组里面存放的是Node对象,Node是HashMap的一个内部类,也可以说是数据节点。」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  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;
  }
}

「四个构造函数」

1:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  public HashMap() {
      this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  }
  构造一个空的HashMap,具有默认的初始容量(16)和默认负载系数(0.75)。

2:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  public HashMap(int initialCapacity) {
      this(initialCapacity, DEFAULT_LOAD_FACTOR);
  }
  构造一个空的HashMap,具有指定的初始容量和默认负载系数(0.75)
  参数:initialCapacity -指定初始容量
  异常:IllegalArgumentException -如果初始容量为**负数**

3:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

  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);
  }
  构造一个空的HashMap,具有指定的初始容量和负载因子。
  参数:initialCapacity - 初始容量
        loadFactor - 负载因子
  异常:IllegalArgumentException - 如果初始容量为负值或负载系数为**非负值**

4:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

  public HashMap(Map<? extends K, ? extends V> m) {
      this.loadFactor = DEFAULT_LOAD_FACTOR;
      putMapEntries(m, false);
  }
  构造一个新的HashMap与指定的相同的映射Map。该HashMap与默认负载因数(0.75)和初始容量足以容纳映射在指定Map创建。
  参数:m - 其映射将放置在此映射中的映射。
  异常:NullPointerException - 如果指定的地图为空

HashMap的真面目##

❝JDK8的是数组+链表+红黑树 一个数组,然后结点类型是链表,在链表的元素大于8的时候,会变成红黑树(当数组长度大于64并且链表长度大于8时,才会转换为红黑树。 如果链表长度大于8,但是数组长度小于64时,还是会进行扩容操作,不会转换为红黑树。因为数组的长度较小,应该尽量避开红黑树。因为红黑树需要进行左旋,右旋,变色操作来保持平衡,所以当数组长度小于64,使用数组加链表比使用红黑树查询速度要更快、效率要更高) 在红黑树的元素小于6的时候会变成链表(这里需要注意,不是元素小于6的时候一定会变成链表,只有resize的时候才会根据UNTREEIFY_THRESHOLD 进行转换,同样也不是到8的时候就变成红黑树(不是等到扩容的时候) 链表与红黑树的转换详情)元素进行尾插HaspMap的数组默认大小为16数组也叫做Hash桶。 ❞

三个基本参数:

  • capacity:当前数组的容量,默认为16,可用扩容,每次扩容为原来的二倍,而且该值始终为2的幂次方。
  • loadFactor:负载因子,默认为0.75。
  • threshold:扩容的阈值,其值等于capacity*loadFactor。

「负载因子决定着哈希表的扩容和冲突,负载因子调高会导致哈希冲突的概率增高和查询速度变慢。」

Table数组的初始化:

我们前面提到过,哈希表的主体是数组,那么HashMap的主体就是一个Node类型的table数组,而且table数组的长度永远是2的幂次方,它的具体算法就是由下面代码实现的,这是一个非常巧妙的算法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  /*得到一个大于或者等于cap的最小的2的幂,用来作为阈值*/
  static final int tableSizeFor(int cap) {
      int n = cap - 1;
      n |= n >>> 1;
      n |= n >>> 2;
      n |= n >>> 4;
      n |= n >>> 8;
      n |= n >>> 16;
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  }

❝"|"按位或:按照所给数二进制位进行计算,只要有一个为1则对应位位1! “>>“带符号右移:低位移除(舍弃),高位的空位补符号! “>>>”无符号右移:低位移除(舍弃),高位的空位补零! ❞

「上述算法让最高位的1后面的位全变为1,再进行n+1操作,那么最终将会返回一个大于或者等于cap的最小的2的幂的结果。」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  int cap = 64;
  System.out.println(Integer.toBinaryString(cap)); ===1000000
  int n = cap - 1;
  System.out.println(Integer.toBinaryString(n)); ===111111
  n |= n >>> 1;
  System.out.println(Integer.toBinaryString(n)); ===111111
  n |= n >>> 2;
  System.out.println(Integer.toBinaryString(n)); ===111111
  n |= n >>> 4;
  System.out.println(Integer.toBinaryString(n)); ===111111
  n |= n >>> 8;
  System.out.println(Integer.toBinaryString(n)); ===111111
  n |= n >>> 16;
  System.out.println(Integer.toBinaryString(n)); ===111111
  System.out.println(n+1); ===64

「这里的cap-1操作也是为了使结果达到这一条件,如果不是cap-1,而是直接使用cap进行操作最终输出的将是1111111,也就是128。很明显这并不是最终结果!」

从前面HashMap的构造函数来看,当我们调用HashMap有关的设置初始容量的构造方法的时候,它会调用tableSizeFor(int cap)方法对初始容量数据进行规格化。其实在刚初始化一个HashMap的时候,它的table是为null的,通过它的构造函数就可以看出,它是没有对table进行相应的初始化赋值的,在进行put操作的时候通过调用resize()方法进行初始化,且其大小始终为2的幂次方!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 if ((tab = table) == null || (n = tab.length) == 0)
 n = (tab = resize()).length;

Put操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  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; //局部tab指向全局的hash数组
    Node<K,V> p; //key所在位置的元素
    int n, i;//n为数组长度,i为索引

    // 如果tab为空,或长度为0,则通过resize()函数初始化table数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //若该位置不存在元素,则直接新建结点插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //否则,发生哈希碰撞
    else {
        Node<K,V> e; //e为临时结点,指向碰撞结点 
        K k; //值
        //如果hash值,和key值相等,则第一个结点就是碰撞结点,则用e指向p
        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) {

                //如果在链表中不包含要插入的结点,即找不到同key元素,则选择新建结点,通过尾插法插入
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                   //如果链表长度已经大于或者等于树化阈值,则将链表转为红黑树或进行扩容操作,只有当数组结点元素数量大于64且链表结点数量大于8才会进行化树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                //如果在链表中包含要插入的结点,即找到了同key元素,则终止遍历操作,此时e指向该结点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果e不为null,则说明在该链表中要插入的位置已存在结点元素,则更新旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
           //为LinkedHashMap留的后路
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果结点元素数量大于阈值,则进行扩容 
    if (++size > threshold)
        resize();
    //为LinkedHashMap留的后路
    afterNodeInsertion(evict);
    return null;
  }

「尽管有注释源码看着还是不太爽?我们来图解源码:」

Put操作就是存放元素的操作,它有着初始化Table数组的作用,而且由于它的位置计算算法,它是不保证插入数据的顺序,既然是存放元素的操作那么位置计算算法就是其关键,那么我们接下来就来探究一下HashMap是如何计算的到自己的哈希地址的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 tab[index = (n - 1) & hash] //这里的n是tab的长度

「1.为什么要通过(数组长度-1)位与hash值得到具体的哈希地址?」

首先明确jdk中定义int占4个字节32位,其大概有40的映射空间,我们内存难以支撑如此庞大的数据量,而且我们也希望在插入元素的时候位置尽可能分布的均匀些,使得每个位置上都只存在一个或者较少的数据,这样我们在取值时就可以更快的得到这个元素。所以我们在得到相应的hash值后应该对其经行相应处理,我们首先想到的肯定是进行(hash % n)的取模操作,而源码采用的是(hash & (n-1))值得到具体的哈希地址。那么这两种操作有什么区别呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  System.out.println(126&15); ===12 
  System.out.println(126%16); ===12

其实当数组长度始终为2的幂次方时,对于哈希地址的最终的取值而言这两种操作是没有区别的,只是由于位运算直接对内存数据进行操作,所以它是相对较快的。

「2.在进行Put操作时是如何得到Hash值的,为什么要有(h = key.hashCode()) ^ (h >>> 16)这个操作???」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

❝由源码可以看出,HashMap中是通过HashCode方法算出的Hash值,然后与其高十六位做异或运算,得到最终的哈希值,这样可以增加随机性。具体的原理是: 我们都知道jdk中规定int占4个字节32位,那么右移16位后得到的正好是高十六位,然后再与自己进行异或就是自己的高十六位与低十六位做异或,这样做不仅可以加大低十六的随机性,而且可以使低位掺杂高位的部分特征,让高位有些参与感。 ❞

❝这一步的存在,是有很大的实际意义的,我们上面提到过,「hash地址其实是(数组长度-1)&hash值得到的」,而且在绝大多数情况下数组长度都是小于2^16的,那么这时hash地址始终都是(数组长度-1)与hash值的低十六位进行位运算得到的,这就会导致哈希冲突的概率增大,所以这时源Hash值的低十六位与高十六位进行异或后得到的新低十六位可以增加最终的随机性,减少哈希冲突的产生。 ❞

「3.把数组长度设计成2的幂次方的原因:」

  • 当将数组长度设置为2的幂次方时,使用位与和取模的结果相同,也就是当数组长度为2的幂次方时,可以使用位运算替代取模运算,即h&(length-1)==h%length,提高效率。
  • 当数组长度为2得幂次方时,其长度length-1后得到得数为奇数,也就是说其二进制位数上全为1,这样就可以使得每个位置都可以参与最终的位与运算,相反,当数组长度不为2的幂次方时,在进行length-1后,其最后一位必定为0,进行位与运算得到的最终结果的最后一位也必将为0,不仅会导致最终地址的映射范围减小,从而导致哈希冲突的增加,而且也会导致空间浪费和位运算操作和取模操作结果不同,即h&(length-1)!=h%length。

continue

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 吃着西瓜学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
史上最强HashMap源码深度解析(3w字图文并茂)
HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
小熊学Java
2023/10/04
1.8K0
史上最强HashMap源码深度解析(3w字图文并茂)
Java面试题:ArrayList底层实现原理、HashMap的实现原理、HashMap的jdk1.7和jdk1.8有什么区别
举个例子:在一开始,如果默认的长度为10的数组已经装满了,在装满的情况下,我一次性要添加100个数据 addAll,很显然 10扩容1.5倍 变成15,还是不够,怎么办? ——> 此时新数组的长度,就以实际情况为准,就是110(100+10)
寻求出路的程序媛
2024/06/04
2140
Java面试题:ArrayList底层实现原理、HashMap的实现原理、HashMap的jdk1.7和jdk1.8有什么区别
深入理解 HashMap
​ HashMap 是基于哈希表的 Map 接口是实现的。此实现提供所有可选操作,并允许使用 null 做为值(key)和键(value)。HashMap 不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当的分布在各个桶之间,可作为基本操作(get 和 put)提供稳定的性能。在jdk1.7中的HashMap是基于数组+链表实现的,在jdk1.8中的HashMap是由数组+链表+红黑树实现的(不懂,一开始就讲那么难的谁受得了?没关系,继续往下看)
全栈程序员站长
2022/09/30
3180
深入理解 HashMap
面试再问HashMap,求你把这篇文章发给他!
总所周知 HashMap 是面试中经常问到的一个知识点,也是判断一个候选人基础是否扎实的标准之一,因为通过 HashMap 可以引出很多知识点,比如数据结构(数组、链表、红黑树)、equals 和 hashcode 方法。
Java技术栈
2020/04/28
4370
内含扩容源码的面试题,目标是手写HashMap!
    推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap。
上分如喝水
2021/08/16
3740
内含扩容源码的面试题,目标是手写HashMap!
了解HashMap
HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
橘子君丶
2023/03/08
4080
了解HashMap
详解HashMap源码解析(上)
HashMap的底层主要基于数组+链表/红黑树实现,数组优点就是查询块,HashMap通过计算hash码获取到数组的下标来查询数据。同样也可以通过hash码得到数组下标,存放数据。
用户10384376
2023/02/26
2720
详解HashMap源码解析(上)
JDK8中的HashMap实现原理及源码分析
在写上一篇线性表的文章的时候,笔者看的是Android源码中support24中的Java代码,当时发现这个ArrayList和LinkedList的源码和Java官方的没有什么区别,然而在阅读HashMap源码的时候,却发现Android中的Java与官方版的出入略大,遂不得不转而用Eclipse导入jdk源码阅读,这里不得不吐槽一句,用惯了IDEA的快捷键,Eclispe还真是用不习惯~~好了,接下来我们言归正传:
技术zhai
2019/02/15
6150
经常被面试官问到的HashMap,详细解读看这一篇就够了
https://juejin.im/post/5d09f2d56fb9a07ec7551fb0
南风
2019/07/08
1.1K0
经常被面试官问到的HashMap,详细解读看这一篇就够了
HashMap源码分析 - jdk8
HashMap可以说是我们在实际开发中最经常使用到的集合类,并且在面试中也是必问的知识点。这篇文章主要是根据JDK8的HashMap来进行分析。
虞大大
2020/08/26
4520
HashMap源码分析 - jdk8
HashMap源码分析
如果减1,那么二进制中的1变成0,后面的0全部变成1,符合上面的length,配合实现取模运算
晚上没宵夜
2020/12/18
3250
面试官再问你 HashMap 底层原理,就把这篇文章甩给他看
HashMap 源码和底层原理在现在面试中是必问的。因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合。文章较长,介绍了很多原理性的问题,希望对你有所帮助~
烟雨星空
2020/06/16
4920
面试官再问你 HashMap 底层原理,就把这篇文章甩给他看
HashMap详解
**Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。**这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
程序员阿杜
2023/08/25
2710
面试 | Java8 HashMap原理
基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保证顺序不随时间变化。
Spark学习技巧
2018/12/24
6060
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
十一、为什么我们需要hash()函数 (n-1)\&hash,而不是直接用key的hashcode直接计算下标
寻求出路的程序媛
2024/10/17
6000
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
面试官:来,问你几个关于HashMap的问题?
最简单形式的 hash,是一种在对任何变量/对象的属性应用任何公式/算法后, 为其分配唯一代码的方法。
用户5546570
2019/06/06
9500
面试官:来,问你几个关于HashMap的问题?
HashMap 与 ConcurrentHashMap 底层实现
我们存放的 hashMap 都会封装成一个节点对象 Entry(key,value),然后将此节点对象存放到一个数组中,存放前首先需要确定存放的数组下标:① 通过 hash(key) 算法得到 key 的 hashcode,并通过 hashcode的高16位和低16位进行异或操作(如果两个相应bit位相同,则结果为0,否则为1)得到32位的 int值,首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算(相同位的两个数字都为1,则为1;若有一个不为1,则为0)那么高十六位所代表的部分特征就可能被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值,这样高位与低位的信息都被保留了 。② int值再与(数组长度-1:底位全为1,高位全为0)进行位运算,获取要存放的下标;③ 如果②中得到相同的值时,判断 key值是否相同,如果相同则新value替换旧value。如果key不相同,将value以链表的形式存放在同一个数组下标下,为了提高存放的速度,新的数据,将存放在原链表的头部。即新数据的 next 指向链表的头元素即可。需要注意的是,每次给链表的头部插入一个新的元素之后,需要将链表的头元素赋值给 table 的下标值。代码展示为 :
Java架构师必看
2021/05/14
4010
HashMap 与 ConcurrentHashMap 底层实现
深入浅出理解HashMap1.8源码设计思想&手写HashMapV1.0
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
须臾之余
2019/12/03
7230
深入浅出理解HashMap1.8源码设计思想&手写HashMapV1.0
一文搞定HashMap的实现原理和面试
HashMap在日常开发中基本是天天见的,而且都知道什么时候需要用HashMap,根据Key存取Value,但是存和取的时候那些操作却是很少去研究。同时在面试中也是面试官们必问的。以下是基于JDK1.8
秃头哥编程
2019/06/19
7430
《Java-SE-第十八章》之HashMap(jdk8)
Map是一种以键值对(key-value)进行存储的集合,Map集中的每一个元素都包含一个 键(key) 对象 和 一个值(value)对象。其其特点都是由键来决定的,Map集合的键都是无序,不重复,无索引,Map集合后面重复的键对应的值会覆盖前的重复键的值,并且键和值都允许为空。
用户10517932
2023/10/07
1930
《Java-SE-第十八章》之HashMap(jdk8)
相关推荐
史上最强HashMap源码深度解析(3w字图文并茂)
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文