散列图;
图(Map)是一种依照键值对的形式进行存储的数据结构。
HashMap基于哈希表实现也可以说HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,JDK1.8之前就是数组+链表。如下如所示:
哈希图是基于哈希表实现的,那么问题就可以换个说法:
哈希表为什么以数组+链表+红黑树的存储方式实现呢,这样的存储方式有什么优点呢?
咱们先不考虑红黑树,因为它是JDK1.8的新特性。咱们先分析为啥要数组+链表!
先普及一下哈希相关知识:
哈希法又称散列法、杂凑法以及关键字地址计算法等,相应的表称为哈希表。这种方法的基本思想是:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。
当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。实际中,冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。
处理冲突的方法有哪些呢?
主要解决冲突方法有两种:
1、开放定址法
2、链地址法(数组+链表)
Java采用的就是第二种解决冲突的方法,所以哈希表是数组+链表实现的。也就是说哈希表不一定要用数组+链表的形式,只不过Java中的哈希表是通过数组+链表实现的。
这种存储方式的优点其实就是哈希技术的优点:查询速度快。
HashMap提供了四个构造函数:
HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。
HashMap(Map
约定前面的数组结构的每一个格格称为桶
约定桶后面存放的每一个数据称为bin
bin这个术语来自于JDK 1.8的HashMap注释。
size表示HashMap中存放KV的数量(为链表和树中的KV的总和)。
capacity译为容量。capacity就是指HashMap中桶的数量。默认值为16。
loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
threshold表示当HashMap的size大于threshold时会执行resize操作。
threshold=capacity*loadFactor
我们以默认HashMap为例:
capacity(桶):16
loadFactor(装载因子):0.75
threshold=16*0.75=12
当size大于12的时候就会引发扩容。
我们创建一个HashMap他的capacity和loadFactor就确定了,然后两者的乘积就是极限值,当hashmap里面的键值对数量大于极限值就扩容,这么一分析这几个参数就好理解了。
背景知识:
HashMap的equals方法是继承自Object的equals方法,Object的equals方法是比较内存地址的是否相等。
HashMap的hashcode方法是继承自Object的hashcode方法,Object的hashcode方法是根据内存地址换算出来的一个值。
题目有两个信息:
1、equals方法不是必须重写的!
2、在重写equals方法的前提下必须重写hashcode方法
第一点大家可能不理解,为什么我见到的equals方法都重写,感觉equals方法就是必须要重写的啊?!
在我们实际应用比如公安系统,判断两个人是否想得是看他们身份证号码,也就是说equals方法比较的是身份证号码是否相同,而不是比较内存地址。也就是说我们看到的equals方法都重写的原因是业务需求,当Object的equals方法不能满足业务需求的时候自然需要重写。当Object的equals方法能满足业务需求的时候可以不重写。
对于第二点,假设在公安系统中有两个人:A(123,张三),B(123,李四)key是身份证号码,value是名字,现实生活中身份证号不能重复,两个人在系统中共存。如果我们重写过equals方法(比较身份证号是否相同)后并没有重写hashcode方法有可能出现什么情况呢?根据hashmap的存储数据流程,它先计算hashcode,然后根据hashcode值来把他分配到桶数组中(如果不重写hashcode代码,问题就出现在这一步),然后再调用equals方法判断这个桶数组中是否有相同的key,有则覆盖没有则直接添加。
但是你能保证这两个人的身份证号经过hashcode之后存在同一个桶数组中吗?如果不存在同一个数组中会出现什么问题呢?存在两个“同样”的人,没错,本应该是重复的人(本应该进行覆盖操作的人)由于你没有修改hashcode进入不同的桶数组中,此时他会在这个桶数组中查找有没有其他身份证.equals相同的数据,没有就添加。
我配个图大家就明白了:
第一幅图是AB地址hashcode相同的情况下,会发生覆盖,也就是说没有重复的元素。
第二幅图AB地址hashcode不同的情况下,不会发生覆盖,也就是说存在了重复的元素。这与实际不符!
其实大家只要记住一个宗旨:keyA.equals(keyB)相同,那么keyA.hascode(keyB)也必须相同。重写hashcode就是为了保证这一点!
HashMap的put方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习。
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
JDK1.8HashMap的put方法源码如下:
1 public V put(K key, V value) {
2 // 对key的hashCode()做hash
3 return putVal(hash(key), key, value, false, true);
4 }
5
6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node[] tab; Node p; int n, i;
9 // 步骤①:tab为空则创建
10 if ((tab = table) == null || (n = tab.length) == 0)
11 n = (tab = resize()).length;
12 // 步骤②:计算index,并对null做处理
13 if ((p = tab[i = (n - 1) & hash]) == null)
14 tab[i] = newNode(hash, key, value, null);
15 else {
16 Node e; K k;
17 // 步骤③:节点key存在,直接覆盖value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步骤④:判断该链为红黑树
22 else if (p instanceof TreeNode)
23 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
24 // 步骤⑤:该链为链表
25 else {
26 for (int binCount = 0; ; ++binCount) {
27 if ((e = p.next) == null) {
28 p.next = newNode(hash, key,value,null);
//链表长度大于8转换为红黑树进行处理
29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30 treeifyBin(tab, hash);
31 break;
32 }
// key已经存在直接覆盖value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k))))
35 break;
36 p = e;
37 }
38 }
39
40 if (e != null) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = value;
44 afterNodeAccess(e);
45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步骤⑥:超过最大容量 就扩容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }
扩充为原来的2倍
在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。
HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap,ConcurrentHashMap引入了分段锁。
除了ConcurrentHashMap以外Collections.synchronizedMap(Map)也可以在多线程中使用,那两者的差别呢?
ConcurrentHashMap
SynchronizedHashMap
参考资料:
http://www.cnblogs.com/xingzc/p/5765572.html
http://panlianghui-126-com.iteye.com/blog/968057#comments
http://blog.csdn.net/chenssy/article/details/18323767
http://blog.csdn.net/cike110120/article/details/8675366
http://blog.csdn.net/not_in_mountain/article/details/77887491
http://blog.csdn.net/hwz2311245/article/details/51454686