null安全とは 実行時にnullが原因のエラー(NullPointerException)を発生させないような仕組みで SwiftやKotlinなど比較的新しい言語では積極的に取り入れられています。比較的新しい言語では言語仕様として取り入れているものが多いのですが Javaでは言語仕様の変更ではなく追加ライブラリという形で取り入れました。Java SE 8でnull安全の仕組みを(部分的に)実現するための Optionalクラスが追加されました。
この章ではnull安全の簡単な説明を行い、続いてOptionalクラスの使い方を見て行きます。
null安全
null安全とは
null安全とは 実行時にnullが原因のエラー(NullPointerException)を発生させないような仕組みです。例えば、次のように オブジェクト参照型の変数がnullの場合にメソッドを呼び出したりするとNullPointerExceptionが発生します。
String str = null;
str.length(); // NullPinterException
Javaの後継候補の1つであり null安全を採用しているKotlinでは 同じようなコードはコンパイルエラーとなります。
val str : String? = null
s.length // コンパイルエラー
String?は nullが許容されるデータ型(nullable)であることを表し、nullチェックを強制します。コンパイルを通すためには 次のように書く必要があります。
var s : String? = null
if(s != null) {
s.length
}
nullチェックをした後は nullではないことが保証されているので nullの可能性のないデータ型(non-null)のStringに変換することができます。Kotlinの場合はスマートキャストという仕組みで 上の例の{}の中はnon-nullであるStringにキャストしたものとして扱ってくれます。non-nullのStringでは lengthの呼び出しでNullPointerExceptionが発生することはありません。
このように null安全を言語仕様として取り入れている言語では、null安全ではない処理を行おうとするとコンパイル時にエラーが出ます。(Pythonのようにコンパイルのない言語では mypyによる静的チェックでエラーを検出できます。)そのため 実行時にnullが原因のエラーが発生するのを防ぐことができます。
null安全による生産性
null安全を導入すると nullチェックに関わるコードを記述しなくてはならず、一見生産性が落ちるのではないかと感じられるかも知れませんが、そのようなことはありません。
nullチェックに関わるコードの記述が必要なのは 大別すると次の2つです。
- nullである可能性のある変数・引数・戻り値のなどのnullチェック。(本来必要)
- 文脈的にnullではないことがわかっているが、nullableなデータ型のため 静的解析ツールやコーディング規約等によりチェックを余儀なくされている。(本来不要)
1については null安全を取り入れていようといまいと必要なコードです。2については 文脈的にnullでないことが分かっているのであれば non-nullのデータ型に変換してしまえば余分なnullチェックは行う必要はありません。
したがって、null安全を取り入れたからといって生産性が落ちるというわけでもなく、むしろコンパイルや静的解析などの段階でエラーを検出することができ 堅牢性を上げることにつながります。
Javaのnull安全
Javaの場合は 言語仕様を変更してnull安全を取り入れるのではなく、Optionalクラスを追加してnull安全を部分的に実現する形が取られました。Optionalクラスがnullableなデータ型という位置づけになるのですが、Javaのnull安全で決定的に欠けているのは non-nullのデータ型がないということです。例えばnullである可能性のある文字列をOptionalでラッピングすると Optional<String>がnullableの位置づけなのですが、Strng自体がnullableなため 更にnullチェックをしないといけません。null安全の仕組みを完全なものにするには、non-nullにあたる参照型にはnullを代入しないように 運用対処をする必要があります。また、運用対処をしてもチェックする手段がないため、漏れがないようにするのは至難の業です。
また、JavaのOptionalは 基本的にメソッドの戻り値として使うことのみを意図されていて 汎用的ではありません。Serializableではなくクラスのフィールドとして使うことも推奨されていません。
更に、既存のコードやライブラリなど 本来nullableである箇所がOptionalになっていない物が多く存在するため、Optionalに対応したライブラリのみを利用するという環境でない限り どうしてもnull安全でないものが混同する形になってしまいます。
このように Javaのnull安全は実用的に欠ける部分が多々ありますが、null安全は今後の主流になっていくと思われるので部分的にでも取り入れてみても良いかも知れません。
Optional
Optionalは ある状況下で値を返さないかも知れないメソッドの戻り値として使用されることを意図しています。Optional<T>は 単一のnullではないT型のオブジェクトを持つか、何も持たない選択型の不変なコンテナです。何も持たないOptionalは 空(empty)と言われます。空ではないOptionalは 値が存在すると言われます。Optionalを返すことによって 呼び出し側に戻り値が空の場合の処理を強制することになり、コードの堅牢性を上げることができます。
呼び出し側に 戻り値が空の場合の処理を強制させるという点は チェック例外に似ていますが、Optionalとチェック例外は次のような点が異なります。
- Optionalは 値を返さないことを表現することはできますが、エラーの理由を表すことができないため、エラーを表現する用途には使えません。(ScalaではEitherでエラーを表現することができます。)
- チェック例外は文で Optionalは式です。チェック例外は try節で発生した例外に対する処理をcatch節に書くため 処理の流れが追いづらいくなります。一方 Optionalはメソッドチェーンで戻り値に対する処理を記述できるため、処理の流れが追い易くなります。
以降の節では、まず メソッド呼び出しの結果 Optionalが返された場合の処理の仕方を見ていきます。続いて、Optionalを返すメソッドの実装の仕方について見ていきます。
Optional が返された場合の処理の仕方
メソッド呼び出しの結果、Optionalが返された場合の処理の仕方を見ていきます。Optionalが登場する以前のコードとOptionalを使ったコードを併記して比較してみます。
この章の例では 汎用的なOptional<T>を使っていますが、int値をOptionalでラップする場合は 性能面を考慮するとOptionalIntを使った方が良いです。OptionalIntについては後述します。
値が存在する場合だけ処理を行う
始めに メソッドの戻り値がnullや空でないことを確認して、副作用を起こす処理(Consumer)を実施する場合の例です。
// Optional を使わない場合。getString()はnull を返す可能性のあるメソッド
String str1 = getString();
if(str1 != null) {
System.out.println(str1.toLowerCase());
}
// Optional を使う場合。getNullableString()はOptional<String>を返すメソッド
Optional<String> str2 = getNullableString();
str2.ifPresent(s -> System.out.println(s.toLowerCase()));
Optionalを使わない場合は if文でnullチェックを行い分岐処理を行いますが、Optionalを使う場合は OptionalのifPresent()メソッドにConsumerを渡します。Optionalが空の場合は Consumerの処理は実施されません。
Optionalが空の場合は何も行われないため、空の場合にエラー処理やログ記録をしたいような場合には使えません。そのような場合は 後述のmap()やflatMap()を使う必要があります。
空の場合はデフォルト値を採用する
続いて、戻り値がnullまたは空の場合は デフォルト値を採用する場合の例です。
// Optional を使わない場合
String str1 = getString(); // null を返す可能性のあるメソッド
if(str1 == null) {
str1 = "default value";
}
// 条件演算子を使っても OK
//str1 = (str1 == null)? "default value" : str1;
// Optional を使う場合。getNullableString()はOptional<String>を返すメソッド
String str2 = getNullableString().orElse("default value");
String str3 = getNullableString().orElseGet(() -> "default value"); // 遅延評価
Optionalを使わない場合は if文でnullチェックをして分岐処理を行います。または、条件演算子を使って1文で記述することもできます。
一方で Optionalを使う場合は OptionalのorElse()メソッドでデフォルト値を指定します。Optionalが空の場合はデフォルト値を返します。引数にSupplierを渡すorElseGet()メソッドも用意されていて、値を返すだけでなく何らかの処理を行うこともできます。こちらは遅延評価となり 値が存在する場合はSupplierは呼び出されません。例えば Supplierが高コストな処理を行う場合などに orElseGet()を使うメリットが出てきます。
値を変換する(Optionalでラッピングする)
続いて、メソッドの戻り値を変換したり 戻り値のオブジェクトのメソッドを呼び出して別の値を返すような場合の例です。
// Optional を使わない場合
String str1 = getString(); // null を返す可能性のあるメソッド
int len1 = 0;
if(str1 != null) {
len1 = str1.length();
}
//len1 = (str1 != null)? str1.length() : 0; // 条件演算子を使う例
// Optional を使う場合。getNullableString()はOptional<String>を返すメソッド
int len2 = getNullableString().map(s -> s.length()).orElse(0);
Optionalのmap()メソッドには Function<T, U>を渡します。Optionalに値が存在すれば、Function<T, U>を適用した結果の値をOptionalでラッピングして返します。Optionalが空であれば 何もせずにその空のOptionalを返します。map()は Function<T, U>を適用した結果を Optionalでラッピングする点がポイントです。
map()の戻り値はOptionalになるので、続けて前述のorElse()やorElseGet()を呼び出すことも可能です。
値を変換する(Optionalでラッピングしない)
Optionalを返すメソッドの結果と 別のOptionalを返すメソッドの結果を掛け合わせる場合に、単純にmap()を使うと Optional<Optional<T»とOptionalのネストになってしまいます。
Optional<Integer> width = getWidth();
Optional<Integer> height = getHeight();
Optional<Optional<Integer>> area = width.map(w -> height.map(h -> w * h));
int result = area.orElse(Optional.of(-1)).orElse(-2);
このように Optionalのネストにならないように、Optionalへのラッピングを行わない flatMap()メソッドが用意されています。
Optional<Integer> width = getWidth();
Optional<Integer> height = getHeight();
Optional<Integer> area = width.flatMap(w -> height.map(h -> w * h));
int result = area.orElse(-1);
flatMap()メソッドはOptionalへのラッピングを行わないため、関数を適用した結果がOptionalでない場合は 自前でOptionalへのラッピングを行う必要があります。(flatMap()の戻り値はOptionalなのでOptionalにラッピングしないデータを返すことはできず コンパイルエラーになります。)
map()をflatMap()で書き換えると、次のようになります。
// map()を使う場合。
int len1 = getNullableString().map(s -> s.length()).orElse(0);
// flatMap()で書き換えた場合。
int len2 = getNullableString().flatMap(
s -> (s == null)? Optional.<Integer>empty() : Optional.of(s.length())).orElse(0);
flatMap()もmap()と同様に、メソッドチェーンでorElse()やorElseGet()を呼び出すことができます。
条件判定
Optionalクラスには その他にPredicate<T>を渡す filter()メソッドもあります。filter()は 条件に一致すれば自身を返し、一致しなければ空のOptionalを返します。
isPresent()
Optionalには値が存在するか それとも空かを判定するための isPresent()メソッドが用意されています。これにより値が存在する場合と空の場合の分岐処理を行うことができますが、isPresent()を用いた分岐処理は たいていは これまでに紹介してきたメソッド群で簡潔に置き換えることができます。これは Optionalがモナドであるためです。Optionalとモナドについては、この章の「Optionalとモナド」を参照してください。
Optionalに慣れないうちは isPresent()の利用を思いついてしまうかも知れませんが、たいていは 他のメソッドで簡潔に置き換えられるので、一考してみてください。
Optional を返すメソッドの作成
Optionalを生成するには、OptionalのクラスメソッドofNullable()を使用します。引数で渡した値を持つOptionalを生成します。引数にnullを渡すと空のOptionalを生成します。
似たようなクラスメソッドでof()もありますが、こちらはnullを渡すとNullPointerExceptionが発生するので、通常はofNullable()の方を使います。
Optionalを返すメソッドにおける 最大の注意点は、絶対にnullを返さないということです。Optionalはオブジェクト参照型であるため nullを返すこともできてしまいますが、nullを返してしまうと 戻り値をOptionalにしている意味が無くなってしまうため nullを返してはいけません。
また、コレクション・配列・ストリームのように「空」を表現できるコンテナ型を戻り値として返す場合は Optionalでくるまずに 空のコレクション・空の配列・空のストリームを返すようにします。
プリミティブ型用のOptional
値を返さないかも知れないメソッドの戻り値の型が プリミティブ型の場合、ラッパークラスでラッピングして 更にOptionalでラッピングするため、二重にラッピングされることになります。性能が重要な場面において この二重のラッピングおよび取り出しのコストを少しでも減らせるように、int・long・double型用のOptionalクラスが用意されています。それぞれ OptionalInt、OptionalLong、OptionalDoubleです。int・long・doubleをOptionalでラッピングしたい場合は Optionalではなく これらのプリミティブ型用のOptionalクラスを使うようにします。
Optionalとモナド
Optionalはモナドです。モナドは数学の圏論を取り入れたもので 関数型言語で良く用いられる概念です。モナドについての詳細は関数型言語の入門書などを参照してください。大雑把に言うと モナドは次のような性質を持っています。
- モナドはコンテナ構造で、中身を格納したコンテナを生成する手段を提供しています。
Optionalでは of()やofNullable()クラスメソッドが該当します。 - コンテナから中身を取り出さないまま 中身に関数等の操作を適用する手段を提供しています。
言い換えると、コンテナから中身を取り出し、中身に関数等の操作を適用し、再度コンテナに格納する手段を提供しています。
Optionalでは map()が該当します。 - コンテナがネストする場合に ネストを解消する(フラットにする)手段を提供しています。
Optionalではネストを解消するだけの手段を提供していませんが、代わりに コンテナから中身を取り出し、中身に関数等の操作を適用し、再度コンテナに格納しない手段を提供しています。
Optionalでは flatMap()メソッドが該当します。
Optionalは 値が存在するか または空の選択型のコンテナです。Javaにおける Optional以外のモナドについては、この章の「ストリーム・コレクションとモナド」で触れます。
また、モナドとは別に、空のOptionalに対して関数等の操作を適用しても 空のOptionalがそのまま返るというのも重要な特徴の一つです。
もし map()が無い場合は、Optionalの中身が存在するかしないかで処理を分岐する必要があります。Optionalの中身を2乗して返したい場合に map()を使わないと 次のようにする必要があります。
public static Optional<Integer> square(Optional<Integer> param) {
if(param.isPresent()) {
int tmp = param.get(); // 値を取り出す。
return Optional.of(tmp * tmp); // 中身に関数を適用してOptionalでくるむ。
} else {
return Optional.empty();
}
}
public static void main(String[] args) {
System.out.println(square(Optional.of(2))); // Optional[4]
System.out.println(square(Optional.empty())); // Optional.empty
}
map()を使うと square()メソッドは次のように簡潔に置き換えることが出来ます。
public static Optional<Integer> square(Optional<Integer> param) {
return param.map(tmp -> tmp * tmp);
}
これがモナドの威力です。中身の有無で分岐したり、Optionalから中身を取り出したり、再度Optionalで包むといった処理を記述する必要がありません。
flatMap()の例も見てみます。2つのOptionalの乗算を返すメソッドの例です。flatMap()を使わないと 次のようになります。
public static Optional<Integer> multiply(Optional<Integer> x, Optional<Integer> y) {
Optional<Integer> result;
if(x.isPresent()) {
if(y.isPresent()) {
result = Optional.of(x.get() * y.get());
} else {
result = Optional.empty();
}
} else {
result = Optional.empty();
}
return result;
}
public static Optional<Integer> multiply2(Optional<Integer> x, Optional<Integer> y) {
return x.flatMap(xx -> y.map(yy -> xx * yy));
}
public static void main(String[] args) {
System.out.println(multiply(Optional.of(2) , Optional.of(3))); // Optional[6]
System.out.println(multiply(Optional.empty(), Optional.of(3))); // Optional.empty
System.out.println(multiply(Optional.of(2) , Optional.empty())); // Optional.empty
System.out.println(multiply(Optional.empty(), Optional.empty())); // Optional.empty
}
flatMap()を使うと multiply()メソッドは次のように簡潔に置き換えることができます。
public static Optional<Integer> multiply(Optional<Integer> x, Optional<Integer> y) {
return x.flatMap(xx -> y.map(yy -> xx * yy));
}
ストリーム・コレクションとモナド
モナドがコンテナ構造であることから、ストリームやコレクションも同様ではないかと思われるかも知れません。ストリームについてはその通りで、of()・map()・flatMap()と言ったメソッドを提供していて、Optionalと同じように ストリームから要素を取り出さないまま、要素に関数等の操作を適用することができます。Optionalとストリームで同じ名前のメソッドを持っているのは モナドの概念によるものです。
一方、同じコンテナでもコレクションは map()やflatMap()と言ったメソッドは持っていません。コレクションはモナドの概念が適用できるものであり、Scalaではコレクションライブラリに map()やflatMap()といったメソッドが用意されていて、モナドの性質を持っています。
Javaではコレクションがモナドになっていないのは、次のような理由からではないかと考えられます。Javaにおける モナドの概念を含む関数型言語の特徴は 後から取り入れたもので、既存のコレクションにこれらのメソッドを追加するにはデフォルトメソッドとして追加する必要があり、既存のコードに影響が出る可能性がありました。Collectionのstream()メソッドで 容易にコレクションからストリームを生成することができることもあり、既存のコレクションには手を加えないという判断をされたのではないかと考えられます。