List操作的一些常见问题
1. Arrays.asList转换基本类型数组
在实际的业务开发中,我们通常会进行数组转List的操作,通常我们会使用Arrays.asList来进行转换,但是在转换基本类型的数组的时候,却出现转换的结果和我们想象的不一致。
import java.util.Arrays;
import java.util.List;/*** Arrays.asList数组常见问题* @author 百里*/
public class BaiLiTestDemo {public static void main(String[] args) {int[] arr = {1, 2, 3};List list = Arrays.asList(arr);System.out.println("list.size:" + list.size());for (int i = 0; i < list.size(); i++) {System.out.println("循环打印:" + list.get(i));}}
}
观察下asList的实现,可以看到是入参是使用的是泛型,所以会将{1, 2, 3}三个整数放入一个泛型列表中返回。
public static List asList(T... a) {return new ArrayList<>(a);
}
那我们该如何解决呢?只需要在声明数组的时候,声明类型改为包装类型。
import java.util.Arrays;
import java.util.List;/*** Arrays.asList数组常见问题* @author 百里*/
public class BaiLiTestDemo {public static void main(String[] args) {Integer[] arr = {1, 2, 3};List list = Arrays.asList(arr);System.out.println("list.size:" + list.size());//size = 3for (int i = 0; i < list.size(); i++) {System.out.println("循环打印:" + list.get(i));}}
}
这就是第一个坑了,然而Arrays.asList不止这一个需要注意的问题,我们继续往下看:
2. Arrays.asList返回的List不支持增删操作
我们接着上面的demo,增加list加减的逻辑,运行demo会提示UnsupportedOperationException:
import java.util.Arrays;
import java.util.List;/*** Arrays.asList数组常见问题* @author 百里*/
public class BaiLiTestDemo {public static void main(String[] args) {Integer[] arr = {1, 2, 3};List list = Arrays.asList(arr);System.out.println("list.size:" + list.size());list.add(4);}
}
为什么会这样?我们看下asList的实现,它返回的ArrayList是Arrays的内部类,而不是我们通常使用的java.util.ArrayList:
可以看到内部类中的ArrayList没有add()与remove(),那我们怎么可以使用增减方法呢,继续往下看:
可以看到ArrayList继承了AbstractList类,我们观察AbstractList类的add()与remove():
现在是不是就理解Arrays.asList返回的List不支持增删操作了。
3. 对原始数组的修改会影响到我们获得的那个List
基于第一个demo我们继续改造,修改原arr[0]=10,这个时候打印Arrays.asList返回的list值也发生了改变:
import java.util.Arrays;
import java.util.List;/*** Arrays.asList数组常见问题* @author 百里*/
public class BaiLiTestDemo {public static void main(String[] args) {Integer[] arr = {1, 2, 3};List list = Arrays.asList(arr);System.out.println("list.size:" + list.size());arr[0] = 10;//修改原数组for (int i = 0; i < list.size(); i++) {System.out.println("循环打印:" + list.get(i));}}
}
为什么呢?观察ArrayList的实现,可以知道asList创建了 ArrayList,但它直接引用原本的数据组对象。所以只要原本的数组对象一发生变化,List也跟着变化。
解决方案:new一个新的ArrayList装Arrays.asList返回数据。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** Arrays.asList数组常见问题* @author 百里*/
public class BaiLiTestDemo {public static void main(String[] args) {Integer[] arr = {1, 2, 3};List list = new ArrayList<>(Arrays.asList(arr));arr[0] = 10;for (int i = 0; i < list.size(); i++) {System.out.println("循环打印:" + list.get(i));}}
}
4. ArrayList.subList强转ArrayList导致异常
当使用ArrayList.subList的返回list强转ArrayList时,会出现java.lang.ClassCastException,看以下代码:
import java.util.ArrayList;
import java.util.List;/*** ArrayList.subList常见问题* @author 百里*/
public class BaiLiArrayListDemo {public static void main(String[] args) {List<String> names = new ArrayList<String>() {{add("one");add("two");add("three");}};ArrayList strings = (ArrayList) names.subList(0, 1);System.out.println(strings);}
}
Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayListat BaiLiArrayListDemo.main(BaiLiArrayListDemo.java:15)
同样的,我们看下sublist的实现:
可以看到SubList()实际上没有创建一个新的List,而是直接引用了原来的List,指定了元素的范围。并且返回的是一个内部类实现的SubList对象,该对象只是原始ArrayList的一个引用,而不是一个全新的ArrayList,因此无法直接将其强制转换为ArrayList类型。
由于是引用的原List,因此也会存在asList的问题,也就是针对subList进行增减数据,会影响原List的值。
import java.util.ArrayList;
import java.util.List;/*** ArrayList.subList常见问题* @author 百里*/
public class BaiLiArrayListDemo {public static void main(String[] args) {List<String> names = new ArrayList<String>() {{add("one");add("two");add("three");}};List strings = names.subList(0, 1);strings.add(0,"four");System.out.println(strings);//[four, one]System.out.println(names);//[four, one, two, three]}
}
需要注意修改原List-names的值会出导致strings的遍历、增加、删除产生ConcurrentModificationException异常。
import java.util.ArrayList;
import java.util.List;/*** ArrayList.subList常见问题* @author 百里*/
public class BaiLiArrayListDemo {public static void main(String[] args) {List<String> names = new ArrayList<String>() {{add("one");add("two");add("three");}};List strings = names.subList(0, 1);names.add("four");System.out.println(strings);System.out.println(names);}
}
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)at java.util.ArrayList$SubList.listIterator(ArrayList.java:1091)at java.util.AbstractList.listIterator(AbstractList.java:299)at java.util.ArrayList$SubList.iterator(ArrayList.java:1087)at java.util.AbstractCollection.toString(AbstractCollection.java:454)at java.lang.String.valueOf(String.java:2994)at java.io.PrintStream.println(PrintStream.java:821)at BaiLiArrayListDemo.main(BaiLiArrayListDemo.java:17)
上面问题的解决方案跟asList同样,直接new一个新的ArrayList装Arrays.subList返回数据就可以了。
import java.util.ArrayList;
import java.util.List;/*** ArrayList.subList常见问题* @author 百里*/
public class BaiLiArrayListDemo {public static void main(String[] args) {List<String> names = new ArrayList<String>() {{add("one");add("two");add("three");}};List strings = new ArrayList<>(names.subList(0, 1));strings.add("four");System.out.println(strings);//[one, four]System.out.println(names);//[one, two, three]}
}
5. ArrayList中的subList切片造成OOM
subList所产生的List,其实是对原来List对象的引用,这个产生的List只是原来List对象的视图,也就是说虽然值切片获取了一小段数据,但是原来的List对象却得不到回收,如果这个原来的对象很大,就会出现OOM的情况。我们将VM参数调小:-Xms20m -Xmx40m
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;/*** ArrayList.subList常见问题* @author 百里*/
public class BaiLiArrayListDemo {public static void main(String[] args) {List data = new ArrayList<>();IntStream.range(0, 1000).forEach(i ->{List<Integer> collect = IntStream.range(0, 100000).boxed().collect(Collectors.toList());data.add(collect.subList(0, 1));});}
}
出现OOM的原因:原数组无法被回收,会一直在内存中。
解决方案:new一个新的ArrayList接收subList返回。
6.Copy-On-Write 是什么?
Copy-On-Write它是一种在计算机科学中常见的优化技术,主要应用于需要频繁读取但很少修改的数据结构上。
简单的说就是在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了!
既然是一种优化策略,我们看一段代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;/**
* @author 百里
*/
public class BaiLiIteratorTest {private static List<String> list = new ArrayList<>();public static void main(String[] args) {list.add("1");list.add("2");list.add("3");Iterator<String> iter = list.iterator();while (iter.hasNext()) {System.err.println(iter.next());}System.err.println(Arrays.toString(list.toArray()));}
}
上面的Demo在单线程下执行时没什么毛病,但是在多线程的环境中,就可能出异常,为什么呢?
因为多线程迭代时如果有其他线程对这个集合list进行增减元素,会抛出java.util.ConcurrentModificationException的异常。
我们以增加元素为例子,运行下面这Demo:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** 并发迭代器问题示例代码* @author 百里*/
public class BaiLiConcurrentIteratorTest {// 创建一个ArrayList对象private static List<String> list = new ArrayList<>();public static void main(String[] args) throws InterruptedException {// 给ArrayList添加三个元素:"1"、"2"和"3"list.add("1");list.add("2");list.add("3");// 开启线程池,提交10个线程用于在list尾部添加5个元素"121"ExecutorService service = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {service.execute(() -> {for (int j = 0; j < 5; j++) {list.add("121");}});}// 使用Iterator迭代器遍历list并输出元素值Iterator<String> iter = list.iterator();for (int i = 0; i < 10; i++) {service.execute(() -> {while (iter.hasNext()) {System.err.println(iter.next());try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}});}service.shutdown();}
}
这里暴露的问题是什么呢?
- 多线程场景下迭代器遍历集合的读取操作和其他线程对集合进行写入操作会导致出现并发修改异常
解决方案:
- CopyOnWriteArrayList避免了多线程操作List线程不安全的问题
7.CopyOnWriteArrayList介绍
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
CopyOnWriteArrayList原理:
在写操作(add、remove等)时,不直接对原数据进行修改,而是先将原数据复制一份,然后在新复制的数据上执行写操作,最后将原数据引用指向新数据。这样做的好处是读操作(get、iterator等)可以不加锁,因为读取的数据始终是不变的。
接下来我们就看下源码怎么实现的。
8.CopyOnWriteArrayList简单源码解读
add()方法源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {final ReentrantLock lock = this.lock;//重入锁lock.lock();//加锁啦try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组newElements[len] = e;setArray(newElements);//将引用指向新数组 1return true;} finally {lock.unlock();//解锁啦}
}
可以看到,CopyOnWriteArrayList中的写操作都需要先获取锁,然后再将当前的元素数组复制一份,并在新复制的元素数组上执行写操作,最后将数组引用指向新数组。
@SuppressWarnings("unchecked")
public E next() {if (! hasNext()) //是否存在下一个元素throw new NoSuchElementException(); //没有下一个元素,则会抛出NoSuchElementException异常//snapshot是一个类成员变量,它是在创建迭代器时通过复制集合内容而获得的一个数组。//cursor是另一个类成员变量,初始值为0,并在每次调用next()时自增1,表示当前返回元素的位置。return (E) snapshot[cursor++];
}
而读操作不需要加锁,直接返回当前的元素数组即可。
这种写时复制的机制保证了读操作的线程安全性,但是会牺牲一些写操作的性能,因为每次修改都需要复制一份数组。因此,适合读远多于写的场合。
所以我们将多线程Demo中的ArrayList改为CopyOnWriteArrayList,执行就不会报错啦!
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/**
* 并发迭代器问题示例代码
* @author 百里
*/
public class BaiLiConcurrentIteratorTest {// 创建一个ArrayList对象private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();public static void main(String[] args) throws InterruptedException {// 给ArrayList添加三个元素:"1"、"2"和"3"list.add("1");list.add("2");list.add("3");// 开启线程池,提交10个线程用于在list尾部添加5个元素"121"ExecutorService service = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {service.execute(() -> {for (int j = 0; j < 5; j++) {list.add("121");}});}// 使用Iterator迭代器遍历list并输出元素值Iterator<String> iter = list.iterator();for (int i = 0; i < 10; i++) {service.execute(() -> {while (iter.hasNext()) {System.err.println(iter.next());try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}});}service.shutdown();}
}
9.CopyOnWriteArrayList优缺点
优点:
- 线程安全。CopyOnWriteArrayList是线程安全的,由于写操作对原数据进行复制,因此写操作不会影响读操作,读操作可以不加锁,降低了并发冲突的概率。
- 不会抛出ConcurrentModificationException异常。由于读操作遍历的是不变的数组副本,因此不会抛出ConcurrentModificationException异常。
缺点:
- 写操作性能较低。由于每一次写操作都需要将元素复制一份,因此写操作的性能较低。
- 内存占用增加。由于每次写操作都需要创建一个新的数组副本,因此内存占用会增加,特别是当集合中有大量数据时,内存占用较高。
- 数据一致性问题。由于读操作遍历的是不变的数组副本,因此在对数组执行写操作期间,读操作可能读取到旧的数组数据,这就涉及到数据一致性问题。
10.CopyOnWriteArrayList使用场景
- 读多写少。为什么?因为写的时候会复制新集合
- 集合不大。为什么?因为写的时候会复制新集合
- 实时性要求不高。为什么,因为有可能会读取到旧的集合数据
相关文章:

List操作的一些常见问题
1. Arrays.asList转换基本类型数组 在实际的业务开发中,我们通常会进行数组转List的操作,通常我们会使用Arrays.asList来进行转换,但是在转换基本类型的数组的时候,却出现转换的结果和我们想象的不一致。 import java.util.Arra…...

如何使用Java和RabbitMQ实现延迟队列?
前言 今天我们使用Java和RabbitMQ实现消息队列的延迟功能。 前期准备,需要安装好docker、docker-compose的运行环境。 需要安装RabbitMQ的可以看下面这篇文章。 如何使用PHP和RabbitMQ实现消息队列?-CSDN博客 今天讲的是依赖RabbitMQ的延迟插件实现…...

AI论文速读 | TF-LLM:基于大语言模型可解释性的交通预测
论文标题: Explainable Traffic Flow Prediction with Large Language Models 作者:Xusen Guo, Qiming Zhang, Mingxing Peng, Meixin Zhu(朱美新)*, Hao (Frank)Yang(杨昊) 机构:香港科技大学(广州),约翰…...

智慧矿山视频智能监控与安全监管方案
一、行业背景 随着全球能源需求的日益增长,矿业行业作为国民经济的重要支柱,其发展日益受到广泛关注。然而,传统矿山管理模式的局限性逐渐显现,如生产安全、人员监管、风险预警等方面的问题日益突出。因此,智慧矿山智…...

2024春算法训练4——函数与递归题解
一、前言 感觉这次的题目都很好,但是E题....(我太菜了想不到),别人的题解都上百行了,晕; 二、题解 A-[NOIP2010]数字统计_2024春算法训练4——函数与递归 (nowcoder.com) 这种题目有两种做法:…...
【C++】C++知识点复习
牛客cpp:牛客网在线编程 2024年4月10日:BC1—>BC8 BC4:浮点数精度保留 问题:不加入fixed输入0.359813,最后得到0.36,并不是强制保留0.360。这种写法会保留小数点后三位精度,但是最后输出会省略掉最后…...

SpringBoot+Vue,轻松实现网页版人脸登录与精准识别
目录 1、技术介绍 2、技术原理 2.1、人脸检测 ①参考模板法 ②人脸规则法 2.2、人脸跟踪 2.3、人脸比对 ①特征向量法 ②面纹模板法 识别过程 案例 一、springboot后端项目 1,拉取项目后,导入相关依赖jar包 2,执行sql文件夹下面…...

深入浅出 -- 系统架构之垂直架构
当业务复杂度增加、访问量逐渐增大出现高并发时,单体架构无法满足需求,可以根据业务功能对系统进行拆分,以提高访问效率。 垂直架构介绍 1.垂直架构一般是因为单体架构太过于庞大而进行的拆分,拆分后各个系统应满足独立运行互相不…...

深入浅出 -- 系统架构之微服务架构选型参考图
技术选型架构图 是一个用于展示项目中所采用的各种技术和组件之间关系的图表。 它通常包括以下几个部分: 1. 项目名称和描述:简要介绍项目的背景和目标。 2. 技术栈:列出项目中使用的主要技术和工具,如编程语言、框架、数据库…...

Java 使用 ant.jar 执行 SQL 脚本文件
Java 使用 ant.jar 执行 SQL 脚本文件,很简单。 在 pom.xml 中导入 ant 依赖 <dependency><groupId>org.apache.ant</groupId><artifactId>ant</artifactId><version>1.10.11</version> </dependency>sql 脚本文件…...

【随笔】Git 高级篇 -- 快速定位分支 ^|~(二十三)
💌 所属专栏:【Git】 😀 作 者:我是夜阑的狗🐶 🚀 个人简介:一个正在努力学技术的CV工程师,专注基础和实战分享 ,欢迎咨询! 💖 欢迎大…...
git环境切换
文章目录 一. 操作步骤:1.查看全局设置3.Git 切换本地git设置4.切换仓库并推送 一. 操作步骤: 1.查看全局设置 $ Git config --global --list credential.https://codeup.aliyun.com.providergeneric user.namebiejiahao user.emailxxxxxxxxqq.com3.Gi…...
hyperf websocket
composer require hyperf/websocket-server 配置 Server 修改 config/autoload/server.php,增加以下配置。 <?phpreturn [servers > [[name > ws,type > Server::SERVER_WEBSOCKET,host > 0.0.0.0,port > 9502,sock_type > SWOOLE_SOCK_TCP…...

用Echarts词云数据可视化热词表白
目录 1、使用前准备 2、准备工作 3、盒子搭建 4、整体展现 1、使用前准备 找到表白对象(重中之重!),不要一见钟情(个人觉得:一见钟情属于见色起意!),因为数据可视化需…...
VUE 实现路由的基本原理
路由 基本概念 在前端技术早期,所有页面的跳转通过更改url,浏览器页面刷新获取新的页面内容,这种粗糙的交互方式,一直等待优化。 后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求࿰…...
Android 11 添加系统属性
在初识Android 属性一文中提到,系统会默认加载以下文件 /system/etc/prop.default /system/build.prop /system_ext/build.prop /vendor/default.prop /vendor/build.prop /odm/etc/build.prop /product/build.prop /factory/factory.prop要弄清楚我们应该在哪里添…...

docker 创建容器过程
结合下图,本文讨论docker 创建容器过程: START└── [用户通过Docker Client发出指令]└── (1) docker run 或 docker create 命令├── (2) Docker Client与Docker Daemon建立通信连接└── (3) Docker Daemon接收到创建容器请求├── (4) 检查…...

OSI七层网络攻击行为及防范手段
2020年3月3日,360安全大脑披露美国中央情报局攻击组织(APT-C-39)对我国大型互联网公司、政府部门及相关企业进行长达11年的网络攻击渗透,该组织所使用的网络武器和CIA“Vault7”项目中的网络武器完全吻合。如今随着互联网技术的蓬…...

第100+5步 ChatGPT文献复现:ARIMAX预测肺结核 vol. 5
基于WIN10的64位系统演示 一、写在前面 我们继续往下看,首先例行回顾文章: 《PLoS One》杂志的2023年一篇题目为《A comparative study of three models to analyze the impact of air pollutants on the number of pulmonary tuberculosis cases in …...

论文| Convolutional Neural Network-based Place Recognition - 2014
2014-Convolutional Neural Network-based Place Recognition...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...
【LeetCode】3309. 连接二进制表示可形成的最大数值(递归|回溯|位运算)
LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 题目描述解题思路Java代码 题目描述 题目链接:LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 给你一个长度为 3 的整数数组 nums。 现以某种顺序 连接…...
Spring Security 认证流程——补充
一、认证流程概述 Spring Security 的认证流程基于 过滤器链(Filter Chain),核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤: 用户提交登录请求拦…...
Python 高效图像帧提取与视频编码:实战指南
Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...