- 浏览: 806786 次
- 性别:
- 来自: 杭州
文章分类
最新评论
-
huan19900606:
像haskell这些脚本语言很容易定义DSL,实现对应的词法语 ...
DSL的基本介绍(groovy来进行构建) -
express_wind:
https://yq.aliyun.com/album/130 ...
qlexpress规则引擎初探 -
readxuxuegang:
博主你好。如果groovy的代码是保存在数据库里,不是文件,这 ...
在java中使用groovy怎么搞 (java and groovy) -
express_wind:
hi,兄弟,有没有兴趣来阿里巴巴专门做这方面的研究,https ...
qlexpress规则引擎初探 -
langcaiye:
有2个问题请教:1. 这里的base32算法为什么需要以负数的 ...
【原】geohash算法详解
关于hashmap在平时写代码的时候经常用,但是hashmap的一些原理貌似知道的不是很多,翻了下代码,得出如下结论。
(1)HashMap是啥?
HashMap是基于哈希表的Map实现,能够满足所有的Map操作,同时支持空的key和空的value,非线程安全的,
不保证map中键值的顺序,特别是不保证顺序是不变的(翻译自java 源代码)。
(2)如果Map<K,V> map = new HashMap<K,V>()这种情况,容器默认参数是啥?
源代码查看,有三个常量,
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
三个常量中可以看出,默认的容器大小是16,最大长度是2的30次方,load factor默认是0.75,扩充的临界值是16*0.75=12
(3)如果已经知道Map的大小,如何提升性能?
HashMap的实例有两个参数影响着他的性能,
一个是initial capacity
(初始化容量,容量是哈希表中的空间数,初始化容量是HashMap创建的时候的大小,当然,后面是会自动扩容的);
一个是load factor
(负荷系数,当目前的容量达到负荷系数的时候,重新build,扩充到原来的两倍)。
通用的规则,laod factor默认是0.75,在空间和时间上面一个不错的权衡。
如果确定有很多mapping的数据放在HashMap的实例里面,初始化的时候创建一个大一点的容量比hashmap自己去扩容要有效的多。
因为在数组扩充的时候,会重新new一个数组出来,然后老数组数据重新赋值到新数组,转换成本消耗资源。
(4)如果自定义initial capacity的大小,如果保证Map大小是2的指数次方?
这个看HashMap的构造行数,
如下,通过while循环,初始值1的移位来使大小始终是2的指数次,初始化的数组大小是小于入参的最大的2的指数次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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)){ //如果laodFactor小于等于零或者不是number,抛异常
throw new IllegalArgumentException( "Illegal load factor: " + loadFactor);
}
/**
* 这里设计比较巧妙了,保证HashMap的大小始终是2的指数次
* 经过这个while处理后,初始化的数组是大于这个值的最小的2的指数
* 例子:如果initialCapacity=13,则2进制数值有2、4、8、16,大约13的是16,则此时初始化的数组大小是16
*/
int capacity = 1 ;
while (capacity < initialCapacity){
capacity <<= 1 ;
}
this .loadFactor = loadFactor;
threshold = ( int )(capacity * loadFactor);
table = new Entry[capacity];
init();
}
|
(5)为什么HashMap的大小要是2的指数次呢?
key经过hash后,可以取模来进行放入数组,也不会出现越界的情况,
之所以没有使用取模,而是按位与的形式,是因为计算机的二进制运算效率比取模效率高。
如果Map的大小不是2的进制,我们设置为7
7的二进制是:111,(length-1)大小是6,按位与是和6进行,6的二进制是:110
结果如下,有些数组中的位置没有被设置,有些重复了,一是导致空间浪费,同时增加了碰撞的几率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
for ( int i= 0 ;i< 10 ;i++){
System.out.println
( "数值i=" +i+ ", 二进制=" +Integer.toBinaryString(i)+ "(" +Integer.toBinaryString( 6 )+ ")" + " ,和6按位与=" +(i& 6 ));
} 数值i= 0 , 二进制= 0 ( 110 ) ,和 6 按位与= 0
数值i= 1 , 二进制= 1 ( 110 ) ,和 6 按位与= 0
数值i= 2 , 二进制= 10 ( 110 ) ,和 6 按位与= 2
数值i= 3 , 二进制= 11 ( 110 ) ,和 6 按位与= 2
数值i= 4 , 二进制= 100 ( 110 ) ,和 6 按位与= 4
数值i= 5 , 二进制= 101 ( 110 ) ,和 6 按位与= 4
数值i= 6 , 二进制= 110 ( 110 ) ,和 6 按位与= 6
数值i= 7 , 二进制= 111 ( 110 ) ,和 6 按位与= 6
数值i= 8 , 二进制= 1000 ( 110 ) ,和 6 按位与= 0
数值i= 9 , 二进制= 1001 ( 110 ) ,和 6 按位与= 0
|
然后我们设置8,(length-1)大小是7,7的二进制是111,打印看结果,空间充分利用,并且减少了碰撞的几率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
for ( int i= 0 ;i< 10 ;i++){
System.out.println(
"数值i=" +i+ ", 二进制=" +Integer.toBinaryString(i)+ "(" +Integer.toBinaryString( 7 )+ ")" + " ,和7按位与=" +(i& 7 ));
} 数值i= 0 , 二进制= 0 ( 111 ) ,和 7 按位与= 0
数值i= 1 , 二进制= 1 ( 111 ) ,和 7 按位与= 1
数值i= 2 , 二进制= 10 ( 111 ) ,和 7 按位与= 2
数值i= 3 , 二进制= 11 ( 111 ) ,和 7 按位与= 3
数值i= 4 , 二进制= 100 ( 111 ) ,和 7 按位与= 4
数值i= 5 , 二进制= 101 ( 111 ) ,和 7 按位与= 5
数值i= 6 , 二进制= 110 ( 111 ) ,和 7 按位与= 6
数值i= 7 , 二进制= 111 ( 111 ) ,和 7 按位与= 7
数值i= 8 , 二进制= 1000 ( 111 ) ,和 7 按位与= 0
数值i= 9 , 二进制= 1001 ( 111 ) ,和 7 按位与= 1
|
(6)HashMap的整体结构啥样?
整体情况见下图,包括继承实现关系以及属性等。
1、HashMap中包含了一个Entry的数组,是存放数据的地方,每一个数组元素是一个Entry对象,Entry中有属性next,
如果两个key经过hash后,在数组中index相同,则会保存在同一个位置,通过next属性来形成链表结构。
2、size是数组的大小,threshold是数组扩充的阀值,modCount是table被修改的次数,这个在迭代器中有用,
loadFactor是数组扩充阀值系数,threshold=loadFactor*table.length。
(7)HashMap的添加属性以及扩容是如何进行的?
废话少说,直接上代码。
1、添加属性的时候,如果两个key的index位置相同,则会通过链表保存在同一个数据元素中,而后添加的在链表的前面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void addEntry( int hash, K key, V value, int bucketIndex) {
/**
* 首先把index中的值赋予一个对象e,
* 从这里能够看出,如果两个key的hash值相同,那么在数组中的位置index会相同,
* 那此时这两个key就需要组成链条来同时保存在这一个位置中,
* 后一个添加的Entry总是在链条的第一个
*/
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//如果目前数组的长度大于阀值,则进行resize,扩充为原来的2倍
if (size++ >= threshold){
resize( 2 * table.length);
}
}
|
2、在添加属性的时候,每次都会判断一下是否需要扩容,若果达到了阀值,则进行扩容,
扩容的时候会重新new一个table出来,然后新老数据数据进行转换,
调用transfer方法,transfer方法通过循环遍历的形式记性数据的“交接”,
注意一点,while里面的代码会造车在多线程并发下put出现死循环情况,如果涉及到多线程put情况,不要使用HashMap。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
void resize( int newCapacity) { //table的数组容量大于了阀值threshold,则进行扩充,变为原来的2倍;
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //如果已经达到了最大值,则threshold为Integer最大值,数组不进行扩充
threshold = Integer.MAX_VALUE;
return ;
}
Entry[] newTable = new Entry[newCapacity];
//新老数组数据转换,将老数组中的数据赋予新的table
transfer(newTable);
//将新的table赋值给引用,每次扩充,需要重新new一个数组,抛弃原先的数组
table = newTable;
threshold = ( int )(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
//循环遍历数组中的每个Entry
for ( int j = 0 ; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null ) {
src[j] = null ;
/**
* while循环遍历一个数组元素的链表,把原来链表的顺序反置了
* 多线程并发put下,在进行扩充的时候,会造成死循环;
* Entry1-->Entry2-->null 正常情况下,顺序反置回事Entry2-->Entry1-->null
* 多线程下会出现:Entry1-->Entry2,Entry2-->Entry1的情况,在while处造成死循环
*/
do {
Entry<K,V> next = e.next;
//从这里看出,元素在数组中的位置重新进行了计算
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null );
}
}
}
|
(8)hash碰撞问题HashMap如何解决的?
传入的数据,会出现key经过hash后,hash值相同,这就是hash碰撞问题,
HashMap如何解决这种碰撞问题的呢,看代码可以得出结论。
每个数组元素是一个Entry对象,对象中有个next的应用,指向下一个,对于hash值相同,则在Entry中以链表的形式进行存储。
见put函数代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public V put(K key, V value) {
//如果key是null,则调用单独的方法
if (key == null ){
return putForNullKey(value);
}
//获取key的hash值,通过key值的hashCode值来进行高位转换
int hash = hash(key.hashCode());
//通过hash值和数组长度进行按位与,获取这个key值在数据中的位置
int i = indexFor(hash, table.length);
/**
* 获取数组中index为i的Entry,如果entry不为空,则进行判断是否相同,如果相同则新老value进行替换;
* 这里有个for循环,因为一个数据元素中可能保存了一个Entry的链表
*/
for (Entry<K,V> e = table[i]; e != null ; e = e.next) {
Object k;
//hash值相同,并且==或者equals,则表明两个对象相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess( this );
return oldValue;
}
}
//如果index为i的数组中是null,则调用addEntry来添加新的Entry
modCount++;
//传入这个entry的hash值,KV,以及在数组中的位置
addEntry(hash, key, value, i);
return null ;
}
|
发表评论
-
系统分布式情况下最终一致性方案梳理
2015-09-05 19:34 40962前言 目前的应用系 ... -
Storm核心概念剖析
2015-03-20 20:42 3228最近团队中有分析的场 ... -
池和流的两种数据处理方式
2014-11-19 22:59 1396在抽象层面,想了一下,目前很多的数据处理形式,一般分为池和流 ... -
关于CodeReview(java)
2014-10-29 20:42 1908关于codereview,在平时的开发中,经常忽略的环节,参 ... -
java中各种各样的数据结构
2014-07-13 20:26 2469在java中,有非常丰富的数据结构,可能是因为大多数的软件 ... -
关于JVM的ClassLoader(笔记)
2014-07-13 12:19 1875众所周知,java是编译型的语言,写的是java文 ... -
关于事务的几个概念介绍
2014-06-06 22:22 1944啥是事务? 有一组操 ... -
开发中遇到的编码问题
2014-05-22 19:39 18841、说到编码,最大的问题就是乱码了,为啥会有乱码呢 ? 因 ... -
ThreadLocal源代码解析
2014-04-24 17:54 2408最开始的时候,理解的ThreadLocal,我的理解是这样的 ... -
关于单例模式(代码篇)
2014-04-23 10:47 2419很早的时候,转发过一篇单例模式的文章:http://iamz ... -
今天遇到的两个spring相关的两个问题
2014-04-18 21:56 2561今天在项目中写代码,遇到两个Spring的问题,记录一下。再 ... -
Activiti中的命令模式解析
2014-04-11 13:10 3199最近在看Activiti的源代码,发现是基于命令模式进行的开 ... -
关于java中的本地缓存-总结概述
2014-03-31 19:00 18374java中的本地缓存,工作后陆续用到,一直想写,一直无从下 ... -
使用guava中的EventBus构建内存级别的事件引擎
2014-03-25 19:27 6408这个EventBus是guava中比较给力的一个类,从字面 ... -
DSL的基本介绍(groovy来进行构建)
2014-03-04 23:32 17055什么是DSL? 领域特定 ... -
qlexpress规则引擎初探
2014-02-25 22:28 25122qlexpress是啥? 这个是阿里内部的一个开源的jav ... -
在java中使用groovy怎么搞 (java and groovy)
2014-01-15 23:17 10961什么是groovy? 一种基于Java虚拟机的动态语言, ... -
java中记录方法调用时间,结果按照方法的层级树状的输出
2013-12-21 17:36 4687 在java中,最常用的埋点时间的方法就 ... -
一次CMS GC问题排查过程(理解原理+读懂GC日志)
2013-12-14 22:21 41350这个是之前处理过的一个线上问题,处理过程断断续续,经历了两 ... -
令牌桶算法和漏桶算法以及流量控制浅谈
2013-11-27 23:20 20795 在双十一等大促环节,系统需要限流,外部 ...
相关推荐
在深入探讨HashMap的实现原理之前,我们需要了解两个关键的接口方法:`hashCode()`和`equals()`。 根据《Effective JAVA》的建议,当重写`equals()`方法时,也应重写`hashCode()`方法。这是因为在HashMap中,`...
Java HashMap原理分析 Java HashMap是一种基于哈希表的数据结构,它的存储原理是通过将Key-Value对存储在一个数组中,每个数组元素是一个链表,链表中的每个元素是一个Entry对象,Entry对象包含了Key、Value和指向...
### HashMap原理详解 #### 一、HashMap简介与应用场景 HashMap是Java集合框架中一个非常重要的组成部分,它提供了基于键值对(key-value)映射的高效数据存储方式。由于其内部采用了数组加链表(以及红黑树优化)的...
HashMap是Java编程语言中最常用的集合类之一,它属于`java.util`包,提供了一种以键值对形式存储数据的数据结构。HashMap的核心在于其高效的数据查找、...阅读“HashMap原理.pdf”文件,可以获得更深入的细节和示例。
HashMap底层原理.md
详细介绍了hashMap原理,值得一看,对于面试者有很大帮助
一线大厂BATJ面试题讲解-hashmap原理实现
hashMap基本工作原理,图解分析,基础Map集合
HashMap的扩容机制也是其高效性的关键。当HashMap的负载因子(已存储元素数量 / 容量)达到默认的0.75时,会触发扩容操作。扩容会创建一个新的、容量翻倍的Entry数组,并将旧数组中的所有元素重新插入到新数组中。这...
要理解HashMap的工作原理,首先需要了解它如何结合数组和链表的特点来提升性能。数组的特点是查找速度快,因为它们在内存中是连续分布的,这使得通过下标访问特定元素的时间复杂度是O(1)。但数组的缺点在于其大小是...
在Java中,HashMap是一种广泛使用的数据结构,它基于哈希表的Map接口实现。哈希表是一种通过哈希过程将键映射到特定位置的数据结构,该位置...理解其基于哈希的工作原理对于充分利用HashMap的性能优势是非常有帮助的。
hashMap基本工作原理,图解分析,基础Map集合
深入探讨HashMap的底层结构、原理、扩容机制,
HashMap的扩容机制在JDK 1.8中有优化,其容量大小必须为2的n次方,主要是为了保证在计算索引位置时,通过位运算可以实现快速定位。具体来说,通过哈希值与数组长度减一的结果进行位运算,可以得到一个更分散的索引...
二、HashMap底层原理 HashMap的内部实现基于数组+链表/红黑树的结构。数组中的每个元素都是一个Entry对象,每个Entry包含键值对和指向下一个Entry的引用。当冲突较多导致链表过长时,会自动转换为红黑树,以保证查找...
**HashMap简介** HashMap是Java编程语言...总的来说,HashMap是Java中不可或缺的数据结构,通过理解其工作原理、存储结构和优化策略,可以帮助开发者更有效地利用HashMap,优化程序性能,同时避免潜在的线程安全问题。
HashMap原理的深入理解 HashMap是基于哈希表的Map接口的非同步实现,提供了所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久...
本文将深入探讨HashMap的内部机制,包括其构造、工作原理、哈希函数、冲突解决策略以及扩容机制。 首先,HashMap的基本结构是由数组(Entry[] table)和链表组成的。每个元素是一个内部类Entry,它包含了键值对...
hashmap实现原理.pdf