一个由List.removeAll()失效引发的思考

article/2025/11/8 10:28:26

前言:

本来以为是个错误使用的问题,稍微那么深究一下,发现脑海中,关于这个部分的知识库存已经告急了,可不能啊。

removeAll() 失效重现

今天做一个批量删除的功能,我使用了 List.removeAll()这个方法,但是该代码执行前后,被操作的列表的 size 并没由发生改变。

排查了一下,是因为两个列表中存储对象不同的原因。

为了更加清楚的理解,我写了简单的小例子,复现了错误的场景:

实体类:

public class Bean {private int id;private String name;private String address;public Bean(int id, String name, String address) {this.id = id;this.name = name;this.address = address;}
}

构建场景:

ArrayList<Bean>  allStudents = new ArrayList<>();ArrayList<Bean>  boyStudents = new ArrayList<>();for (int i = 0; i < 10 ; i++) {Bean  bean = new Bean(i,"name is "+i,"address is "+i);allStudents.add(bean);}for (int i = 0; i < 5 ; i++) {Bean  bean = new Bean(i,"name is "+i,"address is "+i);boyStudents.add(bean);}System.out.println("allStudents.size()------before-------------->"+allStudents.size());System.out.println("remove result : "+allStudents.removeAll(boyStudents));System.out.println("allStudents.size()-------after-------------->"+allStudents.size());

输出结果是:


allStudents.size()------before-------------->10
remove result : false
allStudents.size()-------after-------------->10

但是,换 String 对象执行 removeAll() 竟然可以成功!

因为操作对象不同,这是一个很简单的原因,但是接下来要实验的另一个小例子,绝对让你非常吃惊,我们讲Bean 替换成 String 字符串试一下。

ArrayList<String>  allStudents = new ArrayList<>();ArrayList<String>  boyStudents = new ArrayList<>();for (int i = 0; i < 10 ; i++) {String  bean = "name is "+i+"address is "+i;allStudents.add(bean);}for (int i = 0; i < 5 ; i++) {String  bean = "name is "+i+"address is "+i;boyStudents.add(bean);}System.out.println("allStudents.size()------before-------------->"+allStudents.size());System.out.println("remove result : "+allStudents.removeAll(boyStudents));System.out.println("allStudents.size()-------after-------------->"+allStudents.size());

输出结果是 :

allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5

揭开这一切的面纱

从打印结果很明白的看到,removeAll() 成功执行。String也是对象,为什么会这样?代码不会说谎,我们去源码中去寻找答案。
从源码中发现,ArrayList 执行 removeAll() 方法流程如下图所示:

在这里插入图片描述
通过控制变量法分析,很容易就聚焦到 equals()这个方法,这个方法是 Object 的方法,默认实现是比较对象在内存的地址。

public boolean equals(Object obj) {return (this == obj);}

再看一下 String 中 equals() 方法,重写了 Object 的这个方法,不再是比较地址,而是比较字符串是否相同。


public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String) anObject;int n = count;if (n == anotherString.count) {int i = 0;while (n-- != 0) {if (charAt(i) != anotherString.charAt(i))return false;i++;}return true;}}return false;}

这样的话,引发了一个思考,也就是说,如果自定义对象,重写 equals() 中的实现,也是可以实现非相同对象的情况下,成功 removeAll()的。这里我用 上面例子的 Bean 实体类简单实验一下:

public class Bean {private int id;private String name;private String address;public Bean(int id, String name, String address) {this.id = id;this.name = name;this.address = address;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Bean bean = (Bean) o;if (id != bean.id) return false;if (!name.equals(bean.name)) return false;return address.equals(bean.address);}@Overridepublic int hashCode() {int result = id;result = 31 * result + name.hashCode();result = 31 * result + address.hashCode();return result;}
}

再次执行第一个例子的程序,ArrayList 成功 removeAll,打印信息如下:

allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5

重写 equals() 方法一定要符合规范!

但是这里我们要特别注意的是,当我们重写 equals() 方法的时候,一定要遵守它的规范,否则在程序使用中,使用错误实现 equals() 的类时可能出现无法预料的问题,而且很有可能,找了很久都定位不到问题在哪!,想想都后背发冷。

以下是 Object 中 equals()的方法说明注释,绝对原汁原味!但是我相信肯定有人看了一遍还是不知道说的啥,非常正常,我看两遍也是,英语渣没办法,只能花更多时间去理解它,因为真的真重要。


/*** Indicates whether some other object is "equal to" this one.* <p>* The {@code equals} method implements an equivalence relation* on non-null object references:* <ul>* <li>It is <i>reflexive</i>: for any non-null reference value*     {@code x}, {@code x.equals(x)} should return*     {@code true}.* <li>It is <i>symmetric</i>: for any non-null reference values*     {@code x} and {@code y}, {@code x.equals(y)}*     should return {@code true} if and only if*     {@code y.equals(x)} returns {@code true}.* <li>It is <i>transitive</i>: for any non-null reference values*     {@code x}, {@code y}, and {@code z}, if*     {@code x.equals(y)} returns {@code true} and*     {@code y.equals(z)} returns {@code true}, then*     {@code x.equals(z)} should return {@code true}.* <li>It is <i>consistent</i>: for any non-null reference values*     {@code x} and {@code y}, multiple invocations of*     {@code x.equals(y)} consistently return {@code true}*     or consistently return {@code false}, provided no*     information used in {@code equals} comparisons on the*     objects is modified.* <li>For any non-null reference value {@code x},*     {@code x.equals(null)} should return {@code false}.* </ul>* <p>* The {@code equals} method for class {@code Object} implements* the most discriminating possible equivalence relation on objects;* that is, for any non-null reference values {@code x} and* {@code y}, this method returns {@code true} if and only* if {@code x} and {@code y} refer to the same object* ({@code x == y} has the value {@code true}).* <p>* Note that it is generally necessary to override the {@code hashCode}* method whenever this method is overridden, so as to maintain the* general contract for the {@code hashCode} method, which states* that equal objects must have equal hash codes.** @param   obj   the reference object with which to compare.* @return  {@code true} if this object is the same as the obj*          argument; {@code false} otherwise.* @see     #hashCode()* @see     java.util.HashMap*/
public boolean equals(Object obj) {return (this == obj);
}

equals 方法实现的时候必须要满足的特性:

1.(reflexive)自反性:

对于任何非 null 的引用值 x,x.equals(x) 必须为 true;

2.(symmetric)对称性:
对于任何非 null 的引用值 x,y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 也要返回 true 。

3.(transitive)传递性:
对于任何非 null 的引用值 x,y,z, 如果 x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 一定要返回 true。

4.(consistent)一致性:
对于任何非 null 的引用值 x,y,只要 equals() 方法没有修改的前提下,多次调用 x.equals(y) 的返回结果一定是相同的。

5.(non-nullity)非空性
对于任何非 null 的引用值 x,x.equals(null) 必须返回 false。

这些特性约束我们重写 equals()的时候,写条件判断一定要谨慎,下面是提供的一个模版:

@Override
public boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;(MyClass) myclass = (MyClass) o;if (this.xx != bean.myclass.xx) return false;return myclass.equals(myclass.xx);}

重要!覆盖 equals 时,一定要同时覆盖 hashCode

这样做的目的是保证每一个 equals()返回 true 的两个对像,要有两个相同的 hashCode 。

在上面演示的例子中,不覆盖 hashCode ,equals 方法表现的也很好,调用 List.removeAll 也能成功执行。看似是没有什么问题,但是当我们试图使用 hashMap 做存取操作的时候,就会出现问题。


HashMap<Bean,String> allStudents = new HashMap<>();for (int i = 0; i < 10 ; i++) {Bean  bean = new Bean(i,"name is "+i,"address is "+i);allStudents.put(bean,"i :"+i);}
Bean bean = new Bean(1,"name is 1","address is 1");System.out.println(" allStudents.get(bean)----------------------->"+ allStudents.get(bean));

输出结果:

Bean 中不正确覆盖 hashCode(),取不到值:

allStudents.get(bean)----------------------->null

Bean 中正确覆盖 hashCode(),能取到值:

allStudents.get(bean)----------------------->i :1

原因在于,HashMap 执行 get() 操作的时候是通过散列码,也就是对象的 HashCode 来搜索数据的。所以,当不重写 hashCode() 方法或者重写的不规范的时候,就会出现这样的问题。
使用散列码判断对象的,有 HashMap ,HashSet,HashTable 等,因此,覆盖 equals() 时,一定要同时覆盖 hashCode()。

快速生成euqals() and hashCode()!

看到上面的解释,估计大家都感觉覆盖这俩方法的时候都有点害怕了,但是告诉大家一个好消息,Android Studio 有这两个方法的快速生成模版,使用方法是 右键->generate->euqals() and hashCode(),也可以直接使用快捷键 command+N ->euqals() and hashCode()。

在这里插入图片描述

最后

到这里,我想到,一个在面试的时候,经常被问到的 java 基础题:

java 中 == 和 equals 和 hashCode 的区别?

我想现在,如果再被问到这个问题,我肯定可以比之前回答的要好一点了。

java 中有两种类型,值类型和引用类型。其中,== 值类型和引用类型都可以运算,equals 和 hashCode 是引用类型特有的方法。

对于值类型,== 比较的是它们的值,对于引用类型,== 比较的是两者在内存中的物理地址。

equals() 是 Object 类中的方法,默认实现是使用 == 比较两个对象的地址,但是在一些子类,例如 String,Float,Integer 等,它们对 equals进行覆盖重写,就不再是比较两个对象的地址了。

hashCode() 也是 Object 类的一个方法。返回一个离散的 int 型整数。在集合类操作中使用,为了提高查询速度。

当覆盖 equals() 的时候,一定要同时覆盖 hashCode(),保证 x.equals(y) 返回为 true 的时候,x,y 要有相同的 HashCode 。

回答完毕。

参考资料

Effective Java 中文版第二版


欢迎关注个人微信公众号「浅浅同学的开发笔记」,最新的博客,好玩的事情,都会在上面分享,期待与你共同成长。

在这里插入图片描述


http://chatgpt.dhexx.cn/article/QRglS6a8.shtml

相关文章

java removeall对象_list 删除对象 remove 和 removeAll 区别 及迭代器删除

可以看到remove 有两个方法&#xff0c;一个返回值是Boolean。一个返回值是删除的对象类型&#xff0c;这个参数是该对象在列表中的位置(用的少)。 区别&#xff1a;remove是删除List中的一条数据&#xff0c;参数是List<> 的一个泛型对象&#xff0c;删除也只删除一条。…

Java 中 HashSet 的 removeAll 性能分析

1. 概述 HashSet是一个用于存储唯一元素的集合。 在本文中&#xff0c;我们将讨论java.util.HashSet 类中removeAll()方法 的性能。 2. HashSet.removeAll() HashSet 的 removeAll 方法删除所有包含指定集合的元素&#xff1a; Set<Integer> set new HashSet<In…

Python爬虫案例解析:五个实用案例及代码示例(学习爬虫看这一篇文章就够了)

导言&#xff1a; Python爬虫是一种强大的工具&#xff0c;可以帮助我们从网页中抓取数据&#xff0c;并进行各种处理和分析。在本篇博客中&#xff0c;我们将介绍五个实用的Python爬虫案例&#xff0c;并提供相应的代码示例和解析。通过这些案例&#xff0c;读者可以了解如何应…

python爬虫实验总结_python爬虫总结

python2转成python3的问题: 使用python3下边的2to3.py 打开cmd,进到python安装目录下的 \Tools\scripts文件夹中 输入python 2to3.py -w 目标py文件路径/目标.py 通过这种方式可以将一些格式的区别进行转化。 import格式的区别: py2和py3的import机制不同,详情可以百度…

利用Python爬虫技术爬取京东商品评论

这是我第一次接触python时&#xff0c;我们学校做的项目实训&#xff0c;其实整个项目实训过程很简单&#xff0c;并没有什么难度&#xff0c;认真学学就会。 首先&#xff0c;我们要明确我们的目标&#xff1a;从京东上爬取产品的评论。一般评论都是进行情感分析&#xff0c;但…

通过Python爬虫技术获取小说信息

资源下载地址&#xff1a;https://download.csdn.net/download/sheziqiong/85673772 实验目的 使用Python爬虫技术获取小说信息&#xff0c;包括小说名称、小说作者以及小说简介等作品信息&#xff01;在实验中掌握Python的第三方库requests和lxml 实验内容 明确实验需求—…

Python爬虫详解

从今天开始&#xff0c;给大家介绍Python爬虫相关知识&#xff0c;今天主要内容是爬虫的基础理论知识。 一、爬虫简介 爬虫是指通过编写程序&#xff0c;来模拟浏览器访问Web网页&#xff0c;然后通过一定的策略&#xff0c;爬取指定内容。因此&#xff0c;爬虫的编写通常分为…

python爬虫技术简介-python网络爬虫---简介与认识HTTP

一、python爬虫环境与简介 二、认识HTTP 三、简单静态网页爬取 四、常规动态网页爬取 五、模拟登陆 六、PC客户端抓包 七、Scrapy爬虫 一、python爬虫环境与简介 1、认识爬虫 &#xff08;1&#xff09;爬虫的概念 网络爬虫也被称为网络蜘蛛、网络机器人&#xff0c;…

python 爬虫总结

requests模块 import reqeusts # get 请求 # 网址 url_login "url://123.com"# 请求头 headers {User-Agent: Apipost client Runtime/https://www.apipost.cn/ }# 参数&#xff0c;形式字典 kw {key:value} response reqeusts.get(urlurl_login,paramskw)# pos…

Python爬虫介绍

一、什么是爬虫 爬虫&#xff1a;一段自动抓取互联网信息的程序&#xff0c;从互联网上抓取对于我们有价值的信息 二、Python爬虫架构 Python爬虫架构主要由五个部分组成&#xff0c;分别是调度器、URL管理器、网页下载器、网页解析器、应用程序&#xff08;爬取的有价值数据…

Python实用技术——爬虫(一):爬虫基础

目录 爬虫这门技术本身是不违法的&#xff0c;但是应该注意&#xff1a; 1&#xff0c;爬取什么数据 2&#xff0c;如何爬取得来的 3&#xff0c;爬取之后如何使用 二&#xff0c;HTTP协议 1&#xff0c;万维网 2&#xff0c;协议&#xff1a; 三&#xff0c;HTTP知识 …

Python爬虫讲解(超详细)

Python爬虫是一种通过编写程序自动从互联网上获取数据的技术。下面是Python爬虫的详解&#xff1a; 爬虫的基本原理 爬虫的基本原理是**通过模拟浏览器的行为**&#xff0c;访问目标网站&#xff0c;并获取目标页面中的数据。Python爬虫可以使用requests库来发送HTTP请求&…

python爬虫技术整理

Python爬虫——新闻热点爬取 显示更多 可以看到相关的数据接口&#xff0c;里面有新闻标题以及新闻详情的url地址 如何提取url地址 1、转成json&#xff0c;键值对取值&#xff1b; 2、用正则表达式匹配url地址&#xff1b;根据接口数据链接中的pager 变化进行翻页&#xf…

Pytorch创建多任务学习模型

在机器学习中&#xff0c;我们通常致力于针对单个任务&#xff0c;也就是优化单个指标。但是多任务学习(MTL)在机器学习的许多应用中都取得了成功&#xff0c;从自然语言处理和语音识别到计算机视觉和药物发现。 MTL最著名的例子可能是特斯拉的自动驾驶系统。在自动驾驶中需要…

多任务学习 Pytorch实现

多任务学习MTL的简单实现&#xff0c;主要是为了理解MTL 代码写得挺烂的&#xff0c;有时间回来整理一下 import torch import torch.nn as nn import torchvision import torchvision.transforms as transforms import numpy as np import matplotlib.pyplot as plt import ma…

【综述】多任务学习

前言 本文对多任务学习(multi-task learning, MTL)领域近期的综述文章进行整理&#xff0c;从模型结构和训练过程两个层面回顾了其发展变化&#xff0c;旨在提供一份 MTL 入门指南&#xff0c;帮助大家快速了解多任务学习的进化史。 1. 什么是多任务学习&#xff1f; 多任务学习…

多任务学习原理与优化

文章目录 一、什么是多任务学习二、为什么我们需要多任务学习三、多任务学习模型演进Hard shared bottom 硬共享Soft shared bottom 软共享软共享&#xff1a; MOE & MMOE软共享&#xff1a; CGC & PLE加入FMMMOE/PLE 的调参ESMM 四、 loss权重1&#xff0c; 利用任务的…

【多任务学习-Multitask Learning概述】

多任务学习-Multitask Learning概述 1.单任务学习VS多任务学习多任务学习的提出多任务学习和单任务学习对比 2.多任务学习共享表示shared representation&#xff1a;多任务学习的优点那么如何衡量两个任务是否相关呢&#xff1f; 当任务之间相关性弱多任务MLP特点总结多任务学…

多任务学习(Multi-Task Learning, MTL)

目录 [显示] 1 背景2 什么是多任务学习&#xff1f;3 多任务学习如何发挥作用&#xff1f; 3.1 提高泛化能力的潜在原因3.2 多任务学习机制3.3 后向传播多任务学习如何发现任务是相关的4 多任务学习可被广泛应用&#xff1f; 4.1 使用未来预测现在4.2 多种表示和度量4.3 时间序…

Tensorflow 多任务学习

之前在caffe上实现了两个标签的多任务学习&#xff0c;如今换到了tensorflow&#xff0c;也想尝试一下&#xff0c;总的来说也不是很复杂。 建立多任务图 多任务的一个特点是单个tensor输入(X)&#xff0c;多个输出(Y_1,Y_2...)。因此在定义占位符时要定义多个输出。同样也需要…