TreeSetにnullをaddする話
Java で TreeSet に null を add すると NullPointerException になります。*1
Airof:~ irof$ groovysh Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Groovy Shell (1.8.2, JVM: 1.6.0_26) Type 'help' or '\h' for help. ----------------------------------------------------------------------------------------------------------- groovy:000> s = new TreeSet() ===> [] groovy:000> s.add(1) ===> true groovy:000> s.add(null) ERROR java.lang.NullPointerException: null at java_util_Set$add.call (Unknown Source) at groovysh_evaluate.run (groovysh_evaluate:2) ... groovy:000>
groovysh ですけど Exception は Java で出てますから問題ありません。
ところで JavaSE6 では1件目に null が入るらしいです。試してみます。
Airof:~ irof$ groovysh Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Groovy Shell (1.8.2, JVM: 1.6.0_26) Type 'help' or '\h' for help. ----------------------------------------------------------------------------------------------------------- groovy:000> s = new TreeSet() ===> [] groovy:000> s.add(null) ===> true
あら、本当に入った。
でも続けるとこうなる。
Airof:~ irof$ groovysh Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Groovy Shell (1.8.2, JVM: 1.6.0_26) Type 'help' or '\h' for help. ----------------------------------------------------------------------------------------------------------- groovy:000> s = new TreeSet() ===> [] groovy:000> s.add(null) ===> true groovy:000> s.add(null) ERROR java.lang.NullPointerException: null at java_util_Set$add.call (Unknown Source) at groovysh_evaluate.run (groovysh_evaluate:2) ... groovy:000> s.add(1) ERROR java.lang.NullPointerException: null at java_util_Set$add.call (Unknown Source) at groovysh_evaluate.run (groovysh_evaluate:2) ...
つまりその後一切追加出来ない状態です。これは嬉しくない。
これだとどこで出てるか解らないのでStackTraceを出してみます。上の方だけ。
groovy:000> try{s.add(1)}catch(Exception e){e.printStackTrace()} java.lang.NullPointerException at java.lang.Integer.compareTo(Integer.java:978) at java.lang.Integer.compareTo(Integer.java:37) at java.util.TreeMap.put(TreeMap.java:545) at java.util.TreeSet.add(TreeSet.java:238) at java_util_Set$add.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
予想通り、 compareTo です。 TreeSet は自然順序付けなので要素同士を比較するわけです。
ちなみに add するのが null の場合はこうなります。
groovy:000> try{s.add(null)}catch(Exception e){e.printStackTrace()} java.lang.NullPointerException at java.util.TreeMap.put(TreeMap.java:541) at java.util.TreeSet.add(TreeSet.java:238) at java_util_Set$add.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
ちょっと発生個所が違っているのは、 compareTo 呼ぶ前に Key の null チェックを行っているから。と言う事で、そもそも TreeMap の Key に null は使えない*2感じですね。それに引き摺られて TreeSet にも使えないという事です。詳しくは TreeMap を開いて該当行を見て下さい。
TreeMap を見てると、そもそもデフォルトコンストラクタの場合は Key を Comparable にキャストして比較している事に気づきます。と言うわけで、 Comparable でないものを TreeMap の Key にしたり、 TreeSet に入れれば「それと比較した時」に ClassCastException になるようです。つまり、当然ながらこれがアウト。*3
import java.util.TreeSet; public class Hoge { public static void main(String[] args) { TreeSet set = new TreeSet(); set.add(new Object()); set.add(new Object()); } }
実行するとこのように、2つ目の add で ClassCastException が発生します。
Airof:sandbox irof$ java -version Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 java version "1.6.0_26" Java(TM) SE Runtime Environment (build 1.6.0_26-b03-383-11A511) Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02-383, mixed mode) Airof:sandbox irof$ java Hoge Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Comparable at java.util.TreeMap.put(TreeMap.java:542) at java.util.TreeSet.add(TreeSet.java:238) at Hoge.main(Hoge.java:7)
正確には「してました」になります。
JavaSE7 だと*4こうなります。Hoge.java:6、つまり1つ目の add で ClassCastException が発生してます。
Airof:sandbox irof$ java -version Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 openjdk version "1.7.0-ea" OpenJDK Runtime Environment (build 1.7.0-ea-b215) OpenJDK 64-Bit Server VM (build 21.0-b17, mixed mode) Airof:sandbox irof$ java Hoge Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Comparable at java.util.TreeMap.compare(TreeMap.java:1188) at java.util.TreeMap.put(TreeMap.java:531) at java.util.TreeSet.add(TreeSet.java:255) at Hoge.main(Hoge.java:6)
これが"正しい挙動"です。そもそも一つ目が add 出来たのがおかしくて、それが JavaSE7 で修正されたってことらしいです。
このエントリを書いてるのは「JavaOne 2011 報告会 at 大阪」の細かい話「TreeSet の1件目 null のバグが修正された」のデモ時に見えたのが NullPointerException ではあるものの、発生個所が add でも put でもなく compare だったのが気になったからです。
なんかまとまらなくなって来た感じがありますが、この辺で JavaSE7 で TreeSet に null をぶっ込みます。
Airof:~ irof$ groovy -version Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Groovy Version: 1.8.2 JVM: 1.7.0-ea Airof:sandbox irof$ groovy -e "new TreeSet().add(null)" Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Caught: java.lang.NullPointerException java.lang.NullPointerException at java_util_Set$add.call(Unknown Source) at script_from_command_line.run(script_from_command_line:1) Airof:~ irof$
はい、1件目でも無事 NullPointerException が発生してます。発生個所は先ほど new Object() を put してたとこと同じです。
TreeMap のputを見ると、531行目にこんな記述がありました。
528 public V put(K key, V value) { 529 Entry<K,V> t = root; 530 if (t == null) { 531 compare(key, key); // type (and possibly null) check 532 533 root = new Entry<>(key, value, null); 534 size = 1; 535 modCount++; 536 return null; 537 }
compare に 同じ key を渡してます。なんでこんな事をしてるかと言うと、「比較出来るか」を実際比較する事で試してるみたいです。compare自体の実装は至ってシンプルですね。
1185 * Compares two keys using the correct comparison method for this TreeMap. 1186 */ 1187 final int compare(Object k1, Object k2) { 1188 return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) 1189 : comparator.compare((K)k1, (K)k2); 1190 }
だいじなこと
最期に一つだけ誤解して欲しくないのが「TreeMap の Key に null が使えない」では無い事。デフォルトコンストラクタを使用する場合は使えないだけです。つまり、これは通ります。JavaSE6 だろうと JavaSE7 だろうと関係なく。
groovy:000> s = new TreeSet([compare:{o1,o2 -> 1}] as Comparator) ===> [] groovy:000> s.add(null) ===> true groovy:000> s.add(1) ===> true groovy:000> s.add(1) ===> true
ただしこの場合、常にcompareが1を返すので、二回目の add(1) が true になっているように、同じ値でもいくらでも追加されていきます。Compareが0だと1つしか追加出来ません。そして contains は常に false を返します。TreeSet の場合は順序付けのみが同じかの判断基準になるためです。
まぁJavadocに書いてるんだけどね
ハイ!><add
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
public boolean add(E e)
(説明略)
例外
NullPointerException - 指定された要素が null であり、このセットが自然順序付けを使用しているかそのコンパレータが null 要素を許可しない場合
そのまま。 Set の実装次第( Comparator が許容する場合)では null は add 出来る事になってます。
ちなみに HashSet (これも内部では HashMap )では null は特別扱いしつつ add 出来るようにごにょってます。hash値とか計算しようないしコケるんじゃねーの?とか思ってました。
なお Set インタフェースの add は実装が許さない場合のみ NullPointerException を投げると書かれてるだけです。