この記事の目的
同じ要素を排除した一覧をつくるときはSetインターフェースを使うのが便利です。Setインターフェースの実装クラスであるTreeSetクラスで思いがけない挙動を見つけたのでご紹介します。
「同じ」ことをどう判断するかがポイントです。
サンプルプログラム
名前(name)と年齢(age)をメンバ変数にもつHumanクラスを考えます。
import lombok.Value; // lombokによってgetNameメソッド、getAgeメソッドが実装される // equalsメソッドもOverrideされ、nameとageが両方等しいときのみtrueを返す @Value public class Human { private final String name; private final int age; }
年齢の若い順に並び替えるComparatorを渡してTreeSetを作成します。Setインターフェースで一般に使われるHashSetとの違いは、「要素の並び替えを行う機能がついていること」です。
年齢は同じですが名前の異なる2人をTreeSetに登録し、一覧を出力します。
import java.util.Comparator; import java.util.Set; import java.util.TreeSet; public class TreeSetAddTestMain { // 年齢の若い順に並び替え private static final Comparator<Human> COMPARATOR = Comparator.comparing(Human::getAge); public static void main(String[] args) { Set<Human> set = new TreeSet<>(COMPARATOR); set.add(new Human("Taro", 20)); set.add(new Human("Hanako", 20)); set.stream().forEach(System.out::println); } }
TaroとHanakoの2人が出力されそうですが、期待に反してTaroしか出力されません。
Human(name=Taro, age=20)
どうすれば期待通り動くのか
並び替えのルールを変えれば期待通り動きます。年齢が同じときは名前の辞書順で並び替えるようにしてみます。
import java.util.Comparator; import java.util.Set; import java.util.TreeSet; public class TreeSetAddTestMain { // 年齢の若い順に並び替え、年齢が同じときは名前の辞書順で並び替え private static final Comparator<Human> COMPARATOR = Comparator.comparing(Human::getAge) .thenComparing(Human::getName); public static void main(String[] args) { Set<Human> set = new TreeSet<>(COMPARATOR); set.add(new Human("Taro", 20)); set.add(new Human("Hanako", 20)); set.stream().forEach(System.out::println); } }
今度はTaroとHanakoの2人が出力されます。HとTではHのほうが辞書順が先なので、Hanakoが先に出力されます。
Human(name=Hanako, age=20) Human(name=Taro, age=20)
期待に反する挙動になる理由
Setインターフェースは通常、「同じ」ことの判定をequalsメソッドで行います。
一方、TreeSetクラスは「同じ」ことの判定をcompareTo/compareメソッドで行います。つまり並び替えの順序が同じであれば、他に何か違いがあっても同じものと扱ってしまうということです。
TreeSet
インスタンスはそのcompareTo
メソッドまたはcompare
メソッドを使用してすべての要素比較を実行するので、・・・(中略)・・・Set
インタフェースの一般規約には準拠していません。出典: docs.oracle.com
まとめ
TreeSetクラスの思いがけない挙動をご紹介しました。このような現象に遭遇すると、テストをすることの大切さが分かりますね。
コメント