You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java实现不修改元素类hashCode/equals的自定义HashSet

自定义无需修改元素类equals/hashCode的HashSet实现

需求说明

  • 实现支持自定义去重规则的Set集合,可基于元素的一个或多个属性判断元素是否重复
  • 不需要修改存储元素类自身的hashCode()/equals()方法,避免影响其他业务场景下的对象相等判断逻辑
  • 示例场景:对如下User实体,需要实现仅按name属性去重的Set,同名不同邮箱/年龄的User对象判定为重复,不允许重复存入,其他业务场景仍保留User对象原生的地址相等判断逻辑
@FieldDefaults(level = AccessLevel.PRIVATE)
@Getter @Setter
public class User{
    String name;
    String email;
    String age;
}

现有实现的问题

你提交的CustomizableHashSet实现可通过简单场景测试,但存在多处严重缺陷,不建议生产环境使用:

  • equals逻辑违反Java对象契约:内部ClassWrapper的equals方法仅通过hashCode值相等判定对象相等,而Java中hashCode相等是对象相等的必要不充分条件,哈希碰撞时会直接将不相等的对象判定为重复,导致元素丢失。同时实现仅支持自定义hashCode逻辑,没有传入自定义相等判断的入口,根本无法实现“按属性值相等判定重复”的核心需求。
  • 性能完全退化:contains/remove/removeAll/containsAll/retainAll/removeIf等核心方法均通过全量遍历底层存储、重建集合实现,时间复杂度从HashSet原生的O(1)退化为O(n),数据量较大时性能完全不可用,额外内存开销也极高。
  • 构造函数逻辑bug:传入集合与自定义hashCode的构造方法中,先将元素包装存入底层HashSet,之后才给customHashCode赋值,存入元素时使用的仍是Object原生hashCode,传入的自定义规则完全不生效。
  • 不符合Set接口规范:返回的迭代器不支持遍历过程中调用remove()删除元素,contains/remove等方法未做类型校验,容易抛出类型转换异常。

正确实现方案

核心思路是基于JDK原生HashSet做封装,通过内部包装类持有自定义去重规则,包装类的hashCode/equals严格按照自定义规则实现,完全不需要修改原元素类的代码。推荐直接继承AbstractSet复用接口默认实现,避免重复编写大量样板代码:

import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;

public class CustomizableHashSet<T> extends AbstractSet<T> {
    private final HashSet<Wrapper<T>> storage;
    private final Function<? super T, ?> keyGenerator;
    private final BiPredicate<? super T, ? super T> equalsLogic;

    /**
     * 最简构造方法:传入key提取器,基于key的原生equals/hashCode实现去重
     * 例如按name去重传入 User::getName 即可
     * 多字段去重可传入组合key,例如 u -> List.of(u.getName(), u.getAge())
     */
    public CustomizableHashSet(Function<? super T, ?> keyGenerator) {
        this.storage = new HashSet<>();
        this.keyGenerator = keyGenerator;
        this.equalsLogic = (a, b) -> Objects.equals(keyGenerator.apply(a), keyGenerator.apply(b));
    }

    /**
     * 全参构造方法:支持完全自定义hashCode计算和equals判断逻辑
     */
    public CustomizableHashSet(Function<? super T, Integer> customHash, BiPredicate<? super T, ? super T> customEquals) {
        this.storage = new HashSet<>();
        this.keyGenerator = customHash;
        this.equalsLogic = customEquals;
    }

    // 支持初始化容量、负载因子的构造方法可按需补充
    public CustomizableHashSet(int initialCapacity, float loadFactor, Function<? super T, ?> keyGenerator) {
        this.storage = new HashSet<>(initialCapacity, loadFactor);
        this.keyGenerator = keyGenerator;
        this.equalsLogic = (a, b) -> Objects.equals(keyGenerator.apply(a), keyGenerator.apply(b));
    }

    @Override
    public boolean add(T t) {
        return storage.add(new Wrapper<>(t));
    }

    @Override
    public boolean contains(Object o) {
        if (o == null) return storage.contains(new Wrapper<>(null));
        if (storage.isEmpty()) return false;
        Class<?> elementType = storage.iterator().next().value.getClass();
        if (!elementType.isInstance(o)) return false;
        @SuppressWarnings("unchecked")
        T casted = (T) o;
        return storage.contains(new Wrapper<>(casted));
    }

    @Override
    public boolean remove(Object o) {
        if (o == null) return storage.remove(new Wrapper<>(null));
        if (storage.isEmpty()) return false;
        Class<?> elementType = storage.iterator().next().value.getClass();
        if (!elementType.isInstance(o)) return false;
        @SuppressWarnings("unchecked")
        T casted = (T) o;
        return storage.remove(new Wrapper<>(casted));
    }

    @Override
    public Iterator<T> iterator() {
        Iterator<Wrapper<T>> innerIterator = storage.iterator();
        return new Iterator<>() {
            @Override
            public boolean hasNext() {
                return innerIterator.hasNext();
            }

            @Override
            public T next() {
                return innerIterator.next().value;
            }

            @Override
            public void remove() {
                innerIterator.remove();
            }
        };
    }

    @Override
    public int size() {
        return storage.size();
    }

    /**
     * 内部包装类,所有hashCode和equals逻辑委托给自定义规则
     */
    private class Wrapper<T> {
        private final T value;

        Wrapper(T value) {
            this.value = value;
        }

        @Override
        public int hashCode() {
            if (value == null) return 0;
            Object key = keyGenerator.apply(value);
            return key == null ? 0 : key.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            @SuppressWarnings("unchecked")
            Wrapper<T> other = (Wrapper<T>) obj;
            return equalsLogic.test(this.value, other.value);
        }
    }
}

实现优势

  • 完全保留原生HashSet的O(1)增删查性能,没有全量遍历、重建集合的额外开销
  • 严格遵守Java equals/hashCode契约,不会出现哈希碰撞导致的元素丢失问题
  • 继承AbstractSet后自动获得addAll/removeAll/retainAll/stream等所有Set接口方法的默认实现,不需要手动编写冗余代码
  • 迭代器支持原生remove操作,所有方法符合Set接口规范
  • 支持单属性、多属性、自定义逻辑等任意去重规则,使用灵活,且完全不需要修改存储元素类的原生方法

内容的提问来源于stack exchange,提问作者Jakub Znamenáček

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 04:33:27