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
相关产品推荐
相关产品推荐

