【问题标题】:How to guarantee the behavior of Set.removeAll with different type of sets?如何保证不同类型集合的 Set.removeAll 行为?
【发布时间】:2019-04-13 06:55:40
【问题描述】:

我对 HashSet 和 TreeSet 操作有疑问。 这是一个解释我的问题的简单 JUnit 4 测试:

import java.util.HashSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.Assert;
import org.junit.Test;

public class TreeSetTest<T> {

    @Test
    public void test() {
        final HashSet<Object> set1 = new HashSet<>();
        final TreeSet<Object> set2 = new TreeSet<>((a, b) -> a.toString().compareTo(b.toString()));
        Assert.assertEquals(0, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set1.add(new AtomicReference<>("A"));
        set1.add(new AtomicReference<>("B"));
        set1.add(new AtomicReference<>("C"));
        Assert.assertEquals(3, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set2.add(new AtomicReference<>("A"));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
        Assert.assertEquals(1, set2.size());
        set2.add(new AtomicReference<>("B"));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
        Assert.assertEquals(2, set2.size());
        set2.add(new AtomicReference<>("C"));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // Error Everything has been removed and size is now 0
        Assert.assertEquals(3, set2.size());
    }

}

当从set1 中删除set2 的所有元素时,我希望使用set1 的相等比较器,只要set2 的大小小于set1 的大小就可以了但如果set2 的大小大于或等于set1 的大小,则从set2 进行比较。

这对我来说非常糟糕,因为它使程序变得不可预测。

我认为它可以被视为 java 实现中的一个错误,但我担心的是: 如何在不重写所有内容的情况下保证预期的行为?

在@FedericoPeraltaSchaffner 评论后编辑 1:

AtomicReference 只是为了提供一个简单的例子。事实上,我正在使用一个库中的最终类,所以我不能轻易改进它。 但即使考虑正确实现hashCodeequals 的有效类,我的问题仍然存在。现在考虑一下:

package fr.ncenerar.so;

import java.util.HashSet;
import java.util.TreeSet;

import org.junit.Assert;
import org.junit.Test;

public class TreeSetTest<T> {

    public static class MyObj {
        private final int value1;
        private final int value2;

        public MyObj(final int v1, final int v2) {
            super();
            this.value1 = v1;
            this.value2 = v2;
        }

        public int getValue1() {
            return this.value1;
        }

        public int getValue2() {
            return this.value2;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + this.value1;
            result = prime * result + this.value2;
            return result;
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (this.getClass() != obj.getClass()) {
                return false;
            }
            final MyObj other = (MyObj) obj;
            if (this.value1 != other.value1) {
                return false;
            }
            if (this.value2 != other.value2) {
                return false;
            }
            return true;
        }
    }

    @Test
    public void test() {
        final HashSet<MyObj> set1 = new HashSet<>();
        final TreeSet<MyObj> set2 = new TreeSet<>((a, b) -> a.getValue1() - b.getValue1());
        Assert.assertEquals(0, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set1.add(new MyObj(0, 0));
        set1.add(new MyObj(1, 1));
        set1.add(new MyObj(2, 2));
        Assert.assertEquals(3, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK
        Assert.assertEquals(0, set2.size()); // OK
        set2.add(new MyObj(0, 1));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
        Assert.assertEquals(1, set2.size());
        set2.add(new MyObj(1, 2));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // OK Nothing has been removed
        Assert.assertEquals(2, set2.size());
        set2.add(new MyObj(2, 3));
        set1.removeAll(set2);
        Assert.assertEquals(3, set1.size()); // Error Everything has been removed
        Assert.assertEquals(3, set2.size());
    }

}

问题依然存在,MyObj 的实现是正确的。问题来自我从两个不同方面使用对象的事实。在一组中,我想根据它们的相等性保留每个对象的一个​​实例(如对象的equals 方法),在另一组中,我想要第一组的一个子集,其中对于每个value1,我只想保留第一个插入的元素。

使用TreeSet 似乎是有效的。

编辑 2:

我的错,我错过了TreeSet 文档的那一部分:

请注意,由集合维护的顺序(无论是否 提供了显式比较器)必须与equals一致,如果它 就是正确实现Set接口。 (见可比或 用于精确定义一致不等于的比较器。)这是 所以因为 Set 接口是根据等式定义的 操作,但 TreeSet 实例执行所有元素比较 使用它的 compareTo(或比较)方法,所以两个元素是 这种方法认为相等,从集合的角度来看,相等。 集合的行为是明确定义的,即使它的排序是 与equals不一致;它只是不遵守总合同 设置界面。

如果我理解正确,我可以使用 TreeSet 来达到我的目的,但不能指望它的行为像我想要的那样。

感谢大家的帮助。

【问题讨论】:

  • @FedericoPeraltaSchaffner 感谢您的评论。我更新了我的问题以添加一些信息。
  • 你不能让传递给树集的比较器与equals一致吗? IE。仅当元素根据 equals 方法相等时才返回 0
  • @FedericoPeraltaSchaffner 不幸的是,没有。事实上,我使用的库为实际上相等的对象(从我的观点)创建了新实例(不同于 equals 的观点)。

标签: java collections set removeall


【解决方案1】:
 AtomicReference a = new AtomicReference<>("A");
 AtomicReference a2 = new AtomicReference<>("A");
 System.out.println(a==a2);

你的答案在里面。

如果您使用自定义类而不是 Object 并且您覆盖 equals 方法,它将按预期工作。

让这个工作

class AtomicString{
private AtomicReference<String> s;

public AtomicString(String s) {
    this.s = new AtomicReference<>(s);
}

@Override public boolean equals(Object o) {
    if (this == o)
        return true;
    if (o == null || getClass() != o.getClass())
        return false;
    AtomicString that = (AtomicString) o;
    return this.s.get().equals(that.getS().get());
}

public AtomicReference<String> getS() {
    return s;
}

@Override public int hashCode() {
    return Objects.hash(s.get());
}

【讨论】:

  • 问题是我依赖于库中的最终类并且无法轻松适应它。但我更新了我的问题以添加一些信息。无论如何,谢谢你的回答。
【解决方案2】:

问题确实是不一致的“平等”逻辑。 TreeSetHashSet 都继承 AbstractSet#removeAll,它迭代较小的集合,因此使用该集合的对象比较。事实证明,可以使用TreeSet 覆盖“平等”逻辑。

您可以通过选择两个Set 实现之一来避免这个问题。如果您选择TreeSet,那么您也必须使用相同的比较器。

在这种情况下,您不能真正使用HashSet,因为AtomicReference 没有适合您的equals/hashCode 的实现。所以你唯一实际的选择是使用TreeSet:

Comparator<Object> comparator = (a, b) -> a.toString().compareTo(b.toString());
final Set<Object> set1 = new TreeSet<>(comparator);
final Set<Object> set2 = new TreeSet<>(comparator);

这将破坏您当前的测试,但是因为现在正在按应有的方式删除元素(根据您的比较器的逻辑)。

【讨论】:

  • 我更新了我的问题以添加一些信息并尽可能准确。你是对的,但不幸的是,我必须混合不同类型的sets
【解决方案3】:

在正确搜索和阅读有关TreeSet的文档后,结果是:

请注意,由集合维护的顺序(无论是否 提供了显式比较器)必须与equals一致,如果它 就是正确实现Set接口。 (见可比物 比较器,用于精确定义与等于一致。)这是 所以因为 Set 接口是根据等式定义的 操作,但 TreeSet 实例执行所有元素比较 使用它的 compareTo(或比较)方法,所以两个元素是 这种方法认为相等,从集合的角度来看,相等。 集合的行为是明确定义的,即使它的排序是 与equals不一致;它只是不遵守总合同 设置界面。

这意味着示例中使用的TreeSet 不能用作Set。因此,最简单的解决方案是为removeAll操作创建一个HashSet,方法是替换每个:

set1.removeAll(set2);

通过

set1.removeAll(new HashSet<>(set2));

从性能的角度来看,这可能不是最佳解决方案,但却是一个可行的解决方案。

谢谢大家!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多