Как гарантировать поведение Set.removeAll с различными типами наборов? - PullRequest
0 голосов
/ 09 ноября 2018

У меня проблема с манипуляциями 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, но моя проблема: Как я могу гарантировать ожидаемое поведение, не переписывая все?

Изменить 1 после комментария @FedericoPeraltaSchaffner:

AtomicReference был только для того, чтобы предоставить простой пример. На самом деле, я использую класс, который является окончательным из библиотеки, поэтому я не могу его легко улучшить. Но даже если учесть, что правильный класс реализует hashCode и equals, моя проблема остается. Рассмотрим это сейчас:

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 документации:

Обратите внимание, что порядок поддерживается набором (независимо от того, явный компаратор предоставляется) должен соответствовать равным, если он это правильно реализовать интерфейс Set. (См. Сравнительный или Компаратор для точного определения последовательных выражений.) Это так, потому что интерфейс Set определяется через равные операция, но экземпляр TreeSet выполняет все сравнения элементов используя его метод CompareTo (или сравнение), поэтому два элемента, которые считаются равными по этому методу, с точки зрения множества равны. Поведение набора четко определено, даже если его порядок несовместимый с равными; он просто не соблюдает общий договор Настроить интерфейс.

Если я правильно понимаю, я могу использовать TreeSet для своих целей, но не могу ожидать, что он будет вести себя так, как я хочу.

Спасибо всем за помощь.

Ответы [ 3 ]

0 голосов
/ 09 ноября 2018

Проблема действительно в противоречивой логике "равенства". И TreeSet, и HashSet наследуют 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);

Это нарушит ваши текущие тесты, но поскольку элементы теперь удаляются, как они должны (согласно логике вашего компаратора).

0 голосов
/ 12 ноября 2018

После поиска и правильного прочтения документации о TreeSet получается, что:

Обратите внимание, что порядок поддерживается набором (независимо от того, явный компаратор предоставляется) должен соответствовать равным, если он это правильно реализовать интерфейс Set. (См. Comparableor Компаратор для точного определения в соответствии с равными.) Это так, потому что интерфейс Set определяется с точки зрения равных операция, но экземпляр TreeSet выполняет все сравнения элементов используя его метод CompareTo (или сравнение), поэтому два элемента, которые считаются равными по этому методу, с точки зрения множества равны. Поведение набора четко определено, даже если его порядок несовместимый с равными; он просто не соблюдает общий договор Настроить интерфейс.

Это означает, что TreeSet, использованный в примере, не может использоваться как Set. Поэтому самое простое решение - создать HashSet для операций removeAll, заменив каждое:

set1.removeAll(set2);

от

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

Возможно, не лучшее решение с точки зрения производительности, но рабочее.

Спасибо всем!

0 голосов
/ 09 ноября 2018
 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());
}
...