プログラマに優しい現実指向JVM言語 Kotlin入門

第5回null安全

前回はKotlinにおけるクラスとその周辺の機能、文法を紹介しました。今回はKotlinのユニークな機能であるnull安全について解説します。

背景

java.lang.NullPointerExceptionは、Javaプログラマがよく出会う例外でお馴染みです。オブジェクトが必要な場面でnullを使用してしまうことでスローされる例外です。具体的にはString型の変数にnullを代入しておき、その変数に対してlengthメソッドを呼び出した場合にNullPointerException⁠ぬるぽ」の愛称で親しまれる例外(以下、NPEと表記)が投げられます。

nullは、値が存在しないときに使用されます。たとえば、指定したIDを持ったユーザが存在しないときにfindUserByIdのようなメソッドがUserクラスのインスタンスを返す代わりにnullを返すと言った具合です。

このような観点で、nullは便利に働きます。しかしこのnullのおかげで筆者たちは見たくもない例外、NPEと遭遇するはめになるのです。適切にnullチェック、つまりif文などでnullでないことを確認すれば回避できるのですが、なぜそれができないのでしょうか。

nullを返さないことがわかっているメソッドの戻り値に対してnullチェックはしないのが普通だと思います。ここが重要なのですが、仕様的にnullを返し得ないメソッドがあっても、Javaのコードでそのことを保証することはできません。Javaにおいて、変数やメソッドの戻り値はいつでもどこでもnullになり得ます。つまりnullチェックをすべきものと、nullチェックが不要なものがごちゃまぜになっているので、うっかりNPEを招いてしまうのです。

nullと上手に付き合う方法

nullかもしれないものと、nullではないものを区別するための方法が世の中にはいくつかあります。

メソッドシグネチャの工夫

原始的な方法です。nullを返す可能性があるメソッドのシグネチャを工夫して、プログラマに注意喚起します。たとえばgetNameOrNullのような名前のメソッドです。名前を見ればnullが返されるかもしれないことに気づくわけです。

静的解析ツール

メソッドにアノテーションを付けて、静的解析ツールに指摘してもらう方法です。getNameメソッドがnullを返すかもしれない場合には@Nullable String getName() {...}と記述し、nullを返し得ない場合には@Nonnull StringgetName() {...}と記述します。

型で表現

存在しない可能性のある値を表現するためにnullの代わりに新しく定義した型を使う方法です。具体的にはJava SE 8で導入されたjava.util.Optionalクラスです。値が存在しないときにはOptional#emptyで返されるオブジェクトを使用し、値が存在するときはその値をOptional#ofの引数に渡してラップします。Optional型とそれ以外の型で、存在しないかもしれない値と絶対に存在する値の区別が容易になるだけでなく、Optionalにはさまざまな便利なメソッドが提供されています。

Kotlinのnull安全

静的解析ツールやOptionalを使うことはとても良いことです。しかし繰り返しになりますが、Javaにおいて変数やメソッドの戻り値はいつでもどこでもnullになり得ます。すべてを台無しにするコードをご覧くださいリスト1⁠。

リスト1 誰にもnullは止められない!
// Javaコードです
@Nonnull
Optional<String> getName() {
  return null;
}

そこでKotlinのnull安全機構の登場です。Kotlinではnullの可能性のある値(以下Nullable)nullではない値(以下NotNull)の区別を言語組込みの機能としてサポートします。

Optionalを使う方法とは異なり、新しいインスタンスの生成(とGC)が不要なのでその分のオーバーヘッドがなく、Androidなどのリソースが限られた環境で有利のようです。

基本的な使い方

前回、前々回と使用してきたごく普通の変数初期化と再代入をリスト2に示します。変数aString型です。ここでは型を明示していますが省略しても問題ありません。varキーワードにより変更可能な変数として宣言しているので"Goodbye"を代入できます。しかしその次の行のnullを代入する部分でコンパイルエラーが起こります。変数aはNotNullとして宣言されているのでnullの代入をコンパイラが許しません! 逆を言えば、変数aは常にnullではないと安心して使用できます。

リスト2 NotNullの変数
var a: String = "Hello"
a = "Goodbye"
a = null // ここでコンパイルエラー

では、nullを代入できるNullableな変数はどのように宣言すれば良いのでしょうか。簡単です。通常の型アノテーションのあとに?を置くだけです。リスト3を見てください。変数bの型アノテーションがString?になっています。これはnullが代入可能なString型」と読めます。2行目でnullを代入していますが、コンパイルに成功します。今回の場合、変数bの型アノテーションを省略できないことに注意してください。なぜなら、"Hello"String?ではなくStringとして推論されるからです。

リスト3 Nullableの変数
var b: String? = "Hello"
b = null

KotlinではNullableとNotNullを明確に区別することがわかりました。NotNullの変数にはnullが入ってこないので、これを扱ううえでNPEは起こらないので安全です。Nullableの変数にはnullが入る可能性があるのでNPEが起こりそうです。ということでNPEを起こしてみましょうリスト4⁠。

リスト4 NPEを起こしたい
val s: String? = null
s.length() // ここでコンパイルエラー

String?な変数snullを代入して初期化しています。このslengthメソッドを呼び出してNPEを起こそうとしています。が、実際にはコンパイルエラーとなります。KotlinはNPEを起こさせたくないので、NPEの可能性のある操作をコンパイルエラーとするのです。Nullableに対するメソッド、プロパティアクセスは禁止されています。

しかし現実問題、Nullableのメンバにアクセスできないのは不便どころか使い物になりません。もちろんアクセスする方法が用意されています。その方法とは、nullチェックすることです!

リスト5のように変数snullでないことを確認すると、それが保証される範囲内(ifのブロック内)sをNotNullとして扱えるようになります。s"Hello"が代入されていればリスト5を実行すると「5」と出力されます。

リスト5 nullチェックするとNotNullになる
if(s != null) {
  println(s.length())
}

Nullableの便利な機能

KotlinのNullableを使ううえで必要な知識は、前節までの内容でまかなえます。ですがnullチェックをするコードを書くのは退屈で面倒な作業ですので、KotlinはNullableを便利に使える機能を提供しています。

安全呼び出し

リスト5ではNullableのメソッドを呼び出すためにnullチェックを行いました。これを簡潔に記述できるように、安全呼び出し(Safe Call)と呼ばれるしくみがあります。

リスト6の最初の行ではString?型の変数slengthメソッドを安全に呼び出しています。通常のメソッド呼び出しと異なるのは、ドットの前に?を置くことです。これにより安全呼び出しとなり、Nullableのメソッドを安全に、すなわちNPEの心配なく呼び出すことができます。

リスト6 安全呼び出し
// 安全呼び出し方式
val length1: Int? = s?.length()
// nullチェック方式
val length2: Int? = if(s != null) s.length() else null

仮にsnullだった場合には、メソッドを呼び出さずnullを返します。1行目の安全呼び出し方式と、2行目のnullチェック方式は等価です。

安全呼び出しはメソッドチェーンを形成したい場合などではとくに効果を発揮しますリスト7⁠。foo()?.bar()?.baz()のように記述した場合、途中でnullが返されても安全呼び出しがチェーンして最終的にnullが返されるだけです。

リスト7 安全呼び出しでメソッドチェーン
// 安全呼び出し方式
val result1 = foo()?.bar()?.baz()
// nullチェック方式
val foo = foo()
val result2 = if(foo != null) {
  val bar = foo.bar()
  if(bar != null) bar.baz() else null
} else {
  null
}

デフォルト値

デフォルト値、nullだった場合に使用する値、を簡単に指定できます。Nullable のあとに続けてエルビス演算子?:とデフォルト値を記述します。リスト8を見てください。s?.length()は安全呼び出しにより、文字列の長さかnullが返されます。nullが返された場合、デフォルト値として0を使用するように指定しています。nullでない場合、デフォルト値は評価されません。

リスト8 デフォルト値
// エルビス演算子
val length1: Int = s?.length() ?: 0
// nullチェック
val len: Int? = s?.length()
val length2: Int = if(len) len else 0

禁断の!!演算子

最後に紹介するのは禁断の演算子です。

!!演算子は、Nullableの直後に置き、強制的にNotNullへの変換を試みます。リスト9ではString?である変数s!!演算子により強制的にStringに変換しています。

リスト9 強制的にNotNull化
val s: String? = "Hello"
println(s!!.length()) // => 5

この例はたまたまうまく行きました。しかしリスト10は実行時に例外を投げてクラッシュします。nullであるものに対して!!演算子を使うとKotlinNullPointerExceptionを投げます。

リスト10 実行時例外が起こる
val s: String? = null
println(s!!.length())

nullの場合に例外が投げられる。これって結局今までと同じです。!!演算子を使用したくなったらnullチェックや安全呼び出し、デフォルト値の使用を検討してください。どうしてもやむを得ない場合に限り!!演算子を使いましょう。その際にはコメントとして使った理由や経緯を記しておくと良いでしょう。

1つ、!!演算子を使いたくなるような例を示します。要素としてnullを許容するリストList<T?>から、nullを排除してNotNullな要素だけの新しいリストList<T>を得るための関数がほしいとします。その場合の実装はリスト11のようになります。

リスト11 自作filterNotNull
fun <T> filterNotNull(list: List<T?>): List<T> =
  list.filter { it != null }
      .map    { it!! }
val a: List<String?> = listOf("foo", null, "bar")
val b: List<String> = filterNotNull(a)
println(b) // => ["foo", "bar"]

list.filter { it != null }により、要素がnull以外のものに絞り込みます。しかしリストの型は依然List<T?>のままです。そこで次のmap { it!! }で強制的に要素の型をT?からTの変換しています。

実際には!!演算子を使用せずに実装できますし、filterNotNullはコレクションの標準メソッドとして提供されています。

標準拡張関数 let

Kotlinの標準ライブラリとして、任意の型に対してletという拡張関数[1]が提供されていますリスト12⁠。

リスト12 標準拡張関数let
fun <T, R> T.let(f: (T) -> R): R = f(this)

letは、レシーバとなるオブジェクトthisに、引数に取る関数fを適用しているだけです。使用例をリスト13に示します。

リスト13 letの使用例
5.let {
  println(it * 3) // => 15
}

関数リテラルに渡る唯一の引数(暗黙の変数itletのレシーバと同一オブジェクトですので、この例ではit5です。

さて、この単純な関数は何の役に立つのでしょうか。ずばり、NullableにNotNullな引数を取る関数を適用するときに便利です。具体的に見て行きましょう。

リスト14で、NotNullなIntを引数に取る関数succを定義しました。Nullableである変数aを、この関数の引数として渡したいです。しかしNullableとNotNullの違いがあるので、素直には渡せません。succは関数であり、Intのメソッド(あるいは拡張関数)ではないので安全呼び出しも使えません。となるとifnullチェックしてNullableを安全にNotNullとして扱えるようにするしかありません。

リスト14 NotNullを受け取る関数を適用したい
// NotNullを受け取る関数
fun succ(n: Int): Int = n + 1
// Nullableな変数
val a: Int? = 3
val b: Int? =
  if(a != null) succ(a)
  else null

println(b) // => 4

ここでletの登場です。まず、letは任意の型の拡張関数ですからa?.let {...}のような安全呼び出しができます。そしてletの引数となる関数(リスト12におけるfが受け取る引数は、レシーバと同じ型のNotNullです。レシーバがnullのときはlet拡張関数は呼び出せないので理に適っていますね。

ということでletを使うことでリスト14のsucc適用の部分はリスト15のように書き換えられます。ちなみにリスト15はもっと簡潔に記述できます。第3回で紹介した定義済み関数の参照を得るスタイルで記述するとリスト16のようになります。

リスト15 letを使うとすっきりする
val b: Int? = a?.let { succ(it) }
リスト16 関数参照でよりすっきり
val b: Int? = a?.let(::succ)

JavaですでにOptionalに親しんでいる人には、letはOptionalにおけるmap、flatMap、ifPresentの3役こなす存在だと思っていただけると理解しやすいでしょう。

Javaからコードを呼び出す

KotlinからJavaコードを呼び出す場合、NotNull/Nullableの扱いが特殊になります。リスト17のようなhelloメソッドをKotlinから呼び出して結果を変数に代入する際、val msg= Sample.hello("World")のように型アノテーションを省略した場合、変数msgはNotNullとしても、Nullableとしても扱える型となります。そのためmsg.length()もmsg?.length()もコンパイラは許可します。もしmsgnullであればmsg.length()はNPEを起こします。

リスト17 Javaからコードを呼び出す
// Javaコードです
public class Sample {
  public static String hello(String name) {
    return "Hello, " + name + "!";
  }
}
// Kotlinコードです
val msg = Sample.hello("World")
println(msg.length()) // => 13

型アノテーションを明示することでNotNull/Nullableを表明できます。val msg: String = Sample.hello("World")とすれば、msgはNotNullとして扱えます。もしString.hello("World")nullを返すようなことがあれば、NotNullを表明しているmsgに代入する時点でjava.lang.IllegalStateExceptionを投げます。これは例外を投げるタイミングが早いという観点で、msgの型アノテーションを省略したうえでNotNullとして扱うより幾分マシです。安全側に倒すならval msg: String?とNullableを表明するとよいでしょう。

ただし、Javaのコンストラクタ、intbooleanなどのプリミティブ型を返すメソッドが返す値は、Kotlinでは常にNotNullとなります。

まとめ

今回はKotlinのユニークな機能であるnull安全に関して、そのモチベーションから文法、活用方法まで紹介しました。

少なくとも現時点のJavaではNotNullとNullableを厳格に区別することはできません。Kotlinではこの区別を厳格にすること、さらにNullableの扱い方をとことん注意深くすることでNPEとの決別を図っています。Nullableはそのままでは扱えないことが多く不便に思われがちですが、nullチェック後にNotNullとして使えることや、安全呼び出し、デフォルト値など言語組込みのサポートにより簡単に扱えるようになっています。標準ライブラリとして提供されているlet拡張関数を組み合わせればさらにnull安全ライフが楽しいものとなります。

いよいよ次回はKotlinによるAndroidプログラミングについて解説します。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧