集合
为什么使用集合
集合就是用来保存对象的,他的长度可变,数组的长度每次定义好之后就不可变了,但是我们不知道要存储多少个元素,所以我们就要使用集合。
集合中的数据都是存放在内存中的。当程序关闭或重启后集合中的数据丢失,所以集合是一种临时存储数据的容器
集合分类
迭代器
迭代器是一种设计模式,接口,专门用来遍历循环集合,它只使用容器Collection,并不关心容器类型。
应用场景:我们使用容器时必须指定类型,但是我想把list的操作给set。就会使用到Iterator.
每个实现类都提供了.iterator();返回值就是迭代器对象。
public class TestIterator {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
//获取下一个元素
Integer next = iterator.next();
System.out.println(next);
}
//lterable用来返回迭代器,实现了该接口的类就算是可迭代对象了,可以用for-each循环访问数据。省去了next操作
Collection list1 = new ArrayList<>();
for(Object o : list1){
System.out.println(o);
}
}
}
1
2
3
注意
java中的迭代器是一个接口,名为Iterator,将通用性做到了极致,可以访问不同特性的集合数据而无需关心他们的内部实现。集合并不是直接去实现Iterator接口,而是实现的Iterable接口,用这个Iterable定义的方法区返回当前集合的迭代器,Collection就继承了这个接口,所以Collection体系内的集合都的按照这种方式返回迭代器collection.iterator(),以供大家访问数据。
why?
如果集合直接实现迭代器的话,那别人调用了当前集合的next方法,就会影响到你遍历数据,比如你希望从头开始遍历数据,但是别人已经遍历完了,你就拿不到数据了,而通过实现Iterable这种方式,就可以每次返回新的迭代器,不同迭代器遍历数据时互不影响。所以这就可以看出迭代器是具有独立性和隔离性的。但如果我们正确的返回了迭代器,那么我们就可以用for-each来循环数据。
总结
Iterable用来返回迭代器,实现了该接口的类就算是可迭代对象了,可以用for-each循环访问数据。省去了next操作
Iterator就是迭代器,用来遍历集合的数据,并无需关心集合的内部实现。for-each底层用的就是迭代器
Iterator
只能单向遍历
.hasNext(); 检查集合中是否还存在元素
.next(); //获取集合的下一个元素
.remove(); 将迭代器返回的元素删除
ListIterator
双向遍历
.hasNext(); 检查集合中是否还存在元素
.next(); //获取集合的下一个元素
.remove(); 将迭代器返回的元素删除
.add(); 添加元素
public class TestListIterator {
public static void main(String[] args) {
//创建一个集合对象
List<Integer> list = new ArrayList<Integer>();
//向集合中添加分数
list.add(78);
list.add(80);
list.add(89);
ListIterator<Integer> lit = list.listIterator();
//从前向后遍历
while(lit.hasNext()){
lit.next();
}
//从后向前遍历 ---- 前提是必须正着来一遍
while(lit.hasPrevious()){
int elem = lit.previous();
System.out.println(elem +" "+lit.nextIndex()+" "+lit.previousIndex());
}
}
}
89 2 1
80 1 0
78 0 -1
区别
1.适用范围不同: iterator : 适用于Collection集合 ---- list和set 。Listiterator :适用于list
2.遍历顺序不一样: Listiterator 双向遍历。iterator 单向遍历(向后)
3.操作不一样:Listiterator 在循环元素时可以可以add、remove,set ,Iterator:remove
Listiterator 可以定位当前索引位置,nextIndex(),previousIndex(),Interator不可以。
foreach和Iterator
Iterator只能用来遍历集合。foreach可以遍历集合和数组,但是遍历集合时不能操作数据,对数据进行添加删除等操作,Iterator可以。
JDK1.8之后添加了removelf方法,可以条件删除数据
源码
以ArrayList为例
public class TestIterator {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//获取集合的迭代器
Iterator<Integer> iterator = list.iterator();
//.hasNext() 判断是否有下一个元素
while (iterator.hasNext()) {
//获取下一个元素
Integer next = iterator.next();
System.out.println(next);
}
}
}
/**
* The size of the ArrayList (the number of elements it contains).
*ArrayList的大小(它包含的元素数)。
*/
private int size; // 数组容量 = 3
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
这个列表被结构修改的的次数。结构修改是指改变列表的大小,或以其他方式干扰它,
使进行中的迭代可能产生不正确的结果
*
*This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
此字段由迭代器和列表迭代器实现使用 由迭代器和方法返回。如果此字段的值发生意外变化,则迭代器(或列表
将抛出一个代码并发修改异常 .对于{next}、{remove}、{previor},{set}或{add}操作。。提供 了fail-fast(快速失败)行为,不是非确定性行为
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
子类使用此字段是可选的,如果子类希望提供快速失败迭代器(和列表迭代器)只需在其{add(int,E)}和
{remove(int)}方法(及其重写的任何其他方法导致列表的结构修改)。
一次呼叫{add(int,E)}或{remove(int)}添加的数量不得超过或者迭代器(和列表迭代器)将抛伪 ConcurrentModificationExceptions。如果实施者不希望提供快速失败迭代器,此字段可忽略
*/
//在 AbstractList 定义的全局变量,他在add,remove。set等修改数据的时候会+1
protected transient int modCount = 0;
private class Itr implements Iterator<E> {
// 调用next方法返回的元素索引
int cursor; // 0 1 2 3
// 最近一次调用next方法返回的元v素索引-如果没有,则为-1
int lastRet = -1; //-1 0 1 2
// expectedModCount 修改次数
int expectedModCount = modCount; // 3 -- 循环调用next方法 3次
//判断集合中是否还有元素
public boolean hasNext() {
//第一次调用的元素索引是否等于集合长度
return cursor != size; // 0 != 3 ; true
}
// 返回下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 判断是否进行了修改操作
checkForComodification();
// i = 0 第一次调用的元素下标
int i = cursor;
// next返回的索引 大于 数组
if (i >= size) // 0 >=3 flase
//无搜索元素异常
throw new NoSuchElementException();
// 转换为数组
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
//并发修改异常
throw new ConcurrentModificationException();
//next返回的下标 +1
cursor = i + 1; // 0+1 = 1
// 返回元素
return (E) elementData[lastRet = i]; // elementData【0】
}
// 删除迭代器返回的元素
public void remove() {
//判断 最近一次访问的元素下标 是否正确
if (lastRet < 0) // 0< 0 flase
//非法状态异常 --- 集合不存在
throw new IllegalStateException();
checkForComodification();
try {
//删除元素
ArrayList.this.remove(lastRet); // 0 modCount +1 = 4
// 1 = 0
cursor = lastRet;
//最近一次调用的下标赋值为-1
// 0 = -1
lastRet = -1;
// 3 = 4
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// fail-fast(快速失败机制)单线程和多线程中会出现
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
fail-fast 快速失败机制
java集合中的一种错误检查机制,是不确定性。主要用于线程操作(单线程也会发生)
触发条件
1.在循环遍历集合时,向集合中插入值或删除集合中值时会出现这个异常。
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
for (Integer integer : list) {
list.remove(integer);
}
}
}
2.当多个线程对同一个Collection集合进行修改数据的操作。其中一个线程用迭代器进行循环,但是当前集合的数据被其他线程所改变。单线程也会。
why
迭代器在调用add,next、等方法时中会调用checkForComodification() 方法,他主要用来检测modCount 是否 expectedModCount相同,不相同就会触发异常。
解决
1.使用线程安全的集合 ConcurrentHashMap,CopyOnWriteArrayList ,CopyOnWriteArraySet
2.添加锁,在所有涉及改变数据结构即modcout值的操作加上Synchronized。
List
有序可重复,允许多个null值,按照插入顺序添加数据,获取元素时可以根据迭代器和获取元素下标
ArrayList
底层是一个数组,增删慢,查询快,最常使用
数组长度固定,超出以后需要扩容,也就是创建一个新的数组,将旧的内容放到新的里面,使用尾插法并指定初始容量可以极大提高性能,甚至超过Liked list(需要创建大量的node对象)
LinkedList
底层是一个双向数组,保存下一个元素的下标引用,查询慢,增删快
循环数据时只能使用迭代器,不能用for,因为数据是零散的存储在内存中,当我们用for循环来get某个元素时,都要对List重新进行遍历,性能消耗极大,并且indexof(获取内容第一次出现的下标),结果为空时会遍历整个集合
Set
无序不重复,最多允许一个null值,通过迭代器或者转变为数组来获取元素
HaseSet
底层是哈希表,无序,查询速度最快,TreeSet其次(按内容查询,如果按索引查询,Arraylist最快)
TreeSet
底层是TreeMap(红黑树),有序(可以自定义)
LinkedHaseSet
底层是哈希表+链表,有序(插入顺序)
注意-----唯一性
1.存储自定义类在一个底层实现为哈希表的时候,我们要重写hasecode 和 equals 方法。
java定义的类比如String,本身就已经重写了这两个方法,如果我们自定的类没有重写着两种方法,那么会存入相同的数据,破坏了唯一性。
2.存储元素在一个底层为红黑树的集合时想要自定义排序,我们要实现comparable(内部比较器)或Comparator(外部比较器)接口,内部比较器只有一个,外部比较器有多个。
Comparable 外部比较器 <0 倒序 , = 0 相同 > 0 正序
被abstract修饰,不能作为内部类使用
public class Student implements Comparable<Student>{
private Integer age;
private String name;
//重写compareTo方法
@Override
public int compareTo(Student o) {
return this.age - o.age; // 按年龄正序排列
//return o.age - this.age; 按年龄倒序排列
}
}
Comparator 内部比较器 有多个参数
public class iterator1 {
public static void main(String[] args) {
test1 test1 = new test1(1,"A");
test1 test2 = new test1(2,"Q");
test1 test3 = new test1(2,"C");
// 匿名内部类 --- 当这个比较方法只使用一次,我们淡出创建一个类有点浪费
Set<test1> tests = new TreeSet<test1>(new Comparator<test1>() {
@Override
public int compare(test1 o1, test1 o2) {
double age = o1.age -o2.age;
if (age> 0) return 1;
else if (age< 0) return 0;
// 如果年龄相同,按照姓名倒序
else return o1.name.compareTo(o2.name);
}
});
}
Map
key - value 形式存储,key不重复
HaseMap:无序,key可以为null
TreeMap: 有序 key 不为null
LinkedHaseMap: key不为null,有序(按照插入顺序,get的元素放在最后面)
key相同时,后者会覆盖前者,返回前者
Hase
1.为什么使用
无序数组的查询时间复杂度为O(n) , 全部遍历一遍
有序数组 用二分查找,时间复杂度0(long2^n)
二叉平衡树 用二分查找,时间复杂度0(long2^n)
hase表 通过hase算法得到存储地址 时间复杂度 O(1)
2.hase冲突
hase算法计算的数据是无限,但是计算后的范围是有限的,所以会出现不同的数据,他们的hasecode是相同的
解决
1.链地址法:HaseMap常用(JDK1.7之前链表+数组,JDK1.8之后链表+数组+红黑树)
Hash Map采用的是单向链表法,它具有Next
指针,可以连接下一个实体,以此来解决Hash
冲突的问题。
2,再哈希法:提供多个hase函数,如果第一冲突了,就用第二个hase值去计算
3,公共溢出法:创建一个公共区域。将这些溢出的元素都放在这里
4,开发地址法:当key的哈希地址出现冲突时,从冲突的位置向下找一个为空的数组下标进行存储,如果到最后一个依旧没有找到,那么就从头开始查找(注:该方法有三种分类,这只是其中一个)
3.HaseMap
https://www.bilibili.com/video/av60318686
https://blog.csdn.net/wangzhipeng47/article/details/106751172#comments
JDK1.7之前链表+数组(头插),JDK1.8之后链表+数组+红黑树(尾插)
1.7:在构造方法中创建一个长度16的 Entry【】table数组对象用来存储数据
1.8:在第一次调用put方法创建数组 Node【】table用来存储数据
底层实现
从Java8开始HashMap底层由数组+链表变成数组+链表+红黑树。
使用HashMap时,当使用无参构造方法实例化时,设置扩容因子为默认扩容因子0.75。
当向HashMap添加内容时,会对Key做Hash计算,把得到的Hash值和数组长度-1按位与,计算出存储的位置。
如果数组中该没有内容, 直接存入数组中(Node节点对象), 该下标中有Node对象了, 把内容添加到对应的链表或红黑树中。
如果添加后链表长度大于等于8,会判断数组的长度是否大于等于64,如果小于64对数组扩容,扩容长度为原长度的2倍,扩容后把原Hash表内容重新放入到新的Hash表中。如果Hash长度大于等于64会把链表转换红黑树。
最终判断HashMap中元素个数是否已经达到扩容值(threshold),如果达到扩容值,需要进行扩容,扩容一倍。
反之,如果删除元素后,红黑树的元素个数小于等于6,由红黑树转换为链表。
问题
1. key值怎么选择,用自定义类需要注意什么
一般选择Interager和String类型,因为他们是被final修饰的,不可改变,保证hase值具有不可更改性和计算准确性,减少hase碰撞的几率
1.被final修饰,保证了其不可变性,也就是key不可更改,不会存在获取到的hase值不同的情情
2.重写了equals和hasecode方法,不会出出现hase值计算错误
使用自定义类时,我们要重写equals和hasecode方法.
2. 为什么使用红黑树而不用二叉树
二叉树在特殊情况下会生成一条线性结构,和链表相似,查询速度太慢。红黑树是二叉平衡树,会通过左旋优璇等操作保持平衡,虽然会付出一些代价,但是比起遍历链表而言,代价比较小,如果链表长度过小,就没必要转换为红核数,转换了反而浪费性能,查询速度慢。
3. 什么时候需要把链表转为红黑树
链化:红黑树节点<6
树化:链表长度大于等于8,数组长度大于等于64
4. 加载因子为什么是0.75
加载因子就是元素个数/数组容量
如果加载因子为1,空间利用率大大提升,但是产生hase冲突的机会就会大大增加,链表过长,查询效率慢
如果加载因子为0.5,hase冲突大大减少,但是空间利用率不高,频繁扩容,代价太大。
5. 为什么扩容
当我们添加的数据过多,产生的链表也就越长,当我们查询元素时,就需要遍历整个列表,速度太慢,哈希表的意义也就没有了。为了避免这种情况发生,我们就需要减少链表的长度-----扩容也就是增加数组的长度。
6.什么时候扩容
1.集合初始化,也就是第一次put的时候,数组长度默认为16
2.数组长度<64,链表长度>8
3.达到了阀值 = 加载因子*数组容量
7.HaseMap扩容机制
1.7之前: 数组+链表
1.先生成一个新的数组,之前的2倍
2.遍历老数组中所有的元素
3.获取每个元素的key值,并基于新数组的长度重新进行哈希运算,得到新的下标
4.将元素添加新的数组中
5.所有元素添加完成之后,将新数组赋值给对HaseMap象的table
1.8之后: 数组+链表+红黑树
1.先生成一个新的数组,之前的2倍
2.遍历老数组的每一个元素
链表:直接获取key值,重新计算下标,添加
红黑树:先遍历红黑树,计算出每个元素的下标,统计每个下标位置的元素个数,如果这个位置的元素大于8,则生成一个新的红黑树,并将红黑树的根节点添加到新数组的对应的位置,小于8,生成链表,将头节点添加到新数组对应的下标位置
3.所有元素添加完成之后,将新数组赋值给对HaseMap象的table
8. 存储在Node中的hash值, 是否就是key的Hasecode()?
不是,存储的是对Key先做(hasecode)计算, 然后再无符号右位移16, 再按位异或,为了减少hase冲突。`
9. 怎么获取一个线程安全的HaseMap
1.加锁 --- 使用Collections中的`Collections.synchronizedMap(map);`方法返回一个线程安全的HaseMap但是返回的并HaseMap不是真正的。只是一个引用
2.使用ConcurrentHashMapMap,HaseTable(已经废弃)
10. HaseMap和HaseTable的区别
1.线程是否安全: HaseMap是非线程安全的,HaseTable是线程安全的,内部使用的方法基本都经过synchronized修饰(如果想保证线程安全建议使用ConcurrentHashMap,HaseTable已经被淘汰了)
2.效率: 因为线程问题,HaseMap要比HaseTable效率要高一些
3.null值: HaseMap中可以支持key和value为null,key只有一个,value可以为多个。HaseTable key不能为null
4.初始容量和扩容: HaseMap默认初始容量为16,每次扩容,数组为之前的两倍,如果指定了初始容量,将其扩容为2的幂次方。HaseTable默认初始容量为11,每次扩容,数组为之前的两倍+1,如果指定了初始容量,将其扩容为2的幂次方
5.底层结构: HaseMap在JDK1.8之后,使用链表+数组+红黑树来提高查询效率
6.hase计算: HaseMap的对key的Hasecode进行了二次hash,以获得更好的散列值,然后对table数组长度取模,Hashtable直接使用key的hashcode对table数组的长度直接进行取模
11.哈希表底层采用什么算法计算hase值,还有什么算法可以用
底层采用key的hasecode方法值结合数组长度进行无符号右移,按位异或(^),按位与(&)计算出索引。
换可以用:平方取中法,取余数,伪随机数法,位运算效率最高,其他较低
HaseMap为什么线程不安全
put
有两个线程A和B
首先A插入一个key-value值到HashMap中,计算得到桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了
此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面
假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的
那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,
如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
resize扩容
JDK1.7时会发生,因为他使用的头插法
单向链表对表头操作效率最高,因此我们要保证先加入的数据在表头,然后将链表整体下移,因此新添加得数据放在链头,最先加入的数值放在链尾,但如果这个位置没有数据,那就直接放就完了。
【大厂经典面试题,CTO带你深入解读:为什么HashMap会产生死循环?【Java面试】-哔哩哔哩】 https://b23.tv/0fGpcmU
两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,
转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。
我们假设有两个线程同时需要执行resize操作
此时线程A中运行结果如下:
线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:
这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry
都是主存中最新值:7.next=3,3.next=null。
此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:
newTable[3]=e ----> newTable[3]=3 e=next ----> e=7
此时结果如下:
继续循环:
e=7 next=e.next ----> next=3【从主存中取值】
e.next=newTable[3] ----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3
结果如下:
再次进行循环:
e=3 next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7
即:3.next=7
newTable[3]=e ----> newTable[3]=3
e=next ----> e=null
注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。
结果如下:
源码JDK1.8之后
1.基本属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列化和反序列化时使用相同的id
private static final long serialVersionUID = 362498820763181265L;
//初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形阈值
static final int TREEIFY_THRESHOLD = 8;
//链形阀值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形容量
static final int MIN_TREEIFY_CAPACITY = 64;
//节点
transient Node<K,V>[] table;
//存储键值对的个数
transient int size;
//散列表被修改的次数
transient int modCount;
//扩容之后的数组大小 容量*加载因子
int threshold;
//负载因子
final float loadFactor;
}
2. 构造方法
//无参构造器
public HashMap() {
//加载因子默认为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//指定容量大小的构造器,调用的是全参构造器
public HashMap(int initialCapacity) {
// 指定的容器长度 , 加载因子 0.75
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//全参构造器 指定的容器长度 加载因子
public HashMap(int initialCapacity, float loadFactor) {
// 如果指定的容器长度 小于 0
if (initialCapacity < 0)
// 抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//HashMap 的最大容量只能是 MAXIMUM_CAPACITY,哪怕传入的数值大于最大容量,也按照最大容量赋值
// 如果容器大小大于 最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
// 容器长度 设置为 最大容量
initialCapacity = MAXIMUM_CAPACITY;
// 加载因子小于0 或者 不是一个数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//加载因子为0.75
this.loadFactor = loadFactor;
//扩容之后的数组大小
this.threshold = tableSizeFor(initialCapacity);
}
//返回给定目标容量的 2 大小的幂。
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//创建一个新的HaseMap将传入的子Map中的全部元素逐个添加到HashMap中
public HashMap(Map<? extends K, ? extends V> m) {
//设置加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
3.Node 结点
1.7 之前是Entry 结点,1.8之后则是 Node 结点,其实相差不大,因为都是实现了 Map.Entry (Map 接口中的 Entry 接口)接口,即,实现了 getKey() , getValue() , equals(Object o )和 hashCode() 等方法;
static class Node<K,V> implements Map.Entry<K,V> {
//hash 值
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;
}
//返回与此项对应的键
public final K getKey() { return key; }
//返回与此项对应的值
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//hash 值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断2个Entry是否相等,必须key和value都相等,才返回true
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
4. 添加键值对
4.1 put()方法
//添加键值对
public V put(K key, V value) {
/*
*参数一: 调用hash()方法
*参数二: 键
*参数三: 值
**/
return putVal(hash(key), key, value, false, true);
}
4.2 hash()方法
static final int hash(Object key) {
int h;
//key为空,hase值就是 0 ,否则hasecode移位右移16位进行按位异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
4.3 putVal()方法
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
onlyIfAbsent如果为真,则不更改现有值
*@param onlyIfAbsent if true, don't change existing value
exist如果为false,则表处于创建模式。
* @param evict if false, the table is in creation mode.
return上一个值,如果没有,则返回null
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//申明tab 和 p 用于操作原数组和结点
Node<K,V>[] tab; Node<K,V> p;
int n, i;
//如果原数组是空或者原数组的长度等于0,那么通过resize()方法进行创建初始化
if ((tab = table) == null || (n = tab.length) == 0)
//获取到创建后数组的长度n
n = (tab = resize()).length;
//通过key的hash值和 数组长度-1 计算出存储元素结点的数组中位置(和1.7一样)
//并且,如果该位置为空时,则直接创建元素结点赋值给该位置,后继元素结点为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//否则,说明该位置存在元素
Node<K,V> e; K k;
//判断table[i]的元素的key是否与添加的key相同,若相同则直接用新value覆盖旧value
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 {
//遍历table[i],并判断添加的key是否已经存在,和之前判断一样,hash和equals
//遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
for (int binCount = 0; ; ++binCount) {
//如果遍历的下一个结点为空,那么直接插入
//该方法是尾插法(与1.7不同)
//将p的next赋值给e进行以下判断
if ((e = p.next) == null) {
//直接创建新结点连接在上一个结点的后继上
p.next = newNode(hash, key, value, null);
//如果插入结点后,链表的结点数大于等7(8-1,即大于8)时,则进行红黑树的转换
//注意:不仅仅是链表大于8,并且会在treeifyBin方法中判断数组是否为空或数组长度是否小于64
//如果小于64则进行扩容,并且不是直接转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//完成后直接退出循环
break;
}
//不退出循环时,则判断两个元素的key是否相同
//若相同,则直接退出循环,进行下面替换的操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//否则,让p指向下一个元素结点
p = e;
}
}
//接着上面的第二个break,如果e不为空,直接用新value覆盖旧value并且返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//添加成功后,判断实际存在的键值对数量size是否大于扩容阈值threshold(第一次时为12)
if (++size > threshold)
//若大于,扩容
resize();
//添加成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
return null;
}
4.4 resize()方法
//该函数有两种使用情况:初始化哈希表 前数组容量过小,需要扩容
final Node<K,V>[] resize() {
//获取原数组
Node<K,V>[] oldTab = table;
//获取到原数组的容量oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取原扩容之后的大小
int oldThr = threshold;
//新的容量和阈值目前都为0
int newCap, newThr = 0;
if (oldCap > 0) {
//如果原数组容量大于等于最大容量,那么不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//而没有超过最大容量,并且大于初始容量 16,那么扩容为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容为原2倍
newThr = oldThr << 1; // double threshold
}
//经过上面的if,那么这步为初始化容量(使用有参构造器的初始化)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//否则,使用的无参构造器
//那么,容量为16,阈值为12(0.75*16)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//计算新的resize的上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//使用新的容量床架一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新的数组引用赋值给table
table = newTab;
//如果原数组不为空,那么就进行元素的移动
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)
//如果无链表,确定元素存放位置,
//扩容前的元素位置为 (oldCap - 1) & e.hash ,所以这里的新的位置只有两种可能:1.位置不变,
//2.变为 原来的位置+oldCap,下面会详细介绍
newTab[e.hash & (newCap - 1)] = e;
//判断是否是树结点,如果是则执行树的操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否则,说明该元素上存在链表,那么进行元素的移动
//根据变化的最高位的不同,也就是0或者1,将链表拆分开
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//最高位为0时,则将节点加入 loTail.next
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//最高位为1,则将节点加入 hiTail.next
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
ConcurrentHashMap
https://blog.csdn.net/qq_41737716/article/details/90549847
JDK1.7之前
首先将数据分为一段一段的进行存储,然后每一段数据配上一把锁(lock锁),当一个线程占用锁访问其中一个数据时,其他的数据段也可以被其他的线程进行访问。
JDK1.8 之后
锁的的是节点,volatile+CAS实现无锁化,底层使用数组+链表+红黑树
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通