在TreeSet中修改在compareTo中使用的值是否可以?

huangapple go评论75阅读模式
英文:

Is it ok to change a value that is used in compareTo in a TreeSet?

问题

以下是翻译好的部分:

我有这个简单的 Pojo:

public static class Pojo implements Comparable<Pojo> {

    Integer value;

    @Override
    public int compareTo(Pojo o) {
        return value.compareTo(o.value);
    }

    // 省略了 getters 和 setters

}

在一个 TreeSet 中改变 Pojovalue 字段是否可以?如果这会破坏 TreeSet 中的排序,我并不介意。

我很好奇,因为在实例在 HashMap 中时改变用于 hashCode()equals()value 是不明智的。

英文:

I have this simple Pojo:

public static class Pojo implements Comparable&lt;Pojo&gt; {

    Integer value;

    @Override
    public int compareTo(Pojo o) {
        return value.compareTo(o.value);
    }

    // getters and setter ommited

}

Is it ok to change value of a Pojo while it's in a TreeSet? I don't mind if this breaks the sorting in the TreeSet.

I'm curious because changing value used in a hashCode() or equas() while the instance is in a HashMap is not a good idea.

答案1

得分: 5

如果我们改变影响元素顺序的属性,就会违反TreeSet的契约。

根据TreeSet的文档,它

> ... 对基本操作(addremovecontains)提供了确保的O(log n)时间复杂度。

TreeSet可以通过保持集合有序来保证这些时间成本。然而,如果我们改变元素的属性,从而改变了集合内部的顺序,就会打破这个契约。TreeSet并不会不断地“监视”所有元素的变化。此外,一次变化可能会触发重新排序,这将产生n * log(n)的成本(其中nTreeSet当前的大小)。

我们可以通过以下代码来展示这种形式的契约违反效果:

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;

class Ideone {
    public static void main(String[] args) {
        Foo fooOne = Foo.of(1);
        Foo fooTwo = Foo.of(2);
        Foo fooThree = Foo.of(3);
        TreeSet<Foo> set = new TreeSet<>();
        set.addAll(List.of(fooOne, fooTwo, fooThree));
        System.out.println(set.contains(fooOne));
        fooOne.setValue(4);
        System.out.println(set.contains(fooOne));
        System.out.println(new ArrayList<>(set).contains(fooOne));
    }
}

class Foo implements Comparable<Foo> {
    private int value;

    private Foo(int value) {
        this.setValue(value);
    }

    public static Foo of(int value) {
        return new Foo(value);
    }

    public int getValue() {
        return value;
    }

    public Foo setValue(int value) {
        this.value = value;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Foo foo = (Foo) o;
        return getValue() == foo.getValue();
    }

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

    @Override
    public int compareTo(Foo that) {
        return getValue() - that.getValue();
    }
}

如果TreeSet会在元素属性变化时重新排序,这三行代码都应该显示为true。然而,由于我们将fooOnevalue1更改为4(因此预期它将成为集合中的最后一个元素),第二个contains(fooOne)将返回false,因为TreeSet不再排序。然而,将TreeSet转换为ArrayList并在ArrayList中搜索会产生预期的结果,因为ArrayList不会对元素顺序做出任何假设。

如果基本操作的O(log n)时间成本对使用情况不是必要的,我建议使用不依赖于元素顺序的不同数据结构(例如List)。然而,请记住,这会导致(至少部分)基本操作的时间成本增加。

编辑:

正如在评论中由@Scratte提到的,我们甚至可以通过将(修改后,但仍与==相同)的fooOne添加到TreeSet中,在更基本的层面上破坏TreeSet,从而将其大小增加一:

class Ideone {
    public static void main(String[] args) {
        Foo fooOne = Foo.of(1);
        Foo fooTwo = Foo.of(2);
        Foo fooThree = Foo.of(3);
        TreeSet<Foo> set = new TreeSet<>();
        set.addAll(List.of(fooOne, fooTwo, fooThree));
        System.out.println(set.size());
        fooOne.setValue(4);
        set.add(fooOne);
        System.out.println(set.size());
    }
}
英文:

If we change attributes that influence the element order, we violate contracts of TreeSet.

As per TreeSet's documentation, it

> ... provides guaranteed log(n) time cost for the basic operations (add, remove and contains).

The TreeSet can guarantee those time costs by keeping the set ordered. If we, however, change attributes of elements, that would in turn change the ordering within the set, we break this contract. The TreeSet does not constantly "monitor" all elements for changes. Furthermore, a change could trigger a reorder, which would have costs of n * log(n) (with n being the current size of the TreeSet).

We can make the effects of this form of contract violation visible with the following code:

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
class Ideone {
public static void main(String[] args) {
Foo fooOne = Foo.of(1);
Foo fooTwo = Foo.of(2);
Foo fooThree = Foo.of(3);
TreeSet&lt;Foo&gt; set = new TreeSet&lt;&gt;();
set.addAll(List.of(fooOne, fooTwo, fooThree));
System.out.println(set.contains(fooOne));
fooOne.setValue(4);
System.out.println(set.contains(fooOne));
System.out.println(new ArrayList&lt;&gt;(set).contains(fooOne));
}
}
class Foo implements Comparable&lt;Foo&gt; {
private int value;
private Foo(int value) {
this.setValue(value);
}
public static Foo of(int value) {
return new Foo(value);
}
public int getValue() {
return value;
}
public Foo setValue(int value) {
this.value = value;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Foo foo = (Foo) o;
return getValue() == foo.getValue();
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public int compareTo(Foo that) {
return getValue() - that.getValue();
}
}

<kbd>Ideone demo</kbd>

If the TreeSet would reorder on element attribute changes, all three lines should show true. However, since we changed fooOne's value from 1 to 4 (hence it would be expected to be the last element in the set), the second contains(fooOne) will return false since the TreeSet is no longer sorted. Converting the TreeSet into an ArrayList and searching within the ArrayList, however, yields the expected result since the ArrayList does not make any assumption about element order.

If the guaranteed log(n) time cost for basic operation is not essential to the use case, I would suggest using a different data structure (e.g. a List) that does not rely on element ordering. Keep in mind, however, that this comes at a higher time cost for (at least some of the) basic operations.

EDIT:

As was mentioned by @Scratte in the comments, we can even break the TreeSet on a more fundamental level by adding the (modified, but still identical wrt. ==) fooOne to the TreeSet, increasing its size by one:

class Ideone {
public static void main(String[] args) {
Foo fooOne = Foo.of(1);
Foo fooTwo = Foo.of(2);
Foo fooThree = Foo.of(3);
TreeSet&lt;Foo&gt; set = new TreeSet&lt;&gt;();
set.addAll(List.of(fooOne, fooTwo, fooThree));
System.out.println(set.size());
fooOne.setValue(4);
set.add(fooOne);
System.out.println(set.size());
}
}

<kbd>Ideone demo</kbd>

huangapple
  • 本文由 发表于 2020年9月18日 21:57:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/63957173.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定