この記事はKotlin Advent Calendar
の1日目の記事です。
この記事の内容は完全にJava
ですが、現在Java
を書いている人がKotlin
を導入するきっかけになることを目指して書きますのでご容赦ください。
前書き
Kotlin
のJava
に対する大きな利点の1つは、コードの安全性を簡単に担保できることです。
一方、安全性はアピールが難しく、「安全だからKotlin
を導入したい!」と主張しても「それ、Java
でもできるのでは?」「どれ位メリット有るの?」と突っ込まれることも有るでしょう。
実際、自分が経験した限りJava
でも安全なコードを書く(正確には、書ける環境を整える)ことは可能です。
一方、それを実現するには非常に大きな労力が必要で、現実的には安全性を妥協する部分が出てくると思っています。
このシリーズでは、Kotlin
の導入材料の1つになれることを目指しながら、Java
で安全なコードを書く大変さと、Kotlin
で安全なコードを書く簡単さについて書きます。
前編では、Java
で安全性を高めるための工夫として、以下の4点を書きます。
-
null
を安全に取り扱う - メソッド・クラス・変数を基本的に変更できないようにする
-
Collection
を読み取り専用にする - 一致を安全に判定する
後編では、これらがKotlin
でどのような表現になるかと、Java
で実現した場合と比べた簡単さについて書きます。
nullを安全に取り扱う
素のJava
では、プリミティブ型等を除いてnullability
を指定する文法は無く、IDE
で得られる警告も限定的です。
この状態では、null
チェックの抜け漏れで実行時エラーになる危険性が有ります。
// 引数や戻り値のnullabilityはJavadoc等に書く以外の形で表現できない
public String func(String foo, String bar) { /* 略 */ }
これを安全に扱うための方法は幾つか考えられますが、ここではSpotBugs
の静的解析と、spotbugs-annotations
から導入されるjavax.annotation.Nonnull
/Nullable
アノテーションを組み合わせる例を紹介します。
また、今後のコード例はこれらのアノテーションを付与した状態にします。
補足として、アノテーション等によってnullability
を示し、解析や実行時のチェックに用いることは、Lombok
やJetBrains Annotations
といったライブラリ・フレームワークでも実現できます。
どのライブラリ・フレームワークを用いるかは、プロジェクトで利用しているIDE
や、ライブラリの導入状況に応じて決めることになります。
実際にやってみた様子
以下はspotbugs-gradle-plugin
とspotbugs-annotations
を導入した上で、引数と戻り値にNonnull
/Nullable
アノテーションを付与した様子です。
これによって、利用方法が間違っている個所が有ればSpotBugs
の静的解析により警告が得られるようになります(SpotBugs
の具体的な利用方法については触れません)。
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@Nonnull
public String func(@Nonnull String foo, @Nullable String bar) { /* 略 */ }
ただし、これを行っていても今回紹介した状態ではコンパイルエラーや実行時エラーは発生しません12。
この対策としては、静的解析をCI
等に組み込むこと3、アノテーションの付け忘れをコードレビューで見逃さないよう意識することなどが考えられます。
また、IDE
によってはこの警告をGUI
上にリアルタイムで表示できます。
以下はNonnull
指定した引数foo
にわざとnull
を渡すコードを書き、Intellij IDEA
で警告を表示した様子です。
メッセージの通り、null
禁止の引数にnull
を渡していることが警告されています。
メソッド・クラス・変数を基本的に変更できないようにする
Java
では基本的に全てのものが変更(継承・再代入)可能になっています。
一方、変更が可能ということは、それらの操作を誤って行ってしまう危険性が有るということです。
このため、メソッド・クラス・変数はそれぞれfinal
で修飾して変更できないようにしておく方が安全です。
なお、基本的にfinal
にするとしても、結局は手動で修飾するしかない4ため、こちらも最後はコードレビューでのチェックを徹底する必要が有ります。
ここでは、メソッド・クラス・変数を変更できないようにする方法について紹介します。
メソッド/クラスを変更できないようにする
メソッドをfinal
修飾した場合は継承先クラスでのoverride
が、クラスをfinal
修飾した場合はクラスそのものの継承が出来なくなります。
これによって意図せぬ継承を防ぐことができ、安全になります。
public class Temp {
public final void func() {}
}
// コンパイルエラーになる
public class TempEx extends Temp {
@Override
public void func() { super.func(); }
}
public final class Temp {}
// コンパイルエラーになる
public class TempEx extends Temp {}
変数を変更できないようにする
変数を再代入可能な状態で宣言していると、誤って書き換えてしまう危険性が有ります。
よって、一時変数やフィールドはできるだけfinal
修飾しておいた方が安全です5。
// funcの戻り値をfinalで設定
@Nonnull final String foo = func(bar);
Java
では、他にも引数、拡張for
文の変数、ラムダ式のレシーバーも再代入可能です。
これらもfinal
修飾することで、より安全に取り扱うことができます。
// コンパイルエラーにならない
public void f1(@Nonnull Integer arg) { arg = 0; }
// コンパイルエラーになる
public void f2(@Nonnull final Integer arg) { arg = 0; }
// コンパイルエラーにならない
for (@Nonnull Integer i : list) { i = 0; }
// コンパイルエラーになる
for (@Nonnull final Integer i : list) { i = 0; }
// コンパイルエラーにならない
list.forEach(i -> { i = 0; });
// コンパイルエラーになる
list.forEach((@Nonnull final Integer i) -> { i = 0; });
補足: lombokのvalについて
Java10
より、var
で変数を宣言することで型推論が出来るようになりました。
lombok.val
は、final var
と同等の変数宣言になります。
乱用すると可読性を損なうため注意が必要ですが、これを用いると少なくともローカル変数は一々final
修飾しなくて済むため、安全なコードを書きやすくなります。
import javax.annotation.Nonnull;
import lombok.val;
void func(@Nonnull final String str) {
val foo = str; // final varで宣言したのと同等
foo = ""; // コンパイルエラーになる
}
Collectionを読み取り専用にする
Java
のList
やMap
は基本的にmutable
で、値の追加・削除・置き換えといった処理が自由に行えます。
一方、変更を自由に行える状況では、意図しない書き換えでバグが発生する危険性が有ります。
このため、Collection
は読み取り専用にする方が安全です。
ここでは、Collections.unmodifiable...
を用いる方法と、Collection
をラップするクラスを定義する方法を紹介します。
Collections.unmodifiable...を用いる方法
java.util.Collections
には、値の変更処理を呼び出した際にUnsupportedOperationException
を発生させるラッパークラスと、そのインスタンスを生成するファクトリメソッドが定義されています。
例えばList
をunmodifiable
にする場合以下のように記述します。
import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
@Nonnull List<Integer> unmodifiableList = Collections.unmodifiableList(list);
この方法ではList
などのインターフェースはそのままになるため、外部ライブラリなどで扱いやすいという利点が有ります。
一方、値の変更処理を不用意に呼び出すと実行時エラーになる危険性は残っています。
仮に型レベルで変更を防ぎたい場合、後述するCollection
をラップするクラスを定義する方法が考えられます。
Collectionをラップするクラスを定義する方法
こちらはadd
やremove
といった変更処理を省いたラッパークラスを定義する方法です。
例えば以下のように定義する方法が考えられます。
ライブラリなどに値を渡したり、内容を変更する処理も考慮して、mutable
な値はgetUnsafeValue
で取得できるようにしています。
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Iterator;
/**
* 変更できないCollection
*/
public abstract class ImCollection<E> implements Iterable<E> {
@Nonnull private final Collection<E> rawValue;
protected ImCollection(@Nonnull Collection<E> rawValue) {this.rawValue = rawValue;}
@Override
@Nonnull
public final Iterator<E> iterator() { return rawValue.iterator(); }
/**
* @return 変更するためのCollection
*/
// 継承先で具体的な型を指定するためにabstractで定義している
@Nonnull
public abstract Collection<E> getUnsafeValue();
public int size() { return rawValue.size(); }
public boolean isEmpty() { return rawValue.isEmpty(); }
public boolean contains(E e) { return rawValue.contains(e); }
// 以降Collectionに定義された変更しない操作
}
import javax.annotation.Nonnull;
import java.util.List;
/**
* 変更できないList
*/
public final class ReadOnlyList<E> extends ImCollection<E> {
@Nonnull private final List<E> value;
public ReadOnlyList(@Nonnull List<E> value) {
super(value);
this.value = value;
}
@Override
@Nonnull
public List<E> getUnsafeValue() {return value;}
public E get(int index) { return value.get(index); }
public int indexOf(E e) { return value.indexOf(e); }
public int lastIndexOf(E e) { return value.lastIndexOf(e); }
// 以降Listに定義された変更しない操作
}
実用上は以下のような処理全てを実装する必要が有り、この方法は多少手間がかかりますが、Collection
の操作だけを考えるなら最も安全な方法です。
-
Collection
インターフェースに定義された処理全て -
Stream
のCollector
-
JSON
等へのシリアライズ/デシリアライズ時の変換処理
一致を安全に判定する
Java
で一致を判定する場合、primitive
型とオブジェクト型で異なる方法を用いる必要が有ります6。
また、オブジェクト型の一致判定で両方ともnullable
だった場合、少なくとも片方がnull
でないことをチェックする必要が有ります。
// primitive型同士
boolean isEquals(int i, int j) { return i == j; }
// primitive型とオブジェクト型
boolean isEquals(int i, @Nonnull Integer j) { return i == j; }
// オブジェクト型同士
boolean isEquals(@Nonnull Integer i, @Nonnull Integer j) { return i.equals(j); }
この比較方法の違いとnull
チェックの必要性はバグに繋がる危険性が有ります。
またInteger
等のラッパー型であれば、特定の値の範囲では==
で比較しても正常な結果が返ってくる場合が有るなど、深いバグになりやすいという点で更に高い危険性が有ります。
比較方法の違いやnull
チェックを吸収して安全に比較する方法としては、java.util.Objects.equals
が提供されています。
安全性を重視するのであれば、比較は必ずこちらを用いる形とするのが良いでしょう。
import java.util.Objects;
// a, bの型やnullabilityに関わらず判定できる
final bool result = Objects.equals(a, b);
一々Objects.equals
と書くのが長いようであれば、以下のようなショートハンドを定義し、static import
して利用することも考えられます。
final class Util {
public static boolean eq(@Nullable Object a, @Nullable Object b) { return Objects.equals(a, b); }
}
終わりに
この記事ではJava
で安全性を高めるための工夫について書きました。
書き上げた上で、自分は「例え新規プロジェクトでもこれ全部やるのは難しそうだな」と感じましたがいかがでしょうか?
Kotlin
では、これらの安全性を簡単に実現できます。
明日公開の後編ではこの安全性とJava
で実現した場合と比べた簡単さについて書きますので、お読み頂ければ幸いです。
追記: 後編を公開しました
-
不正が有ればコンパイルエラーにする方法も探しましたが、軽く調べただけでは見つけられませんでした。 ↩
-
例えば
Lombok
の場合、アノテーション処理によってコードにnull
チェックが加えられ、引数がnull
ならその場でエラーになるという効果が有ります。 ↩ -
サラッと言っていますが、特に後付けで導入する場合、これを
CI
に組み込むのは結構大変な作業になると思います。 ↩ -
全て
final
が基本になるようなコンパイラオプションが無いかと探しましたが、Java8
のドキュメントを読んでも見つけられませんでした。 ↩ -
フィールドを
final
にする場合、コンストラクタ等で初期化する形にする必要が有ります。 ↩ -
正確には、オブジェクト型同士を
==
で比較した場合、同一インスタンスか否かの判定結果が返ってきますが、実用上同一インスタンスかの判定は滅多に行わないため、ここでは言及していません。 ↩