「スゴく大事な製品コードでいきなりIllegalArgumentExceptionが出てアプリケーション落ちちゃったよ!」
悪いのは呼び出し側なのに、と思いつつ、仕方なくリスト4のようなコードに変更します(*1)。
*1)nullが渡されるべきではない場合に、nullチェック後の処理としてどうすべきかはさまざまな意見があるようです。前述のように例外を投げる実装も選択肢の1つです。
void doSomething(ParamObj obj){
if(obj == null){
return; //何もせず終了
}
……
}
リスト4 nullを渡されたら黙って何もしないことに
これで呼び出し元から怒られることがなくなって一件落着……と言えるほどすっきりはしませんね。実装者として何とも釈然としない思いが残ります。本来「nullが渡されるケースは設計上意味を持たない」にもかかわらず、万が一のNullPointerExceptionを避けるためだけに、nullが渡された場合の処理を何かしら書かなければならないのです。
ここに示したのはあくまで氷山の一角ですが、Javaプログラマーは参照型変数を使うたびに「もしかしたらこれはnullかもしれない」と疑心暗鬼になりながら、nullチェックコードを山のように積んでいかなければなりません。
もちろん、nullが常に悪者というわけではありません。例えばリスト5のようなコードでは、nullチェックが重要な役割を果たします。
public class Singleton{
Singleton instance;
public Singleton getInstance(){ //唯一のインスタンスを取得するメソッド
if(instance == null){ //nullチェックして
instance = new Singleton(); //nullなら新しいインスタンスを作る
}
return instance; //インスタンスを返す
}
private Singleton(){} //外部からインスタンスを作れないようにする
}
リスト5 Singletonコード例
これはSingletonパターンと呼ばれる「あるクラスのインスタンスが常に1つであることを保証する」デザインパターンの"伝統的"な実装例です(*2)。ここでは「instanceフィールドがnullである場合はインスタンスを生成して返し、そうではない場合はそのままinstanceフィールドを返す」実装になっています。
*2)この書き方はマルチスレッド動作の際に誤動作する可能性があるため現在では推奨されません。あくまでもnullチェックが明確に意味を持つ例としてご覧ください。
先ほどのコードとの違いは何でしょうか? いろいろな説明の仕方がありますが、例えば前者は「そもそも変数がnullであってはならない(=nullであるケースに意味がない)のに、言語レベルでnullを排除できないため、やむなく自前でnullチェックを行っている」のに対し、後者は「ある変数がnullであることに意味があるので、nullチェックで分岐して処理している」ケースと言い換えられるかもしれません。
このNull参照は多くの問題をもたらしました。Null参照の発明者とされるアントニー・ホーア氏自身が2009年に「それ(Null参照)は10億ドルにも相当する私の誤りだ」と述べたほどです。
参考「Null References: The Billion Dollar Mistake」
このNull参照につきまとう問題を解決するため、さまざまな努力が払われてきました。Kotlinを含め、Swift、TypeScript 2といったモダンな言語では、言語レベルでnull安全を保証しています。Java自身もJava 8で「Optional」というクラスを追加し、プログラムの堅牢性を高め、煩雑な記述を緩和する方向へと動いていますし、C#は今後のバージョンで、言語レベルでのnull安全を保証することを検討しているようです。
Kotlinのnull安全の仕組み〜「非Null型」と「Null許容型」の下克上!?〜
やや枕が長くなってしまいましたが、いよいよKotlinのnull安全の仕組みに入っていきましょう。Kotlinでは言語自体にnull安全の仕組みが用意されており、「ある変数がnullではないことを言語レベルで保証」できます。そのアイデアは実にシンプルなもので、「nullを許容する参照型とnullを許容しない参照型を明確に分ける」というものです。
リスト6を見てみましょう。
var a: String = "はろー" //String型の変数
a = null //コンパイルエラー!?
リスト6 String型にnullを代入……できない
Javaプログラマーであれば、このコードがコンパイルできないのは不可解に思えるでしょう。しかし、Kotlinにおいては2行目のnull代入でコンパイルエラーが発生し、このコードはコンパイルできません。nullを代入したいなら、リスト7のように書く必要があります。今度は、コンパイルエラーは発生しません。
var b: String? = "はろー"
b = null //String?型ならばnullが代入可能
リスト7 String?型にはnullを代入できる
Kotlinにおいては「型名」だけで宣言した変数はnullを許容しない「非Null型」(Non-Null Type)として扱われ、「型名?」のように型名の後に「?」を付けることで「Null許容型」(Nullable Type。Javaの参照型はこちらに該当)として扱われます。つまり「新たに非Null型という特別な型を作る」ことをせず、「非Null型が特殊なのではなく、むしろNull許容型の方が特殊なのだ」という逆転の発想で「基本は非Null型を使って、nullが必要な場合にだけNull許容型を使ってください」ということにしたのです。
この発想の転換は見事で、「nullを前提にした世界」を「明示しない限りnullがない世界」に作り替えてしまったといっていいでしょう。
このため、nullを扱う必要がないなら、非Null型だけを使ってプログラムを書くようにしましょう。そうすればNull参照に苦しめられることはありません。例えば、前述のJavaで取り上げたケースでは、引数のobjを非Null型にするだけで問題が解決します。
Null許容型は、いきなり参照できない
とはいえ、nullが必要になる場面が現実的には多々存在するので、ここからはNull許容型の使い方を見ていきましょう。リスト8のように、Null許容型のプロパティにアクセスすると、コンパイルエラーが発生します。
var a: String = "はろー"
println(a.length) //非Null型の参照は当然問題なし
var b: String? = "はろー"
println(b.length) //Null許容型を参照するとコンパイルエラー
リスト8 Null許容型のプロパティにアクセスするとコンパイルエラー
すぐ上の行で変数bにnullではない文字列を代入していますので、本来このコードは動くはずですが、コンパイラは「いきなりNull許容型を参照することはまかりならぬ」として通してくれません。また、リスト7のように、「Null許容型変数を非Null型変数に代入する」こともできません。
var b: String? = "はろー"
var c: String = b //コンパイルエラー
リスト9 Null許容型変数を非Null型変数に代入するとコンパイルエラー
何と、String型とString?型は全く別の型で互換性がありません(*3)。
*3)StringはString?の子クラスのような扱いになるため、逆に「Null許容型変数に非Null型変数を代入する」場合は問題なく動作します。
何とも融通が利かないと感じるかもしれませんが、コンパイル時のこの厳密な型チェックが非常な安心感をもたらしてくれるのです。
明示的なnullチェック
Null許容型を参照したり、非Null型に代入したりしたい場合は幾つかの方法があります。まず、リスト10のように明確にnullチェックをする方法です。nullチェックさえ済んでしまえば、参照も非Null型への代入も自由に行えます。
var b: String? = "はろー"
if(b != null){ //nullチェック
println(b.length) //参照できる
var c: String = b //非Null型に代入できる
}
//nullチェックしたifのスコープから外れているためコンパイルエラー
println(b.length)
when{ //whenでnullチェックしてもOK
b != null -> println(b.length)
}
リスト10 Null許容型をnullチェックするとエラーなく参照、非Null型に代入できる
?.演算子
Null許容型を参照するための別の方法は、リスト11のように「?.」という演算子を使う方法です。
var d: String? = null
println(d?.length) //エラーは起きない。「null」が表示される
var e: String? = "はろはろ"
println(e?.length) //「4」が表示される
リスト11 ?.演算子を使って参照
?.演算子(safe call operator。訳語は安全呼び出し演算子、セーフコール演算子など)は、「第1項(演算子の前)がnullの場合はnullを、そうではない場合は第2項(演算子の後)の結果を返す」演算子です。リスト11では、変数の値がnullであってもなくても、コンパイルエラーもNullPointerExceptionも発生することなく、その値を参照できています。ここではプロパティを参照していますが、メソッド呼び出しも同様に可能です。
?.演算子の効能を見てみましょう。リスト12は、深いオブジェクト階層にある値を参照する例を、JavaとKotlinで書いたものです。
value = null;
//nullチェックが多重に必要
if(obj != null){
if(obj.param1 != null){
if(obj.param1.param2 != null){
value = obj.param1.param2.param3;
}
}
}
↓
//Kotlinで同じことをする。?.演算子を連ねれば1行で完了
value = obj?.param1?.param2?.param3
リスト12 深い階層の参照をJavaとKotlinで比較
もはや語るまでもないという印象ですね。Javaだと多重のNullチェックが必要なのに対し、Kotlinでは1行で書けます。連続して呼び出している?.演算子のどこかで結果がnullになったとしても、以降の?.演算子は自動的にnullを返すわけです。
ちなみに、C#ではNull条件演算子と呼ばれる、同じ記法(?.)の演算子があり、使い方もほぼ同じなので、C#プログラマーにとっては、すっかりおなじみの演算子でしょう。
?:(エルビス)演算子
?:(エルビス)演算子(*4)は、「第1項がnullではない場合には第1項を、nullの場合には第2項を返す」演算子です。リスト13では、Null許容型を返すgetName関数の戻り値を変数に代入しています。もし戻り値がnullだった場合には?:演算子の後ろの「名無しの権兵衛」という値が設定されます。
*4)不思議な名称ですが、「?:」がエルビス・プレスリーの顔文字に似ているから、だそうです……。
//getName関数はNull許容型のString?を返す
fun getName(): String?{……}
……
//?:演算子でgetName関数の結果がnullだった場合の値を設定
val name2 :String = getName() ?: "名なしの権兵衛"
println(name2)
リスト13 ?:演算子を使ってnullだった場合の値を設定
何かのデフォルト値を設定する場合などに使えそうですね。なお、C#ではnull合体演算子(記法は??)という、?:演算子に似た機能が利用可能です。
!!演算子
この他、NullPointerExceptionが忘れられない人のために「nullチェックしていなくても、Null許容型を無理やり非Null型として扱う」!!演算子という演算子も用意されています。これを使えばリスト14のようにKotlinにおいてもNullPointerExceptionを実現(?)できます。ただ、あえてnull安全の世界から飛び出すこの演算子、明確な用途がない限り普段は使うことはなさそうですね。
//!!演算子を使ってnullの入ったNull許容型を無理やり非Null型に変換して参照
var f: String? = null
//コンパイルはできるが、実行時にめでたくNullPointerException発生
println(f!!.length)
リスト14 !!演算子を使って無理やりNullPointerExceptionを発生させる
コラム「Null許容型のプリミティブ型」
本文ではNull参照問題を取り上げるため、主に参照型のケースを扱いましたが、Javaのプリミティブ型に相当するInt、Longといった型においても、Null許容型を使えます。もともとInt、Longなどにnullを代入することはできませんが、参照型の場合と同様にInt?、Long?のように「?」を末尾に付けることで、Null許容型の変数として扱うことが可能です。詳しくはレファレンスなどをご確認ください。
次回は、Kotlinのクラス関連機能について
今回はnull安全について熱く語ってみましたが、いかがだったでしょうか。nullが不要なケースでは全く意識することなく書けますし、nullをハンドリングする必要がある場合でもnull安全のメリットは大きいです。Javaではnullチェックを忘れると実行時にNullPointerExceptionが投げられますが、Kotlinであればnullチェックを忘れてもコンパイル時に気付くことができるのです。
こうした言語レベルでのnull安全がごくシンプルなアプローチで出来上がっていることに魅力を感じていただけならば、ぜひKotlinを使って体感してみてください。nullの落とし穴がどこにあるか分からない世界にはもう戻れなくなるかもしれません……。
次回以降はKotlinにおけるクラス関連のもろもろの機能を解説します。お楽しみに。
1行目はタイトルです(この行にタイトルとなる文字を書き込んで下さい)
2行目から本文です。