【JavaとKotlinの違うところ】null許容型をわかりやすく解説
Javaでプログラミングをしていると、例外の中でも最も多く遭遇するであろうNullPointerException。
アプリ開発でこのNullPointerExceptionを放置してしまうと、アプリがフリーズしてしまう原因にもなります。
Kotlinには、この憎き「ぬるぽ」が発生しないようにするため、少し文法が厳しく設計されています。
この厳しい文法のおかげで、Javaに比べると、いわゆる「ぬるぽ」が発生する可能性は大幅に減少されたのですが、Javaを学んでKotlinに進んだ方にはわかりにくい仕様の一つでもあります。
このわかりにくさを理解するためには、Kotolinのnull許容型について学ぶ必要があります。
そもそもNullPointerExceptionとは
Kotlinからはいったん離れて、Javaを思い出して考えてみましょう。
次のプログラムはどのような動作結果になるでしょう?
public static void main(String[] args){
String str=null;
System.out.println(str.length());
}
もちろんこのプログラムは正常に動きません。
NullPointerExceptionが発生します。
public static void main(String[] args){
String str=null;
System.out.println(str.length());
}
の部分で例外が発生するのですが、それは、
public static void main(String[] args){
String str=null;
System.out.println(str.length());
}
の部分でstrにnull、つまり「何もない」という状態を代入しているにも関わらず、
何もないstrに対して、.length()というメソッドを実行しようとしたために
「何もありませんよー」という例外「NullPointerException」が発生してしまったのです。
もちろん、
String str=null;
の部分で、
String str=”テストの文章です”;
など、何かしらのテキストを代入すれば、例外は発生せず、文字数がカウントされて表示されます。
nullは「何も存在しない」「無効である」といった意味です。
何もない変数に対して命令を実行しようとしてもできないのです。
null許容型とは
では、JavaではNullPointerExceptionが発生したプログラムですが、Kotlinで同様のプログラムを入力すると、どのようになるでしょうか?
fun main(){
val str:String =null;
println(str.length);
}
Kotlinの場合、このプログラムはコンパイルエラーが発生します。
コンパイルエラーということは、実行ができない、ということです。プログラマーが修正しなければ実行できないのです。
なので、実行時に発生するNullPointerExceptionは発生しません。
Kotlinの場合、
fun main(){
val str:String =null;
println(str.length);
}
の部分でコンパイルエラーが発生します。
「String型にnullは代入できませんよ」というエラーです。
プログラマーは強制的にプログラムの修正を迫られることになります。できる限りNullPointerExceptionが発生しないように、文法が厳しく決められているのです。
でも、プログラムを書いていると、nullを代入したいときもあります。
その場合はどのようにプログラムすればいいのでしょうか?
以下のように記述すると、String型の変数にもnullを代入することが可能になります。
fun main(){
val str:String? =null;
println(str?.length);
}
変わった部分は、strの宣言の際のデータ型に「?」がついて、「String?」になっていることです。
このクエスチョンマークを変数型に続けて記述することで、nullをセットすることが可能となります。
このような変数を「null許容型」といいます。nullを許す型です。
また、null許容型で宣言した場合、そのあとの変数名についても
str?.length
のように「?」を付ける必要があります。
セーフコール(安全呼び出し)とは
先ほどのプログラム
val str:String? =null;
println(str?.length);
において、変数strにはnullが代入されています。
この状態で、
println(str?.length);
はNullPointerExceptionは発生しないのでしょうか?
実は、str?.lengthの部分でクエスチョンマークを付けることによって、Kotlinは「null許容型の変数がnullでなければ通常の処理を行い、もしnullだったらnullとする」という処理を行ってくれます。
例外は発生しないのです。
変数名の後に?.を付けて関数を実行したり、変数を参照する方法を「セーフコール」「安全呼び出し」と呼びます。
Javaではnullでないことをチェックしてから関数の実行をする必要があるのですが、毎回その記述するのは煩雑で、忘れてしまうこともありました。
常にnullではないか?を意識しながらプログラミングをしなければならなかったのです。
関数の引数にnull許容型を使う
さて、文字列を受け取って文字列を表示する関数を宣言してみましょう。
fun printCountLength(str:String){
println(str.length)
}
この関数を実行する際に、null許容型を指定してみます。
val str:String? ="あいうえお";
printCountLength(str)
エラーが出たでしょうか?
val str:String? ="あいうえお";
printCountLength(str)
の部分でエラーが発生するはずです。実行できません。
まず、printCountLength側の立場で考えてみましょう。
printCountLengthが受け取ることを想定しているのは、通常のString型です。null許容型ではありません。
ですが、関数を実行する際にnull許容型が渡されてしまったら?困ることになります。
もしかするとその変数はnullかもしれないのです。null許容型ということは、nullが代入されている可能性があるのです。
nullである値が渡された場合、
println(str.length)
のプログラムを実行する際に困ってしまいます。
strがnullだった場合に、実行ができないからです。
Kotlinではこのような関数の呼び出しはできない仕様となっています。
null許容型を引数として渡す必要がある場合は、関数の宣言側でもnull許容型を受け取るように宣言する必要があります。
fun printCountLength(str:String?){
println(str?.length)
}
のように関数を宣言すれば、引数をnull許容型として指定しても関数を呼び出すことは可能です。
null許容型を非null許容型に変えたい場合
チームで開発をする場合、自由に関数の仕様を変更することができない場合があります。
先ほどの例も、関数をnull許容型を受け取れるように変更できれば問題ないのですが、もしできない場合はどうすればいいのでしょうか?
つまり、
fun printCountLength(str:String){
println(str.length)
}
といった関数に対して、
val str:String? ="あいうえお";
と宣言している変数strを関数の引数として渡すことは絶対に不可能なのか、ということです。
Kotlinには、「null許容型を非null許容型に変える」ために「!!演算子」が用意されています。
printCountLength(str!!)
のように関数を呼び出せば、null許容型だったstrが非null許容型となり、
fun printCountLength(str:String)
と宣言されている関数に引数を渡すことができます。 では、実際にnullが引数として渡されたとして、関数内の処理である
println(str.length)
が実行されたらどうなるのでしょう?
実はNullPointerExceptionが発生することになります。
そうなのです!Kotlinであっても、完全にNullPointerExceptionを防ぐことはできないのです。
ただし、これは「!!演算子を使った」場合に限られることです。この演算子を使わなければ、NullPointerExceptionが発生することはありません。
「!!演算子」を使おうとしている場合、絶対に使わなければいけないのかを考えましょう。
プログラマーとしてNullPointerExceptionが発生するリスクを負わないために、必ず立ち止まって、本当にそれは必要か?を考えてみましょう。