文章目录
- 1 Collection
- 1.1 List 有序,可重复
- 1.1.1 ArrayList
- 1.1.1.1 线程不安全:
- 1.1.1.2 报错并发修改异常(java.util.ConcurrentModificationException)
- 1.1.1.3 解决
- 1.1.2 Vector
- 1.1.3 LinkedList
- 1.2 Set 无序,唯一
- 1.2.1 HashSet(无序,唯一)
- 1.2.3 LinkedHashSet(FIFO插入有序,唯一)
- 1.2.4 TreeSet(有序,唯一)
- 2 Map
- 2.1 HashMap
- 2.1.1 hashcode
- 2.1.2 哈希冲突
- 2.1.3 底层数据结构
- 2.1.4 默认容量,负载因子
- 2.1.5 hashMap数组下标的计算规则
- 2.1.6 为什么要2的幂以及扩容也要为2的幂?
- 2.1.7 hashMap的put过程
- 2.1.8 hashMap的resize()操作对之前数据的处理
1 Collection
1.1 List 有序,可重复
1.1.1 ArrayList
线程不安全,效率高
底层数据结构:数组
优点:查询快
缺点:增删慢
默认大小:10
扩容:1.5倍(10->15)(15->22)
1.1.1.1 线程不安全:
public static void main(String[] args) {
List<String> list = new ArrayList<>();//替换行
for (int i = 1; i <= 5; i++){
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
1.1.1.2 报错并发修改异常(java.util.ConcurrentModificationException)
1.1.1.3 解决
1. vector
2. List list = Collections.synchronizedList(new ArrayList<>());
3. List list = new CopyOnWriteArrayList<>(); //写时复制 先复制再修改
1.1.2 Vector
线程安全,效率低
底层数据结构:数组
优点:查询快
缺点:增删慢
默认大小:10
扩容:2倍(10->20)
1.1.3 LinkedList
线程不安全
底层数据结构:双向链表
优点:增删快
缺点:查询慢
1.2 Set 无序,唯一
Set存储是借助Map实现,add代码
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其中PRESENT为new Object
Set把值存进map的key中实现值的唯一性
而Map的key唯一性用到了hashcode(),equals()
1.2.1 HashSet(无序,唯一)
线程不安全
底层数据结构:哈希表
1.2.3 LinkedHashSet(FIFO插入有序,唯一)
底层数据结构:链表和哈希表
1.由链表保证元素有序
2.由哈希表保证元素唯一
1.2.4 TreeSet(有序,唯一)
底层数据结构:红黑树
1.自然排序,比较器排序保证元素有序
2.根据比较的返回值是否是0来决定,保证元素唯一性
2 Map
由于Map的key的唯一性需要hashcode(),equals()保证,
所有对象要放进Map时要重写这两个方法
2.1 HashMap
2.1.1 hashcode
Java中的hashCode方法就是根据一定的规则将与对象相关的信息映射成一个数值,这个数值称作为散列值
2.1.2 哈希冲突
计算出的哈希值是一样的时候
2.1.3 底层数据结构
hashMap在
jdk1.7的实现为数组+链表
jdk1.8的实现为数组+链表+红黑树
当哈希冲突的时候用链表解决
但是哈希冲突剧烈的时候链表可能较长,所以引入了红黑树
形成红黑树的条件:数组容量到达64并且链表的长度到达8
2.1.4 默认容量,负载因子
初始容量是16,负载因子默认0.75,最大容量2^30
当数组容量到达12(16*0.75=12)的时候触发扩容
2.1.5 hashMap数组下标的计算规则
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 第一步
key.hashCode():先用jdk里面的hashcode算出对象本身的哈希值
h >>> 16:无符号右移16位 ,>>> 跟>>很像,只是无符号右移动之后二进制前面的高位被0补充
然后对算出的两个值 异或(^)
右移16位可以保留下高16位与低16位的特征 - 第二步
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
其中n=当前数组的长度,数组的下标为0~n-1,。
索引= (数组的长度-1)& hash
上面的索引公式相当于 hash%数组的长度(当数组的长度为2的n次幂)
为什么要%数组的长度呢?
因为hash的值可能超过了当前数组的长度,%之后hash就在数组的范围之内了为什么不用hash%数组的长度呢?
按位与是直接二进制计算,所以计算的速度比%快
2.1.6 为什么要2的幂以及扩容也要为2的幂?
1.1 n为2的幂次方时 二进制1较多
如16(10000)- 1 = 15(1111)
如 8(1000) - 1 = 7(111)
1.2 n不为2的幂次方时二进制0较多
如17(1 0001) -1 = 16(10000)
如19(1 0011) -1 = 18(1 0010)
1.3 当再进行按位与(&)运算的时候(&运算二进制对应位位都为1才为1)n为2的幂次方时由于1较多,与hash运算之后可以有效的避免hash冲突(1和0应该分布较均匀),而n不为2的幂次方时,由于0的位数较多,计算出来的数组下标大概率0的位数也较多(&的特性),这可能增大hash冲突取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作
(也就是说 hash%lengthdehash&(length-1)的前提是 length 是2的n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。
2.1.7 hashMap的put过程
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 第一次put会扩容(数组为16)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
- 经过计算得到数组下标如果当前下标为null(即当前数组还没有存放元素)就放元素到当前下标的位置
if ((p = tab[i = (n - 1) & hash]) == null)
ab[i] = newNode(hash, key, value, null);
下面是发生的hash碰撞的的情况
- 当put的key于之前的key相同时时会把之前的值覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
- 当阈值大于8并且数组大于64时,链表已经转换为红黑树,把元素添加到红黑树中
else if (p instanceof TreeNode) //判断当前节点是否为红黑树的节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
- 遍历链表进行值的覆盖或转化为红黑树
for (int binCount = 0; ; ++binCount) { //遍历链表
if ((e = p.next) == null) { //当节点到达末尾时
p.next = newNode(hash, key, value, null); //添加一个节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //当节点大于阈值8时,转换为红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //与之前的key重复,值覆盖
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
- 当超过数组的扩容边界时,进行扩容
++modCount;//节点数量增加1
if (++size > threshold) //当超过数组的扩容边界时,进行扩容
resize();
2.1.8 hashMap的resize()操作对之前数据的处理
它会重新计算一次数组中的哈希值
计算公式:之前计算好的哈希值&旧数组的长度
如果e.hash & oldCap == 0还是在原来的下标处
如果e.hash & oldCap == 1 则在原来下标的基础上再加上旧数组的长度