学习JavaEE的日子 Day27 手撕HashMap底层原理

news2024/10/7 20:25:23

Day27

1.手撕HashMap底层原理(重点)

public class Test01 {
	public static void main(String[] args) {
		
//		Float float1 = new Float("0.0f");
//		Float float2 = new Float("0.0f");
//		Float result = float1/float2;
//		System.out.println(result);//NaN 不是一个数字
//		System.out.println(Float.isNaN(result));
//		HashMap<Student, String> map = new HashMap<>(16,result);
		
		HashMap<Student, String> map = new HashMap<>();
		map.put(new Student("任浩", '男', 23, "2401", "001"), "拍电影");
		map.put(new Student("马智威", '男', 20, "2401", "002"), "打篮球");
		map.put(new Student("李林俊", '男', 21, "2401", "003"), "玩游戏");
		map.put(new Student("李林俊", '男', 21, "2401", "003"), "写代码");
		map.put(null, "aaa");
		map.put(null, "bbb");
		
		Set<Entry<Student,String>> entrySet = map.entrySet();
		for (Entry<Student, String> entry : entrySet) {
			System.out.println(entry);
		}
	}
}

学生类

public class Student {

	private String name;
	private char sex;
	private int age;
	private String classId;
	private String id;

    //无参构造,有参构造,get,set省略
	
    //设置每个对象的hashCode值都是20
	@Override
	public int hashCode() {
		return 20;
	}

    //重写equals方法
	@Override
	public boolean equals(Object obj) {
		if(this == obj){ //判断是不是同一个对象
			return true;
		}
		if(obj instanceof Student){  //判断传进来的是不是Student对象
			Student stu = (Student) obj;//向下转型
            //怎么比较
			if(this.classId.equals(stu.classId) && this.id.equals(stu.id)){
				return true;
			}
		}
		return false;
	}
	
	@Override
	public String toString() {
		return name + "\t" + sex + "\t" + age + "\t" + classId + "\t" + id;
	}
	
}

2.底层源码

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    //默认初始化容量 -- 必须是2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //空内容的数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
    //hash数组/hash表
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//new Entry[16];
    //元素个数
    transient int size;//4
    //阈值(数组长度*负载因子)
    int threshold;//12
    //负载因子
    final float loadFactor;//0.75f
    //外部操作数(记录添加、删除的次数)
    transient int modCount;//4
    //hash种子数
    transient int hashSeed = 0;//0
    
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    //initialCapacity - 16
    //loadFactor - 0.75f
    public HashMap(int initialCapacity, float loadFactor) {
        //判断数组初始化容量如果小于0,就报错
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        
        //判断数组容量大于最大容量,就把最大容量赋值给初始化容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        
        //判断负载因子如果小于等于0 或者 判断负载因子不是一个数字,就报错
        if (loadFactor <= 0 || Float.isNaN(loadFactor))//NaN - Not a Number
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();//作用:让子类去重写(LinkedHashMap),子类做初始化功能
    }
    
    void init() {
    }
    
    //key - null
    //value - "bbb"
    public V put(K key, V value) {
        
        //第一添加时,进入的判断
        if (table == EMPTY_TABLE) {
            //1.计算出阈值 -- 12
            //2.初始化hash数组 -- new Entry[16]
            //3.初始化hashSeed(Hash种子数)
            inflateTable(threshold);
        }
        
        if (key == null)
            return putForNullKey(value);
        
        //通过key获取hash值 -- 20
        int hash = hash(key);
        //利用key的hash值计算在数组中的下标 -- 4
        int i = indexFor(hash, table.length);
        
        //判断当前下标上是否有元素 -- 进入到该循环就说明hash碰撞了
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			//判断key和Entry中的key是否相同(hash && == || equals)
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                //oldValue - 玩游戏
                V oldValue = e.value;
                //e.value - 写代码
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回被替换的值
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    //value - "bbb"
    private V putForNullKey(V value) {
        //判断下标为0的位置上是否有元素 -- 进入到该循环就说明hash碰撞了
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            
            //判断Entry里的key是否为空,说明下标为0的位置上可能会存储其他key不为空的Entry对象
            if (e.key == null) {
                //oldValue - aaa
                V oldValue = e.value;
                //e.value - bbb
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回被替换的值
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
    
    //子类的挂钩:让子类(LinkedHashMap)重写的方法
    void recordAccess(HashMap<K,V> m) {
    }
    
    //hash - 
    //key - 
    //value - 
    //bucketIndex - 
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //判断元素个数大于等于阈值并且当前下标的元素不为null,就扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容 -- 原来数组长度的2倍
            resize(2 * table.length);
            //通过key重新计算hash值
            hash = (null != key) ? hash(key) : 0;
            //通过hash值重新计算在数组中的下标
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
    
    //newCapacity - 32
    void resize(int newCapacity) {
        //获取table
        Entry[] oldTable = table;
        //oldCapacity - 16
        int oldCapacity = oldTable.length;
        
        //如果数组长度已经达到数组的最大值(1<<30)
        //就将int类型的最大值赋值给阈值,并且结束当前方法
       	//目的:以后大概率不会再次调用resize()
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //newTable = new Entry[32];
        Entry[] newTable = new Entry[newCapacity];
        
        //1.initHashSeedAsNeeded(newCapacity) --重新计算hash种子数
        //2.将table的Entry数据赋值给newTable
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        
        //将newTable的内存地址赋值给table
        table = newTable;
        //重新计算阈值:threshold-24
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    //newTable - new Entry[32];
    void transfer(Entry[] newTable, boolean rehash) {
        //newCapacity - 32
        int newCapacity = newTable.length;
        
        //遍历hash数组
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    
    //hash - 0
    //key - null
    //value - "aaa"
    //bucketIndex - 0
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //e - null
        Entry<K,V> e = table[bucketIndex];
        
        //JDK1.7版本的HashMap是头插法
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    
    //h - 20
    //length - 16
    static int indexFor(int h, int length) {
        
        //20 -- 0001,0100
        //15 -- 0000,1111
        //		0000,0100
        
        //    20 & (16-1)
        return h & (length-1);
    }
    
    //k - new Student("任浩", '男', 23, "2401", "001")
    final int hash(Object k) {
        //获取hash种子数
        int h = hashSeed;
        //判断种子数不等于0 并且 k的类型为String
        if (0 != h && k instanceof String) {
            //利用stringHash32()计算字符串的hash值(目的:减少hash碰撞)
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    //toSize - 16
    private void inflateTable(int toSize) {
        // 2的幂的数字的特点:在二进制表示中只有一位为1,其余全是0
        //toSize-16,返回16
        //toSize-19,返回32
        //toSize-30,返回32
        
        // capacity - 16
        int capacity = roundUpToPowerOf2(toSize);

        //threshold - 12
        //threshold = (int) Math.min(16 * 0.75f, (1<<30) + 1);
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        
        //初始化hash数组 --  new Entry[16];
        table = new Entry[capacity];
        //初始化hash种子数
        initHashSeedAsNeeded(capacity);
    }
    
    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    
    //number - 16
    private static int roundUpToPowerOf2(int number) {
		// 保留二进制中最高位的1,其余变成0
        // Integer.highestOneBit((number) << 1)
        
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    
    //映射关系类/节点类
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; --------- key
        V value; ------------- value
        Entry<K,V> next; ----- 下一个节点的地址
        int hash; ------------ key的hash值

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
     }
}
场景:
    	HashMap<Student, String> map = new HashMap<>();
		
		map.put(new Student("任浩", '男', 23, "2401", "001"), "拍电影");
		map.put(new Student("马智威", '男', 20, "2401", "002"), "打篮球");
		map.put(new Student("李林俊", '男', 21, "2401", "003"), "玩游戏");
		map.put(new Student("李林俊", '男', 21, "2401", "003"), "写代码");
		map.put(null, "aaa");
		map.put(null, "bbb");

在这里插入图片描述

添加元素过程

1.获取key的hash值 – key.hashCode()

2.通过hash值计算出在数组中的下标

3.判断下标上是否有元素

​ 4.1 没有元素 – 创建Entry对象,并存入数组中

​ 4.2 有元素 – 判断下标上的Entry对象中的key是否相同(hashCode&&==||equals)

​ 5.1 不相同 – 创建Entry对象,并存入数组中 JDK1.7头插法/JDK1.8 尾插法

​ 5.2 相同 – 不添加,达到去重的效果,并替换value值

3.相关面试题

JDK1.7版本的HashMap是什么数据结构?

一维数组+单向链表

什么是Hash桶?

hash数组里的单向链表

什么是hash碰撞/hash冲突?

key的hash值一致,在数组中的下标上有重复的元素

HashMap里的hash碰撞是如何优化的?

根据需求重写hashCode(),尽可能保证hash值不相同,减少hash碰撞的次数

HashMap默认数组长度是多少?

长度是1<<4,就是16的长度

HashMap数组的长度为什么必须是2的幂?

2的幂的数字的特点为二进制中只有1位为1,其余为0(16–0001,0000)

2的幂的数字-1的特点为二进制中原来为1的位置变为0,后续的位置全变成1(15–0000,1111)

计算key在数组中的下标的算法:hash值 & 长度-1

如果数组长度不是2的幂会导致散列不均匀

HashMap数组的最大容量是多少?

1<<30

HashMap数组的最大容量为什么是1<<30?

最大容量为int类型,int类型的最大值是2的31次方-1

因为HashMap数组必须是2的幂,1<<30是int取值范围内最大的2的幂的数字

所以HashMap数组最大容量是1<<30

HashMap默认负载因子是多少?

0.75f

HashMap的负载因子的作用是什么?

数组长度*负载因子 等于 阈值,阈值是控制何时扩容

HashMap数组默认的负载因子为什么是0.75f?

取得了空间和时间的平衡

如果负载因子过大(1),会导致数组全部装满后,再扩容。利用了空间,浪费了时间

如果负载因子过小(0.2),会导致数组装了一点点元素,就扩容。利用了时间,浪费了空间

HashMap何时扩容?

元素个数大于等于阈值并且当前下标的元素不为null,就扩容

HashMap扩容机制是什么?

原来长度的2倍

HashMap存放null键的位置在哪?

hash数组下标为0的位置

HashMap的hash回环/死循环是何时发生的?

在多线程的情况下,一个线程不断的添加数据,导致扩容,链表地址发生回环。一个线程不断的遍历数据。

如果发生hash回环应该是程序员负的责任,因为HashMap明确表示该实现不是一个线程安全的,多线程下应该使用ConcurrentHashMap

JDK1.7的HashMap和JDK1.8的HashMap有什么区别:

区别1 - 获取key的hash值:

​ JDK1.7 – 调用key的hashCode() + 位运算

​ JDK1.8 – 将key的hash值(int-32)分为高16位和低16位,两者进行异或的位运算,比之前更简洁

区别2 - 插入链表的法则:

​ JDK1.7 – 头插法

​ JDK1.8 – 尾插法

区别3 - 数据结构:

​ JDK1.7 – 一维数组 + 单向链表

​ JDK1.8 – 一维数组 + 单向链表 + 红黑树(目的:加上红黑树提高查询效率)

JDK1.8版本的HashMap数据结构是如何切换的?

初始数据结构为一维数组 + 单向链表

当一维数组长度大于64并且单向链表长度大于8时 --> 一维数组 + 红黑树

当链表长度小于6时 --> 一维数组 + 红黑树 转换为一维数组 + 单向链表

JDK1.8的HashMap为什么链表长度大于8会将单向链表转换为红黑树?

为了提高查询效率,大于8是因为泊松分布

总结

1.手撕HashMap底层原理
shCode() + 位运算

​ JDK1.8 – 将key的hash值(int-32)分为高16位和低16位,两者进行异或的位运算,比之前更简洁

区别2 - 插入链表的法则:

​ JDK1.7 – 头插法

​ JDK1.8 – 尾插法

区别3 - 数据结构:

​ JDK1.7 – 一维数组 + 单向链表

​ JDK1.8 – 一维数组 + 单向链表 + 红黑树(目的:加上红黑树提高查询效率)

JDK1.8版本的HashMap数据结构是如何切换的?

初始数据结构为一维数组 + 单向链表

当一维数组长度大于64并且单向链表长度大于8时 --> 一维数组 + 红黑树

当链表长度小于6时 --> 一维数组 + 红黑树 转换为一维数组 + 单向链表

JDK1.8的HashMap为什么链表长度大于8会将单向链表转换为红黑树?

为了提高查询效率,大于8是因为泊松分布

总结

1.手撕HashMap底层原理
注重面试题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1524623.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文阅读】DiffSpeaker: Speech-Driven 3D Facial Animation with Diffusion Transformer

DiffSpeaker: 使用扩散Transformer进行语音驱动的3D面部动画 code&#xff1a;GitHub - theEricMa/DiffSpeaker: This is the official repository for DiffSpeaker: Speech-Driven 3D Facial Animation with Diffusion Transformer paper&#xff1a;https://arxiv.org/pdf/…

【SQL】1174. 即时食物配送 II (窗口函数row_number; group by写法;对比;定位错因)

前述 推荐学习&#xff1a; 通俗易懂的学会&#xff1a;SQL窗口函数 题目描述 leetcode题目&#xff1a;1174. 即时食物配送 II 写法一&#xff1a;窗口函数 分组排序&#xff08;以customer_id 分组&#xff0c;按照order_date 排序&#xff09;&#xff0c;窗口函数应用。…

从零自制docker-4-【PID Namespace MOUNT Namespace】

文章目录 PID namespace代码mountnamespace通俗理解代码 PID namespace 每个命名空间都有独立的PID空间&#xff0c;即每个命名空间的进程都由一开始分配。 新建立的进程内部进程ID为1 代码 package main import ("log""os/exec""os""sy…

pyspark基础 -- DataFrame的理解与案例

DataFrame(df)介绍 datafram就是一个内存中的二维表结构&#xff0c;具备表结构的三个基本属性&#xff1a; 行列表结构描述 在结构层面&#xff0c;pyspark中的StructType对象描述了表结构&#xff0c;StructField对象描述了表的一个列信息&#xff1b;在数据层面&#xff…

银行信息系统应用架构导论-引用

一级目录二级目录金融标准和参考文档一、银行企业级应用系统架构规划企业级应用系统架构规划《金融科技发展规划&#xff08;2022-2025年&#xff09;&#xff08;2022年1月中国人民银行印发&#xff09;》 《关于银行业保险业数字化转型的指导意见&#xff08;2022年1月中国银…

【linux】CentOS查看系统信息

一、查看版本号 在CentOS中&#xff0c;可以通过多种方法来查看版本号。以下是几种常用的方法&#xff1a; 使用cat命令查看/etc/centos-release文件&#xff1a; CentOS的版本信息存储在/etc/centos-release文件中。可以使用cat命令来显示该文件的内容&#xff0c;从而获得C…

ThingsBoard初始化数据库Postgres

视频教程&#xff1a; ThingsBoard初始化数据库postgres_哔哩哔哩_bilibilihingsBoard是一个基于Java的开源物联网平台&#xff0c;旨在实现物联网项目的快速开发、管理和扩展。本课程主要从0到1带你熟悉ThingsBoard&#xff0c;学习优秀的物联网变成思维与思想&#xff0c;主…

Nginx离线安装(保姆级教程)

1、下载与安装gcc-c环境 获取rpm包的方式很多&#xff0c;在这里推荐使用yum工具获取&#xff0c;因为手动从官网下载&#xff0c;手动执行rpm -Uvh *.rpm --nodeps --force命令进行安装&#xff0c;可能会缺少某个依赖&#xff0c;我们也不确定到底需要哪些依赖。 因此需要准…

游戏引擎中网络游戏的基础

一、前言 网络游戏所面临的挑战&#xff1a; 一致性&#xff1a;如何在所有的主机内都保持一样的表现可靠性&#xff1a;网络传输有可能出现丢包安全性&#xff1a;反作弊&#xff0c;反信息泄漏。多样性&#xff1a;不同设备之间链接&#xff0c;比如手机&#xff0c;ipad&a…

第九节:Vben Admin实战-系统管理之角色管理实现-上

系列文章目录 第一节:Vben Admin介绍和初次运行 第二节:Vben Admin 登录逻辑梳理和对接后端准备 第三节:Vben Admin登录对接后端login接口 第四节:Vben Admin登录对接后端getUserInfo接口 第五节:Vben Admin权限-前端控制方式 第六节:Vben Admin权限-后端控制方式 第七节…

RUST egui体验

egui官方提供了web版的demo&#xff0c;效果还是很不错的&#xff0c;就是用的时候有点一头雾水&#xff0c;没有找到明确的指导怎么把这些组件插入到自己的application或者web。花了一天时间撸了一遍流程&#xff0c;记录一下&#xff0c;说不定以后能用到呢 >_< efram…

蓝桥杯并查集|路径压缩|合并优化|按秩合并|合根植物(C++)

并查集 并查集是大量的树&#xff08;单个节点也算是树&#xff09;经过合并生成一系列家族森林的过程。 可以合并可以查询的集合的一种算法 可以查询哪个元素属于哪个集合 每个集合也就是每棵树都是由根节点确定&#xff0c;也可以理解为每个家族的族长就是根节点。 元素集合…

【Java基础知识总结 | 第三篇】深入理解分析ArrayList源码

文章目录 3.深入理解分析ArrayList源码3.1ArrayList简介3.2ArrayLisy和Vector的区别&#xff1f;3.3ArrayList核心源码解读3.3.1ArrayList存储机制&#xff08;1&#xff09;构造函数&#xff08;2&#xff09;add()方法&#xff08;3&#xff09;新增元素大体流程 3.3.2ArrayL…

R:简易的Circos图

library(grid) library(circlize) library(RColorBrewer) library(ComplexHeatmap) setwd("C:/Users/fordata/Downloads/Circos") # 创建颜色调色板 coul <- colorRampPalette(brewer.pal(9, "Set3"))(12) # 读取基因组数据 genome <- read.table(ci…

【启动npm run serve 奇怪的报错】

报错如下&#xff1a; INFO Starting development server... utils.js:587Uncaught TypeError [ERR_INVALID_ARG_VALUE]: The argument path must be a string or Uint8Array without null bytes. Received E:\\#\u0000#idea-workspace\\wonderful-search\\wonderful-search-v…

julia语言中的决策树

决策树&#xff08;Decision Tree&#xff09;是一种基本的分类与回归方法&#xff0c;它呈现出一种树形结构&#xff0c;可以直观地展示决策的过程和结果。在决策树中&#xff0c;每个内部节点表示一个属性上的判断条件&#xff0c;每个分支代表一个可能的属性值&#xff0c;每…

JavaSE、JavaEE和Jakarta EE的历史、区别与联系

JavaSE、JavaEE和Jakarta EE是Java平台中的三个重要组成部分&#xff0c;它们各自承担着不同的角色&#xff0c;同时也有着密切的联系。在理解它们之间的历史、区别和联系之前&#xff0c;我们首先需要了解它们的基本概念。 JavaSE&#xff08;Java Standard Edition&#xff…

电脑充电器能充手机吗?如何给手机充电?

电脑充电器可以给手机充电吗&#xff1f; 电脑充电器可以给手机充电&#xff0c;但前提是电脑充电器的功率输出与手机的功率匹配且接口匹配。 假设电脑充电器的输出功率为5V/2A&#xff0c;手机也支持5V/2A的输入功率。 只要接口匹配&#xff0c;就可以使用电脑充电器给手机充…

基于单片机的光电传感转速测量系统的设计

摘要:针对在工程实践中很多场合都需要对转速这一参数进行精准测量的目的,采用以STC89C52 芯片为核心,结合转动系统、光电传感器、显示模块等构成光电传感器转速测量系统,实现对电机 转速的测量。通过测试表明该系统具有结构简单、所耗成本低,测量精度高、稳定可靠等优点,…

Day67:WEB攻防-Java安全JNDIRMILDAP五大不安全组件RCE执行不出网

知识点&#xff1a; 1、Java安全-RCE执行-5大类函数调用 2、Java安全-JNDI注入-RMI&LDAP&高版本 3、Java安全-不安全组件-Shiro&FastJson&JackJson&XStream&Log4j Java安全-RCE执行-5大类函数调用 Java中代码执行的类&#xff1a; GroovyRuntimeExecPr…