探秘Java HashSet remove失效:那些让人“抓耳挠腮”的事儿与神奇解法
一、开场“吐槽”
嘿,各位Java小伙伴们!咱在使用HashSet的remove方法时,是不是偶尔会碰到一些让人摸不着头脑的情况,感觉这方法像是在跟咱“捉迷藏”,死活不按套路出牌,移除操作直接失效,是不是超郁闷?今天咱就来好好唠唠这些让人“抓狂”的实际案例,顺便找出破解之道!
二、那些“坑人”的实际案例
案例一:自定义类的“HashCode与Equals”迷局
import java.util.HashSet;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 只重写了equals方法,没有重写hashCode方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
}
public class HashSetRemoveFailureExample1 {
public static void main(String[] args) {
HashSet<Person> people = new HashSet<>();
Person person1 = new Person("Alice", 25);
people.add(person1);
// 创建一个与person1内容相同的新对象
Person person2 = new Person("Alice", 25);
// 预期移除person2,但由于hashCode未重写,移除失败
boolean removed = people.remove(person2);
System.out.println("元素是否移除成功: " + removed);
}
}
瞧这代码,咱定义了个Person类,满心欢喜地重写了equals方法,想着这下万事大吉啦。结果呢,却忘了重写hashCode方法。这就好比你给家门换了把新锁(equals方法),却没告诉大家新的开门密码(hashCode方法)。当你把person1放进HashSet这个“房子”里,再想通过person2(虽然内容一样,但hashCode不同)把它移除时,HashSet就懵圈啦,根本找不到要移除的对象,移除操作只能“凉凉”咯!
案例二:添加后修改属性,引发的“HashCode混乱风暴”
import java.util.HashSet;
class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public int hashCode() {
return title.hashCode() + author.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return title.equals(book.title) && author.equals(book.author);
}
}
public class HashSetRemoveFailureExample2 {
public static void main(String[] args) {
HashSet<Book> books = new HashSet<>();
Book book1 = new Book("Java核心技术", "Cay S. Horstmann");
books.add(book1);
// 修改book1的title属性
book1.title = "Effective Java";
// 预期移除book1,但由于hashCode变化,移除失败
boolean removed = books.remove(book1);
System.out.println("元素是否移除成功: " + removed);
}
}
这个案例也挺逗的。Book类一开始挺“乖巧”,老老实实地重写了hashCode和equals方法。可咱把book1放进HashSet这个“书架”后,又调皮地修改了book1的title属性,这一改可不得了,hashCode也跟着变了。这就好比你把一本书放在书架的某个位置(根据原来的hashCode),结果书的特征变了(hashCode变了),当你再按原来的位置去找它(移除操作),书架(HashSet)就找不到啦,移除自然就失败咯!
案例三:不同类加载器引发的“对象身份谜团”
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
class MyClass {
private int value;
public MyClass(int value) {
this.value = value;
}
@Override
public int hashCode() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass myClass = (MyClass) o;
return value == myClass.value;
}
}
public class HashSetRemoveFailureExample3 {
public static void main(String[] args) throws Exception {
URL url = new URL("file:///tmp/");
URLClassLoader classLoader1 = new URLClassLoader(new URL[]{url});
URLClassLoader classLoader2 = new URLClassLoader(new URL[]{url});
Class<?> myClass1 = classLoader1.loadClass("MyClass");
Class<?> myClass2 = classLoader2.loadClass("MyClass");
Object instance1 = myClass1.getConstructor(int.class).newInstance(10);
Object instance2 = myClass2.getConstructor(int.class).newInstance(10);
HashSet<Object> set = new HashSet<>();
set.add(instance1);
// 预期移除instance2,但由于类加载器不同,移除失败
boolean removed = set.remove(instance2);
System.out.println("元素是否移除成功: " + removed);
}
}
这个案例简直像在玩“神秘游戏”。通过两个不同的类加载器加载同一个MyClass,就好像给这个类穿上了不同的“隐形衣”。虽然instance1和instance2在逻辑上看是一样的(hashCode和equals方法判断相等),但在HashSet这个“大舞台”上,因为它们是由不同类加载器带来的,就被当成了不同的角色。当你想移除instance2时,HashSet就一脸懵:“这是谁呀?我不认识,没法移除!”移除操作又双叒失败咯!
三、神奇的解决方案大揭秘
方案一:正确重写HashCode与Equals,打造“通关密码”
对于自定义类,正确重写hashCode和equals方法可是关键中的关键!这俩方法就像是打开HashSet正确操作大门的“通关密码”。遵循下面原则,包你搞定:
- 如果两个对象通过
equals方法比较返回true,那么它们的hashCode值必须相同。这就好比两把钥匙能开同一扇门(对象相等),那这两把钥匙的“特征码”(hashCode)得一样。 - 如果两个对象的
hashCode值相同,它们不一定相等(这是哈希冲突的情况)。就像有时候不同的钥匙可能有相同的“特征码”,但开的门不一样。
比如Person类,这样重写就靠谱啦:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
}
这下,HashSet就能准确识别对象,移除操作也就顺顺利利啦!
方案二:属性修改“先移后改再加”,避开“HashCode陷阱”
要是需要修改元素属性,而且这些属性还会影响hashCode值,那咱就按这个套路来:先从HashSet中把元素“揪”出来,修改属性后再重新“放”回去。就像这样:
HashSet<Book> books = new HashSet<>();
Book book1 = new Book("Java核心技术", "Cay S. Horstmann");
books.add(book1);
// 先移除
books.remove(book1);
// 修改属性
book1.title = "Effective Java";
// 重新添加
books.add(book1);
这样就能巧妙避开因为属性修改导致hashCode变化,从而移除失败的“陷阱”啦!
方案三:统一类加载器,打破“对象身份隔阂”
在类加载器捣乱的场景里,尽量用同一个类加载器加载类,这就好比给所有对象都安排在同一个“户口本”下,避免因为“户口”不同导致身份识别混乱。要是实在没办法得用多个类加载器,那就想点别的招,比如自定义个比较器,在比较对象时直接忽略类加载器的差异,让HashSet能正确识别对象。
方案四:IdentityHashMap闪亮登场,专治各种“不服”
IdentityHashMap可是个神奇的“救星”!它不按常规套路出牌,不像普通的HashMap那样依赖对象的hashCode和equals方法,而是直接靠对象的内存地址(对象标识)来确定键值对的唯一性。这在解决HashSet remove失效问题时,简直是“神器”!
对于案例一,用IdentityHashMap模拟类似HashSet的结构,就像这样:
import java.util.IdentityHashMap;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class IdentityHashMapSolution {
public static void main(String[] args) {
IdentityHashMap<Person, Boolean> identitySet = new IdentityHashMap<>();
Person person1 = new Person("Alice", 25);
identitySet.put(person1, true);
Person person2 = new Person("Alice", 25);
identitySet.remove(person2);
// 判断是否移除成功
boolean isRemoved =!identitySet.containsKey(person2);
System.out.println("元素是否移除成功: " + isRemoved);
}
}
这里IdentityHashMap就像一个只认“脸”(内存地址)不认其他的“严格门卫”,不管Person类有没有正确重写hashCode和equals方法,都能根据对象的实际身份进行移除操作。
案例二和案例三,IdentityHashMap同样能“大显身手”。因为它不依赖那些容易出问题的hashCode和equals方法,直接基于对象内存地址操作,完美避开各种导致移除失效的“坑”,让移除操作稳稳当当!
四、欢乐总结
好啦,小伙伴们!咱们今天一起“深挖”了Java HashSet remove失效的那些让人头疼的问题,还找到了各种超棒的解决方案。以后再遇到这些问题,就别慌啦,按照这些方法对症下药,保证HashSet的remove方法乖乖听话!希望大家在Java的世界里玩得开心,代码写得顺风顺水,再也不被这些小问题困扰咯!要是还有啥好玩的经验或者问题,欢迎在评论区一起分享交流呀!
[我的公众号,欢迎关注,分享AI,技术,生活]