JAVA集合学习

一、结构

List和Set继承了Collection接口,Collection继承了Iterable

Object类是所有类的根类,包括集合类,集合类中的元素通常是对象,继承了Object类中的一些基本方法,例如toString()equals()hashCode()

Collection的增强for底层就是简化版本的迭代器遍历,可以DEBUG看到过程

对集合的遍历:list - 删除元素 ConcurrentModificationException-CSDN博客

Object常用方法

  • toString():返回对象的字符串表示形式。

  • equals(Object obj):判断两个对象是否相等。

  • hashCode():返回对象的哈希码值。

  • getClass():返回对象的运行时类。

  • clone():创建并返回此对象的副本。

  • finalize(): 该方法用于释放资源。

  • wait():睡眠

  • notify():唤醒

二、List

1、ArrayList

  • 线程不安全

  • 有序(添加和取出顺序一致),可重复,可以添加多个null

  • 默认大小是10,扩容是1.5倍

  • 每个元素都有对应的顺序索引,即支持索引。get()体现。

常用方法

  • add()/addAll()

  • set()

  • get()

  • remove()

  • size()

  • isEmpty()

  • indexOf()

  • contains()

  • replaceAll()

  • subList()

底层

  • ArrayList维护一个Object类型的数组elementData,就是一个空数组

  • 当创建ArrayList对象时,如果使用的是无参构造器,初始容量是0,第一次添加扩容到10,如果再扩容是1.5倍,用 Arrays.copyOf 方法旧数组数据拷贝进去,再添加新的进去

  • 如果是指定大小的构造器,初始是指定大小,再次扩容是1.5倍

transient避免序列化

(1)无参构造

确认最小容量,赋值为10

modCount记录集合被修改的次数,此时minCapacity是10,但是elementData是空,就需要扩容

向右移动一位,就是除以2,比如初始化是个空数组,那么在newCapacity的时候还是0,所以第一次扩容不是直接扩,是在第一个if才扩容赋值为10,copyOf后都是null

(2)有参构造

同上

2、Vector

  • 线程安全的,操作方法带有synchronized

  • 默认10,扩容是2倍

底层

(1)扩容

确定是否需要扩容

int类型初始化是0,所以newCapacity是两倍

(2)克隆

实现了Cloneable接口

调用 Object 类的 clone 方法,创建一个浅拷贝,对于数组元素,使用 Arrays.copyOf 方法进行深拷贝,重置副本的修改次数

3、LinkedList

  • 线程不安全

  • 双向链表和双端队列

  • 添加元素可重复,可添加null

LinkedList维护了两个属性first和last指向首节点和尾节点,每个节点对象,又维护了prev、next、itm三个属性,通过prev指向前一个,next指向后一个节点,实现双向链表。

常用方法

【带有first和last就是实现了Deque,常用的是实现Queue】

add(E e):实现了List接口。在链表后添加一个元素;

addFirst(E e)/push(E e)、addLast(E e):实现了Deque接口。在链表头部/尾部插入一个元素;

offerFirst(E e)、offerLast(E e):实现了Deque接口。在链表头部插入一个元素,成功返回true,失败false

offer(E e):实现了Queue接口。将指定的元素插入到队列的末尾,成功返回true,失败false

peek():获取第一个元素,但是不移除;

poll():查询并移除第一个元素 ;

remove() :移除链表中第一个元素;

底层:

(1)add

first = null, last = null

第一个节点加入的时候,first和last都指向newNode,prev=next=null

再加入一个节点, l指向第一个节点的last,再将l赋值到newNode的prev,将第一个节点的last指向newNode,此时l指向的是第一个节点不为空,将l的next指向newNode,完成链表,size=modCount=2

(2)removeFirst

4、比较

ArrayListLinkedListVector
底层结构可变数组双向链表可变数组
线程安全不安全(Collections.synchronizedList)不安全安全(synchronized)
初始值10,扩容1.5倍10,扩容2倍空链表
排序重复有序,可重复,多个null有序,可重复,多个null有序,可重复,多个null
效率增删较低,查找快(索引)增删较高,查找慢(顺序遍历)
适用大部分频繁插入和删除

2、拓展Collections.synchronizedList

Collections.synchronizedList方法接受一个普通的List作为参数,并返回一个具有同步方法的List。可以使用这个同步的List在多线程环境中进行操作。尽管synchronizedList提供了一些基本的同步,但在复合操作(例如迭代和修改)时,仍然需要手动同步以确保线程安全。

https://www.cnblogs.com/jiading/articles/13038607.html

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
​
public class ManualSynchronizationExample {public static void main(String[] args) {// 创建一个普通的ArrayListList<String> normalList = new ArrayList<>();
​// 使用Collections.synchronizedList方法创建线程安全的ListList<String> synchronizedList = Collections.synchronizedList(normalList);
​// 创建两个线程,一个线程向列表中添加元素,另一个线程遍历列表Thread addThread = new Thread(() -> {for (int i = 0; i < 1000; i++) {addElement(synchronizedList, "Element" + i);}});
​Thread iterateThread = new Thread(() -> {iterateList(synchronizedList);});
​// 启动线程addThread.start();iterateThread.start();
​// 等待两个线程执行完成try {addThread.join();iterateThread.join();} catch (InterruptedException e) {e.printStackTrace();}}
​// 使用 synchronized 关键字确保对列表的安全添加操作private synchronized static void addElement(List<String> list, String element) {list.add(element);}
​// 使用 synchronized 关键字确保对列表的安全遍历操作private synchronized static void iterateList(List<String> list) {for (String element : list) {// 在迭代过程中对列表进行操作System.out.println(element);}}
}
​
//synchronizedList里的synchronized内部锁,是在并发执行的add/remove的时候,
//不要把多个线程操作的数据实现到一个地方去。
//外部再使用synchronized是保证添加和遍历的时候对线程的安全的保护,两个锁是不同层面的并发问题

三、Set

1、HashSet

  • 底层是实现的HashMap,数组+链表+红黑二叉树,线程不安全

  • 默认大小16,加载因子0.75,扩容到原先的2倍

  • 无序,不允许重复,最多一个null、

(1)add

Set s = new HashSet();
set.add("a");                  //true
set.add("a");                  //false
set.add(new Student("lee"));    //true
set.add(new Student("lee"));    //true
set.add(new String("lee"));     //true
set.add(new String("lee"));     //false

如果put返回的是null,相当于是没有这个e,那么才返回true,否则代表已经存在

这里的PRESENT是个占位的目的,也就是add(“a”),这个a是在key上的,value是个空对象

右移16位并且异或,核心目的是为了让hash值的散列度更高,减少hash冲突,将hashCode的高位移动到了低位,使低位包含更多的高位信息

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//定义辅助变量,table就是HashMap的一个数组,类型是Node[]Node<K,V>[] tab; Node<K,V> p; int n, i;//看下个代码解析,计算如果当前tab是null或者大小是0,此时的tab大小就是16了if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//计算hash值,应该放在table表的哪个索引位置,并且这个位置的对象赋值给p//如果p是null,表示还没有创建过数据,就放进去//绝大部分情况下,n的值小于2的16次方,所以i的值始终是hash的低16位和n-1取模运算,造成key的散列度不高,大量的key集中存储几个位置,影响性能if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {//相同的数据,例如第一次加入A,第二次加入ANode<K,V> e; K k;//p是指向当前索引位置对应的第一个元素,如果和添加的key的hash值一样,并且添加的key是同一个对象,或者不是同一个对象但是内容相同。代表两个数据是相同的,不能加入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 {//是一个链表,for循环比较for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//添加后立即判断链表的长度是否达到8,转为红黑树if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果已经存在相同的k,但是v不同,那么就会替换vif (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();//这个是为了让HashMap的子类去实现的afterNodeInsertion(evict);return null;}
 
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;}else if (oldThr > 0)newCap = oldThr;else {//static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 也就是默认16newCap = DEFAULT_INITIAL_CAPACITY;//默认加载因子0.75,使用到12的时候就要扩容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;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//此时的table的大小就是16了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)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else {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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}

2、LinkedHashSet

  • 继承了HashSet,底层是LinkedHashMap,维护的是 数组+双向链表

  • 默认大小是16,扩容为2倍

  • 数组是HashMap$Node[],存放节点类型是LinkedHashMap$Entry

继承关系是在静态内部类完成的

同上

3、TreeSet

  • 当无参构造的时候是无序的,根据提供的构造器的比较器可以完成有序

  • 底层实现TreeMap

TreeSet treeSet = new TreeSet(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {//比较方式return 0;}
});

添加元素时,cpr就是匿名内部类,compare()动态绑定到匿名内部类

第一次添加的时候,比较的是相同的key不接收返回值,所以添加第一个元素时不影响

4、区别

HashSetLinkedHashSetTreeSet
底层HashMapHashSet+链表TreeMap+红黑树
是否有序无序有序无参无序,构造器有序
性能O(1),速度快O(1),链表有影响O(log n),性能较差

四、Map

1、HashMap

  • key不能重复,值可以重复,允许为null唯一

  • 相同的k,会覆盖原来的kv

  • 线程不安全

  • 最大容量Integer.MAX_VALUE,2 的 31 次方-1

  • 负载因子0.75,扩容为2倍

  • 底层=数组+链表+红黑二叉树

(1)put()同理set集合

(2)遍历

存有的K-V结构的数据,一对k-v是放在一个HashMap$Node中的,Node实现了Exntry接口,一对k-v就是一个Entry。

返回是一个HashMap$Node

k-v为了方便遍历,还会创建EntrySet集合,存放的元素类型Entry,而一个Entry对象就有k-v

transient Set<Map.Entry<K,V>> entrySet;

这个entrySet保存的kv只是指向了Node的kv

entrySet中定义的是Map.Entry, 但实际上存放的是HashMap$Node,因为后者implements前者

提供了常用方法

  • getKey()

  • getValue()

  • Set set = map.keySet();

  • Collection values = map.values();

(3)1.7和1.8的区别

JDK1.7JDK1.8
底层entry数组+链表node数组+链表+红黑树(链表长度>8 && 元素数量>64)
扩容全部按照之前的方法进行重新计算索引按照扩容后的规律计算(扩容后的位置=原位置or原位置+旧容量)
计算hash4次位运算 + 5次异或预算1次位运算+1次异或运算
初始容量0(第一次执行put初始)16(put时初始)
插入头插法(造成死循环)尾插法

(4)问题

1.8后扩容后的规律计算?

扩容后,若hash值新增参与运算的位=0,那么元素在扩容后的位置=原始位置;

扩容后,若hash值新增参与运算的位=1,那么元素在扩容后的位置 =原始位置+扩容后的旧位置;

为什么链表的长度大于8就要转换成红黑二叉树?

最开始使用链表的时候,空间占用较少,对查询速度没有太大的影响,当链表越来越长,就转换为红黑二叉树,保证查询速度。红黑二叉树是平衡二叉树,也避免不平衡二叉树偏向一边导致查询速度慢的问题。

如果 hashCode 分布良好,那么红黑二叉树几乎不会用到,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。因此默认8为转换的阈值。

为什么要右移16位异或?

绝大部分情况下,n的值小于2的16次方,所以i的值始终是hash的低16位和n-1取模运算,造成key的散列度不高,大量的key集中存储几个位置,影响性能。右移16位并且异或,将hashCode的高位移动到了低位,使低位包含更多的高位信息

加载因子为什么是0.75?

扩容因子值越大,触发扩容的元素个数越多,虽然空间利用率高,但是哈希冲突概率增加。

扩容因子值越小,触发扩容的元素个数越少,哈希冲突概率减少,但是占用内存,增加扩容频率。

扩容因子的值的设置,是在统计学中泊松分布有关,再冲突和空间利用率之间的平衡

头插法到尾插法的优化是为什么?

只会在1.7中出现,1.7采用的是数组+链表且是头插法。当有两个线程TA和TB,对HashMap进行扩容,都指向链表头结点NA,而TA和TB的next都是NB,假设TB休眠,TA执行扩容操作,一直到TA完成后,TB被唤醒,TB.next还是NB,但是TA执行成功已经发生了顺序改变,出现一个死循环。

为什么数组容量必须是2次幂? 桶的索引计算公式为 i = (n-1) & hash。效果等于与 hash%n 的计算效果,但是位运算比取余运算要高效的多。如果n为2次幂,那么n-1的低位就全是1,哈希值进行与操作时可以保证低位的值不变,从而保证分布均匀,不会受到与运算对数据的变化影响。

2、HashTable(不使用)

  • k-v都不能为空,否则抛出NullPointException

  • 线程安全

  • 底层是HashTable$Entry[],初始化大小11,扩容是2倍+1

3、TreeMap

同TreeSet

4、ConCurrentHashMap

1.71.8
锁结构ReentrantLock+Segment+HashEntrysynchronized+CAS+红黑树
锁力度Segment节点
size()调用的时候才计算。不加锁直接进行统计,最多执行三次,如果前后两次计算的结果一样,则直接返回。若超过了三次,则对每一个Segment进行加锁后再统计已经计算好的预估值。维护一个baseCount属性用来记录结点数量,每次进行put操作之后都会CAS自增baseCount
put()两次定位,先对Segment定位,再对内部数组下标定位,定位不到会使用自旋锁加锁,自旋超过64次进入阻塞状态一次定位,CAS+synchronized,如果对应下标没有结点,则没有哈希冲突,直接CAS插入,成功返回,不成功使用synchronized加锁插入
get()一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表使用CAS保证并发安全,因为Node数组是共享的。根据哈希定位到桶,为空返回null,怒为空查找链表/红黑树

1.8为什么使用synchronized来代替重入锁ReentrantLock?

因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。 synchronized在1.8之后,JVM 使用了锁升级的优化方式,先使用偏向锁尝试获取锁,没有获取到代表锁已经偏向其他线程,升级到自旋锁,达到一定次数后,才升级到重量级锁。

为什么get不加锁可以保证线程安全?

整个get方法使用了大量的volatile关键字,其实就是保证了可见性,get只是读取操作,所以只需要保证读取的是最新的数据。Node的成员val是用volatile修饰,数组用volatile修饰保证数组扩容时的可见性。

使用volatile关键字会强制将修改的值立即写入主存,使用volatile关键字,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效,由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。volatile就是强制多线程的情况下读取数据区内存获取最新的值。

五、Collections工具类

  • reverse(List list):反转指定List集合中元素的顺序

  • shuffle(List list):对List中的元素进行随机排序(洗牌)

  • sort(List list):对List里的元素根据自然升序排序

  • sort(List list, Comparator c):自定义比较器进行排序

  • swap(List list, int i, int j):将指定List集合中i处元素和j出元素进行交换

  • rotate(List list, int distance):将所有元素向右移位指定长度

  • binarySearch(List list, Object key):使用二分搜索法,以获得指定对象在List中的索引,前提是集合已经排序

  • max(Collection coll):返回最大元素

  • max(Collection coll, Comparator comp):根据自定义比较器,返回最大元素

  • min(Collection coll):返回最小元素

  • min(Collection coll, Comparator comp):根据自定义比较器,返回最小元素

  • fill(List list, Object obj):使用指定对象填充

  • frequency(Collection Object o):返回指定集合中指定对象出现的次数

  • replaceAll(List list, Object old, Object new):替换

  • emptyXxx():返回一个空的不可变的集合对象

  • singletonXxx():返回一个只包含指定对象的,不可变的集合对象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/168748.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI绘画神器DALLE 3的解码器:一步生成的扩散模型之Consistency Models

前言 关于为何写此文&#xff0c;说来同样话长啊&#xff0c;历程如下 我司LLM项目团队于23年11月份在给一些B端客户做文生图的应用时&#xff0c;对比了各种同类工具&#xff0c;发现DALLE 3确实强&#xff0c;加之也要在论文100课上讲DALLE三代的三篇论文&#xff0c;故此文…

C语言概述

目录 ​编辑 1. C语言发展史 2. C语言特点 3. C语言标准 4. C语言编程机制 4.1 预处理(Preprocessing) 4.2 编译(Compilation) 4.3 汇编(Assemble) 4.4 链接(Linking) 结语 1. C语言发展史 C语言是由美国贝尔实验室的Dennis Ritchie于1972年设计开发的一种编…

AI 绘画 | Stable Diffusion 进阶 Embeddings(词嵌入)、LoRa(低秩适应模型)、Hypernetwork(超网络)

前言 Stable Diffusion web ui&#xff0c;除了依靠文生图&#xff08;即靠提示词生成图片&#xff09;&#xff0c;图生图&#xff08;即靠图片提示词生成图片&#xff09;外&#xff0c;这两种方式还不能满足我们所有的绘图需求&#xff0c;于是就有了 Embeddings&#xff0…

【vue】vue项目批量下载二维码,且打包成压缩包.

一. 先看效果演示 二. 下插件 npm i vue-qr -S // 二维码显示插件 我的版本^4.0.9 npm i html2canvas -S // 将二维码转为图片 我的版本^1.4.1 npm i file-saver -S // 下载图片 我的版本^2.0.5 npm i jszip -S // 打包成压缩包 我的版本^3.10.1 // 一次性下载 npm i vue-qr h…

DDoS攻击剧增,深入解析抗DDoS防护方案

当下DDoS攻击规模不断突破上限&#xff0c;攻击方式越发复杂。面对复杂的攻击形式&#xff0c;对于企业和组织来说无疑需要更完备的抗DDoS方案&#xff0c;依靠传统的解决方法并不能做到一劳永逸。在服务器抵抗DDoS防护上&#xff0c;你不会忽略F5的产品&#xff0c;让我们一起…

uniapp中在组件中使用被遮挡或层级显示问题

uniapp中在组件中使用或croll-view标签内使用uni-popup在真机环境下会被scroll-view兄弟元素遮挡&#xff0c;在开发环境下和安卓系统中可以正常显示&#xff0c;但在ios中出现了问题 看了许多文章都没有找到问题的原因&#xff0c;最后看到这一个文章http://t.csdnimg.cn/pvQ…

【Spring】Spring IOC DI

Spring IOC & DI IOC DI入门什么是Spring什么是容器什么是IOC IOC介绍传统程序开发解决方案 DI IOC详解Bean的存储Controller(控制器存储)Service(服务存储)Repository(仓库存储)Component(组件存储)Configuration(配置存储) 为什么需要这么多类注解类注解之间的关系方法注…

实战Leetcode(三)

Practice makes perfect&#xff01; 实战一&#xff1a; 带环问题其实我们小学时就接触过&#xff0c;就比如在操场上比赛跑步的追击问题&#xff0c;这里也是一样&#xff0c;如果我们定义两个指针&#xff0c;一个快指针&#xff0c;一个慢指针&#xff0c;快指针走的快&…

云栖大会丨桑文锋:打造云原生数字化客户经营引擎

近日&#xff0c;2023 云栖大会在杭州举办。今年云栖大会回归了 2015 的主题&#xff1a;「计算&#xff0c;为了无法计算的价值」。神策数据创始人 & CEO 桑文锋受邀出席「生态产品与伙伴赋能」技术主题&#xff0c;并以「打造云原生数字化客户经营引擎」为主题进行演讲。…

【多线程 - 01、概述】

进程 几乎所有的操作系统都支持进程概念&#xff0c;进程是处于运行过程中的程序&#xff0c;进程是操作系统中进行资源分配的基本单位。 三个基本特征 独立性&#xff1a;指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。而对于未建立任何进程的程序&…

RabbitMQ集群

RabbitMQ概述 1.RabbiMQ简介 RabbiMQ是⽤Erang开发的&#xff0c;集群⾮常⽅便&#xff0c;因为Erlang天⽣就是⼀⻔分布式语⾔&#xff0c;但其本身并不⽀持负载均衡。支持高并发&#xff0c;支持可扩展。支持AJAX&#xff0c;持久化&#xff0c;用于在分布式系统中存储转发消…

C/C++数字判断 2021年9月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C数字判断 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C数字判断 2021年9月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 输入一个字符&#xff0c;如何输入的字符是数字&#x…