重读HashMap
精简篇幅,本篇暂不讨论为java stream服务的特性
1. Interfaces
- Map<K,V>
- Cloneable
- Serializable
2. Abstract super class
- AbstractMap<K,V>
3. Static fields
1 | /** |
4. class fields
1 | /** |
5. Inner classes
5.1简单存储对象
simpleEntry:键值对存储对象。
simpleImmutableEntry:不支持修改的键值对存储对象。
共性:都重写了equal方法,比较key & value
5.2 hash与红黑树
e g:
- hash散列作用
- hash算法选择
- hash算法不可控因素
- 数组与红黑树转换条件、原因
- 红黑树机制简单介绍
5.2.1 什么是hash算法
又称散列算法,是一种从任意文件中创造小的数字「指纹」的方法。
与指纹一样,散列算法就是一种以较短的信息来保证文件唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。因此,当原有文件发生改变时,其标志值也会发生改变,从而告诉文件使用者当前的文件已经不是你所需求的文件。
特点:
- 正向快速
- 逆向困难
- 输入敏感
- 冲突避免:很难找到两段内容不同的明文,使得它们的 hash 值一致(发生冲突)。
常见实现方式:取模/异或/位运算
下面给出在Java中几个常用的哈希码(hashCode)的算法。
- Object类的hashCode. 返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。这个是native方法,取决于JVM的内部设计,一般是某种C地址的偏移。
- String类的hashCode. 根据String类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。
- Integer等包装类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。
- int,char这样的基础类,它们不需要hashCode,如果需要存储时,将进行自动装箱操作,计算方法同上。
5.2.2 散列
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
取hashcode参与运算,因为hashcode返回值为int(32位)
所以,将hashcode右移16位,再做异或运算,混合了高低位信息。
5.2.3 从hash到index
i = (tab.length - 1) & hash;
与运算 – 取一个数中指定位
index取值范围[0, tab.lenth -1]
与运算后,即为对应index。
从上文 [从hash到index](#####5.2.3 从hash到index)可以发现,在寻找map元素位置过程中,使用了与运算取hash结果中指定位。结合map容量字段定义,可以发现,容量为2的指数倍,才能刚好取hash结果中指定位。另一个可得结果是,map的数组大小,不会超过2的16次方。
5.2.4 hash碰撞
散列算法分布均为,也无法控制输入的数据分布。所以碰撞时必然的,甚至分布十分集中。
数组中存储 Node
对象。
当该index中对象碰撞较少时,以链表存储。新node放置在最前面。
当该index中对象碰撞较多时,链表转换为红黑树TreeNode
。红黑树高度为log n,所以其查找、插入、删除操作复杂度均为O(log N)。
5.2.5 红黑树介绍(省略……哈哈)
5.3 并行处理API(忽略)
5.4 map数据访问对象
- entrySet
- keySet
- valueSet
隐藏了底层复杂的数据结构(数组、链表、红黑树)。简化数据遍历。
实例化的内部类,持有hashmap的字段信息。同时自身也会被作为hashmap中字段存储,可视为map的视图。
5.4.1 map数据遍历与删除对象
set对象内部封装了迭代器,允许在迭代器中遍历、删除对象。
iterator
保存了当前Node
和下一个Node
。如果此时其他线程修改了map结构(增加删除key),那么下一个Node
可能就不再存在,或者变成别的。
fast-fail
hashmap是不允许并发操作的(get除外)
- 并发put/remove
- hash碰撞时,链表新元素插入头部。先put的可能消失
- hash碰撞时,红黑树同上(根结点会被替换)
- 并发(put/remove) 和 iterator
- 每次key发生变化,modCount就会+1。iterator每次next后,都会检查next操作后modCount与iterator保存的expectedModCount是否一致。一旦不一致,就会抛出
ConcurrentModificationException
。
- 每次key发生变化,modCount就会+1。iterator每次next后,都会检查next操作后modCount与iterator保存的expectedModCount是否一致。一旦不一致,就会抛出
Ps: fast-fail可以作为一个代码bug的提示。hashmap普通的修改操作不支持并发,可能会丢数据(扩容问题cover later)。iterator操作时,完全不允许key发生变更,故一旦发现操作数发生变化,抛出异常。
6. 扩容过程&并发扩容问题
死循环