位运算:
与& : 全1为1,其余为0
或| : 有1为1,其余为0
异或^:不同为1,相同为0
左移<<:二进制左移 高位丢弃,底为补0
右移>>:二进制右移
概念:
- 散列表,K,V映射,允许null
- 继承AbstractMap,实现了map,cloneable,serizlizable接口
- 实现是不同步的,线程不安全
jdk1.8底层是 数组+链表+红黑树 ,1.8之前由 数组+链表 组成
hash方法:将key经过hash计算后,转换成底层数组中的索引值,可以迅速定位到在hashmap中的位置。
hash冲突:当对一个元素hash,插入的时候,已经被占用。
源码分析
hashMap中常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认容量大小static final int MAXIMUM_CAPACITY = 1 << 30; //table最大容量2的30次方static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子0.75static final int TREEIFY_THRESHOLD = 8; //链表树化阈值static final int UNTREEIFY_THRESHOLD = 6; //树降成链表的阈值static final int MIN_TREEIFY_CAPACITY = 64; //当桶元素数量超过64个,并且链表达到阈值8才会进行树化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; }
hash计算
- 先得到扰动key的hashCode h=key.hashcode^(h>>>16)
- 再映射到数组下标index (n-1) & hash
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
为什么无符号右移动16位进行高位异或运算?
当数组长度短时候,只有地位参与运算
右移shi为了让高位参与进来,更好的均匀散裂,减少碰撞,降低hash冲突概率。
异或运算保证0和1概率相等。
hashMap扩容机制:
当数组元素个数达到16 * 扩容因子 0.75 扩容原来的2倍,重新进行hash运算,重新放置元素
为什么是2的n次方
两个原因:
1.可以方便的将取余运算的逻辑转换为位运算,因为位运算效率高
以下是详细解释
HashMap容量取2的n次方,主要与hash寻址有关。在put(key,value)时,putVal()方法中通过i = (n - 1) & hash来计算key的桶的地址。其实,i = (n - 1) & hash是一个%取余操作。也就是说,HashMap是通过%运算来获得key的散列地址的。但是,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%n=a&(n-1)仅当n是2的n次方的时候方能成立,一句话,可以方便的将取余运算转换为位运算
2.当length为2的N次方的时候,数据分布均匀,不浪费空间,减少冲突
以下是详细解释
这个原因:还是要从i = (n - 1) & hash这个计算公式说起,当n是2的n次方的时候,那么n的二进制形式就是010000000000000...这样的形式。n-1就是01111111111111111.。。。。这样的形式,这样的数字进行和hash进行与运算的时候,计算出来的最后意味可能是0也可能是1,空间不浪费;
如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为 14,对应的二进制为 1110,在于 hash 与操作,与操作的结果是最后一位都为 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大。
put流程:
1.根据键的hash码(调用键的hashcode方法)进行哈希运算(hash()),得到一个整数哈希值(不是数组的下标位置)
2. 判断哈希表是否为空或者长度是否为0,如果是,要对数组进行初始化(初始化为16),如果否,进入3
3. 根据1得到的哈希值计算数组索引(与运算(n - 1) & hash),得到一个和数组存储位置匹配的索引i(确定到桶的位置)
=================================================
4. 判断i号位置是否为null,如果null,就将键和值封装为一个Entry(Node)类型的对象进行插入,如果不为null,进入5
5. 判断i号桶中的节点和新插入的节点的key是否相同(使用equals进行判断),如果存在,覆盖原有的值,如果不存在,进入6
6. 判断i号位置是否为一个树结构,如果是一个树结构,在树中进行插入,插入的时候判断键和树中节点的键是否重复,如果重复,覆盖原有值,不重复,做完新节点插入,如果不是树结构,进入7
7. 为链表结构,对链表进行遍历,判断key是否存在,存在就覆盖,不存在就在链表中插入新的节点
8. 插入新节点后,如果i号位置的元素个数大于等于8且hash表的长度大于等于64,i号位置的所有元素转换为树结构,反之,新节点正常插入结束
9. size++
=====================================================
10. 判断是否要进行扩容,如果需要扩容,就执行Resize()进行扩容
11. 结束
什么是线程安全:
- 单线程
- 多线程无共享资源
- 多线程下对共享资源能做到有序可控制访问
hashMap7 和8的区别:
- 7用的头插法,8之后用的尾插法 头插法在多线程扩容的时候,容易出现环形链表死循环问题。
- 扩容流程不同 8是扩容前插入键值,连同旧值一起转移,一起计算。 7是扩容后进行插入,旧的数据转移到新的数组之后,然后单独计算插入的位置。 8主要是为了减少红黑树和链表来回切换的频率。
- 扩容后数据存储位置的计算方式不一样
- 数据结构不一样
快速失败 fail-fast
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。