はじめに:この記事で伝えたいことのすべて
JavaのSet.ofの引数に重複要素が入る可能性を否定できないときは、Set.ofを使うのはやめましょう。
IllegalArgumentExceptionに泣かされることになりますよ。
というのはきっと有名な話ですよね
Java 27の発表がタイムリーな今日この頃、「今さら?」という声が聴こえる・・・
ですが、長らくJava8の民として生きていた私はまんまとハマってしまったので自戒の念を込めてこの時代でも書きます。
わざわざ重複要素を入れようとしていれば実装段階で気づけますが、特定条件のときにのみ重複要素が入るような場合はテストをすり抜けてしまう場合があります。
で、商用デプロイ後しばらく経って突然猛威を振るったりします。
OracleのIN句1000件問題、mockito.mockStaticクローズ漏れ問題1などとともに時限爆弾と呼んで差し支えないと思います。
繰り返します。重複要素が入る可能性が捨てきれない場合、安易にSet.ofを利用しないようにしましょう。
ちなみにSet.ofではnullも入れられないのでその点も注意が必要です。
が、本記事ではあくまで重複要素のみにフォーカスします。
Set使ってますか?
改めましてみなさん、JavaのSet使ってますか?
特にHashSetは重複不可、順不同で差し支えないコレクションの場合とても便利ですし処理速度も速くなりますよね。私は大好きです。
重複要素を追加しようとしてもいい感じに削ぎ落としてくれるのもとてもよきです。
Set<String> hashset = new HashSet<>(Arrays.asList("A", "B", "C", "A")); // 2つ目のAは追加されない
hashset.add("D");
hashset.stream().forEach(System.out::println);
A
B
C
D
※Setなので出力の都度順番は変動します。以下同様です
Java9でSet.ofが追加された
Set.ofを使えば、わざわざ先んじてSetに詰めたい要素をコレクションにしたり、addでせっせと追加したりする必要がありません。
しかもイミュータブル!安心安全!便利!
Set<String> setByOf = Set.of("dog", "cat", "bird", "horse");
// setByOf.add("cow"); // これはUnsupportedOperationExceptionで怒られる。Set.ofで作るのは不変のSetのため
Java8まではイミュータブルなSetを作りたい場合Collections.unmodifiableSetを使えたものの、あくまでも既存のSetインスタンスをラップして不変ぽく扱っているだけなので、もとのSetインスタンスを変えれば一緒に変わってしまうんですよね。
Set<String> originalSet = new java.util.HashSet<>(Arrays.asList("あ", "い", "う", "あ")); // 2つ目の「あ」は追加されない
Set<String> setByUnmodifiableSet = Collections.unmodifiableSet(originalSet);
setByUnmodifiableSet.stream().forEach(System.out::println);
// setByUnmodifiableSet.add("え"); // Collections.unmodifiableSetで作ったSetを直接修正しようとするとUnsupportedOperationExceptionで怒られる
originalSet.add("お"); // originalSetは可変なのでもちろんこれはOK
setByUnmodifiableSet.stream().forEach(System.out::println); // originalSetに要素を追加したのでそれをラップしたsetByUnmodifiableSetにも反映される
あ
い
う
お <- イミュータブルであってほしいとき、本当はこの子は追加されてほしくない
でもSet.ofならその心配もない!完全なるイミュータブル!
Set.ofの便利さを享受していた中、突如としてIllegalArgumentException発生
なるべく変更の少ない形でインスタンスを保ちたいと思ったときに非常に便利だと感じ、Collections.unmodifiableSetと同じ感覚でSet.ofを使っていると、あるとき突然IllegalArgumentExceptionが発生したではありませんか。
Set<String> setByOf = Set.of("dog", "cat", "bird", "horse", "dog");
setByOf.stream().forEach(System.out::println);
Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: dog
すでに理由は冒頭で書いているので引っ張るつもりはなく、原因はエラーログの通りdogの重複です。
Set.ofは引数に1つでも重複要素があるとIllegalArgumentExceptionが発生する、というのが仕様だからでした。
Set.ofの内部処理でも明確に例外処理がされています。
HashSetで重複がきたときはHashMapのputを内部的に呼んで重複をいい感じに処理してくれていたのでサボれていただけでした。
サンプルコードくらいシンプルならすぐに気づけますが、Setを作るときの引数が前段の処理によって変わる場合など、一見重複要素が入る余地を見抜けないような複雑さはプロダクションコードだとよくありますよね・・・
重複要素が来る可能性は捨てきれないが不変なSetを作りたいときは
こんな感じ↓であらかじめSet.ofに渡す要素から重複を除外するようなメソッドを作ればいいわけですが
private Set<String> createImmutableSet(String... elements) {
String[] nonDuplicatedElements = Arrays.stream(elements)
.filter(Objects::nonNull)
.distinct()
.toArray(String[]::new);
return Set.of(nonDuplicatedElements);
}
でもだったらもうこれ↓でいいじゃん、と言われたらぐうの音も出ないですね2。
private Set<String> createImmutableSet(String... elements) {
return Stream.of(elements).collect(Collectors.toUnmodifiableSet());
}
elementsにArrays.asListを噛ませるのならSet.copyOfも候補に入れられます。Set.copyOfなら重複も除外して作ってくれますし、ぱっと見でわかりやすいですね。個人的にはこれ↓が推しです。
private Set<String> createImmutableSet(String... elements) {
return Set.copyOf(Arrays.asList(elements));
}
ところでSet.copyOfの実装を見てみましょう。
代案3は引数でArrays.asList(elements)を渡しています。この場合、最後のelse句に入ります。
ということは、これ↓でもいけますね。copyOfに浮気なぞできぬ・・・という場合にはおすすめです(?)
private Set<String> createImmutableSet(String... elements) {
return Set.of(new HashSet<>(Arrays.asList(elements)).toArray());
}
上記4つの案はいずれも以下のメソッドを使って同じ結果を返してくれます。
private void printSetElements() {
Set<String> set = createImmutableSet("dad", "mom", "son", "son");
set.stream().forEach(System.out::println); // 出力例)son, dad, mom
}
おわりに:製品コードにおいてSet.ofの使い道は案外少ない
一見便利なSet.ofですが、利用先には要注意です。
- 空の不変のSetを作りたいとき
- 要素1つの不変のSetを作りたいとき
- 直接指定の定数など、絶対に重複があり得ないと言い切れるとき
上記以外は使わないほうが無難・・・と感じています。
そんなことないよ!というご意見は大歓迎ですので、その場合はぜひコメント等お願いします。