尽量避免删改List
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
阶段4、深入jdk其余源码解析
阶段5、深入jvm源码解析
尽管在之前介绍了如何避免并发修改异常,但那篇文章的目的,更多的是为了介绍底层原理及应付面试,实际开发中并不推荐大家对原List做增删改操作。
我的观点是,对于一个初始化完毕的List,尽量把它当做只读的,不要贸然做增删改操作。比如Java8的Stream,它所有的操作都是基于新的List,并不会改变原数据,包括JDK、Google Common以及Apache Common等工具类提供的不可变集合(Immutable Collections),其实都是在传递这种思想(Google Common甚至直接屏蔽了增删改方法):
接下来,给大家分享两个实际开发中遇到的问题,都与List操作有关。
用skip()、limit()代替subList()
对于List的截取,可能大家都习惯用List.subList(),但它有个隐形的坑:对截取后的List进行元素修改,会影响原List(除非你就希望改变原List)。
究其原因,subList()并非真的从原List截取出元素,而是偏移原List的访问坐标罢了:
比如你要截取(5, 6),那么下次你get(index),我就直接返回5+index给你,看起来好像真的截取了。
另外,这个方法限制太大,用起来也麻烦,比如对于一个不确定长度的原List,如果你想做以下截取操作:list.subList(0, 5)或者list.subList(2, 5),当原List长度不满足List.size()>=5时,会抛异常。为了避免误操作,你必须先判断size:
if(list != null && list.size() >= 5) {return list.subList(2, 5);
}
较为简便和安全的做法是借助Stream(Stream一个很重要的特性是,不修改原数据,而是新产生一个流):
public static void main(String[] args) {List<String> list = Lists.newArrayList("a", "b", "c", "d");List<String> limit3 = list.stream().limit(3).collect(Collectors.toList());// 超出实际长度也不会报错List<String> limit5 = list.stream().limit(5).collect(Collectors.toList());List<String> range3_4 = list.stream().skip(2).limit(2).collect(Collectors.toList());// 超出实际长度也不会报错List<String> range3_5 = list.stream().skip(2).limit(3).collect(Collectors.toList());System.out.println(limit3 + " " + limit5 + " " + range3_4 + " " + range3_5);
}
用filter()代替remove()
很多同学对内存占用极其敏感,恨不得用同一份内存把A、B、C三件事都干了(特别是经历了LeetCode摧残的人)。这种想法是好的,但对于List这样有并发修改限制的容器来说,一不留神就有可能出现问题。举个例子:
假设后台要支持配置定向推广的商品,并且需要将配置的商品在当前时间轴置顶(比如09:00下)。原本时间轴的列表是AList,长度为10,而后台配置的商品为BList,长度不确定,在0~10之间。考虑到后台配置的商品可能与原List中的商品重复,所以这里要加一个去重操作。很多人可能会想到利用Set或者Map的key不重复的特性去重,但试了以后会发现顺序可能被打乱。那么,最直观的方法就是双层for遍历:先遍历原来的AList,然后拿着AList的item去BList遍历,如果这个item在BList中已经存在,就把这个item从AList删除。
public class ListRemoveTest {public static void main(String[] args) {// 前台ListList<Item> aList = Lists.newArrayList(new Item(1, "甲"),new Item(2, "乙"),new Item(3, "丙"));// 后台ListList<Item> bList = Lists.newArrayList(new Item(99, "对照数据"),new Item(3, "丙"));// 对前台List去重for (int i = aList.size() - 1; i >= 0; i--) {for (Item user : bList) {if (Objects.equals(user.getId(), aList.get(i).getId())) {aList.remove(i);}}}// 组合去重后的两个List,后台List置顶bList.addAll(aList);System.out.println(JSON.toJSONString(bList));}@Getter@Setter@AllArgsConstructorstatic class Item {private Integer id;private String title;}}
即使我对并发修改异常“了如指掌”,在实际开发时还是写出了上面的代码。最致命的是,上面的代码还不一定会出错!如果重复商品只有一个,且恰好出现在bList的末尾,上面的代码是不会报错的。如果我们将上面bList元素顺序对调,再次运行就会发生数组越界异常:
原因是,当bList重复的元素只有一个且恰好在末尾时,第二层for在执行aList.remove()以后就直接退出第二层for,不会继续执行if逻辑,也就不会执行aList.get(i),所以不会发生数组越界(可能比较难理解,大家可以复制代码实际观察一下)。
当初虽然考虑到并发修改异常的可能,但不巧的是构造测试数据时只构造了一个重复的商品,而且排序系数设置为最高,恰好处于bList的末尾,完美地避开了问题...实际上线几天后的某个早晨,运营配置了多个商品,而且恰好重复了,于是首页直接崩了...这是一个很严重的事故。
一个可行的处理方式是:
public static void main(String[] args) {// 前台ListList<Item> aList = Lists.newArrayList(new Item(1, "甲"),new Item(2, "乙"),new Item(3, "丙"));// 后台ListList<Item> bList = Lists.newArrayList(new Item(3, "丙"),new Item(99, "对照数据"));// 对aList进行筛选(bList中不存在的item)Map<Integer, Item> bItemMap = bList.stream().collect(Collectors.toMap(Item::getId, v -> v, (v1, v2) -> v1));List<Item> filteredAList = aList.stream().filter(aItem -> !bItemMap.containsKey(aItem.getId())).collect(Collectors.toList());// 组合去重后的两个List,后台List置顶bList.addAll(filteredAList);System.out.println(JSON.toJSONString(bList));
}
当然,List本身提供了诸如allAll()、retainAll()、removeAll()等操作,可以很方便的实现并集、交集、差集。所以,上面的去重取并集可以这样:
public class ListRemoveTest {public static void main(String[] args) {// 前台ListList<Item> aList = Lists.newArrayList(new Item(1, "甲"),new Item(2, "乙"),new Item(3, "丙"));// 后台ListList<Item> bList = Lists.newArrayList(new Item(3, "丙"),new Item(99, "对照数据"));// 先去重,再合并aList.removeAll(bList);bList.addAll(aList);System.out.println(JSON.toJSONString(bList));}@Getter@Setter@AllArgsConstructor@EqualsAndHashCode // 注意,这里要重写equals和hash,否则默认比较地址值static class Item {private Integer id;private String title;}}
说了这么多,就是想强调,无论是并发修改异常还是数组越界,通常情况下都不会发生,但当你企图对原List进行增删改操作时,只要没考虑周全,就有极大概率发生。由于Stream的任何操作都不会改变原数据,所以从根源上杜绝了增删改可能隐藏的问题,是比较安全的方式,也推荐大家多使用Stream,无论从代码可读性还是健壮性来说,都会好很多。
相关文章:

尽量避免删改List
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 学习必须往深处挖&…...

【Linux操作系统】探秘Linux奥秘:用户、组、密码及权限管理的解密与实战
🌈个人主页:Sarapines Programmer🔥 系列专栏:《操作系统实验室》🔖诗赋清音:柳垂轻絮拂人衣,心随风舞梦飞。 山川湖海皆可涉,勇者征途逐星辉。 目录 🪐1 初识Linux OS &…...

计算机组成原理复习4
习题 练习题 下列不属于系统总线的为() a.数据总线 b.地址总线 c.控制总线 d.片内总线 D 系统总线中地址总线的功能是() a.选择主存单元地址 b.选择进行信息传输的设备 c.选择外存地址 d.指定主存和I/O设备接口电路的地址 D 解…...
AutoSAR(基础入门篇)3.3-Autosar中RTE的数据一致性与Interface接口
目录 一、RTE的数据一致性 1、什么是数据一致性 2、数据一致性的实现机制 2.1、利用RTE管理<...

超维空间S2无人机使用说明书——52、初级版——使用PID算法进行基于yolo的目标跟踪
引言:在实际工程项目中,为了提高系统的响应速度和稳定性,往往需要采用一定的控制算法进行目标跟踪。这里抛砖引玉,仅采用简单的PID算法进行目标的跟随控制,目标的识别依然采用yolo。对系统要求更高的,可以对…...
<JavaEE> TCP 的通信机制(一) -- 确认应答 和 超时重传
目录 TCP的通信机制的核心特性 一、确认应答 1)什么是确认应答? 2)如何“确认”? 3)如何“应答”? 二、超时重传 1)丢包的概念 2)什么是超时重传? 3)…...

Spark任务调度与数据本地性
Apache Spark是一个分布式计算框架,用于处理大规模数据。了解Spark任务调度与数据本地性是构建高效分布式应用程序的关键。本文将深入探讨Spark任务调度的流程、数据本地性的重要性,并提供丰富的示例代码来帮助大家更好地理解这些概念。 Spark任务调度的…...
【论文阅读】Self-Paced Curriculum Learning
论文下载 代码 Supplementary Materials bib: INPROCEEDINGS{,title {Self-Paced Curriculum Learning},author {Lu Jiang and Deyu Meng and Qian Zhao and Shiguang Shan and Alexander Hauptmann},booktitle {AAAI},year {2015},pages {2694--2700} }1. 摘…...
C++简易线程池
原理说明: 1. 线程池创建时,指定线程池的大小thread_size。当有新的函数任务通过函数addFunction ()添加进来后,其中一个线程执行函数。一个线程一次执行一个函数。如果函数数量大与线程池数量,则后来的函数等待。 2. 线程池内部…...

【MATLAB】PSO粒子群优化LSTM(PSO_LSTM)的时间序列预测
有意向获取代码,请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 PSO粒子群优化LSTM(PSO-LSTM)是一种将粒子群优化算法(PSO)与长短期记忆神经网络(LSTM)相结合的混合模型。该算法通过…...

产品经理学习-怎么写PRD文档
目录 瀑布流方法论介绍 产品需求文档(PRD)介绍 产品需求文档的基本要素 撰写产品需求文档 优先产品需求文档的特点 其他相关文档 瀑布流方法论介绍 瀑布流模型是一种项目的开发和管理的方法论,是敏捷的开发管理方式相对应的另一种方法…...

第3课 获取并播放音频流
本课对应源文件下载链接: https://download.csdn.net/download/XiBuQiuChong/88680079 FFmpeg作为一套庞大的音视频处理开源工具,其源码有太多值得研究的地方。但对于大多数初学者而言,如何快速利用相关的API写出自己想要的东西才是迫切需要…...

Spark编程实验四:Spark Streaming编程
目录 一、目的与要求 二、实验内容 三、实验步骤 1、利用Spark Streaming对三种类型的基本数据源的数据进行处理 2、利用Spark Streaming对Kafka高级数据源的数据进行处理 3、完成DStream的两种有状态转换操作 4、把DStream的数据输出保存到文本文件或MySQL数据库中 四…...
Flink去重计数统计用户数
1.数据 订单表,分别是店铺id、用户id和支付金额 "店铺id,用户id,支付金额", "shop-1,user-1,1", "shop-1,user-2,1", "shop-1,user-2,1", "shop-1,user-3,1", "shop-1,user-3,1", "shop-1,user…...

力扣:62. 不同路径(动态规划,附python二维数组的定义)
题目: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少条不同的路径&…...

2022年全球运维大会(GOPS深圳站)-核心PPT资料下载
一、峰会简介 GOPS 主要面向运维行业的中高端技术人员,包括运维、开发、测试、架构师等群体。目的在于帮助IT技术从业者系统学习了解相关知识体系,让创新技术推动社会进步。您将会看到国内外知名企业的相关技术案例,也能与国内顶尖的技术专家…...

8868体育助力意甲罗马俱乐部 迪巴拉有望付出
8868体育助力意甲罗马俱乐部 迪巴拉有望付出 意甲罗马俱乐部是8868体育合作球队之一,本赛季,在意甲第14轮的比赛中,罗马客场2-1战胜萨索洛,积分上升到意甲第4位。 有报道称,迪巴拉在对阵佛罗伦萨的比赛中受伤ÿ…...

java设计模式实战【策略模式+观察者模式+命令模式+组合模式,混合模式在支付系统中的应用】
引言 在代码开发的世界里,理论知识的重要性毋庸置疑,但实战经验往往才是知识的真正试金石。正所谓,“读万卷书不如行万里路”,理论的学习需要通过实践来验证和深化。设计模式作为软件开发中的重要理论,其真正的价值在…...
小程序wx:if 和hidden的区别?
在小程序中,wx:if 和 hidden 是用于条件渲染的两种不同方式。 选择使用哪种方式取决于具体情况。如果条件变化频繁或节点包含复杂的子节点,可以考虑使用 wx:if 进行条件渲染;如果条件变化较少且节点结构简单,可以使用 hidden 控制…...

自动驾驶学习笔记(二十三)——车辆控制模型
#Apollo开发者# 学习课程的传送门如下,当您也准备学习自动驾驶时,可以和我一同前往: 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo开放平台9.0专项技术公开课》免费报名—>传送门 文章目录 前言 运动学模型 动力学模型 总结…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...

Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...
适应性Java用于现代 API:REST、GraphQL 和事件驱动
在快速发展的软件开发领域,REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名,不断适应这些现代范式的需求。随着不断发展的生态系统,Java 在现代 API 方…...
十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建
【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道
文/法律实务观察组 在债务重组领域,专业机构的核心价值不仅在于减轻债务数字,更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明,合法债务优化需同步实现三重平衡: 法律刚性(债…...