Kotlinのnullableな変数について

採用はこちら

Kotlinでは、変数の型が最初から

  • nullを許可しない型
  • nullを許可する型

にはっきり分かれています。

たとえば、次の String 型の変数には null を代入できません。

var name: String = "Shinichi"

この name は「必ず文字列が入る変数」として扱われます。

一方で、型の後ろに ? を付けると、その変数は nullを許可する型 になります。

var name: String? = null

この String? は、「文字列が入るかもしれないし、nullかもしれない値」を表します。

Kotlinでは、このような型を nullable型 と呼びます。

目次

なぜnullableが重要なのか

多くのプログラミング言語では、null の扱いを間違えると実行時にエラーが起きやすくなります。

代表的なのが、nullに対してメソッドやプロパティへアクセスしてしまうケースです。

Kotlinでは、この問題を減らすために、

  • nullにならない値
  • nullになる可能性がある値

型の段階で明確に区別します。

つまり、nullableは単なる書き方の違いではなく、nullによるバグを減らすための仕組みです。

基本構文

nullを許可しない型

val text: String = "Hello"

この String 型には null を代入できません。

val text: String = null   // エラー

nullを許可する型

val text: String? = "Hello"
val empty: String? = null

この String? 型は、文字列も null も扱えます。

StringString? の違い

次の2つは似ていますが、意味は大きく違います。

val a: String = "abc"
val b: String? = "abc"

a は nullではないことが保証されているので、そのまま使えます。

println(a.length)

一方、b は nullの可能性があるため、そのままではアクセスできません。

println(b.length)   // エラー

Kotlinは「nullかもしれない値を、そのまま使うのは危険」と判断して、コンパイル時に止めてくれます。

nullable型の扱い方

nullable型は、そのままでは使いにくい場面があります。そのためKotlinには、安全に扱うための方法が用意されています。

nullチェックして使う

最も基本的な方法です。

val name: String? = "Kotlin"

if (name != null) {
    println(name.length)
}

name != null と確認したあとのブロックでは、name を non-null として扱える場合があります。

この仕組みを smart cast と呼びます。

ただし、smart cast は常に必ず使えるわけではありません。

途中で値が変わる可能性があるとコンパイラが判断した場合は、smart cast されないこともあります。

セーフコール演算子 ?.

nullable型に対して安全にアクセスする方法です。

val name: String? = null
println(name?.length)

この場合、

  • name が nullでなければ length を返す
  • name が nullなら式全体が nullになる

という動きをします。

val userName: String? = "Shinichi"
val length = userName?.length
println(length)   // 8
val userName: String? = null
val length = userName?.length
println(length)   // null

このとき length の型は Int? です。

Elvis演算子 ?:

nullのときに代わりの値を使いたい場合は、Elvis演算子を使います。

val name: String? = null
val length = name?.length ?: 0
println(length)   // 0

意味はシンプルです。

  • 左側が nullでなければその値を使う
  • nullなら右側の値を使う

たとえばこう書けます。

val message: String? = null
val displayText = message ?: "デフォルトメッセージ"
println(displayText)

とてもよく使う書き方です。

非nullアサーション演算子 !!

!! は、「ここは絶対に nullではない」と自分で断言する書き方です。

val name: String? = "Kotlin"
println(name!!.length)

これで name を non-nullとして扱えます。

ただし、実際には nullだった場合は実行時例外になります。

val name: String? = null
println(name!!.length)   // 例外

そのため、!! は便利ではありますが、できるだけ多用しないほうが安全です。

まずは ?.?:、nullチェックで解決できないか考えるのが基本です。

safe cast as?

安全に型変換したいときは as? が使えます。

val obj: Any = "Hello"
val str: String? = obj as? String
println(str)

変換できる場合はその型の値になり、失敗した場合は null になります。

val obj: Any = 123
val str: String? = obj as? String
println(str)   // null

nullable型と相性のよい書き方です。

smart castとは

Kotlinでは、nullチェックや型チェックの結果をもとに、コンパイラが一時的により具体的な型として扱ってくれることがあります。

val text: String? = "abc"

if (text != null) {
    println(text.length)
}

この if の中では、text は nullではないと判断されるため、String として扱われます。

ただし、これはコンパイラが安全だと判断できる場合だけです。

特に var のプロパティや custom getter を持つプロパティなどでは、smart cast が使えないことがあります。

valvar の違い

val は再代入できないため、smart cast と相性がよいです。

val text: String? = "Hello"

if (text != null) {
    println(text.length)
}

一方で var は値が変わる可能性があるため、状況によってはコンパイラが smart cast を許さないことがあります。

nullable型の代表的な使いどころ

値が存在しない可能性があるとき

fun findUserName(id: Int): String? {
    return if (id == 1) "Shinichi" else null
}

検索結果が見つからない場合などに使えます。

まだ未設定の状態を表したいとき

var selectedItem: String? = null

まだ何も選ばれていないことを表せます。

Javaと連携するとき

Javaでは null安全がKotlinほど厳密ではないため、Kotlinから扱うときは nullable を意識する場面が増えます。

関数とnullable

引数にnullableを使う

fun printLength(text: String?) {
    println(text?.length ?: 0)
}

この関数は null を受け取れます。

戻り値にnullableを使う

fun getMessage(flag: Boolean): String? {
    return if (flag) "OK" else null
}

呼び出し側は、戻り値が null かもしれないことを考えて扱う必要があります。

コレクションとnullable

ここは混乱しやすいポイントです。

List<String?>

要素がnullableなリスト

val list: List<String?> = listOf("A", null, "C")

リスト自体は nullではありませんが、要素に null が入ることがあります。

List<String>?

リスト自体がnullable

val list: List<String>? = null

リストそのものが存在しない可能性があります。

ただし、存在するなら要素は non-null の String です。

List<String?>?

両方nullable

val list: List<String?>? = null
  • リスト自体が nullかもしれない
  • 要素も nullかもしれない

この違いは実務でもかなり重要です。

nullableチェーン

safe call はネストしたオブジェクトにも使えます。

class Address(val city: String?)
class User(val address: Address?)

val user: User? = User(Address("Osaka"))

val city = user?.address?.city
println(city)   // Osaka

もし途中のどこかが nullなら、結果全体も nullになります。

val user: User? = null
val city = user?.address?.city
println(city)   // null

let を使ったnullable処理

nullableな値を扱うときは let もよく使います。

val name: String? = "Kotlin"

name?.let {
    println(it.length)
}

これは「name が nullでないときだけ処理する」という意味です。

it には non-null の値が入ります。

val email: String? = "test@example.com"

email?.let {
    println("メール送信先: $it")
}

nullなら何も実行されません。

代入時のsafe call

safe call は代入にも使えます。

person?.department?.head = manager

この場合、途中のどこかが nullなら代入そのものが行われません。

nullable receiver という考え方

Kotlinでは、nullableな型に対して拡張関数を定義することもできます。

fun String?.printInfo() {
    if (this == null) {
        println("nullです")
    } else {
        println(length)
    }
}

これにより、「nullかもしれない値」に対しても自然に関数を呼び出せます。

初期化とnullable

Kotlinでは、non-null 型のプロパティは基本的に初期化が必要です。

val name: String = "abc"

最初は値がない状態を表したい場合は、nullable型にする方法があります。

var name: String? = null

ただし、本当に null が必要かは設計上よく考えるべきです。

nullable を増やすほど、あとで null対応のコードも増えていきます。

lateinit との違い

nullable と lateinit は似ているようで役割が違います。

nullableで未設定を表す

var name: String? = null

これは「本当に null の可能性がある値」です。

lateinit で後から初期化する

lateinit var name: String

これは「あとで必ず値を入れる non-null のプロパティ」です。

ここで大事なのは次の点です。

  • lateinitvar にしか使えない
  • nullable型には使えない
  • 初期化前にアクセスすると例外になる

つまり、使い分けはこうです。

  • 値が存在しない可能性がある → nullable
  • あとで必ずセットするlateinit

なお、lateinit を初期化前に参照すると、NullPointerException ではなく UninitializedPropertyAccessException が発生します。

Kotlinでnull関連の実行時例外が起きる主な場面

Kotlinはnull安全ですが、実行時例外が絶対に起きないわけではありません。

!! を使ったとき

val text: String? = null
println(text!!.length)

これは実行時例外になります。

Javaとの相互運用

Java由来の値では、Kotlinの厳密なnull安全がそのまま適用できないことがあります。

lateinit の初期化漏れ

lateinit var name: String
println(name)

この場合は NullPointerException ではなく、UninitializedPropertyAccessException が発生します。

実務でよく使う書き方

デフォルト値を入れる

val userName: String? = null
val displayName = userName ?: "ゲスト"

nullでなければ処理する

userName?.let {
    println("こんにちは、$it さん")
}

nullなら return する

fun process(name: String?) {
    val validName = name ?: return
    println(validName.length)
}

この書き方はとても実務的で、読みやすさも高いです。

nullableを使うときの考え方

Kotlinでは、「とりあえず nullable にする」のではなく、本当に値が存在しない可能性があるのか を考えて型を決めることが大切です。

たとえば、空文字で十分なのに nullable にすると、null"" の両方を扱う必要が出てきます。

var title: String? = ""

これより、次のほうが単純な場合もあります。

var title: String = ""

「未設定」という状態そのものに意味があるときだけ nullable を使うという考え方が重要です。

まとめ

Kotlinのnullableな変数とは、nullを取りうる型の変数です。

型名の末尾に ? を付けて表します。

var name: String? = null

ポイントを整理すると、次の通りです。

  • String は null不可
  • String? は null可
  • nullable型にはそのままアクセスできない
  • よく使うのは if (x != null)?.?:?.let {}
  • !! は使えるが危険性がある
  • lateinit は nullable の代わりではなく、後から初期化する non-null 用の仕組み

特によく使うのはこの3つです。

name?.length
name?.length ?: 0
name?.let { println(it) }

最後に理解しやすいサンプル

fun main() {
    val name: String? = null

    println(name?.length)         // null
    println(name?.length ?: 0)    // 0

    val input: String? = "Kotlin"

    if (input != null) {
        println(input.length)     // 6
    }

    input?.let {
        println("文字数は ${it.length}")
    }
}

このサンプルには、nullable型の基本がひと通り入っています。

  • safe call
  • Elvis演算子
  • nullチェック
  • let

をまとめて確認できます。

以上、Kotlinのnullableな変数についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次