特别:下文的“容量”、“数组长度”,“capacity” 都是指底层数组长度,即 table.length
本章要解决的问题:
常量部分:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //链表转红黑树阈值
static final int UNTREEIFY_THRESHOLD = 6; //红黑树转链表阈值
static final int MIN_TREEIFY_CAPACITY = 64; //链表转转红黑树的数组最小容量
transient int size; //HashMap的元素个数
HashMap是一个Node类型的数组,每个元素可以为单节点、链表、红黑树。
Java8之前,HashMap的数据结构如下:
数组+链表:链表是为了解决hash冲突


Tips:
- 确定加载因子
- 根据初始容量参数重新计算扩容阈值(大于或等于初始容量且一定等于2的幂的那个数)
tableSizeFor(initialCapacity):确定扩容阈值:大于或等于初始容量且一定等于2的幂的那个数;比如cap=8则返回8;cap=9则返回16
源码分析如下:
//构造函数一:无参构造函数:加载因子(0.75)和初始容量(16)分别使用默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//构造函数二:
//指定初始容量,调用HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造函数三:同时指定初始容量和加载因子
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;
// 确定扩容阈值:大于或等于初始容量且一定等于2的幂的那个数;比如cap=8则返回8;cap=9则返回16
this.threshold = tableSizeFor(initialCapacity);
}
//构造函数三:创建一个跟参数有相同结构的map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
tips:一个简单的K-V模型的数据体,提供对key value的set get操作
源码如下:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
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); // key value 的hash值再做异或运算
}
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;
}
}
HashMap的容量变化通常存在以下几种情况:
空参数的构造函数:实例化的HashMap默认内部数组是null,即没有实例化。第一次调用put方法时,则会开始第一次初始化扩容,长度为16。【懒加载】
有参构造函数:用于指定容量。根据阈值计算方式【大于或等于初始容量且一定等于2的幂的那个数】,将这个数设置赋值给阈值。第一次调用put方法时,会将阈值赋值给容量(第一步),然后让 阈值=负载因子X容量(第二步)
。(因此并不是我们手动指定了容量就一定不会触发扩容,超过阈值后一样会扩容!!)
如果不是第一次扩容,则容量变为原来的2倍,阈值也变为原来的2倍。(负载因子还是不变)
首次put时,先会触发扩容(底层table初始化),然后存入数据,然后判断是否需要扩容;不是首次put,则不再初始化,直接存入数据,然后再判断是否需要扩容;
参看源码:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//当前数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//非首次put,扩容阈值变为原来2倍;容量变为原来的2倍
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;//达到最大值,不能扩容,返回当前数组
}
//数组容量扩大为原来2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 将扩容阈值变为原来2倍
}
//第一次 put 的时候,【对应使用 new HashMap(int initialCapacity) 初始化后】
else if (oldThr > 0)
newCap = oldThr; //如果初始化Map时指定了初始容量,则数组容量=扩容阈值(参照阈值的计算)
//第一次 put 的时候 ,【对应使用 new HashMap() 初始化后】
else {
newCap = DEFAULT_INITIAL_CAPACITY;//没有指定初始容量,则数组容量=默认初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩容阈值=加载因子*默认初始容量=12
}
//第一次put且有指定初始容量时 :重新计算扩容阈值:新扩容阈值=加载因子*新数组长度
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//计算好容量初始化一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//如果第一次put,这里就直接返回newTab,扩容过程结束;
table = newTab;
if (oldTab != null) {
// 开始遍历原数组,进行数据迁移。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//数组当前位置不存在链表或者红黑树,则直接put到该位置
newTab[e.hash & (newCap - 1)] = e; //返回单节点的Node数组
else if (e instanceof TreeNode)
//红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
源码分析:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param hash:key的hash值
* @param onlyIfAbsent :false表示key存在时,覆盖value
* @param evict if false, the table is in creation mode. 待补充
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//第一次put,先执行数组初始化操作
n = (tab = resize()).length;
//当前key对应的数组下标没有元素,则直接初始化新的Node到该数组位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //注意此时Node没有Next
else {
//当前key对应的数组下标已经存在元素
Node<K,V> e; K k;
// 情况一://该位置的元素跟新put进来的数据key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //注意这里是引用的赋值,因此后面e.value=value可以实现value覆盖
//情况二:当前位置是红黑树结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//情况三:当前位置是链表结构
for (int binCount = 0; ; ++binCount) {
//遍历到达链表末端
if ((e = p.next) == null) {
//创建新的Node并放在链表的末端
p.next = newNode(hash, key, value, null);
// treeify_threshold 为 8,所以,如果新插入的值是链表中的第 8 个,将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//在链表当前位置找到相同的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; //这里break,代码后面统一对e进行value的覆盖处理
//此时既未遍历到链表末端,也没有发现key相等,则继续链表遍历
p = e; //这个赋值,相当于链表的遍历,尾部指向下一个节点的头部,实现链表遍历
}
}
if (e != null) { // existing mapping for key key相等时
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //新的value覆盖旧的value值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//每次put操作完成后,size自增;
//如果size超过阈值,则在put完成后,再次执行扩容
resize();
afterNodeInsertion(evict);//父类抽象方法的实现,HashMap不做任何实现
return null;
}
容量:C
加载因子:F
扩容阈值:T
------ 1、无参初始化:
F=0.75
C=0
T=0
------ 2、有参初始化:容量参数=7
F=0.75
C=0
T=tableSizeFor(7)=8 //1、2、4、8 。。。大于7 所以取8
------ 3、第一次put && 无参初始化时
F=0.75
C=16(默认容量)
T=16*0.75=12
------ 4、第一次put && 有参初始化时
F=0.75
C=T=8 // 容量=阈值
T=8*0.75=6 // 阈值更新:容量*加载因子
HashMap使用方法tableSizeFor()来保证无论你给值是什么,返回的一定是2的幂
/**
* Returns a power of two size for the given target capacity.
*/
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、带着疑问:
key的hashCode为什么右移16位后再进行异或运算?
2、关于 | & ^ 三种运算的特征说明:
- ^按位异或运算:位相同返回0,不同返回1;可推导出:任何数跟0异或返回任何数,任何数跟1异或返回对应的取反
- 异或运算能更好的保留各部分的特征,如果采用逻辑与&运算计算出来的值会向0靠拢(00得0,01得0,11得1 因此0的概率2/3),采用逻辑或|运算计算出来的值会向1靠拢 (00得0,01得1,11得1,因此1的概率为2/3)
3、hash()源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//key.hashCode() ;hashCode是Object对象的一个native方法,由操作系统实现,跟内存地址存在某种映射关系
4、进入分析:
简单点:
因为:(n-1)&hash中,hash的高位数将被数组长度的二进制码锁屏蔽,为确保hash的高位尽可能利用,就先对hash值先右移16位,再跟原hash值进行异或运算,同时保留高位和低位特征。
数组长度二进制码屏蔽是什么意思?
数组长度的数据类型int转化为32位的二进制,因为长度值对比最大值(2的32位)总是比较小的,所以它的高位趋向0,与其他数进行&运算后,结果值的高位趋向0,那么其他数的高位特征就丢失了
下面用例子分析:
### 计算hash
hashCode: 0000 0000 0101 0000 0000 0000 1111 1010
hashCode>>>16: 0000 0000 0000 0000 0000 0000 0101 0000
hashCode^hashCode>>>16: 0000 0000 0101 0000 0000 0000 0110 1010
hash=hashCode^hashCode>>>16 0000 0000 0101 0000 0000 0000 0110 1010
### 计算index时:
(n-1) (假设n=16) 0000 0000 0000 0000 0000 0000 0000 1111
(n-1)&hash 0000 0000 0000 0000 0000 0000 0000 1010
仔细观察上文不难发现,高16位很有可能会被数组长度的二进制码锁屏蔽,
如果我们不做移位异或运算,那么在计算数组index时将丢失高区特征
参考资料:
一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame
HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候
目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标
网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.
1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>
参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍 介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。 内容有: ①:Hub模型的方法介绍 ②:服务器端代码介绍 ③:前端vue3安装并调用后端方法 ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke() 去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on
一、机器人介绍 此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接
目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'
我想使用ruby-prof和JMeter分析Rails应用程序。我对分析特定Controller/操作/或模型方法的建议方法不感兴趣,我想分析完整堆栈,从上到下。所以我运行这样的东西:RAILS_ENV=productionruby-prof-fprof.outscript/server>/dev/null然后我在上面运行我的JMeter测试计划。然而,问题是使用CTRL+C或SIGKILL中断它也会在ruby-prof可以写入任何输出之前杀死它。如何在不中断ruby-prof的情况下停止mongrel服务器? 最佳答案
文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cppdumper例子2-森林whoishe后记认识unity打包目录结构dll一般很大,因为里面是所有的游戏功能编译成的二进制码游戏逆向流程开发人员代码被编译打包到GameAssembly.dll中使用il2ppDumper工具,并借助游戏名_Data\il2cpp_data\Metadata\global-metadata.dat