尽量避免删改List

2023-12-29 16:20
文章标签 list 删改 尽量避免

本文主要是介绍尽量避免删改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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/549990

相关文章

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Collection List Set Map的区别和联系

Collection List Set Map的区别和联系 这些都代表了Java中的集合,这里主要从其元素是否有序,是否可重复来进行区别记忆,以便恰当地使用,当然还存在同步方面的差异,见上一篇相关文章。 有序否 允许元素重复否 Collection 否 是 List 是 是 Set AbstractSet 否

【Python报错已解决】AttributeError: ‘list‘ object has no attribute ‘text‘

🎬 鸽芷咕:个人主页  🔥 个人专栏: 《C++干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 文章目录 前言一、问题描述1.1 报错示例1.2 报错分析1.3 解决思路 二、解决方法2.1 方法一:检查属性名2.2 步骤二:访问列表元素的属性 三、其他解决方法四、总结 前言 在Python编程中,属性错误(At

List list = new ArrayList();和ArrayList list=new ArrayList();的区别?

List是一个接口,而ArrayList 是一个类。 ArrayList 继承并实现了List。 List list = new ArrayList();这句创建了一个ArrayList的对象后把上溯到了List。此时它是一个List对象了,有些ArrayList有但是List没有的属性和方法,它就不能再用了。而ArrayList list=new ArrayList();创建一对象则保留了A

处理List采用并行流处理时,通过ForkJoinPool来控制并行度失控的问题

在使用parallelStream进行处理list时,如不指定线程池,默认的并行度采用cpu核数进行并行,这里采用ForJoinPool来控制,但循环中使用了redis获取key时,出现失控。具体上代码。 @RunWith(SpringRunner.class)@SpringBootTest(classes = Application.class)@Slf4jpublic class Fo

Java中集合类Set、List和Map的区别

Java中的集合包括三大类,它们是Set、List和Map,它们都处于java.util包中,Set、List和Map都是接口,它们有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap。那么它们有什么区别呢? Set中的对象不按特定方式排序,并且没有重复对象。但它的有些实现类能对集合中的对

List对象过滤

List materialInventoryList = materialInventories.stream().filter(mat -> mat.getQty().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); stream().filter()方法可以过滤掉List的数据

c++stack和list 介绍

stack介绍 堆栈是一种容器适配器,专门设计用于在 LIFO 上下文(后进先出)中运行,其中元素仅从容器的一端插入和提取。 堆栈作为容器适配器实现,容器适配器是使用特定容器类的封装对象作为其基础容器 的类,提供一组特定的成员函数来访问其元素。元素从特定容器的 “back” 推送或弹出,这称为堆栈的顶部。 stack接口 stack() 构造空的栈 empty() 检测stack是否为

C++——list的实现

目录 0.前言 1.节点类  2.迭代器类  ①普通迭代器 ②const迭代器  ③模板迭代器 3.list类  3.1 clear、析构函数、swap ①clear ② 析构函数  ③ swap 3.2构造函数  ①无参构造  ②赋值构造 3.3 迭代器 3.4插入函数 ①insert插入 ②头插 ③尾插 3.5 删除函数 ①erase删除 ②头删

Python中如何实现列表推导式(List Comprehension)

Python中的列表推导式(List Comprehension)是一种简洁且高效的方式来创建列表。它不仅让代码更加简洁,而且通常比使用循环和条件语句生成列表更快。列表推导式的基本形式允许你从现有的列表或其他可迭代对象中创建新的列表,同时应用过滤和转换操作。下面我将详细解释列表推导式的概念、基本语法、高级用法以及其在实际应用中的优势。 一、列表推导式的基本概念 列表推导式是Python中的一种