探秘Java HashSet remove失效:那些让人“抓耳挠腮”的事儿与神奇解法

35 阅读7分钟

探秘Java HashSet remove失效:那些让人“抓耳挠腮”的事儿与神奇解法

一、开场“吐槽”

嘿,各位Java小伙伴们!咱在使用HashSetremove方法时,是不是偶尔会碰到一些让人摸不着头脑的情况,感觉这方法像是在跟咱“捉迷藏”,死活不按套路出牌,移除操作直接失效,是不是超郁闷?今天咱就来好好唠唠这些让人“抓狂”的实际案例,顺便找出破解之道!

二、那些“坑人”的实际案例

案例一:自定义类的“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类一开始挺“乖巧”,老老实实地重写了hashCodeequals方法。可咱把book1放进HashSet这个“书架”后,又调皮地修改了book1title属性,这一改可不得了,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,就好像给这个类穿上了不同的“隐形衣”。虽然instance1instance2在逻辑上看是一样的(hashCodeequals方法判断相等),但在HashSet这个“大舞台”上,因为它们是由不同类加载器带来的,就被当成了不同的角色。当你想移除instance2时,HashSet就一脸懵:“这是谁呀?我不认识,没法移除!”移除操作又双叒失败咯!

三、神奇的解决方案大揭秘

方案一:正确重写HashCode与Equals,打造“通关密码”

对于自定义类,正确重写hashCodeequals方法可是关键中的关键!这俩方法就像是打开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那样依赖对象的hashCodeequals方法,而是直接靠对象的内存地址(对象标识)来确定键值对的唯一性。这在解决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类有没有正确重写hashCodeequals方法,都能根据对象的实际身份进行移除操作。

案例二和案例三,IdentityHashMap同样能“大显身手”。因为它不依赖那些容易出问题的hashCodeequals方法,直接基于对象内存地址操作,完美避开各种导致移除失效的“坑”,让移除操作稳稳当当!

四、欢乐总结

好啦,小伙伴们!咱们今天一起“深挖”了Java HashSet remove失效的那些让人头疼的问题,还找到了各种超棒的解决方案。以后再遇到这些问题,就别慌啦,按照这些方法对症下药,保证HashSetremove方法乖乖听话!希望大家在Java的世界里玩得开心,代码写得顺风顺水,再也不被这些小问题困扰咯!要是还有啥好玩的经验或者问题,欢迎在评论区一起分享交流呀! [我的公众号,欢迎关注,分享AI,技术,生活] 请添加图片描述