JAVA-集合

JAVA集合

1.集合概述

●集合就是一个放数据的容器,存储的都是对象的引用而肥非对象,主要有三种集合:set,list,map

2.集合和数组的区别

●数组的长度固定,集合的长度不固定

●数组可以存基本类型或引用类型,集合只能存引用类型

●数组存储的元素必须是一个数据类型,集合存储的可以不是一个数据类型

●数组无法满足无序,不可重复等要求,集合可以有。

3.集合框架

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u117nMq1-1650426015457)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/135.png)]

●容器分为Collection和Map两大类,Collection集合子接口有Set,List,Queue三种

●List:有序容器(元素存入集合的顺序与取出的顺序一致),元素可以重复,可以插入多个null元素,元素都有索引。常见实现类有ArrayList,Vector,LinkedList。检索效率高

●Set:无序(所以也无下标)容器(存入和取出的顺序可能不一致),不可以存储重复元素,所以只允许存一个null元素,常见的实现类有HashSet,LinkedHashSet以及TreeSet。插入效率高(注:无序性 ≠ 随机性,每次遍历的顺序都是相同的,存储的时候数据根据自身的hashCode经过某个散列函数决定索引位置,无序性是指不是按照来的先后顺序挨着一个个添加的而已)

●Map:键值对集合。Key无序且唯一;Value允许重复,常见的实现类:hashMap,TreeMap,HashTable,LinkedHashMap,ConcurrentHashMap。

4.迭代器Iterator和ListIterator

●Iterator是一个接口(所有Collection下的类都实现了这个接口),所有Collection接口下的集合,都可以使用迭代器方法获取迭代器实例(集合使用泛型的话,这个也要用泛型)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yQMwDZcR-1650426015458)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/136.png)]

●Object next();返回集合中下一个元素。(刚开始是指向第一个元素上面的)

●Boolean hasNext();如果Iterator还有下一个元素,则返回true。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zZwqcldB-1650426015459)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/137.png)]

●void remove();删除当前指针指向的元素,可以实现边遍历边删除。

●ListIterator实现Iterator接口,然后添加了一些额外的功能,比如,替换,添加元素。

●Iterator可以遍历Set和List集合,而LIstIterator只能遍历List

●Iterator只能单向遍历,ListIterator可以双向遍历

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9yMQ6Op9-1650426015459)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/138.png)]

5.各种迭代方式对比

●for循环:基于计数器,在集合外部维护一个计数器,依次读取每一个位置的元素。

●迭代器遍历,Iterator:是面向对象的一种设计模式,可以屏蔽不同集合的特点,统一遍历集合的接口。Collection集合中都实现了Iterator接口。

●foreach循环:内部也是使用Iterator的方式,使用时不用显式声明Iterator。但只能做简单遍历,不能在遍历过程中操作集合。

6.ArrayList

●底层是用Object数组实现,是一种随机访问的模式。

●线程不安全

●删除/插入的时候也要做一次复制操作(毕竟底层是Object数组),如果元素比较多就很费性能

●比较适合顺序添加,随机访问的场景

●数组与List相互转换:

​ ■数组转List:Arrays.asList(array);

​ ■List转数组:arrayList.toArray();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXBEOlwC-1650426015460)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/139.png)]

●底层:private transient Object[] elementData;ArrayList实现了序列化接口,而transient的作用是不希望elementData数组被序列化。加上之后,每次序列化的时候先调用defaultWriteObject方法序列化ArrayList中的非transient元素,然后遍历elementData,只序列化已存入的元素,这样加快了序列化速度,也减少了序列化后文件的大小。

7.LinkedList

●底层使用双向链表存储,对于插入,删除频繁,使用此类效率最高。

●不能实现随机访问(毕竟是列表)

●比ArrayList更占内存(毕竟要存两个引用)

●线程不安全

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8TcPpfR0-1650426015460)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/140.png)]

8.Vector

●底层也是Object对象数组

●使用synchronized实现了线程同步,线程安全

●性能比ArrayList低(毕竟安全了嘛)

●扩容为原来的两倍(ArraryList是变为1.5倍)

9.HashSet

●底层是基于HashMap实现的,HashSet的值存放在HashMap的key上,HashMap的value统一为present。

●线程不安全

●添加元素的过程(向Set中添加数据所在的类,必须重写过hashCode和equals方法)

​ ■HashSet set = new HashSet();//底层数组创建,默认长度为16(懒汉式,在首次使用add的时候创建。)

​ ■set.add(a)//首先调用a所在类的hashcode方法,计算出a的哈希值

​ ■使用这个hash值通过某种散列算法计算出HashSet底层数组中的存放位置

​ ■如果这个位置没有其他元素,则添加成功。

​ ■如果有其他元素b(或者以链表形式存在的多个元素),先比较a与b哈希值,如果不同,则添加到链表尾部。

​ ■如果hash值相同,则调用a的equals方法,如果返回false,则添加到链表尾部,否则添加失败。

​ ■对于添加成功的情况,jdk7中元素a放到数组中,指向原来的元素,jdk8中原来的元素在数组中,指向元素a。

●hashCode方法的重写:

​ ■Object类中的hashCode方法是随机生成一个哈希值。

​ ■使用自动重写hashCode时,冲突系数采用31(选择系数时选择尽量大的,计算出来的hash地址大,冲突小,效率高)

​ ■31只用了5bit,而且是一个素数,与一个数字相乘,结果只能被这个素数和被乘数整除(减少冲突),2的五次幂-1,JVM里有很多相关优化。

10.LinkedHashSet和TreeSet

●LinkedHashSet为HashSet的子类,添加数据时,每个数据还维护两个引用,记录此数据的前一个数据和后一个数据,遍历起来效率更高。

●TreeSet:红黑树的一种结构

11.HashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GkcLJIBn-1650426015462)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/141.png)]

●底层为节点数组(transient Node <k,v> []table)+节点链表(内部类,1.7为Entry<k,v>,1.8为Node<k,v>)(JDK7及以前),JDK8以后改为数组+链表+红黑树TreeNode<k,v>

​ ■1.8之前:采用拉链法,即将数组和链表结合,如果遇到哈希冲突,则将冲突值加入到链表中即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5rv4zCl-1650426015462)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/142.png)]

​ ■1.8之后:当链表长度大于阈值(默认为8)的时候,将链表转换为红黑树,以减少搜索时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5M5dwIv9-1650426015463)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/143.png)]

●红黑树:

​ ■一种特殊的二叉树,每个节点都有存储位表示该节点的颜色。

​ ■每个节点是黑色或者是红色,根节点一定是黑色。叶子结点也是黑色(这里的叶子结点是指为空的节点)

​ ■如果一个节点是红色的,那么它的子节点必定是黑色。

​ ■每个节点到叶子节点所经过的黑色节点个数一样(确保没有一条路径比其他路径长两倍,所以红黑树是相对接近平衡二叉树的)

​ ■基本操作是添加删除,进行操作后都会旋转来调整结构,使其重新变为红黑树。

●HashMap线程不安全,可以存储null的键和null的值

●Hash值如何计算的:使用Key的hashCode与hashCode()>>16进行亦或操作,高位补0,一个数与0亦或不变,所以hash函数的作用就是,高16bit不变,低16bit与高16bit做一个亦或,目的是减少碰撞。因为bucket数组的大小是2的幂,下标index = (table.length - 1)&hash,如果不做hash处理,相当于散列生效的只有几个低bit位,为了减少散列的碰撞(默认大小为16预案小于hashCode返回的数值,如果单出用hashcode取余来获取对应bucket会增加哈希碰撞的记录),使用高16bit和低16bit亦或来简单处理减少碰撞,而且JDK8中用了复杂度O(logn)的树结构来提高性能

●Put操作流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHMrGCEJ-1650426015464)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/144.png)]

​ ■判断键值对数组table[i]是否为空,如果是则进行初始化resize()扩容

​ ■根据键值key计算hash值,得到插入索引i,如果table[i] == null,直接将新建节点添加,转向第六步,否则转向第三步

​ ■判断table[i]的首个元素是否与key一样,如果相同(相同指的是hashCode与equals)则覆盖,不同则进入第四步

​ ■判断table[i]是否为红黑树,如果是,则直接在树中插入键值对,否则转向第五步

​ ■遍历table[i],判断链表长度是否大于8,大于8就把链表转换为红黑树,在红黑树中进行插入,否则进行链表的插入;遍历过程如果发现key已经存在则直接覆盖掉value即可。

​ ■插入成功后,判断实际存在的键值对数量size是否超过了最大容量thereshold,如果超过,则进行扩容。

​ 源码:https://juejin.cn/post/6844904125939843079#heading-42

●HashMap的扩容操作resize():

​ ■当hashMap中的键值对大于扩容阈值(默认16*0.75 = 12)时或初始化时,调用resize方法进行扩容;(JDK1.7时,是创建hashmao的时候就创建数组,1.8时是第一次调用put时创建-懒汉式)

​ ■每次扩容都是扩展到原来的两倍

​ ■每次扩容后的Node对象要么在原来的位置,要么移动到原位置+旧容量

​ ■扩容的同时伴随着元素的重新分配

​ ▼JDK1.7中,扩容之后要重新计算每个节点的新位置(因为高位新增了1个bit,所以进行(table.length - 1)%hash后,node节点要么在原位置,要么就在原位置 + 旧容量位置。)

​ ▼如果node节点为空,则直接将键值对映射过去;如果node节点中为红黑树,则通过split拆分;如果是链表,则将链表节点按原顺序分组,并映射到新数组的node节点中,最后更新扩容阈值。

​ ▼1.7中:新位置通过重新hash进行计算,1.8中原位置or原位置+旧容量

​ ▼链表/红黑树转移数据的方法:1.7->头插法,每个新元素都替换原来的头结点,以原来的头结点为next。(缺陷:多个线程同时扩容时,使用transfer方法迁移链表中节点,对于链表中的两个节点A,B,线程1获取头结点A和next节点B.。此时线程A挂起,另一个线程完成了AB节点的转移,此时线程1恢复运行,将B的next指向A,从而产生循环列表的问题)1.8尾插法:每个新节点都放到最后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ybRYM0PR-1650426015464)(http://luxiaolumm.gitee.io/luxiao-lu-mm/pic/145.png)]

​ ▼for循环遍历原数组,遍历每一个entry,首先保存entry中的下一个entry(下一次要循环的)。判断是否要重新计算hash值,如果需要就重新计算。根据hash和新数组的长度算出新索引位置,将新数组上的数据保存到正在循环的entry中entry成员变量上(作为next),再将entry放到索引处,效果上看起来就是在该所引出从头插入了该数据,这就完成了一个数据的转移,最后将要循环的数据换成开始存储的下一条数据,一直执行到转移全部数据,最后将新数组赋值给HashMap的数组上,并且将临界值设置成新数组的长度乘以负载稀疏,扩容完毕。

​ ■默认填充因子为0.75;扩容的临界值 = 容量 * 填充因子

​ ■为什么提前扩容:不是挨个存的,为了减少链表的长度,所以设置填充因子,如果填充因子太小又会造成利用率太低。

​ ■如何解决hash冲突:

​ ▼链表法:相同hash值的对象组织成一个链表放在hash值对应的槽位。

​ ▼开放地址法:当某个槽位已经被占据时,继续基于散列函数寻找下一个可用槽位。
( H ( k e y ) + d i % m ) (H(key) + d_i\%m) (H(key)+di%m)
​ ◑H为哈希函数,di为增量序列,m为表长

​ ◑线性探测再散列:di线性递增

​ ◑二次探测再散列:di平方递增,正负交替

​ ◑伪随机探测再散列:di取随机数

●Map中key-value的特点:

​ ■key:无序不重复,使用Set存储,要求key所在的类重写equals方法和hashCode方法。

​ ■value:无序可重复,使用Collection存储,要求所在类重写equals

●HashMap如果初始给定容量值,那么它的初始容量会将这个容量扩充至2的幂次方大小

​ ■为什么始终保持2的幂次方:为了让HashMap高效存取,尽量少碰撞,即数据均匀分布,每个链表/红黑树长度大致相同。

​ ■怎么做到的:为了均匀分布,一般采用取余操作,当length为2的幂次方时 hash%length = hash &(length - 1)。采用二进制位操作,效率高。

​ ■为什么右移16位:增大hash值低位的随机性,使得分布更均匀,并且达到了最高位和最低位同时参与运算的目的。

12.LinkedHashMap

●在HashMap基础上添加了一对指针(自己定义的新Entry替换jdk7中HashMap的Entry和JDK8中HashMap的Node),保证遍历元素时,可以按照添加的顺序实现遍历。

13.HashTable(不建议使用了)

●线程安全,内部方法经过了synchronized修饰,这个基本被淘汰了,效率过低。

●不可以存储null作为键或值

●默认初始容量11,每次扩充为原来2n+1

●如果指定初始容量就使用给定的大小

14.TreeMap

●底层是通过红黑树实现的,是一个有序的key-value集合

●基于key的自然顺序进行排序或者根据提供的comparator进行排序

●线程不安全

●如果想要在map中进行插入删除以及查询,hashmap更好。遍历的话,TreeMap更好。

15.ConcurrentHashMap

●ConcurrentHashMap对整个桶数组进行了分段分割,然后在每一段上都使用lock锁进行保护,当一个线程占用锁访问其中一个数据段时,其他的段的数据也能被其他线程访问,线程安全。

●不允许键值对中出现null

●JDK1.7时采用Segment+HashEntry的方式实现

​ ■一个ConcurrentHashMap里有一个segment数组,结构为HashEntry数组+链表(HashEntry节点)

​ ■该类包括两个静态内部类:HashEntry和Segement;前者用来封装映射表达键值对,后者用来充当锁的角色。

​ ■segment是一种可重入锁ReettrantLock,每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组中数据进行修改时,必须获得对应的segment锁。

●JDK1.8时采用Node + CAS(compare and swap)+ synchronized来保证并发安全。synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突就不会发生并发。

●Put()插入过程

​ ■首先判断数组是否初始化,如果没有初始化就先用initTable方法初始化。

​ ■通过计算hash值来确定放在数组的哪个位置。如果没有hash冲突就直接CAS插入,如果有hash冲突,则取出这个节点。

​ ■如果取出来的节点hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新数组,则该线程也去帮忙复制。

​ ■如果这个节点不为空也不在扩容,则通过synchronized加锁,然后进行添加操作

​ ▼如果是链表,遍历整个链表,将链表中每个节点的key与要添加的节点的key比较,如果key相同,并且hash也相同的话,说明是同一个key,覆盖掉原来的value,否则添加到末尾。

​ ▼如果是红黑树,调用putTreeVal方法,把元素加到红黑树中

​ ■最后添加完成之后,调用addCount方法统计size,判断在该节点处共有多少个节点,如果达到8个以上的话,则调用treeiyfBin方法将链表转化为红黑树。

●Get()方法:

​ ■就算Hash值,定位到该table索引位置,如果是首节点符合就返回

​ ■如果遇到扩容的时候,会调用标志正在扩容节点的ForwardingNode的find方法,查找该节点,匹配到就返回

​ ■如果以上都不符合就向下遍历,匹配到就返回,否则就返回null。

●Transfer()(扩容)

​ ■计算出每个线程可以处理的个数,根据Map的长度,计算每个线程处理的桶(table数组)的个数,默认每个线程每次处理16个桶,如果小于16则强制变为16。

​ ■对nextTab初始化,如果传入的新的table nextTab为空,则对齐进行初始化,默认为原来的两倍

​ ■引入ForwardingNode,advance,finishing变量辅助扩容,forwardingNode表示该节点已经处理过,不需要再处理,advance表示该线程是否可以下移到一下个桶,finishing表示扩容是否结束。

​ ■在数据转移的过程中会加synchronized锁,锁住头结点,同步化操作,防止putVal时向链表插入数据。

​ ■进行数据迁移,如果这个桶上节点是链表或是红黑树,则会将节点数据分为低位和高位(因为高位新增了1bit,所以进行(table .length - 1)&hash后,node节点要么在原位置,要么在原位置 + 旧容量位置 )

​ ■如果桶上挂载的是红黑树,不仅要分离出低位和高位节点,还要判断低位和高位节点在新表上是以链表形式存放还是以红黑树形式存放。

16.辅助工具类

●Collections:集合类的一个工具类(让我想起了数组类的工具类Arrays),提供了一系列静态方法,用于对集合中的元素进行排序,所搜以及线程安全等安全操作。

●reverse(list):反转list中元素顺序。

●sort(list):根据元素的自然顺序对指定list集合元素按升序排序

●sort(list,Comparator):根据指定的Comparator产生的顺序对list集合元素排序

●max(list)

●max(list,Comparator)

●synchronizedXXX(Map/list/set):将这些集合变为线程安全的

17.TreeMap,TreeSet,Collections中的sort都是如何比较元素的

●TreeSet:要求存放的类必须实现Comparable接口,即重写compareTo方法,当插入元素时会调用该方法比较元素的大小

●TreeMap:要求存放的键值对中的键所在类必须实现Compara接口,从而排序。

●Collections工具类有两种重载的排序形式:

​ ■第一种要求传入的待排序容器中存放的对象实现Comparable接口

​ ■第二种要求传入一个Comparator接口的子类型,相当于定义一个临时排序规则,其实就是通过接口注入比较元素大小的算法,即对回调模式的应用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值