Hash、hash函数、哈希表、哈希冲突、equals
https://blog.csdn.net/u012835097/article/details/79407591
https://blog.csdn.net/weixin_38405253/article/details/91922340
https://blog.csdn.net/fenglibing/article/details/8905007
https://blog.csdn.net/qq_36523667/article/details/81205987
集合
集合本身是一个工具,它存放在java.util包中。
集合和数组既然都是容器,它们有啥区别呢?
1、数组的长度是固定的。集合的长度是可变的。
2、数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
单列集合 java.util.Collection
1、List 的特点是元素有序、元素可重复。元素是以一种线性方式进行存储的。是一个带有索引的集合
-
java.util.ArrayList 集合数据存储的结构是数组结构(对象数组)。元素增删慢,查找快。
ArrayList基本等同于Vector,除了ArrayList是线程不安全的(执行效率高)[看源码,没有用 synchronized],多线程下不建议使用ArrayList。
-
java.util.LinkedList 集合数据存储的结构是链表结构(LinkedList是一个双向链表)。元素查询慢,增删快。
线程不安全,没有实现同步。
2、Set 的特点是元素无序,而且不可重复(所以最多只能又一个null)。没有索引,Set集合取出元素的方式可以采用:迭代器、增强for,不能使用索引的方式来遍历。
-
java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持的。
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于: hashCode 与 equals 方法。
2.2 HashSet****集合存储数据的结构(哈希表)
什么是哈希表呢?
在JDK1.8之前,哈希表底层采用数组+链表(单向)实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。 但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈
希表存储采用数组+链表(单向)+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找
时间。
知识点:
左移:x<<y //x * y个2
右移:x>>y //x / y个2
操作:
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,
这些方法可用于操作所有的单列集合。方法如下:
-
public boolean add(E e) : 把给定的对象添加到当前集合中 。
-
public void clear() :清空集合中所有的元素。
-
public boolean remove(E e) : 把给定的对象在当前集合中删除。
-
public boolean contains(E e) : 判断当前集合中是否包含给定的对象。
-
public boolean isEmpty() : 判断当前集合是否为空。
-
public int size() : 返回集合中元素的个数。
-
public Object[] toArray() : 把集合中的元素,存储到数组中。
补充:
多态:
父类名称 对象名 = new 子类名称(); //左父右子,右侧子类对象就被当作父类进行使用
或者:
接口名称 对象名 = new 实现类名称();接口的多实现:一个类是可以实现多个接口。
接口的多继承:一个接口能继承另一个或者多个接口
迭代器:
//public boolean hasNext() :如果仍有元素可以迭代,则返回 true。
//public E next() :返回迭代的下一个元素。
//所有实现了Collecation接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
//使用迭代器遍历
Iterator<String> it = coll.iterator(); // 泛型指的是 迭代出 元素的数据类
while(it.hasNext()){ //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
//当退出while后,这时iterator迭代器指向最后一个元素
it.next(); //报错NoSuchElementException
//如果想再次迭代遍历,需要重置迭代器
it = col.iterator();
while.....
增强for循环(也称for each循环),专门用来遍历数组和集合的。它的内部原理是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
//格式:
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
//使用增强for遍历数组
for(int a : arr){//a代表数组中的每个元素
System.out.println(a);
}
//使用增强for遍历集合
for(String s :coll){//接收变量s代表 代表被遍历到的集合元素
System.out.println(s);
}
List接口
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操
作集合的特有方法,如下:
-
public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
-
public E get(int index) :返回集合中指定位置的元素。
-
public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
-
public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
ArrayList集合
源码
==1、==ArrayList中维护了一个Object类型的数组elementData
transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列号
==2、==当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
==3、==如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
- 分析—无参构造
//@SuppressWarnings({"all"})//抑制警告处理
public class ArrayListSource {
public static void main(String[] args) {
//使用无参构造器创建ArrayList对象
ArrayList list = new ArrayList();
//ArrayList list = new ArrayList(8);
//使用for给list集合添加 1-10数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用for给list集合添加 11-15数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);
}
}
- Debug
注:我的 jdk版本14
1、设置断点(让程序在断点处停下来)
2、进入debug
3、单步调试
step over:程序向下执行一行(如果当前行有方法调用,这个方法将被执行完毕返回,然后到下一行)
step into:程序向下执行一行。如果该行有自定义方法,则运行进入自定义方法(不会进入官方类库的方法)。
Force step into:该按钮在调试的时候能进入任何方法。
step out:如果在调试的时候你进入了一个方法中,并觉得该方法没有问题,你就可以使用stepout跳出该方法,返回到该方法被调用处的下一行语句。值得注意的是,该方法已执行完毕。
Drop frame:点击该按钮后,你将返回到当前方法的调用处重新执行,并且所有上下文变量的值也回到那个时候。只要调用链中还有上级方法,可以跳到其中的任何一个方法。
3.1、无参构造器,初始elementData容量为0
Force step into进入无参构造
初始化了一个空的对象数组:DEFAULTCAPACITY_EMPTY_ELEMENTDATA—>Ctrl点进去,可见初始化了一个空的对象数组
连续step over跳出无参构造方法。然后进入for循环
3.2、第1次添加,扩容elementData为10
Force step into进入add方法。
首先对int类型做了装箱Integer。(Integer做了缓存,-128至127,当你取值在这个范围的时候,会采用缓存的对象,当不在这个范围,内部创建新的对象。)
连续step over退出int类型的装箱,到add方法,再Force step into真正进入add方法。
E泛型,代表元素的意思,添加的元素为1
modCount++; //记录当前集合被 修改 的次数,是为了防止多线程操作出现异常
modCount:用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并立即抛出 ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你(你已经出错了)。
step over、Force step into进入私有的add方法
if为ture, Force step into进入扩容方法grow()
Force step into进入扩容方法grow()
if不满足,进入else
第1次添加,扩容elementData为10
连续step over,return。
size // 就相当于elementData的索引
连续step over,就出来了
3.3、第2次添加 —> i=2
已经扩容到10 了,不需要进入grow()了
i=3…10步骤同上
3.4、第11次添加–>需要再次扩容,则扩容elementData为1.5倍
满足if条件
oldCapacity >> 1 //相当于*0.5,即10*0.5=5
Force step into进入ArraysSupport.newLength()
ArraysSupport.newLength()方法返回新的数组长度(10+10*0.5=10*1.5=15
)
放入数据,进行扩容elementData。Array.copyof()扩容后可以保留原先的数据(可以进去看源码)
连续step over,return
可以看到从add()方法出来后,elementData扩容到15 了,后四位为null(Not showing null elements)
//注意,Idea 默认情况下,Debug 显示的数据是简化后的,如果希望看到完整的数据需要做设置
3.5、第12次添加 —> i=12
不需要扩容
//使用for给list集合添加 11-15数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100); //直到16添加时,又进行1.5倍扩容,`15+15*1/2=22`..........
- 分析–有参构造
初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
....
ArrayList list = new ArrayList(8);
....
Force step into进入有参构造
只是这个初始化不同,其余扩容…都一样
Vector
1、ector类的定义说明
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
2、Vector底层也是一一个对象数组,protected Object[] elementData
3、Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
4、在开发中,需要线程同步安全时,考虑使用Vector
5、ArrayList和Vector比较
//@SuppressWarnings({"all"})//抑制警告处理
public class VectorSource {
public static void main(String[] args) {
//无参构造器
//有参数的构造
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100);
System.out.println("vector=" + vector);
}
}
源码分析
直接debug就好,挺简单的
LinkedList集合
-
LinkedList 说明
1、LinkedList底层实现了双向链表和双端队列特点
2、可以添加任意元素(元素可以重复),包括null
3、线程不安全,没有实现同步 -
LinkedList 的底层操作机制
1、LinkedList底层维护了一一个双向链表
2、 LinkedList中维护了两个属性first和last分别指向首节点和尾节点
3、每个节点(Node对象) ,里面又维护了prev、 next、 item三个属性,其中通过
prev指向前一个,通过next指向后一个节点。最终实现双向链表
4、所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。 -
如何选择ArrayList和LinkedList:
1、如果我们改查的操作多,选择ArrayList
2、如果我们增删的操作多,选择LinkedList
3、一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
4、在一个项目中, 根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择。
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。
- public void addFirst(E e) :将指定元素插入此列表的开头。
- public void addLast(E e) :将指定元素添加到此列表的结尾。
- public E getFirst() :返回此列表的第一个元素。
- public E getLast() :返回此列表的最后一个元素。
- public E removeFirst() :移除并返回此列表的第一个元素。
- public E removeLast() :移除并返回此列表的最后一个元素。
- public E pop() :从此列表所表示的堆栈处弹出一个元素。
- public void push(E e) :将元素推入此列表所表示的堆栈。
- public boolean isEmpty() :如果列表不包含元素,则返回true。
知识点
public class Test {
public static void main(String[] args) {
/*
//s1改变,s2也会跟着改变
//因为:s1 = s2,两个地址一样
Student s1 = new Student();
Student s2 = s1;
s1.setId(1);
System.out.println(s1); //Student{id=1}
System.out.println(s2); //Student{id=1}*/
//s1改变,s2不会改变
//因为:另一个对象赋值给了s1,s1指向了另一个地址
Student s0 = new Student();
s0.setId(1);
Student s1 = s0;
Student s2 = s1;
System.out.println(s2); //Student{id=1}
Student s3 = new Student();
s3.setId(2);
s1 = s3;
System.out.println(s1); //Student{id=2}
Student s1 = new Student();
Student s2 = null;
/*
//new一个类对象和使用类名创建一个对象
Student s1 = new Student();
Student s2;
存储空间上不同。new出来的在堆上,直接定义的在栈上。
栈上分配的在函数结束后会自己释放,堆上的要自己手工释放。
栈溢出...内存回收....学了JVM应该会清楚
*/
/*
Student s1 = new Student();
Student s2 = new Student();
//堆内存有两个对象 new Student()
*/
}
}
源码分析
//@SuppressWarnings({"all"})//抑制警告处理
public class Time {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList=" + linkedList);
//演示一个删除结点的
linkedList.remove(); // 这里默认删除的是第一个结点
//linkedList.remove(2);
System.out.println("linkedList=" + linkedList);
//修改某个结点对象
linkedList.set(1, 999);
System.out.println("linkedList=" + linkedList);
//得到某个结点对象
//get(1) 是得到双向链表的第二个对象
Object o = linkedList.get(1);
System.out.println(o);//999
//因为LinkedList 是 实现了List接口, 遍历方式
System.out.println("===LinkeList遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next=" + next);
}
System.out.println("===LinkeList遍历增强for====");
for (Object o1 : linkedList) {
System.out.println("o1=" + o1);
}
System.out.println("===LinkeList遍历普通for====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
}
}
直接debug就好,挺简单的
add
size//集合大小
modCount++; //记录当前集合被 修改 的次数,是为了防止多线程操作出现异常
Set接口
HashSet
public static void main(String[] args) {
Set<?> hashSet = new HashSet<>();
}
//java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持
HashSet结构分析:
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层 (HashMap 的底层结构)
//1. 创建一个数组,数组的类型是 Node[]
//2. 有些人,直接把 Node[] 数组称为 表
Node[] table = new Node[16];
//3. 创建结点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;// 将jack 结点挂载到john
Node rose = new Node("Rose", null);
jack.next = rose;// 将rose 结点挂载到jack
Node lucy = new Node("lucy", null);
table[4] = lucy; // 把lucy 放到 table表的索引为3的位置.
System.out.println("table=" + table);
}
}
class Node { //结点, 存储数据, 可以指向下一个结点,从而形成链表
Object item; //存放数据
Node next; // 指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
源码
1、HashSet 底层是HashMap支持的
2、添加一个元素时,先把得到hash值会转成—>table的索引
3、找到存储数据表table,判断该索引位置是否已经存放元素
4、如果没有,直接加入
5、如果有,调用equals比较 该元素的key和准备加入的key是否相同(如重写equals,是按自己定义的规则比较),如果相同,就放弃添加(存入的元素就是key,key不会添加,但是占位的value:PERSENT会替换,占位的value都是PERSENT,所以替换后相当于没有变),如果不相同需要判断是树结构还是链表结构,并作出相应处理。添加时发现容量不够侧需要扩容。
6、在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),并且 table的大小 >=MIN TREEIFY CAPACITY(默认64),(只是这条链表)就会进行树化(红黑树)。
如果某条链表的元素个数到达8,但是table的大小没有64,再次在这条链表加元素(假设元素不重复),并不会树化,还是加在这条链表后面,而且table以2倍数扩容。再次在这条链表上加元素,一样的加在链表后面,一样的table以2倍数扩容。重复操作,直到table大小也到达64,这时,再次在这条链表上加元素,整条链表就会树化。
- 扩容
1、第一次添加时,table 数组扩容到16,临界值(threshold) = 1 6*
加载因子(loadFactor 0.75) = 12 [提前扩容(缓冲层),假如大量线程添加,数组余量不够,再去扩容,就会有时间阻塞]
2、如果table数组使用到了临界值12(不管是加在数组value上还是链表上都算。源码是++size,记录的是加入元素的个数),再一次添加就会以2倍数扩容到16*
2 = 32,新的临界值就是32*
0.75 = 24,依次类推。
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
}
}
debug分析
1、HashSet 底层是HashMap支持的
HashMap构造器,初始化加载因子,loadfactor = 0.75
2、HashSet的add方法就是HashMap 的put方法。
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
//PRESENT是静态的,共享的。不管put多少次,PRESENT都是一个Object对象。他是用来占位的
2.1、Force step into进入hash()方法(2.1和2.2结合着看)
异或:^ (同真异假)
无符号右移:>>> (h >>> 16是用来取出h的高16位)
与:&
源码是:key的 hashcode 异或 他的高16位后 再与数组的长度-1后的值相 &与 来算得数组的索引值
1、这种方法保证了hashcode32位全部参与计算,也保证了0,1平均,使得到的哈希值更加随机,获得的下标索引更加散列,可以减少碰撞。
2、hash%table.length
在数学上等效于(n-1)&hash
。异或,与,无符号右移这些都是关于没有模的位运算,而模运算比按位运算慢,如此可提高计算速度。
3、使用&
操作代替%
,不仅仅是为了速度,而且哈希值是int
类型,可以是负数。对负数取模将是负数,用&
可以保证正索引。(hashcode是int类型32位,当获取的数大于32位时,hashcode值会溢出变成负数)
//源码是:key的 hashcode 异或 他的高16位后 再与数组的长度-1后的值相 与 来算得数组的索引值
return putVal(hash(key), key, value, false, true);
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
tab[i = (n - 1) & hash]
为什么 h >>> 16?
假设您天真地使用以下方法索引到哈希表中
int index = hashcode % table.length;
在一些常见的用例中,这可能会导致许多冲突。例如,假设 table.length 是 2 的小幂(如 32 或 64)。在这种情况下,只用了哈希码的低位确定索引。如果您的对象的哈希值仅在高位不同,这将导致大量冲突。位移位允许哈希值的高位也影响计算的索引。
2.1.1、假设(length-1)直接和key的 hashcode 相与
设一个key的 hashcode = 78897121 转换二进制:100101100111101111111100001,与(length-1)=7 & 运算
0000 0100 1011 0011 1101 1111 1110 0001
&与
0000 0000 0000 0000 0000 0000 0000 0111
= 0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,所以下标为1)
上述运算实质是:0001 与 0111 & 运算。也就是哈希值的低四位与length-1与运算。
补充知识:
当length=2^3=8时 //下标运算结果取决于哈希值的低三位 length-1-->0111
当length=2^4=16时 //下标运算结果取决于哈希值的低四位 length-1-->1111
当length=2^5=32时 //下标运算结果取决于哈希值的低五位 ..
当length=2的N次方时 //下标运算结果取决于哈希值的低N位 ..
由于length 绝大多数情况小于2的16次方(65536),所以始终是hashcode 的低16位(甚至更低)参与运算,这样高16位是用不到的。hashcode是一个32位的 int,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。
如果让哈希值的低位更加随机,那么&的结果就会更加随机。
如何让哈希值的低位更加随机,那就让高16位也参与运算,让其与高位异或。如此就会让得到的下标更加散列。
所以才有hash(Object key)方法。让他的hashCode()和自己的高16位异或。
为什么是^异或?
二进制位计算,a 只可能为0,1,b只可能为0,1。a中0出现几率为1/2,1也是1/2,b同理。
位运算符有三种,|或,&与,^异或。
a,b进行位运算,有4种可能 00,01,10,11
a或b计算 结果为1的几率为3/4,0的几率为1/4
a与b计算 结果为0的几率为3/4,1的几率为1/4,
a异或b计算 结果为1的几率为1/2,0的几率为1/2 所以,进行异或计算,得到的结果肯定更为平均,不会偏向0或者偏向1,更为散列。
右移16位进行异或计算,我将其拆分为两部分,前16位的异或运算,和后16位的亦或运算, 后16位的异或运算,即原hashcode后16位与原hashcode前16位进行异或计算,得出的结果,前16位和后16位都有参与其中,保证了 32位全部进行计算。 前16位的异或运算,即原hasecode前16位与0000 0000 0000 0000进行亦或计算,结果只与前16位hashcode有关,同时异或计算,保证 结果为0的几率为1/2,1的几率为1/2,也是平均的。 结果的后16位保证了hashcode32位全部参与计算,也保证了0,1平均,散列性。
2.1.2、key的 hashcode 异或 他的高16位后 再与数组的长度-1后的值相 与 来算得数组的索引值
//return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
0000 0100 1011 0011 1101 1111 1110 0001
>>> 16
0000 0000 0000 0000 0000 0100 1011 0011
^异或
0000 0100 1011 0011 1101 1111 1110 0001
= 1111 1011 0100 1100 0010 0100 1010 1101
//tab[i = (n - 1) & hash]
1111 1011 0100 1100 0010 0100 1010 1101
&与
0000 0000 0000 0000 0000 0000 0000 0111
= 0000 0000 0000 0000 0000 0000 0000 0101 (就是十进制6,所以下标为6)
2.2、step into进入putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//transient Node<K,V>[] table;
//第一次扩容,到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//产生索引,并赋值
//如果p为null,表示还没有存放元素,就创建一个Node("java",占位PRESENT),就放在此索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
//newNode方法return new Node<>(hash, key, value, next);
tab[i] = newNode(hash, key, value, null);
else {//p已经存放了元素。哈希冲突
Node<K,V> e; K k;
//如果当前索引位置对应的 链表的第一个元素和准备添加的key的hash值一样
//并且满足 下面两个条件之一:
//(1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象
//(2) p指向的Node 结点的 key 的equals() 和准备加入的key比较后相同[equals()自己重写定义的比较规则]
//就不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//p是不是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果table对应索引位置,已经是一个链表, 就使用for循环比较
for (int binCount = 0; ; ++binCount) {
//(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//把元素添加到链表后,立即判断 该链表是否已经达到8个结点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
treeifyBin(tab, hash);
break;
}
//(2) 依次和该链表的每一个元素比较过程中,如果有相同元素,就直接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;
//超过threshold临界值,就扩容
//size 就是我们每加入一个结点Node(k,v,h,next), size++。不管是加在数组value上还是链表上
if (++size > threshold)
resize();
//空方法 void afterNodeInsertion(boolean evict) { }
//用于给HashMap的子类去实现
afterNodeInsertion(evict);
return null;
}
resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//1<<4=1*2*2*2*2=16,默认扩容到16
newCap = DEFAULT_INITIAL_CAPACITY;
//0.75(加载因子)* 16 = 12(threshold临界值)
//缓冲层,提前扩容
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //12
@SuppressWarnings({"rawtypes","unchecked"})
//?????new一个长度为newCap的Node数组,再强转为 Node<K,V>[] 类型??????
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; //table.length=16
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
treeifyBin()
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//在转成红黑树时,先要进行判断。因为树化要满足两个条件
//在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),**并且** table的大小 >=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//如果上面条件成立,先table扩容.
//只有上面条件不成立时,才进行转成红黑树
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
equals
public class Test {
public static void main(String[] args) {
Student s1 = new Student(1,"zhang");
Student s2 = new Student(1,"zhang");
//equals默认比较地址值。(两个对象,地址值肯定不同)
System.out.println(s1.equals(s2)); //false
HashSet hashSet = new HashSet();
hashSet.add(s1);
hashSet.add(s2);
System.out.println("set=" + hashSet);
//set=[Student{age=1, name='zhang'}, Student{age=1, name='zhang'}]
}
}
class Student{
int age;
String name;
//省略有参无参构造、getter、setter、t0String()
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student(1,"zhang");
Student s2 = new Student(1,"zhang");
System.out.println(s1.equals(s2)); //true
HashSet hashSet = new HashSet();
hashSet.add(s1);
hashSet.add(s2);
System.out.println("set=" + hashSet);
//set=[Student{age=1, name='zhang'}]
}
}
class Student{
int age;
String name;
//省略有参无参构造、getter、setter、t0String()
//重写equals和hashCode()
//Choose fields be include in hashCode()
//Choose fields be include in equals()
//重写equals,当age和name都相同时,在计算hashCode()时,返回相同的哈希值
@Override
public boolean equals(Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
}
LinkedHashSet
1、 LinkedHashSet是HashSet的子类
2、 LinkedHashSet底层是LinkedHashMap支持的,LinkedHashMap底层结构 (数组table+双向链表) 3、数组是 HashMap$Node[]
,存放的元素/数据是 LinkedHashMap$Entry
类型。 4、LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。(有序)(加入顺序和取出元素/数据的顺序一致)
5、LinkedHashSet不允许添重复元素
6、第一次添加时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
//继承关系是在内部类完成.
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//@SuppressWarnings({"all"})//抑制警告处理
public class Time {
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(new Customer("刘", 1001));
set.add(123);
set.add("HSP");
System.out.println("set=" + set);
//set=[AA, 456, com.cn.liujp.aastart.Customer@b4c966a, 123, HSP]
}
}
class Customer {
private String name;
private int no;
public Customer(String name, int no) {
this.name = name;
this.no = no;
}
}
TreeSet
源码
/*TreeSet的底层是TreeMap,添加的数据存入了map的key的位置,而value则固定是PRESENT。TreeSet中的元素是有序且不重复的,因为TreeMap中的key是有序且不重复的*/
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
// 使用 NavigableMap 的 key 来保存 Set 集合的元素
private transient NavigableMap<E,Object> m;
// 使用一个 PRESENT 作为 Map 集合的所有 value。
private static final Object PRESENT = new Object();
/*构造器:TreeSet 的 ① 号、② 号构造器的都是新建一个 TreeMap 作为实际存储 Set 元素的容器,而另外 2 个构造器则分别依赖于 ① 号和 ② 号构造器,由此可见,TreeSet 底层实际使用的存储容器就是 TreeMap。*/
// 包访问权限的构造器,以指定的 NavigableMap 对象创建 Set 集合
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet(){ // ①
// 以自然排序方式创建一个新的 TreeMap,
// 根据该 TreeSet 创建一个 TreeSet,
// 使用该 TreeMap 的 key 来保存 Set 集合的元素
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator){ // ②
// 以定制排序方式创建一个新的 TreeMap,
// 根据该 TreeSet 创建一个 TreeSet,
// 使用该 TreeMap 的 key 来保存 Set 集合的元素
this(new TreeMap<E,Object>(comparator));
}
public TreeSet(Collection<? extends E> c){
// 调用①号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
this();
// 向 TreeSet 中添加 Collection 集合 c 里的所有元素
addAll(c);
}
public TreeSet(SortedSet<E> s){
// 调用②号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
this(s.comparator());
// 向 TreeSet 中添加 SortedSet 集合 s 里的所有元素
addAll(s);
}
//TreeSet 的其他方法都只是直接调用 TreeMap 的方法来提供实现
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
.....
debug
public class TreeSet_ {
public static void main(String[] args) {
// TreeSet treeSet = new TreeSet();
//使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则(按照字符串大小来排序)
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面 调用String的 compareTo方法进行字符串大小比较
return ((String) o2).compareTo((String) o1);
//按照长度大小排序(长度相等就认为加入的元素一样)(元素不允许重复,如果一样就加不进去)
//return ((String) o1).length() - ((String) o2).length();
}
});
//添加数据.
treeSet.add("jack");
treeSet.add("tom");//3
treeSet.add("sp");
treeSet.add("a");
treeSet.add("abc");//3 长度大小排序就加不了
System.out.println("treeSet=" + treeSet);
}
}
1、连续 step over && Force step into:
使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则(按照字符串大小来排序)
构造器把传入的比较器对象TreeSet treeSet = new TreeSet(new Comparator() {
,赋给了 TreeSet的底层的 TreeMap的属性this.comparator
2、add()
/*
每当程序希望添加新节点时:系统总是从树的根节点开始比较 —— 即将根节点当成当前节点,如果新增节点大于当前节点、并且当前节点的右子节点存在,则以右子节点作为当前节点;如果新增节点小于当前节点、并且当前节点的左子节点存在,则以左子节点作为当前节点;如果新增节点等于当前节点,则用新增节点覆盖当前节点,并结束循环 —— 直到找到某个节点的左、右子节点不存在,将新节点添加该节点的子节点 —— 如果新节点比该节点大,则添加为右子节点;如果新节点比该节点小,则添加为左子节点。
*/
public V put(K key, V value) {
Entry<K,V> t = root;
//第一次加入(根节点),把k-v 封装到 Entry对象,放入root
// 如果 t==null,表明是一个空链表,即该 TreeMap 里没有任何 Entry
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//把我们传入的comparator赋给cpr
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare
cmp = cpr.compare(key, t.key);
/*
@Override
public int compare(Object o1, Object o2) {
//下面 调用String的 compareTo方法进行字符串大小比较
return ((String) o2).compareTo((String) o1);
//按照长度大小排序
//return ((String) o1).length() - ((String) o2).length();
}
*/
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else//如果相等,把占位的PRESENT替换掉,相当于这个Key没有加入。(元素不允许重复)
return t.setValue(value);
} while (t != null);
}
//没有自定义比较器,走默认的
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
//根据k的类型调用compareTo方法。
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);//节点
/*
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
...
*/
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
双列集合java.util.Map
HashMap
结合HashSet源码一起看
HashSet是单列集合,底层是HashMap。所以 hashSet.add("java");
就是以java为key,value是一个PRESENT,用来占位。
private static final Object PRESENT = new Object();
PRESENT是静态的,共享的。不管put多少次,PRESENT都是一个Object对象。他是用来占位的
map.put("no1", "韩顺平");
…
1、 Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node(HashMap的静态内部类Node)对象中
2、HashSet是(key就是值,PERSET占位),Map就是(key ,value)。key都是通过哈希值来确定的(底层都是以hash表的方式存储的),所以key不允许重复(重复就替换值),无序。Map中的value可以重复。
3、HashMap的key可以为null, value也可以为null,注意key为null,只能有一个,value为null,可以多个。
4、key和value之间存在单向对一关系,即通过指定的key总能找到对应的value
5、HashMap没有实现同步,因此是线程不安全的。(方法没有做同步互斥操作,synchronized)
源码
1、k-v 数据最后是放在 HashMap$Node node = newNode(hash, key, value, null)
中的
2、 k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合 ,该集合存放的元素的类型 Entry, 而一个Entry对象就有k,v 即:EntrySet<Entry<K,V>> 即: transient Set<Map.Entry<K,V>> entrySet
;
3、 entrySet 中, 定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap$Node
这是因为 static class Node<K,V> implements Map.Entry<K,V>
(接口多态:当一个类实现了一个接口,这个类的对象实例就可以赋给我们的一个接口类型)
4、当把 HashMap$Node 对象 存放到 entrySet 就方便我们的遍历, 因为 Map.Entry 提供了重要方法 K getKey(); V getValue();
==5、==总的来说:HashMap底层就是:数组+单向链表+红黑树。数组table表中存储Node<K,V>。为了方便管理(遍历…),将每一个Node<K,V>封装成一个Entry,再把这个Entry放到EntrySet集合中。除次之外,还将key对象封装到KeySet集合中,value对象封装到Values集合中。
static class Node<K,V> implements Map.Entry<K,V> {...
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {...
public class MapSource_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1", "韩顺平");
map.put("no2", "张无忌");
map.put("no1", "张三丰");//当有相同的k , 就等价于替换.
// 通过get 方法,传入 key(计算它的哈希值,相同对象哈希值一样),就可以取出对应的value
System.out.println(map.get("no2"));//张无忌
map.put(new Car(), new Person());//k-v
Set set = map.entrySet();
System.out.println(set.getClass());// HashMap$EntrySet
for (Object obj : set) {
//System.out.println(obj.getClass()); //HashMap$Node
//为了从 HashMap$Node 取出k-v
//1. 先做一个向下转型
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "-" + entry.getValue() );
}
Set set1 = map.keySet();
System.out.println(set1.getClass()); //HashMap$KeySet
Collection values = map.values();
System.out.println(values.getClass()); //HashMap$Values
}
}
class Car {
}
class Person{
}
构造方法:可以初始化容量和加载因子。无参默认第一次添加扩容到16,加载因子为0.75
put()省略
- 【 Node<K,V>的k-v ------>Map.Entry<K,V> ----->EntrySet集合】
put加入k-v后,entrySet已经赋值?????
为了方便程序员的遍历,会 创建 EntrySet 集合, EntrySet 集合里面放的是包含k-v的 Entry<K,V>对象。entrySet 中, 定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap$Node
这是因为 static class Node<K,V> implements Map.Entry<K,V>
。
进入Set entrySet = map.entrySet();
真正的数据是还是放在table数组的 Node<K,V>里面的,EntrySet集合和KeySet集合和Values集合的数据只是指向 Node<K,V>,是数据引用。(Entry<K,V>中的KV是指向Node<K,V>里的KV)
- 遍历
Map map = new HashMap();
1、第一种:先取出 所有的Key , 通过Key 取出对应的Value
Set keyset = map.keySet();
//keySet<key> 将ke放到 keySet集合中(keySet是Set类型集合)
//keyset.getClass() ==> HashMap$keySet keySet是HashMap中的一个内部类
//(1) 增强for
for (Object key : keyset) {
System.out.println(key + "-" + map.get(key));
}
//(2) 迭代器
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
2、把所有的values取出
Collection values = map.values();
//这里可以使用所有的Collections使用的遍历方法
//(1) 增强for、(2) 迭代器
3、通过EntrySet 来获取 k-v
Set entrySet = map.entrySet(); // EntrySet<Map.Entry<K,V>>
//EntrySet<Map.Entry<K,V>> 将ke放到 keySet集合中(keySet是Set类型集合)
//entrySet.getClass() ==> HashMap$EntrySet EntrySet是HashMap中的一个内部类
//(1) 增强for
for (Object entry : entrySet) { // entry :HashMap$Node
//将entry 向下转型成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
//(2) 迭代器
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Object entry = iterator.next();//HashMap$Node -实现-> Map.Entry (getKey,getValue)
//System.out.println(next.getClass());
//向下转型 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
- 重复值就替换
...
map.put("no1", "张三丰");//当有相同的k , 就等价于替换.
....
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//hash:109201 key: "no1" value: "张三丰” onlyIfAbsent: false
Node<K,V>[] tab; Node<K,V> p; int n, i;
//第三次添加,不需要扩容 /判断table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//table[i]已经有元素了,p!=null //根据key的索引判断table该索引是否有值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//有值,哈希冲突
Node<K,V> e; K k;
//p:"no1=韩顺平"的哈希值和hash:109201(张三丰根据key:no1算出来的哈希值)相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //e:"no1=韩顺平"
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;
}
}
//HashSet的value也会替换。(存入的元素就是key,key不会添加,但是占位的value:PERSENT会替换,占位的value都是PERSENT,所以替换后相当于没有变)
if (e != null) { // existing mapping for key
V oldValue = e.value; //oldValue = 韩顺平"
if (!onlyIfAbsent || oldValue == null)
e.value = value; //e:Node<K,V>, e.value =传入的value:张三丰
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
LinkedHashMap
Hashtable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
1、 hashtable的键和值都不能为null, 否则会抛出NullPointerException
2、 hashtable使用方法基本上和HashMap一样
3、hashtable所有的读写操作都加了synchronized关键字,所以同步式线程安全,但是影响了效率。hashMap是线程不安全的
4、实现了Serializable接口支持序列化,实现了Cloneable接口,可以被克隆
5、Hashtable使用的是拉链式,所以它的底层是由数组+链表实现的。
两种解决哈希冲突的方式:一种是开放地址式,第二种是拉链式。
(1)、开放地址式:如果发生冲突,则选择数组的下一个位置进行判断,直到找到没有冲突的位置。
(2)、拉链式: 如果发生冲突,则直接塞入到该数组下标位置的链表中。
- Hashtable 和 HashMap 对比
源码
1、底层有数组 Hashtable$Entry[] 初始化大小为 11。(数组table中存储Entry<K,V>)
2、加载因子:loadFactor:0.75
3、临界值 threshold 8 = 11 * 0.75
4、执行 方法 addEntry(hash, key, value, index); 添加K-V 封装到Entry
5、 当 if (count >= threshold) 满足时,就进行扩容。按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容。(11*2+1=23)
//@SuppressWarnings({"all"})//抑制警告处理
public class Time {
public static void main(String[] args) {
Hashtable table = new Hashtable();
table.put("john", 1);
//table.put(null, 100); //异常 NullPointerException
//table.put("john", null);//异常 NullPointerException
table.put("lucy", 2);
table.put("lic", 3);
table.put("lic", 4);//替换
table.put("hello1", 4);
table.put("hello2", 5);
table.put("hello3", 6);
table.put("hello4", 7);
table.put("hello5", 8);
table.put("hello6", 9);
System.out.println(table);
}
}
构造器:
无参构造方法默认Entry[] 初始化大小为 11,加载因子为0.75
//构造一个与给定的 Map 具有相同映射关系的新Hashtable。
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
put():
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
//hash & 0x7FFFFFFF:获取key的hashcode的绝对值
//因为hashcode是int类型32位,当获取的数大于32位时,hashcode值会溢出变成负数
//对容量取余找到对应的索引值
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//该索引下有元素
for(; entry != null ; entry = entry.next) {
//索引重复,更新值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//该索引下没有元素,就添加 Entry<K,V>
addEntry(hash, key, value, index);
return null;
}
…
Properties
1、Properties类继承自Hashtable类并且实现了Map接口,可以通过 k-v 存放数据,当然key 和 value 不能为 null。public class Properties extends Hashtable<Object,Object> {
2、他的使用特点和Hashtable类似
3、Properties 还可以用于从xxx.properties文件中(常用作配置文件),加载数据到Properties类对象,并进行读取和修改。
@SuppressWarnings({"all"})
public class Properties_ {
public static void main(String[] args) {
//增加
Properties properties = new Properties();
//properties.put(null, "abc");//抛出 空指针异常
//properties.put("abc", null); //抛出 空指针异常
properties.put("john", 100);//k-v
properties.put("lucy", 100);
properties.put("lic", 100);
properties.put("lic", 88);//如果有相同的key , value被替换
System.out.println("properties=" + properties);
//通过k 获取对应值
System.out.println(properties.get("lic"));//88
//删除
properties.remove("lic");
System.out.println("properties=" + properties);
//修改
properties.put("john", "约翰");
System.out.println("properties=" + properties);
}
}
debug
private transient volatile ConcurrentHashMap<Object, Object> map;
自己没有再去看,好像和另一个集合有关ConcurrentHashMap…
TreeMap
结合TreeSet看
源码
TreeMap底层使用红黑树的结构进行数据的增删改查,红黑树是一种自平衡的二叉查找树。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
@SuppressWarnings("serial") // Conditionally serializable
private final Comparator<? super K> comparator; // 可以在构造器方法中传入实例
private transient Entry<K,V> root; //根节点
Comparator和Comparable
TreeMap使用两种方法来保证有序性:Comparator和Comparable。
先使用comparator属性去比较,若comparator为空(没有在构造方法中传入实例)则采用Comparable策略。(有指定的就先用指定的比较器comparator。没有就走默认的Comparable)
//compare(key, key);
final int compare(Object k1, Object k2) {
//
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
public V put(K key, V value) {
Entry<K,V> t = root;
//第一次添加
if (t == null) {
compare(key, key);
//...
}
//自定义比较方法
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
//...
} while (t != null);
}
//走默认的比较
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
//....
} while (t != null);
}
//....
}
补充:Comparator和Comparable的区别
java.lang.Comparable是排序接口。若一个类实现了Comparable接口,就意味着该类支持排序。
public interface Comparable<T> {
//Comparable 接口仅仅只包括一个函数
public int compareTo(T o);
}
java.util.Comparator是比较接口,如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现Comparator接口即可。也就是说,可以通过实现Comparator来新建一个比较器,然后通过这个比较器对类进行排序。
若一个类要实现Comparator接口:它一定要实现compareTo(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。为什么可以不实现 equals(Object obj) 函数呢? 因为任何类,默认都是已经实现了equals(Object obj)的。 Java中的一切类都是继承于java.lang.Object,在Object.java中实现了equals(Object obj)函数;所以,其它所有的类也相当于都实现了该函数。
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
1、Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
2、他们的区别是角色不同,想要实现的目的也不同。一个是内部自然排序,只能有一种定义;一个是外部的比较器,可以定义多个不同的比较器,按需取用。
3、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法
4、实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。
应用:
//Student类内部
private static class Student implements Comparable {
int age;
int id;
@Override
public int compareTo(Object o) {
return this.age - ((Student) o).age;
}
//...
}
//传入一个对象, 将对象和元素自身进行比较。如果元素自身大返回“正整数”,相等返回0,元素自身小于参数则返回“负整数”
private static class StudentCom1 implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
//传入两个相同类型元素进行大小的比较。o1比o2小返回“负整数”;相等返回“零”;o1大于o2返回“正整数”
//1、单一条件排序
Collections.sort(student, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 升序
//return s1.getAge()-s2.getAge();
return s1.getAge().compareTo(s2.getAge());
}
});
//lambda表达式简化代码
//Collections.sort(student, (s1,s2)->(s1.getAge()-s2.getAge()));
//2、多条件排序
Collections.sort(stus, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
int flag;
// 首选按年龄升序排序
flag = s1.getAge()-s2.getAge();
if(flag==0){
// 再按学号升序排序
flag = s1.getId()-s2.getId();
}
return flag;
}
});
//3、自定义条件排序
String[] order = {"语文","数学","英语","物理","化学","生物","政治","历史","地理","总分"};
final List<String> definedOrder = Arrays.asList(order);
List<String> list = new ArrayList<String>(){
{
add("总分");
add("英语");
add("政治");
add("总分");
add("数学");
}
};
Collections.sort(list,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int io1 = definedOrder .indexOf(o1);
int io2 = definedOrder .indexOf(o2);
return io1-io2;
}
});
//lambda表达式简化代码
/*
Collections.sort(list, (o1, o2)->(definedOrder .indexOf(o1)-definedOrder .indexOf(o2)));
*/
for(String s:list){
System.out.print(s+" ");
}
debug
@SuppressWarnings({"all"})
public class TreeMap_ {
public static void main(String[] args) {
//使用默认的构造器,创建TreeMap
//TreeMap treeMap = new TreeMap();
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照传入的 k(String) 的大小进行排序
//return ((String) o2).compareTo((String) o1);
//按照K(String) 的长度大小排序
return ((String) o2).length() - ((String) o1).length();
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("kristina", "克瑞斯提诺");
treeMap.put("smith", "斯密斯");
treeMap.put("hsp", "韩顺平");//长度大小排序就加入不了
System.out.println("treemap=" + treeMap);
}
}
红黑树
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
左旋 和 右旋
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。
添加或删除红黑树中的节点之后,可能就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。
- 左旋
z
x /
/ \ --(左旋)--> x
y z /
y
- 右旋
y
x \
/ \ --(右旋)--> x
y z \
z
仔细观察上面"左旋"和"右旋"的示意图。我们能清晰的发现,它们是对称的。无论是左旋还是右旋,被旋转的树,在旋转前是二叉查找树,并且旋转之后仍然是一颗二叉查找树。
集合选择
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
1、先判断存储的类型一组对象[单列] 或 一组键值对[双列])
2、 一组对象[单列]:Collection接口
允许重复: List
增删多:LinkedList [底层维护了一个双向链表]
改查多:ArrayList [底层维护Object类型的可变数组]
不允许重复:Set
无序:HashSet [底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)]
排序:TreeSet [老韩举例说明]
插入和取出顺序一致:LinkedHashSet , 维护数组+双向链表
3、一组键值对[双列]:Map
键无序HashMap [底层是:哈希表jdk7:数组+链表,jdk8: 数组+链表+红黑树]
键排序:TreeMap [老韩举例说明]
键插入和取出顺序一致: LinkedHashMap
读取文件 Properties
红黑树和平衡二叉树的比较
美团面试题:
一、平衡二叉树(AVL树)
它是一 棵空树 或 它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二、红黑树
(一)相关说明
红黑树是一种自平衡的二叉树,在插入和删除的过程中,红黑树会对树的组织形式进行调整,以尽可能的减少树的高度,从而节省查找的时间。一棵n个结点的红黑树的高度始终为:
红黑树的操作时间跟二叉查找树的时间复杂度是一样的,执行查找、插入、删除等操作的时间复杂度为:
(二)红黑树的特性
结点是红色或黑色
根结点始终是黑色
叶子结点都是黑色
红色结点的两个直接孩子结点都是黑色
从任一结点到每个叶子的所有简单路径都包含相同数目的黑色结点
以上性质保证了红黑树在满足平衡二叉树特征的前提下,还可以做到 从根到叶子的最长路径最多不会超过最短路径的两倍(简单路径指路径上的顶点都不相同的路径)。
由于它的设计,任何不平衡都会在三次旋转之内解决。
(三)红黑树与AVL的比较
红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多。
三、B树
(一)为什么要引用B树
计算机有一个局部性原理,就是说,当一个数据被用到时,其附近的数据也通常会马上被使用。
所以当你用红黑树的时候,你一次只能得到一个键值的信息,而用B树,可以得到最多M-1个键值的信息。这样来说B树当然更好了。
另外一方面,同样的数据,红黑树的阶数更大,B树更短,这样查找的时候当然B树更具有优势了,效率也就越高。
(二)一个 m 阶的B树的特点
每一个节点最多有 m 个子节点
每一个非叶子节点(除根节点)最少有 ⌈m/2⌉ 个子节点
如果根节点不是叶子节点,那么它至少有两个子节点
有n个关键字的话有n+1棵子树
所有的叶子节点都在同一层
(三)图示
四、B+树
(一)B+与B的不同
B+只有叶子节点会带有指向记录的指针,B+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素。
b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢)
B+所有叶子节点之间都有一个链指针,可以直接在叶子节点层横向遍历,b树想要遍历则需要叶子节点和上层节点不停往返。。
在B+树中,具有n个关键字的节点只含有n棵子树;而在B树中,具有n个关键字的节点有n+1棵子
对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历
(二)图示
面试题
- 数组结构。元素增删慢,查找快。
- 链表结构。元素查询慢,增删快。
- 红黑树。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
单列集合总结
1、List 的特点是元素有序、元素可重复。元素是以一种线性方式进行存储的。是一个带有索引的集合
2、Set 的特点是元素无序,而且不可重复(所以最多只能又一个null)。没有索引,Set集合取出元素的方式可以采用:迭代器、增强for,不能使用索引的方式来遍历。
一、List
1、ArrayList
① ArrayList中维护了一个Object类型的数组elementData。
transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列号
② 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
③ 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
2、Vector
ArrayList和Vector比较
3、LinkedList 的底层操作机制
① LinkedList底层维护了一一个双向链表
② LinkedList中维护了两个属性first和last分别指向首节点和尾节点
③ 每个节点(Node对象) ,里面又维护了prev、 next、 item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表。
④ 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
二、Set
1、HashSet
1.1、LinkedHashSet
① LinkedHashSet是HashSet的子类
② LinkedHashSet底层是LinkedHashMap支持的,LinkedHashMap底层结构 (数组table+双向链表)
③ 数组是 HashMap$Node[]
,存放的元素/数据是 LinkedHashMap$Entry
类型。
④ LinkedHashMap根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。(有序)(加入顺序和取出元素/数据的顺序一致)
⑤ LinkedHashSet不允许添重复元素
⑥ 第一次添加时,直接将 数组table 扩容到 16,存放的结点类型是 LinkedHashMap$Entry
面试题: HashMap内部节点是无序的,是根据hash值随机插入的。LinkedHashMap 和 TreeMap是有序。LinkedHashMap怎么实现有序的?
2、TreeSet
双列集合Map及其区别
- HashTable
1、HashTable的键和值都不能为null, 否则会抛出NullPointerException
2、HashTable所有的读写操作都加了synchronized关键字,所以同步式线程安全,但是影响了效率。hashMap是线程不安全的
3、Hashtable使用的是拉链式,所以它的底层是由数组+链表实现的。
两种解决哈希冲突的方式:一种是开放地址式,第二种是拉链式。
(1)、开放地址式:如果发生冲突,则选择数组的下一个位置进行判断,直到找到没有冲突的位置。
(2)、拉链式:如果发生冲突,则直接塞入到该数组下标位置的链表中。
扩容:
1、底层有数组 Hashtable$Entry[] 初始化大小为 11。(数组table中存储Entry<K,V>)
2、临界值 threshold 8 = 11 * 0.75(加载因子loadFactor)
3、 当 if (count >= threshold) 满足时,就进行扩容。按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容。(11*2+1=23)
HashMap 的原理
JDK 1.8 之前是由“数组+链表”组成。 JDK 1.8开始,底层是由“数组+链表+红黑树”组成。
为什么要改成“数组+链表+红黑树”——主要是为了提升在 hash 冲突严重时(链表过长)的查找性能。
数组特点:增删慢、查找快。所以可以根据 hash 值能够快速定位到数组的具体下标。
当出现hash 冲突时,就会顺着链表去查询。链表的时间复杂度是 O(n),而红黑树的是 O(logn)。
所以为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
- HashMap底层就是:数组+单向链表+红黑树。
1、数组table表中存储Node<K,V>
Map中的key和value封装到HashMap$Node(HashMap的静态内部类Node)对象中
2、为了方便管理(遍历…),将每一个Node<K,V>封装成一个Entry,再把这个Entry放到EntrySet集合中。
EntrySet<Entry<K,V>> 。 Map.Entry 提供了重要方法 K getKey(); V getValue();
3、除次之外,还将key对象封装到KeySet集合中,value对象封装到Values集合中。
HashMap 扩容时的流程
1、put添加一个元素时,先把得到hash值会转成—>table的索引,来判断元素加入的位置。
2、判断该索引位置是否已经存放元素。如果没有,直接加入。
3、如果有(hash 冲突),调用equals比较,该元素的key和准备加入的key是否相同(如重写equals,按自己定义的规则比较)
相同,就替换(相同的key替换,相当于不变,value替换为新值)。(hashMap的key不重复)
如果不相同需要判断是树结构还是链表结构,然后再添加。添加时发现容量不够则需要扩容。
4、在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),并且 table的大小 >=MIN TREEIFY CAPACITY(默认64),(只是这条链表)就会进行树化(红黑树)。
如果某条链表的元素个数到达8,但是table的大小没有64,再次在这条链表加元素(假设元素不重复),并不会树化,还是加在这条链表后面,而且table以2倍数扩容。再次在这条链表上加元素,一样的加在链表后面,一样的table以2倍数扩容。重复操作,直到table大小也到达64,这时,再次在这条链表上加元素,整条链表就会树化。
- 扩容机制
1、第一次添加时,table 数组就扩容到16。扩容临界值(threshold) = 1 6*
加载因子(loadFactor 0.75) = 12。
设置临界值的原因:满了临界值就提前扩容(缓冲层),假如大量线程添加,数组余量不够,再去扩容,就会有时间阻塞
在我们新建 HashMap 对象时, threshold 还会被用来存初始化时的容量。HashMap 直到我们第一次插入节点时,才会对 table 进行初始化,避免不必要的空间浪费。
2、如果table数组使用到了临界值12(不管是加在数组value上还是链表上都算。源码是++size,记录的是加入元素的个数),再一次添加就会以2倍数扩容到16*
2 = 32,新的临界值就是32*
0.75 = 24,依次类推。
2倍扩容,源码是
左移:x<<y //x * y个2
。位运算,多CPU来说,效率高。
3、Hash冲突时,不重复元素添加。判断是树结构还是链表结构。先在链表后面添加,当一条链表的元素个数已经到达阈值8,并且 此时数组table的大小 >= 64,(只是这条链表)就会进行树化(红黑树)。
…见上
对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,就会触发红黑树节点转链表节点(untreeify)。
如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。
- HashSet底层就是HashMap支持的
HashSet的add方法就是HashMap 的put方法。
HashSet和HashMap的key是不可以重复的。HashSet的value是用的一个Object 对象来占位。
private static final Object PRESENT = new Object();
//PRESENT是静态的,共享的。不管put多少次,PRESENT都是一个Object对象。他是用来占位的
- HashMap 在 Put 时,新链表节点是放在头部还是尾部
JDK 1.8 之前 put 时采用的是**“头插法”**,多线程下,会导致同一索引位置的节点在扩容后顺序反掉。存在死循环问题。
头插法:原始节点作为新节点的后继节点。
JDK 1.8 之后采用的是**“尾插法”**,扩容后节点顺序不会反掉,不存在死循环问题。
尾插法:遍历链表,将元素放置到链表的最后
- JDK1.8的优化
1、数组+链表 改成了 数组+链表或红黑树。
2、链表的插入方式 从头插法改成了 尾插法。
3、
hash值转成 table的索引
table的索引并不是哈希值,而是做了转换。
//源码是:key的 hashcode 异或 他的高16位后 再与数组的长度-1后的值相 与 来算得数组的索引值
(h = key.hashCode()) ^ (h >>> 16); //key的 hashcode 异或 他的高16位后
数组索引值 : tab[i = (n - 1) & hash] //再和 数组的长度-1后的值 相与
// 异或:^ (同真异假)
// 无符号右移:>>> (h >>> 16是用来取出h的高16位)
// 与:&
1、这种方法保证了hashcode32位全部参与计算,也保证了0,1平均,使得到的哈希值更加随机,获得的下标索引更加散列,可以减少碰撞。
2、h%table.length
在数学上等效于(n-1)&hash
。异或,与,无符号右移这些都是关于没有模的位运算,而模运算比按位运算慢,如此可提高计算速度。
3、使用&
操作代替%
,不仅仅是为了速度,而且哈希值是int
类型,可以是负数。对负数取模将是负数,用&
可以保证正索引。(hashcode是int类型32位,当获取的数大于32位时,hashcode值会溢出变成负数)
(h = key.hashCode()) ^ (h >>> 16);
: 保证hashcode32位全部参与计算
如果直接 key.hashCode() %table.length
===> hashCode() &(n-1)
hash%table.length
在数学上等效于(n-1)&hash
由于length 绝大多数情况小于2的16次方(65536),所以始终是hashcode 的低16位(甚至更低)参与运算,这样高16位是用不到的。
如果对象的哈希值仅在高位不同,这将导致大量冲突。
hashcode是一个32位的 int,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。
所以才有hash(Object key)方法。让他的hashCode()和自己的高16位异或。
- HashMap 的容量为什么必须是 2 的 N 次方?
当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值。
比如:8(1000)-1 = 7(0111)
此时任何值跟 n - 1 进行 & 运算会等于其本身,达到了和 取模同样的效果,实现了均匀分布。
如下图,当 n 不为 2 的 N 次方时,hash 冲突的概率明显增大。
CocurrentHashMap
HashTable 线程安全,但在每次同步执行时都要锁住整个结构。(方法做了同步互斥操作,synchronized)
HashMap没有实现同步,因此是线程不安全的。(方法没有做同步互斥操作,synchronized)
ConcurrentHashMap 锁的方式是稍微细粒度的。
1、JDK1.7版本的ConcurrentHashMap是基于Segment分段实现的。
ConcurrentHashMap分成一个个Segment,Segment本身就相当于一个HashMap对象。每个Segment的扩容机制和HashMap的扩容逻辑类似。
ConcurrentHashMap采用了锁分段技术。每个Segment本身就相当于一把锁。Segment 是一种可重入锁(继承ReentrantLock)。put的时候,当前segment会将自己锁住,此时其他线程无法操作这个segment, 但不会影响到其他segment的操作。segment之间互不影响。
在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
2、1.8版本的ConcurrentHashMap不再基于Segment实现,使用synchronized+CAS实现线程安全。
ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
ConcurrentHashMap是支持多个线程同时扩容的。
当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容。
如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容。
TreeMap 的原理
集合类不安全
不太了解,但是知道:
List不安全
ArrayList 在并发情况下是不安全的!
解决方案:
1、切换成Vector就是线程安全的啦!
2、使用Collections工具类的synchronized包装的Set类——synchronizedList(new ArrayList<>());
List<Object> arrayList = Collections.synchronizedList(new ArrayList<>());
3、使用JUC中的包: CopyOnWriteArrayList<>();
List<Object> arrayList = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制!
①、CopyOnWriteArrayList
读取时是不用加锁的。
在很多应用场景中,读操作可能会远远大于写操作。
由于读操作根本不会修改原有的数据,因此如果每次读取都进行加锁操作,其实是一种资源浪费。
我们应该允许多个线程同时访问 List 的内部数据,毕竟读操作是线程安全的。
②、CopyOnWriteArrayList
类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。
当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。
CopyOnWriteArrayList比Vector厉害在哪里?
Vector底层是使用synchronized关键字来实现的:效率特别低下。
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
Set不安全
多线程情况下,普通的Set集合是线程不安全的;
解决方案还是两种:
- 使用Collections工具类的synchronized包装的Set类
- 使用CopyOnWriteArraySet 写入复制的JUC解决方案
//Set<String> hashSet = Collections.synchronizedSet(new HashSet<>());
Set<String> hashSet = new CopyOnWriteArraySet<>();//解决方案2
Map不安全
结果同样的出现了:异常java.util.ConcurrentModificationException 并发修改异常
解决方案:
- 使用Collections工具类的synchronized包装的Set类——synchronizedMap(new HashMap<>());
- 使用ConcurrentHashMap()
先集合源码 在spring源码
https://www.cnblogs.com/zhangyinhua/p/7687377.html
韩顺平 ==P90数组链表, ==到他的JavaSE有集合的源码分析。
好像 宋红康 老师讲得更好。