当前位置: 首页 > news >正文

Java-集合求差集,如果B集合中的id在A集合中存在就移除,如果不在就返回A集合

“如果B集合中的id在A集合中存在就移除,如果不在就返回原A集合”​​——正是集合运算中的​​差集

(Difference)​​操作。

简单来说,就是求 ​​A - B​​ 的结果,即找出所有属于集合A但不属于集合B的元素。

实现差集的几种核心方法

在Java中,实现集合差集主要有以下几种方式,它们各有特点,您可以根据具体场景选择:

方法

核心思路

是否会修改原集合A?

特点与适用场景

removeAll()方法​

直接调用A.removeAll(B)

​是​

最直接、最常用,但会​​改变原集合A​​。需确保对象正确重写equalshashCode方法。

​Stream API过滤​

利用Stream的filter条件创建新集合

​否​

函数式编程风格,​​不修改原集合​​,代码简洁。推荐在需要保留原集合时使用。

​迭代器遍历删除​

使用Iterator遍历A,手动判断并删除

​是​

更底层的操作,可精细控制删除逻辑。但代码稍显繁琐,需注意避免并发修改异常。

1. 使用 removeAll()方法

这是最经典的实现方式,直接使用Collection接口定义的removeAll(Collection<?> c)方法。

// 假设集合A和B中存放的是自定义对象MyObject
List<MyObject> listA = new ArrayList<>();
List<MyObject> listB = new ArrayList<>();// ... 为listA和listB添加元素 ...// 进行差集操作 (会直接修改listA)
listA.removeAll(listB);
// 此时,listA中已不包含任何在listB中也存在的对象
System.out.println("差集结果: " + listA);

⚠️ 关键点​​:此方法能正确工作的前提是,您的MyObject类必须正确重写了equals(Object obj)

法(通常是根据id来判断对象是否相等)。如果使用HashSetSet集合,还需要重写hashCode()

法。

2. 使用Stream API(推荐,不修改原集合)

如果您希望保留原始的集合A不被改变,可以使用Java 8的Stream API。

List<MyObject> listA = ...;
List<MyObject> listB = ...;// 提取集合B中的所有ID,转换为Set以提高查询效率
Set<String> idSetB = listB.stream().map(MyObject::getId).collect(Collectors.toSet());// 通过过滤创建新集合,原listA不变
List<MyObject> differenceList = listA.stream().filter(obj -> !idSetB.contains(obj.getId())) // 保留ID不在B集合中的元素.collect(Collectors.toList());System.out.println("差集结果: " + differenceList);
System.out.println("原集合A未被修改: " + listA);

这种方法非常灵活且安全,是当前主流的做法。

3. 使用迭代器(Iterator)遍历删除

这是一种更基础的方式,通过迭代器安全地在遍历过程中进行删除。

import java.util.Iterator;
import java.util.List;List<MyObject> listA = ...;
List<MyObject> listB = ...;// 提取集合B的ID列表用于比较
List<String> idListB = listB.stream().map(MyObject::getId).collect(Collectors.toList());// 使用迭代器安全地移除元素
Iterator<MyObject> iterator = listA.iterator();
while (iterator.hasNext()) {MyObject obj = iterator.next();if (idListB.contains(obj.getId())) {iterator.remove(); // 使用迭代器的 remove 方法避免并发修改异常}
}System.out.println(listA); // 此时 listA 已被修改

说明​​:

  • ​直接修改原集合​​:此方法也会​​修改原始的 listA​。

  • ​并发修改异常​​:如果在普通的for循环中直接使用listA.remove(obj),可能会抛出ConcurrentModificationException。使用 ​Iterator(迭代器)​​ 的 remove()方法是安全删除当前正在遍历元素的标准做法。

  • ​性能一般​​:相对于前两种方法,这种循环方式代码稍显繁琐,且性能通常没有优势。

  • ​推荐指数​​:⭐​​不推荐​​在日常编码中优先使用,除非有特殊原因。Stream方式或removeAll通常更优。

如何选择?

  • ​通常情况下,推荐使用Stream API​​。因为它代码清晰,符合现代Java编程风格,并且最重要的一点是​​不修改原集合​​,避免了潜在的副作用。

  • 如果您​​明确需要修改原集合​​,并且对象已正确重写equalshashCode,那么直接使用removeAll()方法最为简便。

  • 使用迭代器的方式通常在需要更复杂的删除逻辑时才会考虑。


为什么必须正确重写了equals(Object obj)方法

正确重写 equals(Object obj)方法是 Java 编程中的一项基础且关键的要求,这主要源于默认的对

象比较方式与我们的业务逻辑需求之间的根本差异。

⚖️ 默认比较的局限

Java 中所有类都隐式继承自 Object类。Object类中定义的默认 equals方法,其行为与 ==运算符

一致,​​比较的是两个对象的内存地址是否相同​​。这意味着,即使两个对象的所有属性值都完全相

同,只要它们是分别通过 new关键字创建的不同实例,默认的 equals方法就会判定它们不相等。

这在大多数业务场景下是不符合逻辑的。例如,我们认为两个 Person对象,如果他们的身份证号

(ID)相同,就应该代表同一个人,是相等的,而不关心它们是否存储在内存的同一位置。

📦 集合框架的正确运作

Java 集合框架(Collection Framework)严重依赖 equals方法来管理元素。如果 equals方法行为

不正确,会导致集合表现出乎意料。

  • List的查找​​:当使用 List.contains(obj)List.indexOf(obj)或 List.remove(obj)等方法时,List会遍历内部元素,并调用 equals方法来判断是否找到目标对象。

  • Set的唯一性​​:Set集合的核心特性是不允许包含重复元素。当向 Set(尤其是 HashSet)中添加一个新元素时,它正是通过 equals方法来校验新元素是否与已有元素重复。

  • Map的键​​:在 Map中,equals方法用于比较键(Key)是否相等。当使用 map.put(key, value)时,如果传入的键与已有键通过 equals比较为 true,则会覆盖原有的值。

📝 重写 equals 必须遵循的规范

重写 equals方法不是随意的,必须遵循一系列严格的通用约定,以确保其行为的可靠性和可预测

性:

原则

说明

​自反性​

x.equals(x)必须返回 true

​对称性​

如果 x.equals(y)为 true,那么 y.equals(x)也必须为 true

​传递性​

如果 x.equals(y)为 true,且 y.equals(z)为 true,那么 x.equals(z)也必须为 true

​一致性​

在对象未被修改的情况下,多次调用 x.equals(y)应该始终返回相同的结果。

​非空性​

对任何非空的引用 xx.equals(null)必须返回 false

🔗 必须同步重写 hashCode 方法

这是一个​​至关重要且必须遵守​​的规则:​​如果你重写了 equals方法,就必须同时重写 hashCode方法​​。

这是因为在 Java 的对象契约中规定:​​如果两个对象根据 equals(Object)方法判断是相等的,那

么调用这两个对象的 hashCode方法必须产生相同的整数结果​​。

这条规则主要是为了保障所有基于哈希表(Hash Table)的集合类

(如 HashMapHashSetHashtable)能够正常工作。这些集合在存储和查找对象时,会先通

过 hashCode来快速定位到一个大致区域(桶),然后再使用 equals方法在该区域内进行精确匹

配。如果两个相等的对象却有不同的哈希码,它们很可能被放入哈希表的不同位置,导致你永远无

法通过键找到对应的值,严重破坏集合的功能。

💡 如何正确重写

一个典型且可靠的重写模式如下(以比较 id和 name字段的 Person类为例):

@Override
public boolean equals(Object o) {// 1. 检查是否為同一個對象if (this == o) return true;// 2. 检查是否為null或類型不同if (o == null || getClass() != o.getClass()) return false;// 3. 類型轉換Person person = (Person) o;// 4. 比較關鍵字段return id == person.id && Objects.equals(name, person.name);
}@Override
public int hashCode() {// 使用 java.util.Objects.hash() 方法,传入equals方法中使用的所有字段return Objects.hash(id, name);
}

最佳实践建议​​:

  • 使用 IDE(如 IntelliJ IDEA 或 Eclipse)的代码生成功能,它们可以自动生成符合规范的 equals和 hashCode方法。

  • 确保在 hashCode方法中使用的字段与在 equals方法中用于比较的字段完全一致。

总而言之,正确重写 equals方法,并同步重写 hashCode方法,是确保你的对象在 Java 世界里

(尤其是在集合框架中)能够被正确比较、存储和检索的基石。


能否举例说明如果不重写hashCode方法会导致哪些具体问题?

不重写 hashCode方法确实会导致一些难以察觉的逻辑错误,尤其是在使用 HashMapHashSet等基

于哈希表的集合时。下面通过几个具体场景来说明。

⚠️ HashMap 中无法正确查找对象

假设有一个 Person类,只重写了 equals方法(基于 name和 age判断相等),但未重写 hashCode

法:

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 25);
map.put(p1, "Value1");Person p2 = new Person("Alice", 25); // 逻辑上与 p1 相等
String value = map.get(p2); // 返回 null,而非预期的 "Value1"

问题根源​​:HashMap在查找键值对时,​​首先通过 key.hashCode()计算哈希值来确定对象在哈希表中

的存储位置(桶)​​。由于 p1和 p2未重写 hashCode,它们使用 Object类的默认实现(基于对象内存

地址),导致即使逻辑相等,哈希值也不同。HashMap会直接去不同的桶中查找,根本不会调

用 equals方法进行深度比较,因此返回 null

🔄 HashSet 中重复存储相等对象

HashSet底层也依赖于 HashMap,它利用键的唯一性来保证元素的唯一。如果向 HashSet添加两个逻

辑相等但 hashCode不同的对象:

Set<Person> set = new HashSet<>();
Person p1 = new Person("Bob", 30);
Person p2 = new Person("Bob", 30); // 逻辑上应与 p1 相同set.add(p1);
set.add(p2);
System.out.println(set.size()); // 输出 2,而非预期的 1

问题根源​​:HashSet在添加元素时,同样先根据 hashCode判断位置。由于 p1和 p2的哈希值不同,

它们会被放入不同的存储桶中,HashSet便认为它们是两个不同的对象,从而违反了集合的​​唯一性​

原则。

📊 核心问题对比

下表清晰地总结了不重写 hashCode方法在哈希集合中引发的主要问题:

问题场景

预期结果

实际结果

原因分析

​HashMap 查找​

通过相等的键能获取到对应的值

返回 null,查找失败

哈希值不同,导致直接定位到错误的存储桶,未触发 equals比较

​HashSet 存储​

相等的对象在集合中只存在一个

重复存储了逻辑相等的对象

哈希值不同,对象被分配到不同桶,被视为独立对象

✅ 正确的做法

要解决上述问题,只需在重写 equals的类中,同时正确重写 hashCode方法,确保逻辑相等的对象

具有相同的哈希码。一个简单可靠的方法是使用 Objects.hash()

@Override
public int hashCode() {// 传入所有在equals方法中用于比较的字段return Objects.hash(name, age);
}

结论​​:重写 equals时必须重写 hashCode,这并非语法强制,而是为了维护 Java 中对象相等的契

约,确保基于哈希的集合类能够正确、高效地工作。忽略这一点,程序可能不会抛出明显的异常,

但会产生隐蔽且严重的逻辑错误。

http://www.dtcms.com/a/515245.html

相关文章:

  • 微服务即时通讯系统——整体架构和组件(1)
  • WPF入门
  • WPF布局控件(界面骨架核心)
  • WPF 常用样式属性及示例笔记
  • 【WPF】自定义颜色拾取器
  • MahApps.Metro WPF 开发使用过程中遇到的问题 - 未能加载文件或程序集“Microsoft.Xaml.Behaviors,
  • 【普中Hi3861开发攻略--基于鸿蒙OS】-- 第 26 章 WIFI实验-AP 建立网络
  • ARM架构深度解析:ARMv7、ARMv8、ARMv9的技术演进、芯片实现与未来展望
  • 线下剧本杀预约小程序核心功能玩法解析:轻量化载体重构娱乐消费生态
  • 【矩阵分析与应用】【第8章 特征分析】【8.3 凯莱-哈密顿定理求解矩阵高次幂详解】
  • 合肥制作企业网站免费收录网站推广
  • 阿里云安装docker-compose
  • Centos 7 :VMware Tools 启动脚本未能在虚拟机中成功运行
  • 基于vue的停车场管理系统
  • 短剧小程序系统开发:开启影视娱乐新纪元
  • 系统架构设计师备考第49天——数字孪生体云计算大数据技术
  • 阿里云渠道商:阿里云哪些功能很必要?
  • 鱼馆网站的前期策划网站审核备案 几天
  • 建设银行申请信用卡网站股权融资
  • 即刻创作:用 Trickle + GLM-4.6 API 构建互动小说创作工具
  • 标定系数为什么会存储在相机模组里面,在标定的时候,算法是在割草机的X3板上运行的啊?
  • windows系统安装wls/Ubuntu子系统教程
  • 【Linux】gcc/g++编辑器 初识动静态库 程序翻译过程
  • AI服务器工作之系统下查看硬件(ubuntu为例)
  • 基于python智慧医疗问答系统 知识图谱 Flask框架 数据可视化 neo4j图数据库 计算机专业 优秀项目(源码)✅
  • P3957 [NOIP 2017 普及组] 跳房子
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段-二阶段(6):文法運用
  • 【数据结构入坑指南】--《层序分明:堆的实现、排序与TOP-K问题一站式攻克(源码实战)》
  • 做网站的公司怎么推销湘潭做网站价格 q磐石网络
  • 用Python Streamlit sqlite3 写一个简单博客