2016-09-24 9 views
2

При добавлении объектов в java.util.TreeSet вы надеетесь, что два равных объекта будут существовать только один раз, когда оба будут добавлены, и следующий тест проходит, как и ожидалось:Почему я могу добавить два объекта, равных друг другу, в TreeSet

@Test 
void canAddValueToTreeSetTwice_andSetWillContainOneValue() { 
    SortedSet<String> sortedSet = new TreeSet<>(Comparator.naturalOrder()); 

    // String created in a silly way to hopefully create two equal Strings that aren't interned 
    String firstInstance = new String(new char[] {'H', 'e', 'l', 'l', 'o'}); 
    String secondInstance = new String(new char[] {'H', 'e', 'l', 'l', 'o'}); 

    assertThat(firstInstance).isEqualTo(secondInstance); 
    assertThat(sortedSet.add(firstInstance)).isTrue(); 
    assertThat(sortedSet.add(secondInstance)).isFalse(); 
    assertThat(sortedSet.size()).isEqualTo(1); 
} 

Оберните эти строки в класс-оболочку, где equals() и hashCode() основаны исключительно на обернутой класса, хотя и тест не пройден:

@Test 
void canAddWrappedValueToTreeSetTwice_andSetWillContainTwoValues() { 
    SortedSet<WrappedValue> sortedSet = new TreeSet<>(Comparator.comparing(WrappedValue::getValue).thenComparing(WrappedValue::getCreationTime)); 

    WrappedValue firstInstance = new WrappedValue("Hello"); 
    WrappedValue secondInstance = new WrappedValue("Hello"); 

    assertThat(firstInstance).isEqualTo(secondInstance); // Passes 
    assertThat(sortedSet.add(firstInstance)).isTrue(); // Passes 
    assertThat(sortedSet.add(secondInstance)).isFalse(); // Actual: True 
    assertThat(sortedSet.size()).isEqualTo(1);   // Actual: 2 
} 

private class WrappedValue { 
    private final String value; 
    private final long creationTime; 

    private WrappedValue(String value) { 
     this.value = value; 
     this.creationTime = System.nanoTime(); 
    } 

    private String getValue() { 
     return value; 
    } 

    private long getCreationTime() { 
     return creationTime; 
    } 

    @Override 
    public boolean equals(Object o) { 
     if (this == o) return true; 
     if (!(o instanceof WrappedValue)) return false; 
     WrappedValue that = (WrappedValue) o; 
     return Objects.equals(this.value, that.value); 
    } 

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

The JavaDoc for TreeSet.add() стат что бы мы ожидали:

Добавляет указанный элемент к этому набору, если он еще не присутствует. Более формально добавляет указанный элемент e к этому набору, если в наборе нет элемента e2, такого как (e==null ? e2==null : e.equals(e2)). Если этот набор уже содержит элемент, вызов оставляет значение неизменным и возвращает false.

Учитывая мое утверждение, что два объекта: equal(), я бы ожидал, что это пройдет. Я работаю над тем, что мне не хватает чего-то мыслящего очевидного, если только TreeSetне фактически использует Object.equals(), но использует в большинстве случаев что-то такое же, что и в большинстве случаев.

Это наблюдалось с использованием JDK 1.8.0.60 - у меня не было возможности протестировать другие JDKs еще, но я предполагаю, что есть некоторые «Оператор Error» где-то ...

+0

Мой фактический прецедент предназначен для кеша, который сортируется по необязательному полю ZonedDateTime, но возвращается к порядку вставки с помощью 'System.nanoTime()'. – Edd

+0

Если равны и ваш компаратор смотрит на разные поля, почему они будут вести себя совместимым способом? –

ответ

3

Проблемы заключается в том, что компаратор дается для сортировки набора несовместим с equals метод WrappedValue. Вы ожидаете, что SortedSet будет вести себя как Set, но в этом случае это не так.

От SortedSet:

Обратите внимание, что порядок поддерживается отсортированный набор [...] должны быть в соответствии с equals если отсортированный набор правильно реализовать интерфейс Set. [...] Это связано с тем, что интерфейс Set определен в терминах операции equals, но сортированный набор выполняет все сравнения элементов, используя его метод compareTo (или compare), поэтому два элемента, которые считаются равными этому методу, с точки зрения сортированного множества, равным. Поведение отсортированного набора равно, если оно упорядочено неравномерно; он просто не подчиняется генеральному контракту интерфейса Set.

Другими словами, SortedSet использует только тот компаратор, который вы дали, чтобы определить, равны ли два элемента.В этом случае компаратором является

Comparator.comparing(WrappedValue::getValue).thenComparing(WrappedValue::getCreationTime) 

, который сравнивает значение, а затем время создания. Но так как конструктор WrappedValue инициализирует (эффективно) уникальное время создания с System.nanoTime(), ни один из двух WrappedValue не будет считаться равным этим компаратором. Поэтому в отношении сортированного набора

WrappedValue firstInstance = new WrappedValue("Hello"); 
WrappedValue secondInstance = new WrappedValue("Hello"); 

- это два отдельных объекта. Действительно, если вы немного измените конструктор, чтобы добавить параметр long creationTime, и одновременно укажите оба момента, вы увидите «ожидаемый» результат (т. Е. Отсортированный набор будет иметь размер 1 после добавления двух экземпляров).

Итак, есть 3 решения здесь:

  1. Закрепить equals и hashCode методы и позволяют им сравнивать значения как и время.
  2. Дайте компаратору только сравнение значения.
  3. Примите тот факт, что SortedSet не ведет себя как Set в данном конкретном случае.
+0

Спасибо. Я думаю, что урок, который вы узнаете здесь, это то, что мне нужно прочитать _all_ документации, а не просто дважды проверить вызываемый метод. Лучшим решением для моего варианта использования, вероятно, является устранение компаратора. – Edd

1

Ваш Equals (который только считает value) не соответствует вашему компаратору, который считал value и creationTime.

Я предполагаю, что у вас есть два объекта с одинаковым значением, таким образом они являются равно истинными, но разными creationTimes поэтому они CompareTo! = 0.