web-dev-qa-db-ja.com

誤った出力を与えるTreeSet-Java8

ツリーセットを操作しているときに、非常に独特な動作を見つけました。

私の理解によると、次のプログラムは2つの同じ行を出力する必要があります。

public class TestSet {
    static void test(String... args) {
        Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        s.addAll(Arrays.asList("a", "b"));
        s.removeAll(Arrays.asList(args));
        System.out.println(s);
    }

    public static void main(String[] args) {
        test("A");
        test("A", "C");
    }
}

しかし奇妙なことにそれは印刷します:

[b]
[a, b] 

理解できません-なぜツリーセットがこのように動作するのですか?

45
Joker

これは、SortedSetのComparatorが並べ替えに使用されているために発生しますが、removeAllは各要素のequalsメソッドに依存しています。 SortedSetドキュメント から:

ソートされたセットがSetインターフェースを正しく実装する場合、ソートされたセットによって維持される順序(明示的なコンパレーターが提供されているかどうかに関係なく)は等しいと一致でなければならないことに注意してください。 (consistent withequals。の正確な定義については、ComparableインターフェイスまたはComparatorインターフェイスを参照してください)これは、Setインターフェイスがで定義されているためです。 equals操作の条件ですが、ソートされたセットは、そのcompareTo(またはcompare)メソッドを使用してすべての要素の比較を実行するため、このメソッドによって等しいと見なされる2つの要素は次のとおりです。ソートされたセットの観点から、等しい。ソートされたセットの動作is順序が等しいと矛盾している場合でも、明確に定義されています。 Setインターフェースの一般的な契約に従わないだけです。

「等しいと一致する」の説明は、 比較可能なドキュメント で定義されています。

クラスCの自然な順序は、e1.compareTo(e2) == 0e1.equals(e2)と同じブール値を持っている場合に限りequalsと一致であると言われます。クラスCのすべての_e1_および_e2_に対して。 nullはどのクラスのインスタンスでもないことに注意してください。また、e.compareTo(null)NullPointerExceptionを返しても、e.equals(null)falseをスローする必要があります。

自然な順序が等しいと一致していることを強くお勧めします(必須ではありません)。これは、明示的なコンパレータのないソートされたセット(およびソートされたマップ)が、自然順序が等しいと矛盾する要素(またはキー)とともに使用される場合、「奇妙に」動作するためです。特に、そのようなソートされたセット(またはソートされたマップ)は、equalsメソッドで定義されているセット(またはマップ)の一般的な契約に違反します。

要約すると、セットのコンパレータは要素のequalsメソッドとは異なる動作をし、異常な(ただし予測可能な)動作を引き起こします。

41
VGR

まあ、これは私を驚かせました、私が正しいかどうかはわかりませんが、AbstractSetでこの実装を見てください:

_public boolean removeAll(Collection<?> c) {
  Objects.requireNonNull(c);
  boolean modified = false;

  if (size() > c.size()) {
    for (Iterator<?> i = c.iterator(); i.hasNext(); )
      modified |= remove(i.next());
  } else {
    for (Iterator<?> i = iterator(); i.hasNext(); ) {
      if (c.contains(i.next())) {
        i.remove();
        modified = true;
      }
    }
  }
  return modified;
}
_

基本的に、この例では、setのサイズは削除する引数のサイズと等しいため、else条件が呼び出されます。その条件では、イテレータの現在の要素を削除する引数のコレクションがcontainsであるかどうかのチェックがあり、そのチェックでは大文字と小文字が区別されるため、c.contains("a")であるかどうかをチェックし、falseを返します。 cには_"A"_ではなく_"a"_が含まれているため、要素は削除されません。セットs.addAll(Arrays.asList("a", "b", "d"));に要素を追加すると、size() > c.size()がtrueになり、containsチェックがなくなるため、正しく機能することに注意してください。

15
Shadov

removeTreeSetが実際に大文字と小文字を区別せずに削除する理由に関する情報を追加するには(@による回答で説明されているようにif (size() > c.size())パスをたどる場合Shadov):

これはremoveTreeSetmethodです:

public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }

内部のremoveからTreeMapを呼び出します:

public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;

    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

getEntryを呼び出します

 final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

(例のように)Comparatorがある場合、エントリはこのComparatorに基づいて検索されます(これはgetEntryUsingComparatorによって実行されます)。そのため、実際に検出されます(その後、削除されました)、ケースの違いにもかかわらず。

3
Arnaud

これは興味深いので、出力を使用したいくつかのテストを次に示します。

_static void test(String... args) {
    Set<String> s =new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
    s.addAll(Arrays.asList( "a","b","c"));
    s.removeAll(Arrays.asList(args));
    System.out.println(s);
}

public static void main(String[] args) {
    test("C");          output: [a, b]
    test("C", "A");     output: [b]
    test("C", "A","B"); output: [a, b, c]
    test("B","C","A");  output: [a, b, c]
    test("K","C");      output: [a, b]
    test("C","K","M");  output: [a, b, c] !!
    test("C","K","A");  output: [a, b, c] !!
}
_

コンパレータがない場合は、ソートされたHashSet<String>()のように機能します。

_    static void test(String... args) {
    Set<String> s = new TreeSet<String>();//
    s.addAll(Arrays.asList( "a","b","c"));
    s.removeAll(Arrays.asList(args));
    System.out.println(s);
}

public static void main(String[] args) {
    test("c");          output: [a, b]
    test("c", "a");     output: [b]
    test("c", "a","b"); output: []
    test("b","c","a");  output: []
    test("k","c");      output: [a, b]
    test("c","k","m");  output: [a, b]
    test("c","k","m");  output: [a, b]
}
_

今ドキュメントから:

public boolean removeAll(Collection c)

このセットから、指定されたコレクションに含まれているすべての要素を削除します(オプションの操作)。指定されたコレクションもセットである場合、この操作はこのセットを効果的に変更して、その値が2つのセットの非対称セット差になるようにします。

この実装は、それぞれでsizeメソッドを呼び出すことにより、このセットと指定されたコレクションのどちらが小さいかを判別します。このセットの要素数が少ない場合、実装はこのセットを繰り返し処理し、イテレーターから返された各要素を順番にチェックして、指定されたコレクションに含まれているかどうかを確認します。そのように含まれている場合は、イテレータのremoveメソッドを使用してこのセットから削除されます。指定されたコレクションの要素数が少ない場合、実装は指定されたコレクションを反復処理し、このセットのremoveメソッドを使用して、イテレーターによって返された各要素をこのセットから削除します。

ソース

0
TiyebM