当前位置: 首页 > news >正文

怎么进行网站维护泸州网站公司

怎么进行网站维护,泸州网站公司,wordpress自建站哪里换logo,网站排名怎么提升文章目录 HashMap集合1.散列2.hashMap结构3.继承关系4.成员变量5.构造方法6.成员方法6.1增加方法6.2将链表转换为红黑树的treeifyBin方法6.3扩容方法_resize6.3.1扩容机制6.3.2源码resize方法的解读 6.4 删除方法(remove)6.5查找元素方法(get)6.6遍历HashMap集合几种方式 7.初始… 文章目录 HashMap集合1.散列2.hashMap结构3.继承关系4.成员变量5.构造方法6.成员方法6.1增加方法6.2将链表转换为红黑树的treeifyBin方法6.3扩容方法_resize6.3.1扩容机制6.3.2源码resize方法的解读 6.4 删除方法(remove)6.5查找元素方法(get)6.6遍历HashMap集合几种方式 7.初始化问题7.1HashMap的初始化问题描述7.2HashMap中容量的初始化 HashMap集合 1.散列 散列又称哈希Hash是一种数据处理方式。它通过特定的算法将输入比如字符串、文件等转换成固定长度的一串通常是数字并且这个过程是不可逆的。这个过程中的算法就称为哈希函数得到的结果就称为哈希值。 散列的主要作用是为了检索数据。通过散列可以将大量的数据进行压缩然后将压缩后的散列值作为索引存储在散列表中。当需要查找数据时只需要用同样的哈希函数处理要查找的数据得到散列值然后在散列表中查找对应的数据就可以大大提高检索的速度。 同时由于散列过程是不可逆的所以也常常用于数据的安全性校验如密码的存储和验证、数字签名等。 示例 一个简单的例子是电话簿。如果你想查找一个人的电话号码你可以按照字母顺序一页一页地查找这就像是在一个数组中查找数据。但这种方法效率很低特别是当电话簿的数据量很大时。 如果你使用哈希函数将每个人的名字转换为一个数字然后按照这个数字存储他们的电话号码这就像是创建了一个哈希表。比如哈希函数可以是将名字中的每个字母转换为其在字母表中的位置A1B2C3等等然后加起来。所以John的哈希值就是101581447。 现在如果你想查找John的电话号码你只需要计算出John的哈希值47然后直接查找哈希表中47对应的电话号码就可以快速找到结果无需遍历整个电话簿。 这就是散列的基本原理和用法。 2.hashMap结构 在Java8 之前 HashMap 由 数组链表 数据结构组成主要有以下不足之处 即使散列函数取得再好也很难达到元素百分百均匀分布HashMap中有大量元素都存放在同一个桶下时该桶下就会有一个长长的链表降低了查询的效率 针对以上情况自从java8开始引入了红黑树来进行优化。新版的HashMap 由 数组链表 红黑树数据结构组成当链表长度大于阈值或者红黑树的边界值默认为 8并且当前数组的长度大于64时此时此索引位置上的所有数据改为使用红黑树存储。 特点 存取无序的 键和值位置都可以是null但是键位置只能是一个null 键位置是唯一的底层的数据结构控制键的 jdk1.8前数据结构是链表 数组 jdk1.8之后是 链表 数组 红黑树 阈值(边界值) 8 并且数组长度大于64才将链表转换为红黑树变为红黑树的目的是为了高效的查询。 3.继承关系 HashMap继承关系如下图所示 说明 Cloneable 空接口表示可以克隆。 创建并返回HashMap对象的一个副本。Serializable 序列化接口。属于标记性接口。HashMap对象可以被序列化和反序列化。AbstractMap 父类提供了Map实现接口。以最大限度地减少实现此接口所需的工作。 补充通过上述继承关系我们发现一个很奇怪的现象 就是HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口那为什么HashMap还要在实现Map接口呢同样在ArrayList中LinkedList中都是这种结构。 据 java 集合框架的创始人Josh Bloch描述这样的写法是一个失误。在java集合框架中类似这样的写法很多最开始写java集合框架的时候他认为这样写在某些地方可能是有价值的直到他意识到错了。显然的JDK的维护者后来不认为这个小小的失误值得去修改所以就这样存在下来了。 4.成员变量 1.序列化版本号 private static final long serialVersionUID 362498820763181265L;2.集合的初始化容量( 必须是二的n次幂 ) //默认的初始容量是16 -- 14相当于1*2的4次方---1*16 static final int DEFAULT_INITIAL_CAPACITY 1 4; 问题 为什么必须是2的n次幂如果输入值不是2的幂比如10会怎么样 HashMap构造方法还可以指定集合的初始化容量大小 HashMap(int initialCapacity) 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。当向HashMap中添加一个元素的时候需要根据key的hash值去确定其在数组中的具体位置。 HashMap为了存取高效要尽量较少碰撞就是要尽量把数据分配均匀每个链表长度大致相同这个实现就在把数据存到哪个链表中的算法。 这个算法实际就是取模hash%length计算机中直接求余效率不如位移运算。所以源码中做了优化,使用 hash(length-1)而实际上hash%length等于hash(length-1)的前提是length是2的n次幂。 为什么这样能均匀分布减少碰撞呢2的n次方实际就是1后面n个02的n次方-1 实际就是n个1 举例 说明按位与运算相同的二进制数位上都是1的时候结果为1否则为零。 例如长度为8时候3(8-1)3 2(8-1)2 不同位置上不碰撞 例如长度length为8时候8是2的3次幂。二进制是1000 length-1 二进制运算1000 - 1 ---------------------111 如下所示 hash(length-1) 3 (8 - 1)3 00000011 3 hash00000111 7 length-1 ---------------------00000011-----》3 数组下标hash(length-1) 2 (8 - 1) 2 00000010 2 hash00000111 7 length-1 ---------------------00000010-----》2 数组下标 说明上述计算结果是不同位置上不碰撞例如长度为9时候3(9-1)0 2(9-1)0 都在0上碰撞了 例如长度length为9时候9不是2的n次幂。二进制是00001001 length-1 二进制运算1001 - 1 ---------------------1000 如下所示 hash(length-1) 3 (9 - 1)0 00000011 3 hash00001000 8 length-1 ---------------------00000000-----》0 数组下标hash(length-1) 2 (9 - 1) 2 00000010 2 hash00001000 8 length-1 ---------------------00000000-----》0 数组下标 说明上述计算结果都在0上碰撞了注意 当然如果不考虑效率直接求余即可就不需要要求长度必须是2的n次方了 小结 ​ 1.由上面可以看出当我们根据key的hash确定其在数组的位置时如果n为2的幂次方可以保证数据的均匀插入如果n不是2的幂次方可能数组的一些位置永远不会插入数据浪费数组的空间加大hash冲突。 ​ 2.另一方面一般我们可能会想通过 % 求余来确定位置这样也可以只不过性能不如 运算。而且当n是2的幂次方时hash (length - 1) hash % length ​ 3.因此HashMap 容量为2次幂的原因就是为了数据的的均匀分布减少hash冲突毕竟hash冲突越大代表数组中一个链的长度越大这样的话会降低hashmap的性能 ​ 4.如果创建HashMap对象时输入的数组长度是10不是2的幂HashMap通过一通位移运算和或运算得到的肯定是2的幂次数并且是离那个数最近的数字。 源代码如下 //创建HashMap集合的对象指定数组长度是10不是2的幂 HashMap hashMap new HashMap(10); public HashMap(int initialCapacity) {//initialCapacity10this(initialCapacity, DEFAULT_LOAD_FACTOR);} public HashMap(int initialCapacity, float loadFactor) {//initialCapacity10if (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);//initialCapacity10 }/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {//int cap 10int 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;}说明 由此可以看到当在实例化HashMap实例时如果给定了initialCapacity(假设是10)由于HashMap的capacity必须都是2的幂因此这个方法用于找到大于等于initialCapacity(假设是10)的最小的2的幂initialCapacity如果就是2的幂则返回的还是这个数。 下面分析这个算法 首先为什么要对cap做减1操作。int n cap - 1; 这是为了防止cap已经是2的幂。如果cap已经是2的幂 又没有执行这个减1操作则执行完后面的几条无符号右移操作之后返回的capacity将是这个cap的2倍。如果不懂要看完后面的几个无符号右移之后再回来看看。 下面看看这几个无符号右移操作如果n这时为0了经过了cap-1之后则经过后面的几次无符号右移依然是0最后返回的capacity是 1最后有个n1的操作。 这里只讨论n不等于0的情况。注意|按位或运算运算规则相同的二进制数位上都是0的时候结果为0否则为1。 ​ 第一次右移 int n cap - 1;//cap10 n9 n | n 1;00000000 00000000 00000000 00001001 //9 | 00000000 00000000 00000000 00000100 //9右移之后变为4 -------------------------------------------------00000000 00000000 00000000 00001101 //按位异或之后是13由于n不等于0则n的二进制表示中总会有一bit为1这时考虑最高位的1。通过无符号右移1位则将最高位的1右移了1位再做或操作使得n的二进制表示中与最高位的1紧邻的右边一位也为1如 00000000 00000000 00000000 00001101第二次右移 n | n 2;//n通过第一次右移变为了n1300000000 00000000 00000000 00001101 // 13 |00000000 00000000 00000000 00000011 //13右移之后变为3 -------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是15注意这个n已经经过了n | n 1; 操作。假设此时n为00000000 00000000 00000000 00001101 则n无符号右移两位会将最高位两个连续的1右移两位然后再与原来的n做或操作这样n的二进制表示的高位中会有4个连续的1。如 00000000 00000000 00000000 00001111 //按位异或之后是15第三次右移 : n | n 4;//n通过第一、二次右移变为了n1500000000 00000000 00000000 00001111 // 15 |00000000 00000000 00000000 00000000 //15右移之后变为0 -------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是15这次把已经有的高位中的连续的4个1右移4位再做或操作这样n的二进制表示的高位中正常会有8个连续的1。如00001111 1111xxxxxx 。 以此类推 注意容量最大也就是32bit的正数因此最后n | n 16; 最多也就32个1但是这已经是负数了。在执行tableSizeFor之前对initialCapacity做了判断如果大于MAXIMUM_CAPACITY(2 ^ 30)则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2 ^ 30)会执行移位操作。所以这里面的移位操作之后最大30个1不会大于等于MAXIMUM_CAPACITY。30个1加1之后得2 ^ 30 。 请看下面的一个完整例子 注意得到的这个capacity却被赋值给了threshold。 this.threshold tableSizeFor(initialCapacity);//initialCapacity103.默认的负载因子默认值是0.75 static final float DEFAULT_LOAD_FACTOR 0.75f;4.集合最大容量 //集合最大容量的上限是2的30次幂 static final int MAXIMUM_CAPACITY 1 30;5.当链表的值超过8则会转红黑树 //当桶(bucket)上的结点数大于这个值时会转成红黑树static final int TREEIFY_THRESHOLD 8;问题为什么Map桶中节点个数超过8才转为红黑树 8这个阈值定义在HashMap中针对这个成员变量在源码的注释中只说明了8是binbin就是bucket(桶)从链表转成树的阈值但是并没有说明为什么是8 在HashMap中有一段注释说明 我们继续往下看 : Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5)*pow(0.5, k)/factorial(k)). The first values are: 因为树节点的大小大约是普通节点的两倍所以我们只在箱子包含足够的节点时才使用树节点(参见TREEIFY_THRESHOLD)。当它们变得太小(由于删除或调整大小)时就会被转换回普通的桶。在使用分布良好的用户hashcode时很少使用树箱。理想情况下在随机哈希码下箱子中节点的频率服从泊松分布 (http://en.wikipedia.org/wiki/Poisson_distribution)默认调整阈值为0.75平均参数约为0.5尽管由于调整粒度的差异很大。忽略方差列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k))。 第一个值是:0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006 more: less than 1 in ten millionTreeNodes占用空间是普通Nodes的两倍所以只有当bin包含足够多的节点时才会转成TreeNodes而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时又会转成普通的bin。并且我们查看源码的时候发现链表长度达到8就转成红黑树当长度降到6就转成普通bin。 这样就解释了为什么不是一开始就将其转换为TreeNodes而是需要一定节点数才转为TreeNodes说白了就是权衡空间和时间的权衡。 这段内容还说到当hashCode离散性很好的时候树型bin用到的概率非常小因为数据均匀分布在每个bin中几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下离散性可能会变差然而JDK又不能阻止用户实现这种不好的hash算法因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布我们可以看到一个bin中链表长度达到8个元素的概率为0.00000006几乎是不可能事件。所以之所以选择8不是随便决定的而是根据概率统计决定的。由此可见发展将近30年的Java每一项改动和优化都是非常严谨和科学的。 也就是说选择8因为符合泊松分布超过8的时候概率已经非常小了所以我们选择8这个数字。 补充 1. Poisson分布(泊松分布)是一种统计与概率学里常见到的离散[概率分布]。 泊松分布的概率函数为泊松分布的参数λ是单位时间(或单位面积)内随机事件的平均发生次数。 泊松分布适合于描述单位时间内随机事件发生的次数。2.以下是一些资料上面翻看到的解释供大家参考 红黑树的平均查找长度是log(n)如果长度为8平均查找长度为log(8)3链表的平均查找长度为n/2当长度为8时平均查找长度为8/24这才有转换成树的必要链表长度如果是小于等于66/23而log(6)2.6虽然速度也很快的但是转化为树结构和生成树的时间并不会太短。6.当链表的值小于6则会从红黑树转回链表 //当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD 6;7.当Map里面的数量超过这个值时表中的桶才能进行树形化 否则桶内元素太多时会扩容而不是树形化 为了避免进行扩容、树形化选择的冲突这个值不能小于 4 * TREEIFY_THRESHOLD (8) //桶中结构转化为红黑树对应的数组长度最小的值 static final int MIN_TREEIFY_CAPACITY 64;8、table用来初始化(必须是二的n次幂)(重点) //存储元素的数组 transient NodeK,V[] table;table在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组jdk8之前数组类型是EntryK,V类型。从jdk1.8之后是NodeK,V类型。只是换了个名字都实现了一样的接口Map.EntryK,V。负责存储键值对数据的。 9、用来存放缓存 //存放具体元素的集合 transient SetMap.EntryK,V entrySet;10、 HashMap中存放元素的个数(重点) //存放元素的个数注意这个不等于数组的长度。transient int size;size为HashMap中K-V的实时数量不是数组table的长度。 11、 用来记录HashMap的修改次数 // 每次扩容和更改map结构的计数器transient int modCount; 12、 用来调整大小下一个容量的值计算方式为(容量*负载因子) // 临界值 当实际大小(容量*负载因子)超过临界值时会进行扩容 int threshold;13、 哈希表的加载因子(重点) // 加载因子 final float loadFactor;说明 1.loadFactor加载因子是用来衡量 HashMap 满的程度表示HashMap的疏密程度影响hash操作到同一个数组位置的概率计算HashMap的实时加载因子的方法为size/capacity而不是占用桶的数量去除以capacity。capacity 是桶的数量也就是 table 的长度length。 loadFactor太大导致查找元素效率低太小导致数组的利用率低存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。 当HashMap里面容纳的元素已经达到HashMap数组长度的75%时表示HashMap太挤了需要扩容而扩容这个过程涉及到 rehash、复制数据等操作非常消耗性能。所以开发中尽量减少扩容的次数可以通过创建HashMap集合对象时指定初始容量来尽量避免。 同时在HashMap的构造器中可以定制loadFactor。 构造方法 HashMap(int initialCapacity, float loadFactor) 构造一个带指定初始容量和加载因子的空 HashMap。2.为什么加载因子设置为0.75,初始化临界值是12 loadFactor越趋近于1那么 数组中存放的数据(entry)也就越多也就越密也就是会让链表的长度增加loadFactor越小也就是趋近于0数组中存放的数据(entry)也就越少也就越稀疏。 如果希望链表尽可能少些。要提前扩容有的数组空间有可能一直没有存储数据。加载因子尽可能小一些。 举例 例如加载因子是0.4。 那么16*0.4---6 如果数组中满6个空间就扩容会造成数组利用率太低了。加载因子是0.9。 那么16*0.9----14 那么这样就会导致链表有点多了。导致查找元素效率低。所以既兼顾数组利用率又考虑链表不要太多经过大量测试0.75是最佳方案。 threshold计算公式capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。这个值是当前已占用数组长度的最大值。当Sizethreshold的时候那么就要考虑对数组的resize(扩容)也就是说这个的意思就是 衡量数组是否需要扩增的一个标准。 扩容后的 HashMap 容量是之前容量的两倍. 5.构造方法 HashMap 中重要的构造方法它们分别如下 1、构造一个空的 HashMap 默认初始容量16和默认负载因子0.75。 public HashMap() {this.loadFactor DEFAULT_LOAD_FACTOR; // 将默认的加载因子0.75赋值给loadFactor并没有创建数组 }2、 构造一个具有指定的初始容量和默认负载因子0.75 HashMap。 // 指定“容量大小”的构造函数public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}3、 构造一个具有指定的初始容量和负载因子的 HashMap。我们来分析一下。 /*指定“容量大小”和“加载因子”的构造函数initialCapacity: 指定的容量loadFactor:指定的加载因子 */ public HashMap(int initialCapacity, float loadFactor) {//判断初始化容量initialCapacity是否小于0if (initialCapacity 0)//如果小于0则抛出非法的参数异常IllegalArgumentExceptionthrow new IllegalArgumentException(Illegal initial capacity: initialCapacity);//判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY-》2的30次幂if (initialCapacity MAXIMUM_CAPACITY)//如果超过MAXIMUM_CAPACITY会将MAXIMUM_CAPACITY赋值给initialCapacityinitialCapacity MAXIMUM_CAPACITY;//判断负载因子loadFactor是否小于等于0或者是否是一个非数值if (loadFactor 0 || Float.isNaN(loadFactor))//如果满足上述其中之一则抛出非法的参数异常IllegalArgumentExceptionthrow new IllegalArgumentException(Illegal load factor: loadFactor);//将指定的加载因子赋值给HashMap成员变量的负载因子loadFactorthis.loadFactor loadFactor;/*tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂如果不是那么会变为比指 定初始化容量大的最小的2的n次幂。这点上述已经讲解过。但是注意在tableSizeFor方法体内部将计算后的数据返回给调用这里了并且直接赋值给threshold边 界值了。有些人会觉得这里是一个bug,应该这样书写this.threshold tableSizeFor(initialCapacity) * this.loadFactor;这样才符合threshold的意思当HashMap的size到达threshold这个阈值时会扩容。但是请注意在jdk8以后的构造方法中并没有对table这个成员变量进行初始化table的初始化被推 迟到了put方法中在put方法中会对threshold重新计算put方法的具体实现我们下面会进行讲解*/this.threshold tableSizeFor(initialCapacity);} 最后调用了tableSizeFor来看一下方法实现/*** Returns a power of two size for the given target capacity.返回比指定初始化容量大的最小的2的n次幂*/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;}说明 对于 this.threshold tableSizeFor(initialCapacity); 疑问解答 tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂如果不是那么会变为比指定初始化容量大的最小的2的n次幂。这点上述已经讲解过。 但是注意在tableSizeFor方法体内部将计算后的数据返回给调用这里了并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写 this.threshold tableSizeFor(initialCapacity) * this.loadFactor; 这样才符合threshold的意思当HashMap的size到达threshold这个阈值时会扩容。 但是请注意在jdk8以后的构造方法中并没有对table这个成员变量进行初始化table的初始化被推迟到了put方法中在put方法中会对threshold重新计算put方法的具体实现我们下面会进行讲解 4、包含另一个“Map”的构造函数 //构造一个映射关系与指定 Map 相同的新 HashMap。 public HashMap(Map? extends K, ? extends V m) {//负载因子loadFactor变为默认的负载因子0.75this.loadFactor DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}最后调用了putMapEntries来看一下方法实现 final void putMapEntries(Map? extends K, ? extends V m, boolean evict) {//获取参数集合的长度int s m.size();if (s 0){//判断参数集合的长度是否大于0说明大于0if (table null) // 判断table是否已经初始化{ // pre-size// 未初始化s为m的实际元素个数float ft ((float)s / loadFactor) 1.0F;int t ((ft (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);// 计算得到的t大于阈值则初始化阈值if (t threshold)threshold tableSizeFor(t);}// 已初始化并且m元素个数大于阈值进行扩容处理else if (s threshold)resize();// 将m中的所有元素添加至HashMap中for (Map.Entry? extends K, ? extends V e : m.entrySet()) {K key e.getKey();V value e.getValue();putVal(hash(key), key, value, false, evict);}} }注意 float ft ((float)s / loadFactor) 1.0F;这一行代码中为什么要加1.0F s/loadFactor的结果是小数加1.0F与(int)ft相当于是对小数做一个向上取整以尽可能的保证更大容量更大的容量能够减少resize的调用次数。所以 1.0F是为了获取更大的容量。 例如原来集合的元素个数是6个那么6/0.75是8是2的n次幂那么新的数组大小就是8了。然后原来数组的数据就会存储到长度是8的新的数组中了这样会导致在存储元素的时候容量不够还得继续扩容那么性能降低了而如果1呢数组长度直接变为16了这样可以减少数组的扩容。 6.成员方法 6.1增加方法 put方法是比较复杂的实现步骤大致如下 1先通过hash值计算出key映射到哪个桶 2如果桶上没有碰撞冲突则直接插入 3如果出现碰撞冲突了则需要处理冲突 ​ a:如果该桶使用红黑树处理冲突则调用红黑树的方法插入数据 ​ b:否则采用传统的链式方法插入。如果链的长度达到临界值则把链转变为红黑树 4如果桶中存在重复的键则为该键替换新值value 5如果size大于阈值threshold则进行扩容 具体的方法如下 public V put(K key, V value) {return putVal(hash(key), key, value, false, true); }说明 ​ 1HashMap只提供了put用于添加元素putVal方法只是给put方法调用的一个方法并没有提供给用户使用。 所以我们重点看putVal方法。 2我们可以看到在putVal()方法中key在这里执行了一下hash()方法,来看一下Hash方法是如何实现的。 static final int hash(Object key) {int h;/*1如果key等于null可以看到当key等于null的时候也是有哈希值的返回的是0.2如果key不等于null首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的 hash值*/return (key null) ? 0 : (h key.hashCode()) ^ (h 16);}从上面可以得知HashMap是支持Key为空的而HashTable是直接用Key来获取HashCode所以key为空会抛异常。 {其实上面就已经解释了为什么HashMap的长度为什么要是2的幂因为HashMap 使用的方法很巧妙它通过 hash (table.length -1)来得到该对象的保存位前面说过 HashMap 底层数组的长度总是2的n次方这是HashMap在速度上的优化。当 length 总是2的n次方时hash (length-1)运算等价于对 length 取模也就是hash%length但是比%具有更高的效率。比如 n % 32 n (32 -1)。} 解读上述hash方法 我们先研究下key的哈希值是如何计算出来的。key的哈希值是通过上述方法计算出来的。 这个哈希方法首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的 hash值。计算过程如下所示 static final int hash(Object key) {int h;/*1如果key等于null可以看到当key等于null的时候也是有哈希值的返回的是0.2如果key不等于null首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的 hash值*/return (key null) ? 0 : (h key.hashCode()) ^ (h 16);} 在putVal函数中使用到了上述hash函数计算的哈希值 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {。。。。。。。。。。。。。。if ((p tab[i (n - 1) hash]) null)//这里的n表示数组长度16。。。。。。。。。。。。。。}计算过程如下所示 ​ 说明 ​ 1key.hashCode()返回散列值也就是hashcode。假设随便生成的一个值。 ​ 2n表示数组初始化的长度是16 ​ 3按位与运算运算规则相同的二进制数位上都是1的时候结果为1否则为零。 ​ 4^按位异或运算运算规则相同的二进制数位上数字相同结果为0不同为1。 简单来说就是 高16 bit 不变低16 bit 和高16 bit 做了一个异或得到的 hashcode 转化为32位二进制前16位和后16位低16 bit和高16 bit做了一个异或 问题为什么要这样操作呢 如果当n即数组长度很小假设是16的话那么n-1即为 —》1111 这样的值和hashCode()直接做按位与操作实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大低位变化很小这样就很容易造成哈希冲突了所以这里把高低位都利用起来从而解决了这个问题。 例如上述 hashCode()值 1111 1111 1111 1111 1111 0000 1110 1010 n-1即16-1--》15 。。。。。。。。。。。。。。。。。。。。。。1111 -------------------------------------------------------------------0000 0000 0000 0000 0000 0000 0000 1010 ----》10作为索引 其实就是将hashCode值作为数组索引那么如果下个高位hashCode不一致低位一致的话就会造成计算的索引还是10,从而造成了哈希冲突了。降低性能。(n-1) hash - 得到下标 (n-1) n表示数组长度16n-1就是15 取余数本质是不断做除法把剩余的数减去运算效率要比位运算低。 现在看putVal()方法看看它到底做了什么。 主要参数 hash key的hash值key 原始Keyvalue 要存放的值onlyIfAbsent 如果true代表不更改现有的值evict 如果为false表示table为创建状态 putVal()方法源代码如下所示 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) {NodeK,V[] tab; NodeK,V p; int n, i;/*1transient NodeK,V[] table; 表示存储Map集合中元素的数组。2(tab table) null 表示将空的table赋值给tab,然后判断tab是否等于null第一次肯定是 null3(n tab.length) 0 表示将数组的长度0赋值给n,然后判断n是否等于0n等于0由于if判断使用双或满足一个即可则执行代码 n (tab resize()).length; 进行数组初始化。并将初始化好的数组长度赋值给n.4执行完n (tab resize()).length数组tab每个空间都是null*/if ((tab table) null || (n tab.length) 0)n (tab resize()).length;/*1i (n - 1) hash 表示计算数组的索引赋值给i即确定元素存放在哪个桶中2p tab[i (n - 1) hash]表示获取计算出的位置的数据赋值给节点p3) (p tab[i (n - 1) hash]) null 判断节点位置是否等于null如果为null则执行代 码tab[i] newNode(hash, key, value, null);根据键值对创建新的节点放入该位置的桶中小结如果当前桶没有哈希碰撞冲突则直接把键值对插入空间位置*/ if ((p tab[i (n - 1) hash]) null)//创建一个新的节点存入到桶中tab[i] newNode(hash, key, value, null);else {// 执行else说明tab[i]不等于null表示这个位置已经有值了。NodeK,V e; K k;/*比较桶中第一个元素(数组中的结点)的hash值和key是否相等1p.hash hash p.hash表示原来存在数据的hash值 hash表示后添加数据的hash值 比较两个 hash值是否相等说明p表示tab[i]即 newNode(hash, key, value, null)方法返回的Node对象。NodeK,V newNode(int hash, K key, V value, NodeK,V next) {return new Node(hash, key, value, next);}而在Node类中具有成员变量hash用来记录着之前数据的hash值的2(k p.key) key p.key获取原来数据的key赋值给k key 表示后添加数据的key 比较两 个key的地址值是否相等3key ! null key.equals(k)能够执行到这里说明两个key的地址值不相等那么先判断后 添加的key是否等于null如果不等于null再调用equals方法判断两个key的内容是否相等*/if (p.hash hash ((k p.key) key || (key ! null key.equals(k))))/*说明两个元素哈希值相等并且key的值也相等将旧的元素整体对象赋值给e用e来记录*/ e p;// hash值不相等或者key不相等判断p是否为红黑树结点else if (p instanceof TreeNode)// 放入树中e ((TreeNodeK,V)p).putTreeVal(this, tab, hash, key, value);// 说明是链表节点else {/*1)如果是链表的话需要遍历到最后节点然后插入2)采用循环遍历的方式判断链表中是否有重复的key*/for (int binCount 0; ; binCount) {/*1)e p.next 获取p的下一个元素赋值给e2)(e p.next) null 判断p.next是否等于null等于null说明p没有下一个元 素那么此时到达了链表的尾部还没有找到重复的key,则说明HashMap没有包含该键将该键值对插入链表中*/if ((e p.next) null) {/*1创建一个新的节点插入到尾部p.next newNode(hash, key, value, null);NodeK,V newNode(int hash, K key, V value, NodeK,V next) {return new Node(hash, key, value, next);}注意第四个参数next是null因为当前元素插入到链表末尾了那么下一个节点肯定是 null2这种添加方式也满足链表数据结构的特点每次向后添加新的元素*/p.next newNode(hash, key, value, null);/*1)节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8如果大于则将链表转换为红黑树2int binCount 0 表示for循环的初始化值。从0开始计数。记录着遍历节点的个 数。值是0表示第一个节点1表示第二个节点。。。。7表示第八个节点加上数组中的的一 个元素元素个数是9TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7如果binCount的值是7(加上数组中的的一个元素元素个数是9)TREEIFY_THRESHOLD - 1也是7此时转换红黑树*/if (binCount TREEIFY_THRESHOLD - 1) // -1 for 1st//转换为红黑树treeifyBin(tab, hash);// 跳出循环break;}/*执行到这里说明e p.next 不是null不是最后一个元素。继续判断链表中结点的key值与插 入的元素的key值是否相等*/if (e.hash hash ((k e.key) key || (key ! null key.equals(k))))// 相等跳出循环/*要添加的元素和链表中的存在的元素的key相等了则跳出for循环。不用再继续比较了直接执行下面的if语句去替换去 if (e ! null) */break;/*说明新添加的元素和当前节点不相等继续查找下一个节点。用于遍历桶中的链表与前面的e p.next组合可以遍历链表*/p e;}}/*表示在桶中找到key值、hash值与插入元素相等的结点也就是说通过上面的操作找到了重复的键所以这里就是把该键的值变为新的值并返回旧值这里完成了put方法的修改功能*/if (e ! null) { // 记录e的valueV oldValue e.value;// onlyIfAbsent为false或者旧值为nullif (!onlyIfAbsent || oldValue null)//用新值替换旧值//e.value 表示旧值 value表示新值 e.value value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}//修改记录次数modCount;// 判断实际大小是否大于threshold阈值如果超过则扩容if (size threshold)resize();// 插入后回调afterNodeInsertion(evict);return null; } 6.2将链表转换为红黑树的treeifyBin方法 节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8如果大于则将链表转换为红黑树转换红黑树的方法 treeifyBin整体代码如下 if (binCount TREEIFY_THRESHOLD - 1) // -1 for 1st//转换为红黑树 tab表示数组名 hash表示哈希值treeifyBin(tab, hash);treeifyBin方法如下所示 /*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.替换指定哈希表的索引处桶中的所有链接节点除非表太小否则将修改大小。NodeK,V[] tab tab 数组名int hash hash表示哈希值*/final void treeifyBin(NodeK,V[] tab, int hash) {int n, index; NodeK,V e;/*如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY 64),就去扩容。而不是将节点变为红黑树。目的如果数组很小那么转换红黑树然后遍历效率要低一些。这时进行扩容那么重新计算哈希值链表长度有可能就变短了数据会放到数组中这样相对来说效率高一些。*/if (tab null || (n tab.length) MIN_TREEIFY_CAPACITY)//扩容方法resize();else if ((e tab[index (n - 1) hash]) ! null) {/*1执行到这里说明哈希表中的数组长度大于阈值64开始进行树形化2e tab[index (n - 1) hash]表示将数组中的元素取出赋值给e,e是哈希表中指定位 置桶里的链表节点从第一个开始*///hd红黑树的头结点 tl :红黑树的尾结点TreeNodeK,V hd null, tl null;do {//新创建一个树的节点内容和当前链表节点e一致TreeNodeK,V p replacementTreeNode(e, null);if (tl null)//将新创键的p节点赋值给红黑树的头结点hd p;else {/*p.prev tl将上一个节点p赋值给现在的p的前一个节点tl.next p;将现在节点p作为树的尾结点的下一个节点*/p.prev tl;tl.next p;}tl p;/*e e.next 将当前节点的下一个节点赋值给e,如果下一个节点不等于null则回到上面继续取出链表中节点转换为红黑树*/} while ((e e.next) ! null);/*让桶中的第一个元素即数组中的元素指向新建的红黑树的节点以后这个桶里的元素就是红黑树而不是链表数据结构了*/if ((tab[index] hd) ! null)hd.treeify(tab);}}小结上述操作一共做了如下几件事 1.根据哈希表中元素个数确定是扩容还是树形化 2.如果是树形化遍历桶中的元素创建相同个数的树形节点复制内容建立起联系 3.然后让桶中的第一个元素指向新创建的树根节点替换桶的链表内容为树形化内容 6.3扩容方法_resize 6.3.1扩容机制 想要了解HashMap的扩容机制你要有这两个问题 1.什么时候才需要扩容2.HashMap的扩容是什么 1.什么时候才需要扩容 当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时就会进行数组扩容loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说默认情况下数组大小为16那么当HashMap中的元素个数超过16×0.7512(这个值就是阈值或者边界值threshold值)的时候就把数组的大小扩展为2×1632即扩大一倍然后重新计算每个元素在数组中的位置而这是一个非常耗性能的操作所以如果我们已经预知HashMap中元素的个数那么预知元素的个数能够有效的提高HashMap的性能。 补充 当HashMap中的其中一个链表的对象个数如果达到了8个此时如果数组长度没有达到64那么HashMap会先扩容解决如果已经达到了64那么这个链表会变成红黑树节点类型由Node变成TreeNode类型。当然如果映射关系被移除后下次执行resize方法时判断树的节点个数低于6也会再把树转换为链表。 2.HashMap的扩容是什么 进行扩容会伴随着一次重新hash分配并且会遍历hash表中所有的元素是非常耗时的。在编写程序中要尽量避免resize。 HashMap在进行扩容时使用的rehash方式非常巧妙因为每次扩容都是翻倍与原来计算的 (n-1)hash的结果相比只是多了一个bit位所以节点要么就在原来的位置要么就被分配到原位置旧容量这个位置。 怎么理解呢例如我们从16扩展为32时具体的变化如下所示 因此元素在重新计算hash之后因为n变为2倍那么n-1的标记范围在高位多1bit(红色)因此新的index就会发生这样的变化 说明5是假设计算出来的原来的索引。这样就验证了上述所描述的扩容之后所以节点要么就在原来的位置要么就被分配到原位置旧容量这个位置。 因此我们在扩充HashMap的时候不需要重新计算hash只需要看看原来的hash值新增的那个bit是1还是0就可以了是0的话索引没变是1的话索引变成“原索引oldCap(原位置旧容量)”。可以看看下图为16扩充为32的resize示意图 正是因为这样巧妙的rehash方式既省去了重新计算hash值的时间而且同时由于新增的1bit是0还是1可以认为是随机的在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数保证了rehash之后不会出现更严重的hash冲突均匀的把之前的冲突的节点分散到新的桶中了。 6.3.2源码resize方法的解读 下面是代码的具体实现 final NodeK,V[] resize() {//得到当前数组NodeK,V[] oldTab table;//如果当前数组等于null长度返回0否则返回当前数组的长度int oldCap (oldTab null) ? 0 : oldTab.length;//当前阀值点 默认是12(16*0.75)int oldThr threshold;int newCap, newThr 0;//如果老的数组长度大于0//开始计算扩容后的大小if (oldCap 0) {// 超过最大值就不再扩充了就只好随你碰撞去吧if (oldCap MAXIMUM_CAPACITY) {//修改阈值为int的最大值threshold Integer.MAX_VALUE;return oldTab;}/*没超过最大值就扩充为原来的2倍1)(newCap oldCap 1) MAXIMUM_CAPACITY 扩大到2倍之后容量要小于最大容量2oldCap DEFAULT_INITIAL_CAPACITY 原数组长度大于等于数组初始化长度16*/else if ((newCap oldCap 1) MAXIMUM_CAPACITY oldCap DEFAULT_INITIAL_CAPACITY)//阈值扩大一倍newThr oldThr 1; // double threshold}//老阈值点大于0 直接赋值else if (oldThr 0) // 老阈值赋值给新的数组长度newCap oldThr;else {// 直接使用默认值newCap DEFAULT_INITIAL_CAPACITY;//16newThr (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 计算新的resize最大上限if (newThr 0) {float ft (float)newCap * loadFactor;newThr (newCap MAXIMUM_CAPACITY ft (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//新的阀值 默认原来是12 乘以2之后变为24threshold newThr;//创建新的哈希表SuppressWarnings({rawtypes,unchecked})//newCap是新的数组长度--》32NodeK,V[] newTab (NodeK,V[])new Node[newCap];table newTab;//判断旧数组是否等于空if (oldTab ! null) {// 把每个bucket都移动到新的buckets中//遍历旧的哈希表的每个桶重新计算桶里元素的新位置for (int j 0; j oldCap; j) {NodeK,V e;if ((e oldTab[j]) ! null) {//原来的数据赋值为null 便于GC回收oldTab[j] null;//判断数组是否有下一个引用if (e.next null)//没有下一个引用说明不是链表当前桶上只有一个键值对直接插入newTab[e.hash (newCap - 1)] e;//判断是否是红黑树else if (e instanceof TreeNode)//说明是红黑树来处理冲突的则调用相关方法把树分开((TreeNodeK,V)e).split(this, newTab, j, oldCap);else { // 采用链表处理冲突NodeK,V loHead null, loTail null;NodeK,V hiHead null, hiTail null;NodeK,V next;//通过上述讲解的原理来计算节点的新位置do {// 原索引next e.next;//这里来判断如果等于true e这个节点在resize之后不需要移动位置if ((e.hash oldCap) 0) {if (loTail null)loHead e;elseloTail.next e;loTail e;}// 原索引oldCapelse {if (hiTail null)hiHead e;elsehiTail.next e;hiTail e;}} while ((e next) ! null);// 原索引放到bucket里if (loTail ! null) {loTail.next null;newTab[j] loHead;}// 原索引oldCap放到bucket里if (hiTail ! null) {hiTail.next null;newTab[j oldCap] hiHead;}}}}}return newTab; }6.4 删除方法(remove) 理解了put方法之后remove方法已经没什么难度了所以重复的内容就不再做详细介绍了。 删除的话就是首先先找到元素的位置如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除树小于6的时候要转链表。 删除remove方法 //remove方法的具体实现在removeNode方法中所以我们重点看下removeNode方法 public V remove(Object key) {NodeK,V e;return (e removeNode(hash(key), key, null, false, true)) null ?null : e.value;}removeNode方法 final NodeK,V removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {NodeK,V[] tab; NodeK,V p; int n, index;//根据hash找到位置 //如果当前key映射到的桶不为空if ((tab table) ! null (n tab.length) 0 (p tab[index (n - 1) hash]) ! null) {NodeK,V node null, e; K k; V v;//如果桶上的节点就是要找的key则将node指向该节点if (p.hash hash ((k p.key) key || (key ! null key.equals(k))))node p;else if ((e p.next) ! null) {//说明节点存在下一个节点if (p instanceof TreeNode)//说明是以红黑树来处理的冲突则获取红黑树要删除的节点node ((TreeNodeK,V)p).getTreeNode(hash, key);else {//判断是否以链表方式处理hash冲突是的话则通过遍历链表来寻找要删除的节点do {if (e.hash hash ((k e.key) key ||(key ! null key.equals(k)))) {node e;break;}p e;} while ((e e.next) ! null);}}//比较找到的key的value和要删除的是否匹配if (node ! null (!matchValue || (v node.value) value ||(value ! null value.equals(v)))) {//通过调用红黑树的方法来删除节点if (node instanceof TreeNode)((TreeNodeK,V)node).removeTreeNode(this, tab, movable);else if (node p)//链表删除tab[index] node.next;elsep.next node.next;//记录修改次数modCount;//变动的数量--size;afterNodeRemoval(node);return node;}}return null;}6.5查找元素方法(get) 查找方法通过元素的Key找到Value。 代码如下 public V get(Object key) {NodeK,V e;return (e getNode(hash(key), key)) null ? null : e.value; }get方法主要调用的是getNode方法代码如下 final NodeK,V getNode(int hash, Object key) {NodeK,V[] tab; NodeK,V first, e; int n; K k;//如果哈希表不为空并且key对应的桶上不为空if ((tab table) ! null (n tab.length) 0 (first tab[(n - 1) hash]) ! null) {/* 判断数组元素是否相等根据索引的位置检查第一个元素注意总是检查第一个元素*/if (first.hash hash // always check first node((k first.key) key || (key ! null key.equals(k))))return first;// 如果不是第一个元素判断是否有后续节点if ((e first.next) ! null) {// 判断是否是红黑树是的话调用红黑树中的getTreeNode方法获取节点if (first instanceof TreeNode)return ((TreeNodeK,V)first).getTreeNode(hash, key);do {// 不是红黑树的话那就是链表结构了通过循环的方法判断链表中是否存在该keyif (e.hash hash ((k e.key) key || (key ! null key.equals(k))))return e;} while ((e e.next) ! null);}}return null; }小结 1.get方法实现的步骤 ​ 1通过hash值获取该key映射到的桶 ​ 2桶上的key就是要查找的key,则直接找到并返回 ​ 3桶上的key不是要找的key,则查看后续的节点 ​ a:如果后续节点是红黑树节点通过调用红黑树的方法根据key获取value ​ b:如果后续节点是链表节点则通过循环遍历链表根据key获取value 2.上述红黑树节点调用的是getTreeNode方法通过树形节点的find方法进行查找 final TreeNodeK,V getTreeNode(int h, Object k) {return ((parent ! null) ? root() : this).find(h, k, null);}final TreeNodeK,V find(int h, Object k, Class? kc) {TreeNodeK,V p this;do {int ph, dir; K pk;TreeNodeK,V pl p.left, pr p.right, q;if ((ph p.hash) h)p pl;else if (ph h)p pr;else if ((pk p.key) k || (k ! null k.equals(pk)))return p;//找到之后直接返回else if (pl null)p pr;else if (pr null)p pl;else if ((kc ! null ||(kc comparableClassFor(k)) ! null) (dir compareComparables(kc, k, pk)) ! 0)p (dir 0) ? pl : pr;//递归查找else if ((q pr.find(h, k, kc)) ! null)return q;elsep pl;} while (p ! null);return null;}3.查找红黑树由于之前添加时已经保证这个树是有序的了因此查找时基本就是折半查找效率更高。 4.这里和插入时一样如果对比节点的哈希值和要查找的哈希值相等就会判断key是否相等相等就直接返回。不相等就从子树中递归查找。 ​ 若为树则在树中通过key.equals(k)查找O(logn) ​ 若为链表则在链表中通过key.equals(k)查找O(n)。 6.6遍历HashMap集合几种方式 1、分别遍历Key和Values for (String key : map.keySet()) {System.out.println(Key: key); } for (String value : map.values()) {System.out.println(value: value); }2、使用Iterator迭代器迭代 IteratorMap.EntryInteger, String iterator map.entrySet().iterator(); while (iterator.hasNext()) {Map.EntryInteger, String entry iterator.next();System.out.println(Key: entry.getKey() , Value: entry.getValue()); }3、通过get方式不建议使用 for (Integer key : map.keySet()) {System.out.println(Key: key , Value: map.get(key)); }说明根据阿里开发手册不建议使用这种方式因为迭代两次。keySet获取Iterator一次还有通过get又迭代一次。降低性能。 4.jdk8以后使用Map接口中的默认方法 default void forEach(BiConsumer? super K,? super V action) BiConsumer接口中的方法void accept​(T t, U u) 对给定的参数执行此操作。 参数 t - 第一个输入参数 u - 第二个输入参数 遍历代码 map.forEach((key, value) - {System.out.println(Key: key , Value: value); })7.初始化问题 7.1HashMap的初始化问题描述 ​ 如果我们确切的知道我们有多少键值对需要存储那么我们在初始化HashMap的时候就应该指定它的容量以防止HashMap自动扩容影响使用效率。 ​ 默认情况下HashMap的容量是16但是如果用户通过构造函数指定了一个数字作为容量那么Hash会选择大于该数字的第一个2的幂作为容量。(3-4、7-8、9-16) .这点我们在上述已经进行过讲解。 《阿里巴巴Java开发手册》中建议我们设置HashMap的初始化容量。 那么为什么要这么建议你有想过没有。 当然以上建议也是有理论支撑的。我们上面介绍过HashMap的扩容机制就是当达到扩容条件时会进行扩容。HashMap的扩容条件就是当HashMap中的元素个数size超过临界值threshold时就会自动扩容。在HashMap中threshold loadFactor * capacity。 所以如果我们没有设置初始容量大小随着元素的不断增加HashMap会有可能发生多次扩容而HashMap中的扩容机制决定了每次扩容都需要重建hash表是非常影响性能的。 但是设置初始化容量设置的数值不同也会影响性能那么当我们已知HashMap中即将存放的KV个数的时候容量设置成多少为好呢 7.2HashMap中容量的初始化 当我们使用HashMap(int initialCapacity)来初始化容量的时候jdk会默认帮我们计算一个相对合理的值当做初始容量。那么是不是我们只需要把已知的HashMap中即将存放的元素个数直接传给initialCapacity就可以了呢 关于这个值的设置在《阿里巴巴Java开发手册》有以下建议 也就是说如果我们设置的默认值是7经过Jdk处理之后会被设置成8但是这个HashMap在元素个数达到 8*0.75 6的时候就会进行一次扩容这明显是我们不希望见到的。我们应该尽量减少扩容。原因也已经分析过。 如果我们通过initialCapacity/ 0.75F 1.0F计算7/0.75 1 10 ,10经过Jdk处理之后会被设置成16这就大大的减少了扩容的几率。 当HashMap内部维护的哈希表的容量达到75%时默认情况下会触发rehash而rehash的过程是比较耗费时间的。所以初始化容量要设置成initialCapacity/0.75 1的话可以有效的减少冲突也可以减小误差。 所以我可以认为当我们明确知道HashMap中元素的个数的时候把默认容量设置成initialCapacity/ 0.75F 1.0F是一个在性能上相对好的选择但是同时也会牺牲些内存。 我们想要在代码中创建一个HashMap的时候如果我们已知这个Map中即将存放的元素个数给HashMap设置初始容量可以在一定程度上提升效率。 但是JDK并不会直接拿用户传进来的数字当做默认容量而是会进行一番运算最终得到一个2的幂。原因也已经分析过。 但是为了最大程度的避免扩容带来的性能消耗我们建议可以把默认容量的数字设置成initialCapacity/ 0.75F 1.0F。
http://www.zqtcl.cn/news/27971/

相关文章:

  • 微信h5商城网站开发哈尔滨如何快速建站
  • 深圳网站建设模板乐云seo商品网站怎么做
  • 成都网站制作是什么wordpress幻灯插件
  • 租用海外服务器的网站有域名吗快看点自媒体平台
  • 手机php网站开发wordpress 嵌套
  • 大众点评如何做团购网站子网站建设经验汇报
  • 南京营销型网站商业网名
  • 自学编程的网站如何制作简单的网站
  • 做论文查重网站代理能赚到钱吗博客园 wordpress.net
  • 网站建设方案申请做淘宝导航网站
  • 长春网站上排名网页设计作业网站素材和效果图
  • 公司网站建设工作wordpress用的编辑器
  • 用cms做网站怎么样关于怎么做网站
  • 东莞网站优化关键词公司镇江高端网站建设工作室
  • 网站上的百度地图标注咋样做wordpress 4.9.8中文版
  • 办公用品企业网站建设方案soso搜搜
  • 济南集团网站建设报价郑州专门做喷绘安装的网站
  • 做动漫姓氏头像的网站深圳做推广哪家比较好
  • 做网站下载哪个软件安徽网站seo
  • 自适应网站怎么做移动配置无锡华诚建设监理有限公司网站
  • 上海网站推广系统南昌网站设计企业
  • 网站备案 如何填石嘴山网站seo
  • 又拍网站怎么做手机显示的网站该怎样设计
  • 唐山开发网站的公司网站开发合作合同
  • 重庆服装网站建设费用wordpress模板top破解
  • 自行车网站模板网络公司构建网站
  • 展示型网站搭建wordpress那个版本好
  • 上海徐汇网站建设中国纪检监察报社长
  • 网站开发应该学哪门语言网站运营和维护都是干什么的
  • 简单电商网站模板php 金融网站源码