如何保证Set.removeAll具有不同类型的集合的行为?

时间:2018-11-09 17:00:26

标签: java collections set removeall

我对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());
    }

}

set2删除set1的所有元素时,我希望使用set1的相等比较器,只要set2具有大小小于set1的一个,但是如果set2的大小大于或等于set1的大小,则从set2进行比较。

这对我来说很不好,因为它使程序无法预测。

我认为可以将其视为Java实现中的错误,但我担心的是: 如何在不重写的情况下保证预期的行为?

在@FedericoPeraltaSchaffner评论后编辑1:

AtomicReference仅用于提供一个简单示例。实际上,我使用的是库中的final类,因此我无法轻松对其进行改进。 但是,即使考虑有效实现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方法),在另一个集合中,我想要第一个集合的子集,其中每个{{1 }},我只想保留第一个插入的元素。

使用value1似乎有效。

编辑2:

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

  

请注意,集合(无论是否   提供显式比较器)是否必须等于equals   是正确实现Set接口。 (请参阅可比或   比较器,用于精确定义等式。)这是   之所以如此,是因为Set接口是用等式定义的   操作,但TreeSet实例执行所有元素比较   使用其compareTo(或compare)方法,因此两个元素   从集合的角度来看,被此方法视为相等的对象相等。   集合的行为是明确定义的,即使其顺序是   与平等不一致它只是不遵守   设置界面。

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

谢谢大家的帮助。

3 个答案:

答案 0 :(得分:2)

 AtomicReference a = new AtomicReference<>("A");
 AtomicReference a2 = new AtomicReference<>("A");
 System.out.println(a==a2);

您的答案就在其中。

如果您使用自定义类而不是Object,并且您重写Override 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());
}

答案 1 :(得分:1)

问题实际上是不一致的“平等”逻辑。 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);

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

答案 2 :(得分:0)

正确搜索并阅读有关TreeSet的文档后,事实证明:

  

请注意,集合(无论是否   提供显式比较器)是否必须等于equals   是正确实现Set接口。 (请参阅比较器   比较器,用于精确定义与equals的一致性。)   因此,因为Set接口是根据等值定义的   操作,但TreeSet实例执行所有元素比较   使用其compareTo(或compare)方法,因此两个元素   从集合的角度来看,被此方法视为相等的对象相等。   集合的行为是明确定义的,即使其顺序是   与平等不一致它只是不遵守   设置界面。

这意味着示例中使用的TreeSet不能用作Set。因此,最简单的解决方案是通过替换以下各项来为HashSet操作创建removeAll

set1.removeAll(set2);

作者

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

从性能的角度来看,也许不是最好的解决方案,但是可行的。

谢谢大家!