ツリーセットを操作しているときに、非常に独特な動作を見つけました。
私の理解によると、次のプログラムは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]
理解できません-なぜツリーセットがこのように動作するのですか?
これは、SortedSetのComparatorが並べ替えに使用されているために発生しますが、removeAllは各要素のequals
メソッドに依存しています。 SortedSetドキュメント から:
ソートされたセットが
Set
インターフェースを正しく実装する場合、ソートされたセットによって維持される順序(明示的なコンパレーターが提供されているかどうかに関係なく)は等しいと一致でなければならないことに注意してください。 (consistent withequals。の正確な定義については、Comparable
インターフェイスまたはComparator
インターフェイスを参照してください)これは、Set
インターフェイスがで定義されているためです。equals
操作の条件ですが、ソートされたセットは、そのcompareTo
(またはcompare
)メソッドを使用してすべての要素の比較を実行するため、このメソッドによって等しいと見なされる2つの要素は次のとおりです。ソートされたセットの観点から、等しい。ソートされたセットの動作is順序が等しいと矛盾している場合でも、明確に定義されています。Set
インターフェースの一般的な契約に従わないだけです。
「等しいと一致する」の説明は、 比較可能なドキュメント で定義されています。
クラス
C
の自然な順序は、e1.compareTo(e2) == 0
がe1.equals(e2)
と同じブール値を持っている場合に限りequalsと一致であると言われます。クラスC
のすべての_e1
_および_e2
_に対して。null
はどのクラスのインスタンスでもないことに注意してください。また、e.compareTo(null)
がNullPointerException
を返しても、e.equals(null)
はfalse
をスローする必要があります。自然な順序が等しいと一致していることを強くお勧めします(必須ではありません)。これは、明示的なコンパレータのないソートされたセット(およびソートされたマップ)が、自然順序が等しいと矛盾する要素(またはキー)とともに使用される場合、「奇妙に」動作するためです。特に、そのようなソートされたセット(またはソートされたマップ)は、
equals
メソッドで定義されているセット(またはマップ)の一般的な契約に違反します。
要約すると、セットのコンパレータは要素のequals
メソッドとは異なる動作をし、異常な(ただし予測可能な)動作を引き起こします。
まあ、これは私を驚かせました、私が正しいかどうかはわかりませんが、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
チェックがなくなるため、正しく機能することに注意してください。
remove
のTreeSet
が実際に大文字と小文字を区別せずに削除する理由に関する情報を追加するには(@による回答で説明されているようにif (size() > c.size())
パスをたどる場合Shadov):
これはremove
のTreeSet
methodです:
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
によって実行されます)。そのため、実際に検出されます(その後、削除されました)、ケースの違いにもかかわらず。
これは興味深いので、出力を使用したいくつかのテストを次に示します。
_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メソッドを使用して、イテレーターによって返された各要素をこのセットから削除します。