この記事の目的
同じ要素を排除した一覧をつくるときは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クラスの思いがけない挙動をご紹介しました。このような現象に遭遇すると、テストをすることの大切さが分かりますね。


コメント