Java集合源码剖析——基于JDK1.8中HashMap的实现原理(下)

简介: Java集合源码剖析——基于JDK1.8中HashMap的实现原理(下)

文章目录:


3.5 hash方法

3.6 resize方法

3.7 size方法

3.8 isEmpty方法

3.9 clear方法

3.10 containsKey方法

3.11 containsValue方法

3.12 replace方法

3.13 关于遍历map集合的三个方法

4.传统HashMap的缺点——引入红黑树


3.5 hash方法


get 方法和put方法中都需要先计算key映射到哪个桶上,然后才进行之后的操作,计算的主要代码如下:

(n - 1) & hash

上面代码中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash是通过下面这个方法计算出来的,采用了二次哈希的方式,其中 key hashCode 方法是一个native 方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


这个 hash 方法先通过 key hashCode 方法获取一个哈希值,再拿这个哈希值与它的高 16 位的哈希值做一个异或操作来得到最后的哈希值,计算过程可以参考下图。


为啥要这样做呢?注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1 即为630x111111),这样的值跟 hashCode()直接做与操作,实际上只使用了哈希值的后 6 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低位都利用起来,从而解决了这个问题。


正是因为与的这个操作,决定了 HashMap 的大小只能是 2 的幂次方,想一想,如果不是2的幂次方,会发生什么事情?即使你在创建HashMap的时候指定了初始大小,HashMap 在构建的时候也会调用下面这个方法来调整大小:

这个方法的作用看起来可能不是很直观,它的实际作用就是把 cap 变成第一个大于等于 2 的幂次方的数。

例如,16 还是 1613 就会调整为 1617 就会调整为 32

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

3.6 resize方法

HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize


那么HashMap 什么时候进行扩容呢

HashMap中的元素个数超过数组大小(数组总大小length不是数组中个数size*loadFactor loadFactor


(DEFAULT_LOAD_FACTOR)
0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。


在这里有一个需要注意的地方,有些文章指出当哈希表的桶占用超过阈值时就进行扩容,这是不对的;

实际上是当哈希表中的键值对个数超过阈值时,才进行扩容的。


3.7 size方法

返回map集合中键值对的个数。

 public int size() {
      return size;
  }

3.8 isEmpty方法

判断map集合是否为空。

  public boolean isEmpty() {
     return size == 0;
 }

3.9 clear方法

清空map集合,首先判断哈希表是否为空,为空的情况下,将size置为0,遍历哈希表依次清空每隔键值对。

public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

3.10 containsKey方法

判断map集合中是否包含指定的key。内部实现是调用了getNode方法,这个在上面将get方法的时候已经说过了。

  public boolean containsKey(Object key) {
      return getNode(hash(key), key) != null;
  }

3.11 containsValue方法

判断map集合中是否包含指定的value

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

3.12 replace方法

map集合中指定key对应的旧值替换为一个新值。

public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

3.13 关于遍历map集合的三个方法

这三个方法分别是 keySetentrySetforEach,源码我这里就不再多说了,下面给出的是测试代码。

@Test
public void test1() {
    Map<String,Object> map = new HashMap<>();
    map.put("1001","张起灵");
    map.put("1002","小哥");
    map.put("1003","小宋");
    System.out.println("keySet方法遍历map集合: ");
    Set<String> stringSet = map.keySet();
    Iterator<String> iterator = stringSet.iterator();
    while (iterator.hasNext()) {
        String key = iterator.next();
        Object value = map.get(key);
        System.out.println("键: " + key + ", 值: " + value);
    }
    System.out.println("======================================");
    System.out.println("entrySet方法遍历map集合: ");
    Set<Map.Entry<String,Object>> entrySet = map.entrySet();
    Iterator<Map.Entry<String,Object>> entryIterator = entrySet.iterator();
    while (entryIterator.hasNext()) {
        Map.Entry<String, Object> entry = entryIterator.next();
        String key = entry.getKey();
        Object value = entry.getValue();
        System.out.println("键: " + key + ", 值: " + value);
    }
    System.out.println("======================================");
    System.out.println("forEach方法遍历map集合: ");
    map.forEach((key,value) -> System.out.println("键: " + key + ", 值: " + value));
}

4.传统HashMap的缺点——引入红黑树


(1)JDK 1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。

(2) HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。

(3)针对这种情况,JDK 1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。

HashMap 是数组+ 链表+红黑树(JDK1.8 增加了红黑树部分)实现的。

image.png

关于红黑树的三个关键参数。

相关文章
|
14天前
|
存储 算法 安全
HashMap的实现原理,看这篇就够了
关注【mikechen的互联网架构】,10年+BAT架构经验分享。深入解析HashMap,涵盖数据结构、核心成员、哈希函数、冲突处理及性能优化等9大要点。欢迎交流探讨。
HashMap的实现原理,看这篇就够了
|
5天前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
22 3
|
10天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
13天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
14天前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
14 0
[Java]静态代理与动态代理(基于JDK1.8)
|
26天前
|
Java
让星星⭐月亮告诉你,jdk1.8 Java函数式编程示例:Lambda函数/方法引用/4种内建函数式接口(功能性-/消费型/供给型/断言型)
本示例展示了Java中函数式接口的使用,包括自定义和内置的函数式接口。通过方法引用,实现对字符串操作如转换大写、数值转换等,并演示了Function、Consumer、Supplier及Predicate四种主要内置函数式接口的应用。
20 1
|
26天前
|
存储 算法 索引
HashMap底层数据结构及其增put删remove查get方法的代码实现原理
HashMap 是基于数组 + 链表 + 红黑树实现的高效键值对存储结构。默认初始容量为16,负载因子为0.75。当存储元素超过容量 * 负载因子时,会进行扩容。HashMap 使用哈希算法计算键的索引位置,通过链表或红黑树解决哈希冲突,确保高效存取。插入、获取和删除操作的时间复杂度接近 O(1)。
26 0
|
6月前
|
存储 Java Windows
Java21 JDK下载安装及Windows环境变量配置
JDK是Java的开发工具包,要进行Java学习或开发之前,需先下载安装,下载地址如下:提示:这网址里面有三个扩展名的文件,分别是“.zip”、“.exe”和“.msi”,鄙人选择的是.exe的文件,下方的安装和环境的配置也是安装该文件的安装程序进行的。
793 2
Java JDK的安装
首先我们先去下载jdk。
|
3月前
|
Java 关系型数据库 MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【8月更文挑战第19天】在Linux上搭建Java Web应用环境,需安装JDK 1.8、Tomcat及MariaDB。本指南详述了使用apt-get安装OpenJDK 1.8的方法,并验证其版本。接着下载与解压Tomcat至`/usr/local/`目录,并启动服务。最后,通过apt-get安装MariaDB,设置基本安全配置。完成这些步骤后,即可验证各组件的状态,为部署Java Web应用打下基础。
56 1