尽量避免删改List

news2024/11/24 12:29:10

作者简介:大家好,我是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) {
        // 前台List
        List<Item> aList = Lists.newArrayList(
                new Item(1, "甲"),
                new Item(2, "乙"),
                new Item(3, "丙")
        );
        // 后台List
        List<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
    @AllArgsConstructor
    static 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) {
    // 前台List
    List<Item> aList = Lists.newArrayList(
            new Item(1, "甲"),
            new Item(2, "乙"),
            new Item(3, "丙")
    );
    // 后台List
    List<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) {
        // 前台List
        List<Item> aList = Lists.newArrayList(
                new Item(1, "甲"),
                new Item(2, "乙"),
                new Item(3, "丙")
        );
        // 后台List
        List<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,无论从代码可读性还是健壮性来说,都会好很多。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

golang 图片加水印

需求&#xff1a; 1&#xff0c;员工签到图片加水印 2&#xff0c;水印文字需要有半透明的底色&#xff0c;避免水印看不清 3&#xff0c;图片宽设置在600&#xff0c;小于600或者大于600都需要等比例修改图片的高度&#xff0c;保持水印在图片中的大小和位置 4&#xff0c;处理…

摸鱼摸出来的vue3+element-plus毒蘑菇后台管理:新标签页的实现。

在浏览器中&#xff0c;点击标签页右边的加号可以新加一个标签页&#xff0c;所以&#xff0c;在毒蘑菇后台管理&#xff08;简称毒蘑菇儿&#xff09;中也可以这样操作。 点击标签页右边的按钮就可以打开一个新标签页了&#xff0c;可以打开多个&#xff0c;互不冲突&#xff…

Animate 2024(Adobe an2024)

Animate 2024是一款由Adobe公司开发的动画和互动内容创作工具&#xff0c;是Flash的演进版本。Animate 2024为设计师和开发者提供了更丰富的功能&#xff0c;让他们能够创建各种类型的动画、交互式内容和多媒体应用程序。 Animate 2024具有以下特点&#xff1a; 强大的设计工…

UE5.1_Gameplay Debugger启用

UE5.1_Gameplay Debugger启用 重点问题&#xff1a; Gamplay Debugger启用不知道&#xff1f; Apostrophe、Tilde键不知道是哪个&#xff1f; Gameplay调试程序 | 虚幻引擎文档 (unrealengine.com) Gameplay Debugger

一些深度学习训练过程可视化以及绘图工具

常见的可视化方法 深度学习训练过程的可视化是一个重要的环节&#xff0c;它可以帮助研究人员和工程师更好地理解和调整他们的模型。常见的可视化方法包括&#xff1a; 损失和准确率曲线&#xff1a; 这是最常见的可视化类型&#xff0c;通常在训练过程中绘制损失函数和准确率…

【网络技术】【Kali Linux】Wireshark嗅探(三)用户数据报(UDP)协议

一、实验目的 本次实验使用wireshark流量分析工具进行网络嗅探&#xff0c;旨在了解UDP协议的报文格式。 二、网络环境设置 本次实验使用Kali Linux虚拟机完成&#xff0c;主机操作系统为Windows 11&#xff0c;虚拟化平台选择Oracle VM VirtualBox&#xff0c;组网模式选择…

软件测试/测试开发丨Windows系统chromedriver安装与环境变量配置

一、selenium 环境配置 1、chrome 浏览器的安装与配置 目前比较常用的浏览器是 Google Chrome 浏览器&#xff0c;所以本教程以 chrome 为主&#xff0c;后面简介一下其他浏览器的环境配置。 &#xff08;1&#xff09;chrome 下载: www.google.cn/chrome/ &#xff08;2&a…

日志记录、跟踪和指标

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 日志记录、跟踪和指标是系统可观察性的三大支柱。 下图显示了它们的定义和典型架构。 记录 日志记录系统中的离散事件。例如&#xff0c;我们可以将传入请求或对…

回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 (多指标,多图)

回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 &#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实ZOA-LSTM基于斑马优化算法优化长短期记忆神经网络的多输入单输出数据回归预测模型 &#xff08;…

ubuntu python播放MP3,wav音频和录音

目录 一.利用pygame&#xff08;略显麻烦&#xff0c;有时候播放不太正常&#xff09;1.安装依赖库2.代码 二.利用mpg123&#xff08;简洁方便&#xff0c;但仅争对mp3&#xff09;1.安装依赖库2.代码 三.利用sox&#xff08;简单方便&#xff0c;支持的文件格式多&#xff09;…

jvm实战之-常用jvm命令的使用

各命令的使用 JMAP 1、查看内存信息&#xff0c;对象实例数、对象占有大小 jmap -histo 进程号>./log.txt2、查看堆的配置信息和使用情况 jmap - heap 进程号3、将堆的快照信息dump下来&#xff0c;使用java自带的jvisualvm.exe打开分析 jmap -dump:formatb,filedump.h…

oracle学习(5)

数据处理 SQL语言的类型&#xff1a; 1. 数据库中&#xff0c;称呼增删改查&#xff0c;为DML语句。(Data Manipulation Language 数据操纵语言)&#xff0c;就是指代&#xff1a; insert、update、delete、select这四个操作。 2. DDL语句。(Data Definition Language 数据…

提升数据库性能的关键指南-Oracle AWR报告

文章目录 一、了解AWR报告&#xff1a;数据库性能的仪表盘二、生成AWR报告三、解读AWR报告的关键部分1.报告开头的系统基础信息2.ADDM发现3.负载概览(Load Profile)4.参数文件5.顶级前台等待事件6.SQL 统计信息-顶级SQL7.SGA Advisory AND PAG Advisory 一、了解AWR报告&#x…

帆软报表中定时调度中的最后一步如何增加新的处理方式

在定时调度中,到调度执行完之后,我们可能想做一些别的事情,当自带的处理方式不满足时,可以自定义自己的处理方式。 产品的处理方式一共有如下这些类型: 我们想在除了上面的处理方式之外增加自己的处理方式应该怎么做呢? 先看下效果: 涉及到两方面的改造,前端与后端。…

出海合规云安全,AWS Landing Zone解决方案建立安全着陆区

在出海的大环境中&#xff0c;企业数字化转型的趋势之一就是上云。然而&#xff0c;上云也带来了新的挑战&#xff0c;特别是对企业的 IT 建设和管理提出了更高的要求。为了构建一个安全合规的云上信息系统环境&#xff0c;满足企业中不同用户的快速增长、资源访问可控、成本可…

动态新增input输入框

实现原理&#xff0c;修改绑定数组的长度。 需要绑定的数组 memberList: [{userName: ,phone: ,position: }], 点击时触发修改绑定数组长度的方法 addItem() {this.memberList.push({name: , phone: , post: })}, deleteItem(item, index) {this.inputForm.memberList.splice(i…

02.Git远程仓库

一、常用的托管服务 1.gitHub 一个面向开源及私有软件项目的托管平台&#xff0c;只支持Git作为唯一的版本库格式进行托管。 2.码云(gitee) 是国内的一个代码托管平台&#xff0c;服务器在国内&#xff0c;所有相较于gitHub使用起来更加方便一点。 3.gitLab 是一个用于仓库管…

如何在MAC OS中的XCODE下添加 <bits/stdc++.h>

mac上使用的编译器是Clang&#xff0c;但是没有万能头文件bits/stdc.h\&#xff0c;本文介绍如何添加万能头文件 Xcode 版本&#xff1a;15.1 - 打开应用程序-Xcode-右键显示包内容 Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/includ…

华为无线ac双链路冷备和热备配置案例

所谓的冷备和热备&#xff0c;冷备就是不用vrrp和hsb协议同步ap和用户信息&#xff0c;主的断了等七十五秒后&#xff0c;备的capwap和ap连接上去。 双链路冷备不用vrrp和hsb 双链路热备份只用hsb同步ap和用户信息&#xff0c;不用vrrp&#xff0c;两个ac可以不用在同一个二层…

java设计模式实战【策略模式+观察者模式+命令模式+组合模式,混合模式在支付系统中的应用】

引言 在代码开发的世界里&#xff0c;理论知识的重要性毋庸置疑&#xff0c;但实战经验往往才是知识的真正试金石。正所谓&#xff0c;“读万卷书不如行万里路”&#xff0c;理论的学习需要通过实践来验证和深化。设计模式作为软件开发中的重要理论&#xff0c;其真正的价值在…