Java集合

Java集合

牛客的课,整理笔记

在这里插入图片描述

在这里插入图片描述

Set

在这里插入图片描述
看集合重点看
1.数据结构,这个集合底层是如何实现的,数据是怎么存储的
2.数据的有序性是如何保证的
3.容量,能存多少,扩容机制,缩容机制

HashSet

在这里插入图片描述
可以看到Set底层是Map,所以简单看一下,主要逻辑在Map中看
但是这里map加了一个transient,指的是禁止序列化,map就是用来存数据的,为什么禁止序列化呢
因为set只是将数据存在了map的key部分,如果直接序列化,会有很多没有意义value被序列化,浪费空间

这里的PRESENT,是存数据时value部分的值,而不是null。为什么要这么做,因为set集合操作中可能会用到判断是否包含一个数据,或者删除,会用到比较,而如果是null的话,无法进行比较。这里定义为一个object起到了比较的作用
看
添加的方法
如果以前map中没有这个key,map.put方法会返回null,然后add方法就返回true,表示添加成功了
如果有这个key,会返回之前对应的value,不等于null,add方法返回false
在这里插入图片描述
移除数据,如果之前有key,返回map.remove方法返回value,然后remove方法就返回true
否则返回false
在这里插入图片描述

迭代器方法,返回set部分的迭代器
在这里插入图片描述
序列化方法,将对象写入流
关键点在于遍历了key部分,只序列化key部分
在这里插入图片描述
readObject将已经序列化过的流的数据中,转换回对象用的方法
在这里插入图片描述
关键也在于for,size是流的字节数,读对象,然后存入提前创建的map中
在这里插入图片描述

TreeSet

在这里插入图片描述
TreeSet底层也是Map,NavigableMap 可排序的Map,也不能序列化
value存的也是PRESENT常量

在这里插入图片描述
默认的构造器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

List

重点

ArrayList

在这里插入图片描述
默认长度为10
这里有两个空的数组,一个EMPTY_ELEMENTDATA,一个DEFAULTCAPCITY_EMPTY_ELEMENTDATA
用来区分创建集合的时候,是按照默认长度构建的,还是传入了一个长度。用默认方式和传入长度方式创建扩容方法不一样
在这里插入图片描述在这里插入图片描述
具体数组对象和元素的个数size
在这里插入图片描述
默认的构造器
在这里插入图片描述

传入长度,若传入0,初始化为一个空的数组,小于0报错
在这里插入图片描述
添加数据,先扩容,再添加数据
在这里插入图片描述

先计算容量,如果是默认构造器构造的数组,那么会考虑默认容量的大小;如果不是无参构造器构造的,那么就直接返回最小容量

计算完以后,传入ensureExplicitCapacity()方法,首先修改次数加一,如果最小容量减去当前数组长度大于0,就扩容;如果小于等于0了,那么说明出现问题了

在这里插入图片描述
扩容方法,新的容量,等于旧的容量的1.5倍
但是这个整型变量有可能会溢出
所以这里有个判断,如果新的容量减去旧的容量小于0,说明整数溢出变成负数了,那么就把最小容量赋给新容量
如果没有溢出,但是超过了数组最大的限制,掉hugeCapacity方法

数组扩容,数组不能动态改变。扩容要创建一个新的数组,把旧的数组数据拷贝到新数组中
在这里插入图片描述
数组最大限制
在这里插入图片描述

如果minCapacity小于0,说明整数溢出了,抛出异常
如果最小容量大于数组最大限制,如果大于,就返回整型最大值,否则,返回数组最大限制
在这里插入图片描述

调用默认的迭代器,返回的是实例,是一个内部类Itr
在这里插入图片描述

另一个迭代器,增强迭代的能力,可以从后往前,从中间开始迭代,也支持插入,覆盖数据。一般ArrayList的迭代,建议使用这个迭代器
在这里插入图片描述

Itr类
cursor 当前迭代的索引下标,游标
lastRet上一次迭代的位置,为了能够更加灵活的迭代(另一个迭代器能从后往前迭代)
期望修改次数,赋值为修改次数。在迭代过程中,会判断是否已经修改,如果修改了,就不能迭代了。用于判断在迭代过程中,是否能对数据进行修改
在这里插入图片描述
next()迭代,找到下一个,会先检查修改次数
在这里插入图片描述
移除数据
在这里插入图片描述
检查修改次数,如果不相等,就抛出异常,意味着在迭代或者删除过程中,有人修改过数据
在这里插入图片描述

ListItr()
初始化,可以指定从哪个位置开始迭代
在这里插入图片描述
向前迭代
在这里插入图片描述
覆盖某一个值,把前一个值覆盖掉
在这里插入图片描述
把元素插到当前位置
在这里插入图片描述
主动缩容的方法
会把数组初始化为size大小的数组;如果size为0,那么就为空数组
在这里插入图片描述

LinkedList

在这里插入图片描述
size、首节点,尾节点
在这里插入图片描述
在这里插入图片描述

Node内部类,前驱和后继节点都有,双向链表
在这里插入图片描述
add加到末尾
在这里插入图片描述

add,指定位置的添加
如果等于size,就加到最后;如果不等于,就加到index前面
在这里插入图片描述
在这里插入图片描述
获取值
在这里插入图片描述

替换值
在这里插入图片描述

删除方法
在这里插入图片描述

node()方法,获取索引位置处的节点
如果index小于size的一半,从前往后遍历;否则从后向前遍历
在这里插入图片描述

Queue和Deque

remove() element() 如果为空,抛出异常;poll() peek()如果为空,返回null
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ArrayDeque

在这里插入图片描述
ArrayDeque 有一个数组
在这里插入图片描述

头指针,尾指针,最大容量为Integer.MAX_VALUE - 8,最小容量为8
tail指向尾部元素后一个元素的位置
在这里插入图片描述

构造器,默认初始化容量为16
或者指定元素个数,但是不是指定为几就初始化为几,而是要计算数组的多长合适,因为有最小长度的限制
在这里插入图片描述
在这里插入图片描述

如何计算
如果比最小容量大,那么要扩展到最接近的2的n次方
ArrayDeque的容量一定是2的n次方
如果溢出了,那么向右移一位
在这里插入图片描述
offer
在这里插入图片描述
offerLast
在这里插入图片描述
add
在这里插入图片描述
addFirst
(head - 1) & (length - 1)相当于做一个取模运算
如果到尾部了,就双倍扩容
在这里插入图片描述

这里创建双倍容量的数组拷贝的时候,先拷贝头指针右侧的元素到新数组0到r位置,然后再拷贝头指针左侧的元素,到新数组r到r + p位置
然后头指针指向0,尾指针指向n
在这里插入图片描述

在这里插入图片描述

删除头部元素,先把头指针指向的元素删了,然后把头指针右移一位
删除尾部元素,先把尾指针左边一位的位置元素删除了,然后把尾指针指向其左边一位(因为尾指针定义是尾节点的后面一位)
在这里插入图片描述
当做栈来用,入栈
在这里插入图片描述
出栈
在这里插入图片描述

removeFirst又是由pollFirst实现的,如果为空,抛出异常
在这里插入图片描述

LinkedList

在这里插入图片描述
和之前一样的
在这里插入图片描述
入队 offer
在这里插入图片描述
add调用linkLast()
在这里插入图片描述
在这里插入图片描述
linkFirst,先拿到链表头部节点f,然后头部指针指向新节点,然后将头部节点的前指针指向新节点
在这里插入图片描述
linkLast同理
在这里插入图片描述
出队,poll
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
栈的方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Map

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

有一个数组table,hashmap内部的数据都是通过这个数组来存储的,数组中的元素是以Node节点封装的
在这里插入图片描述

Node是一个内部类,因为hashmap里面是键值对,而每一个键值对都被封装成一个Entry。Node实现了Entry这个接口
有一个指针next指向了下一个节点,因此Node是单链表
在这里插入图片描述
Map.Entry 是接口Map中的一个接口
在这里插入图片描述

在这里插入图片描述

因此这里看出来,HashMap这里是一个数组,数组中的元素是Node,而Node是一个单向链表。是一个数组加单向链表的结构

树的结构:
TreeNode继承于LinkedHashMap中的内部类Entry,而Entry又继承于HashMap中的Node,而Node刚刚看到了,是继承Map.Entry的
因此TreeNode是间接的继承了Node的,因此如果有一个TreeNode,我们可以看成一个树,也可以看成一个链表;当链表转换为树的时候,是按照树的结构重组了链表,但是树中的节点还保留了链表的关系,所以由树还原退化成链表就非常方便
在这里插入图片描述
在这里插入图片描述

若元素为链表节点,并且长度达到8,不一定就转为树,可能是因为数组太小(小于64),碰撞太多,所以也可能尝试扩容
在这里插入图片描述

put存放数据要先计算key的哈希值
在这里插入图片描述
计算哈希值,有一个高位与低位的异或处理
在这里插入图片描述

首先,第一个if,如果数组为空,初次扩容resize()
第二步,第二个if,计算元素位置(hash & n - 1)相当于取模运算,获得数组中该位置的头节点,如果为null,那么就在这个位置新建一个链表节点
第三步,else中,第一个if,如果头节点的key和传入的key是相等的,那么直接覆盖value部分的值
否则,如果p节点是树节点,那么就加到树中,加完以后,返回添加后的节点
否则,一定就是链表了,首先从头开始遍历这个链表,如果p的后继节点为null,直接插到这个这个后继节点处;要注意这里,如果此时,bitCount大于等于8 - 1,则扩容或者转为树;如果发现要插入的节点是已经存在的节点(key相同),那么叫跳出循环。这段逻辑执行完,说明找到了要插入的位置

一般我们看,都会说如果大于等于8,那么链表就转换为树,而这里我们看到是7,问题也显而易见,我们插入一个元素后,bitcCount是7,所以此时链表的节点个数还是8个,也就是一般所说的大于等于8转换
在这里插入图片描述
在这里插入图片描述

如果此时e不等于null,就说明找到了一个位置插入这个值。先把这个节点的旧值存储下来
如果 !onlyIfAbsent,就是允许更新的话,那么就更新这个值

更新完以后,判断元素的数量是不是大于阈值,如果大于就扩容
在这里插入图片描述
在这里插入图片描述

扩容机制(重要)

这里扩容是将容量翻倍(一直是2的n次方),但是容量翻倍以后,按照常规理解,可能会将原哈希表中每一个值都重新计算一遍对新的容量大小取余,看放在哪个位置;但是这里如下图所示,在计算新的哈希值时,只需要看看索引多出来的一位是否是1,如果是1,那么就将原哈希表对应位置的元素放在新哈希表的高位(也就是加上当前扩容的容量,如下图的16),否则位置不变,即在低位。这样就使哈希表在扩容的时候,并没有那么复杂,而是只需要一个标志位,看看是否为1就可以了。这种运算非常高效

在这里插入图片描述
threshold,人为指定的容量
在这里插入图片描述
负载因子,比这个比率更大发生哈希碰撞的几率就高了,就要考虑扩容了,默认是0.75
在这里插入图片描述
默认构造器里面,初始化了负载因子,0.75。容量是默认值
在这里插入图片描述在这里插入图片描述
指定容量的构造器
在这里插入图片描述

初始化的容量,这里是有要求的,必须是2的n次方。如果不是2的n次方,会进行处理tableSizeFor
在这里插入图片描述
做一个运算,使得容量扩展到2的n次方
在这里插入图片描述

resize()扩容方法
threshold是一个参照值
第一个if,如果旧的容量是大于0的,并且已经扩到最大值了,那么就不再扩容了;如果左移一位以后小于最大值,并且旧容量大于默认初始化容量,就扩展到两倍
如果旧的容量不是大于0的,那么说明这个容量没有被初始化过,就参照threshold初始化容量,这里else if 如果旧的threshold大于0,那么就将这个值赋值给新容量,相当于初始化;否则,threshold没有给定,那么就初始化为默认的容量16,并且新的threshold等于默认容量乘以默认的负载因子
在这里插入图片描述

如果newThr是0,那么就算出一个临界值,即新的容量乘以负载因子
根据临界值,算出newThr
在这里插入图片描述

因为已经计算得到了新的数组的容量大小,所以接下来创建了新的数组,然后就要进行数据的迁移了

如果旧的数组不是空,那么就遍历旧的数组,取出数组当前位置的节点Node,然后清空当前位置。判断当前Node是否是只有一个节点,如果是,那么就直接计算应该放在高位还是低位(上面说过怎么计算),然后直接放到新的数组中
如果当前这个点是一颗树,我们就需要把这个点拆分开来再插入新的数组中

在这里插入图片描述

如果是链表,先定义五个变量。低位槽的首尾节点和高位槽的首尾节点,还有一个next
然后进入循环,如果当前e的哈希值和旧容量相与等于0,那么就加到低位槽中。否则加到高位槽中。这个循环里面只是在高低位都形成一个链表
当跳出循环,两个链表已经形成,然后就放入对应的槽中

最后返回形成的新的数组
在这里插入图片描述
在这里插入图片描述

处理树的split方法,逻辑和处理链表类似,首先对节点位置进行分配,然后再加入到槽中

在这里插入图片描述

第一个for循环,处理各个节点应该在高位还是低位
这里的next其实还是和链表的next是一样的,并没有把这个红黑树当成树来看待。这里逻辑和刚刚链表是一样的

在这里插入图片描述

但是在插入新数组的时候,如果链表的长度是小于等于6的话,会把这个树转回成链表的形式,否则还是以树的形式存储
在这里插入图片描述

在这里插入图片描述
get方法
在这里插入图片描述

getNode
首先计算hash值应该在的位置,并且将该位置的点赋值给first,也就是头节点
然后进一步判断,传入的key是否是头节点的key,如果是,就返回头节点
否则,判断头节点是否是树,如果是树,再调用树的方法得到Node
如果不是树,那么就是链表,就遍历链表中的每一个节点
在这里插入图片描述

得到所有key的集合Set
在这里插入图片描述

KeySet是一个内部类,里面有一个iterator()方法,返回一个具体的迭代器
在这里插入图片描述

而具体的迭代器里面很简单,只有一个next()方法
在这里插入图片描述

继承的父类
这里有一个构造器,里面一个循环,找到数组中不为空的位置,用next指向这个槽的头节点
在这里插入图片描述

nextNode中的核心逻辑和构造器中一样, 也是找一个不为空的槽开始从头节点迭代
在这里插入图片描述

这个支持树结构的最小容量意思是说,当数组容量很小的时候,发生碰撞会认为是因为数组容量太小导致的,不会将链表转为树。只有当数组容量大小到达64的时候,才会开始将链表转为树
在这里插入图片描述

在这里插入图片描述

有一个根节点root,类型是Entry,一个比较器
在这里插入图片描述

可以看到是一颗红黑树
在这里插入图片描述

默认的构造方法,比较器为null,自然排序
在这里插入图片描述

定制排序
在这里插入图片描述

TreeMap的key必须要实现Comparator接口,因为要调用compareTo方法
在这里插入图片描述

put方法
如果根等于null,要新建一个根,但是在创建之前会自己和自己比较,这里的目的是对key做检查,因为key有可能为null,或者key没有实现Comparator接口
在这里插入图片描述

如果比较器不为空,那么比较当前节点和节点 t 的key,如果小于,那么向左子树走,如果大于,右子树;相等直接覆盖
else中,比较器为空,同样的逻辑,
这段代码是为了找到要插入的位置
在这里插入图片描述

然后创建这个节点,直接挂在相应位置,返回
在这里插入图片描述

Collections

在这里插入图片描述

同步集合synchronized…()

在这里插入图片描述

这个类内部很多这种方法
在这里插入图片描述

传入一个集合实例,转换成线程安全的集合(list,set,queue)
在这里插入图片描述

这个类也实现了Collection,有Collection的所有方法,但是在实现过程中,通过锁将传入实例的效果增强了。
mutex锁
在这里插入图片描述

这里基本上所有的方法都加了锁,除了迭代器
如果要想加锁,需要用户自己加锁
在这里插入图片描述

map的转化
在这里插入图片描述

在这里插入图片描述

所有的方法都加锁了,迭代也加锁了
在这里插入图片描述

不变集合(空集合、一个对象的集合、不可变视图)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

空的列表,所有的方法都是基于空列表实现的。比如size返回0,contains返回false,get永远抛出异常
在这里插入图片描述

在这里插入图片描述

集合中只有一个对象,方法也是基于此实现的
在这里插入图片描述
在这里插入图片描述

list变量是final的,不可修改
如果要修改,例如set,add,remove都抛出异常
在这里插入图片描述

JUC包下的集合

在这里插入图片描述

两类,一类CopyOnWrite,一类Concurrent
在这里插入图片描述

CopyOnWriteArrayList

写时复制技术,
读读,读写不互斥,不加锁;写写互斥,加锁
读写的冲突是通过复制新数组规避的, 写写互斥是通过加锁规避的

这种技术还有一个缺点,就是实时性不能保证,因为读是从旧数组中读,并不能读到正在更改的数据
在这里插入图片描述
重入锁 和 一个数组
在这里插入图片描述

无参构造器,长度为0的数组
在这里插入图片描述

传入数组的话,复制了一份被当前成员持有
在这里插入图片描述

传入一个集合,会将集合中的数据拆开,转换成一个数组
在这里插入图片描述

读的方法,get,没有加锁
在这里插入图片描述
在这里插入图片描述

改,add加锁,先获取当前数组,然后将数组拷贝一份,改了以后,再替换原有数组。
但是替换以后,旧的数组不一定被垃圾回收,因为有可能有线程还在读。如果后续有线程读会读新的数组
在这里插入图片描述

迭代的方法
在这里插入图片描述

这里将数组赋给了一个snapshot,快照。迭代参照快照来读
在这里插入图片描述

ConcurrentHashMap

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

没有继承HashMap
在这里插入图片描述

两个table,第二个table是并发扩容用的,数据由table迁到nextTable中
在迁移的过程中,有一部分数组在table中,一部分在nextTable中,如有这时要读取数据,要看一下原来的table中有没有这个数据,如果有直接取,如果没有,table中会存一个重定向、转发的节点,可以理解为一个指针,指向nextTable
在这里插入图片描述

sizeCtl对初始化和扩容机制进行控制
在这里插入图片描述

无参构造器
在这里插入图片描述

传入map,sizeCtl等于默认容量
在这里插入图片描述
在这里插入图片描述

传入容量,先扩展到1.5倍,然后再扩展到2的n次方,这里 sizeCtl等于计算好的容量
在这里插入图片描述

concurrentLevel是并发级别,也就是并发线程数。例如如果电脑为8核,并发级别就是8
如果初始化容量,小于并发级别,那么提升到并发级别
然后计算size,cap,最终sizeCtl等于cap
在这里插入图片描述

put方法
在这里插入图片描述

这个逻辑和HashMap中put逻辑基本一致
首先迭代这个数组,如果数组是空的,就初始化这个数组;
如果不为空,那么判断传入的key的哈希值对应的槽位是否为空,如果为空,那么以cas原子的方式将这个节点放到这个槽中
如果这个槽位非空,如果f,即头节点的哈希值,等于状态值-1。等于-1说明这个槽正在扩容,当前槽里的数据正在往新的数组中迁移,扩容过程中会有意的将头节点的哈希值设置为-1;这个时候,如果要访问这个槽,应该当前线程被阻塞,等待,但是这里为了提高效率,与其阻塞,直接让当前线程也去帮助迁移数据

如果头节点没有锁,并且头节点的key和传入的key相同,那么就找到了要插入的位置,直接覆盖到头节点的val,并直接结束该方法

在这里插入图片描述在这里插入图片描述

给头节点加锁(这里就可以看出锁的不是整个map,而是头节点)
如果fh>=0,那么说明是普通Node,是链表,那么就把传入的节点加到链表中去
否则加到树中
在这里插入图片描述

加进去以后,判断节点数量,大于阈值,转变为树

至此,put方法结束,那么有哪几个地方会导致扩容
第一个,第二个else if中,如果当前节点上锁,说明正在扩容
第二个,最后转换为树的时候
第三个,addCount,加1,超过阈值,扩容
在这里插入图片描述

initTable初始化方法
如果sizeCtl小于0,意味着有别的线程正在初始化,线程让步,转为就绪状态,进入下一轮抢夺
如果不小于0,那么尝试以CAS的方式将sizeCtl改为-1
把数组长度初始化为sc,并且将sc变为原来的3/4。即变成下一次扩容的阈值,即size达到这个阈值的时候,进行扩容
在这里插入图片描述

tabAt以cas的方式获取对应位置的节点
在这里插入图片描述

看一下扩容方法,这里addCount的逻辑不细看了,直接看一下扩容的机制transfer
在这里插入图片描述

stride步长,算一下每个CPU处理多少个节点;如果CPU核数大于1,那么n/8,每个线程处理1/8个节点,然后再除以CPU核数,是它迁移节点的个数;否则,如果只有一个,只能自己迁移全部
如果迁移节点数小于最小范围,那么就强制设置为最小值,默认16

新的数组,是原数组长度的两倍(左移一位)

ForWardingNode 转发节点。如果把一个节点从旧数组迁移到新数组,那么旧数组应该为空,但是并发迁移持续过程中,如果有一个线程来查数据,那么该怎么办?需要在旧数组中存一个转发节点,查到这个节点,就去看新数组对应的槽,在那里读取数据
在这里插入图片描述

循环,有一个循环边界bound,记录并发遍历的边界,因为并发迁移是迁移其中某几个节点
advance表示从后向前遍历,位于范围之内
这里的关键是以cas的方式去修改TRANSFERINDEX,这个是迁移的进度。如果抢到了就去迁移它,并通过stride计算范围
在这里插入图片描述

如果越界,判断是否转换完成,如果完成,替换旧数组,并重新计算sizeCtl
在这里插入图片描述

如果数组中第i个位置为空了,意味着已经迁移完毕了,会将空值变成转发节点
如果头节点正在迁移,那么继续迁移
在这里插入图片描述

否则,对头节点加锁。
这里就是具体搬运数据,如果fh>=0,代表是链表,如果是链表,逻辑和HashMap中扩容的逻辑相似,分高位和低位
在这里插入图片描述

否则,是一个树的结构,但同样也是用链表处理的
在这里插入图片描述
在这里插入图片描述

get方法,读的时候不用加锁
在这里插入图片描述

在这里插入图片描述

补充:LinkedHashMap

LinkedHashMap 刚好就比 HashMap 多一个功能,就是其提供有序,并且,LinkedHashMap的有序可以按两种顺序排列,一种是按照插入的顺序,一种是按照读取的顺序(LRU机制),而其内部是靠建立一个双向链表 来维护这个顺序的,在每次插入、删除后,都会调用一个函数来进行双向链表的维护,准确的来说,是有三个函数来做这件事,这三个函数都统称为 回调函数 ,这三个函数分别是:

第一个函数:void afterNodeAccess(Node<K,V> p) { }
作用是在访问元素之后,将该元素放到双向链表的尾巴处(所以这个函数只有在按照读取的顺序的时候才会执行)
在这里插入图片描述

第二个:void afterNodeRemoval(Node<K,V> p) { }
其作用就是在删除元素之后,将元素从双向链表中删除
在这里插入图片描述

第三个:void afterNodeInsertion(boolean evict) { }
在插入新元素之后,需要回调函数判断是否需要移除一直不用的某些元素。
在这里插入图片描述

补充:集合中元素能否为null

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值