【Null安全】Kotlin Java比較メモ 正しいNull安全の使い方

なんの記事?

Javaで書くとこうなるものってKotlinではどう書くの?
的なコード比較の覚え書き。

【Kotlin】【Java】Kotlin Javaの比較メモ
のNull安全機能部分に特化した記事です。

Kotlinの基礎部分をJavaと比較しながら知りたいんだ!という方は上記の記事をご覧ください。

対象読者

  • Javaの読み書きがある程度できる
  • Kotlinの基本文法はなんとなく読める
  • Kotlinの?とか!!が意味分からない
  • ?付きの型をいい感じに無くす方法を知りたい

Null安全とは?

Nullを原則許容しない仕組みのことを指す。

例えば以下のコード

test.kt
val a: String = null

これはコンパイルエラーになる。
なぜなら、Kotlinではデフォルトでnullを許容していないからだ。
nullを許容したい場合は型の後ろに?を付ける。

test.kt
val a: String? = null // これはOK

オプショナル付きで定義された変数にアクセスする場合は必ずNullチェックが強要される。
Javaと比較してみよう。

test.java
String a = null;
a.contains("hoge"); // 当然nullなのでヌルポで落ちる
test.kt
var a: String? = null
a.contains("hoge") // そもそもコンパイルが通らない

Kotlinのこのソースはコンパイル時点で弾かれる。
non-nullが保障されていないからだ。
Nullableな型にアクセスするためにはこうする。

test.kt
val a: String? = null
a?.contains("hoge")

Nullableな型の関数呼び出しの場合は、?.のようにするだけで良い。
aがNullならcontainsは実行されずNullが返る。

これをJavaで実現しようとすると、、

まずはお馴染みのNullチェック。

test.java
String a = null;

if (a != null) {
    a.contains("hoge"); // nullならここは通らない
}

Java8なら一応Optionalという同等の機能は使える

test.java
Optional<String> a = Optional.of(null);
a.ifPresent(notNull -> notNull.contains("hoge")) // aの中身がnullじゃないときだけcontainsが実行される

Javaと比較しても非常に簡潔に書けることが分かるし、
Javaはあくまで実行時チェックでしかないのでJava8のOptionalを使ったとしても、
そもそもOptionalを使い忘れた場合の根本的解決にはなっていない。
Kotlinはコンパイル時にチェックが入るので、Nullチェックが漏れることもない。

Javaのメソッドを呼んだ場合の戻り値

しかしKotlinのNull安全機能も完璧ではない。
例えば以下のようにKotlinからJavaのメソッドを呼んだ場合

test.kt
var str = System.getProperty("hoge") // strはString!型

JavaにはNull安全機能はないので、Kotlin側では強制的にnon-nullな型として代入される
Javaの値を強制的にnon-nullにした場合は型の後ろに!が付く

この扱いが少し難しくて、non-nullとして振る舞うけどNullの可能性があるということになる。

test.kt
var str = System.getProperty("hoge")
var strLength = str.length // Nullable型じゃないので?.は要らない

でも当然getProperty("hoge")がnullで返る可能性はあるので、strLengthはぬるぽで落ちる可能性がある
これでは絶対的なNull安全が担保できたとは言えない。

Javaメソッドの戻り値を受け取る際に明示的にNullable型で宣言してやればNullable型になる

test.kt
var str: String? = System.getProperty("hoge")
var strLength = str?.length // Nullable型なので?.が必要

この扱いをどうするかが非常に難しい。。
Nullを完全に撲滅するのであれば、明示的にNullableにしてやる必要がある

Nullableのアンラップ

先程の例をもう一度見てみる

test.kt
var str: String? = System.getProperty("hoge")
var strLength = str?.length

この場合変数strの型はString?なので、strのlengthの値が代入されるstrLengthはInt?となる。
つまり、代入元の型がNullableであると、代入先の変数もNullableになってしまう。
これではNullableが前提の変数ばかりができてしまい、Null安全機能の恩恵を十分に受けられない。

一度Nullableにした型をnon-nullにしたい場合は以下の2つの方法がある

強制アンラップ

test.kt
var str: String? = System.getProperty("hoge")
var foo = str!!.length // !!とすることによって強制的にstrをString型にすることができる
// fooはInt型になる

一度Nullableになった型は!!とすると?が外れる。

test.kt
var str: String? = System.getProperty("hoge")
str = str!! // strはString型になる
var foo = str.length // non-nullなので?.や!!.演算子は要らない

当然strがNullならぬるぽで落ちる。
これではNullable型にしている意味がない。

エルビス演算子を使ったアンラップ

test.kt
var str = System.getProperty("hoge") ?: "Unknown Property"
var foo = str.length // strはString型になる

この?:はエルビス演算子と呼ばれ、?:の左式の結果がNullだったときに右式を評価する。

test.java
String str = System.getProperty("hoge") != null ? System.getProperty("hoge") : "Unknown Property";

と同義。

Systrem.getProperty("hoge")がNullであっても、その時専用の値が返るようになっているので、
strはString型になり、fooはInt型になる。
Nullable型がなくなって、安全に変数を扱えるようになった。

なお、専用の値を返したくない場合、つまりNullだったら以後の処理を継続しない場合は

test.kt
var str = System.getProperty("hoge") ?: return // strはString型になる

とすればSystem.getProperty("hoge")がNullだった場合はそのブロックの処理を抜けることができる。

実用的な話

test.kt
var str = System.getProperty("hoge") ?: return // strはString型になる

このようにアンラップをすることでぬるぽで落ちることはなくなったが、
しかし、ただreturnするだけでは落ちずに処理を抜けるだけで、エラーを検知することはできない。
そもそもこのコードはSystem.getProperty("hoge")がNull出ないことを前提にしているはずだ。

ぬるぽを発生させず、かつ想定外のreturnを検知したいならば

test.kt
var str = System.getProperty("hoge") ?: run {
   println("Property was not found!!!")
   return
}

のようにして、何かしらのログを吐かせるなりする必要がある。
runの説明はこのスコープ外なので割愛。

実用的な話2

AndroidのViewのようにインスタンス化の時点では初期化ができず、
onCreateなどの特定のメソッド内で初期化しなければならないケースなども存在する

test.kt
class HogeActivity: AppCompatActivity(){

    private var textView: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.hoge_layout)
        textView = findViewById(R.id.textView) as TextView
    }
}

textViewはonCreateでinflateされないとViewが取得できないので、
初期値はnullを入れざるを得ない。そうなると型もTextView?のようにNullableになってしまう。

このTextViewをnon-null型にするためのアプローチが2つある。

lateinit修飾子を使う

lateinitは初期化を遅延評価するための演算子で、変数定義時に初期値を設定しなくてもコンパイルが通るようになる。

test.kt
class HogeActivity: AppCompatActivity(){

    private lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.hoge_layout)
        textView = findViewById(R.id.textView) as TextView
    }
}

textViewの型から?が外れて変数宣言と同時に初期値を設定する必要がなくなった。
findViewByIdで初めてtextViewに値が入る。

しかしlateinitで気をつけなければいけないのは、変数(var)にしか使えないことと、
setterが呼ばれる前にgetterが呼ばれるとクラッシュしてしまうこと。

by lazyを使う

byの説明はこれだけで少しややこしいので詳しくは説明しませんが、
別のクラスにプロパティの設定を委譲できる修飾子です。
lazyは初めてlazyが定義されているプロパティにアクセスした時に実行される関数。

この2つを組み合わせることで変数の初期化を別のクラスの任意の関数で行うことができる。

test.kt
class HogeActivity: AppCompatActivity(){

    private val textView: TextView by lazy { findViewById(R.id.textView) as TextView }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.hoge_layout)
    }
}

こうするとtextViewのgetterが初めて呼ばれたときにgetterの前に
findViewById(R.id.textView) as TextView
が実行され初期値が入る。

by lazyは逆に定数(val)にしか使えない。
その代わりlateinitのようにクラッシュする心配はない。

by lazyが使えるならばこちらを使うほうが良いと思う。
lateinitの使い所は、DIのdaggerのように他のクラスから定義されるケースなどだろうか。

さいごに

Null安全は強力な機能ではあるものの、全てを解決する銀の弾ではない。
逆にぬるぽで落ちるからこそ、他のソースへの不具合の波及を防いでいるとも取れる。

Null安全機能を使うときはしっかりとその裏にある危険を把握した上で使って行きたいですね。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.