null Pointer Exception!
皆さん嫌いですよね?
僕も嫌いです
なぜなら実行時にしか気づかないエラーだから
無駄にコンパイル時間と手動テストの時間を増やしたくない
そんなあなたにnull安全な言語をオススメします。
Kotlinは、null安全な言語です。
Scalaは、null安全じゃないですが、
Scalaでは、nullは、Option型でラップして、
Noneに変換することにより、
コンパイル前に警告が出て時間を節約できます
scala> Option(null)
res5: Option[Null] = None
具体的な例
HelloJava.javaでnullを返すメソッドを用意します
public class HelloJava {
public String get1() {
return "1";
}
public String get2() {
return "2";
}
public String getNull() {
return null;
}
}
Scalaの場合
HelloScala.scala
object HelloScala {
def main(args: Array[String]): Unit = {
val helloJava = new HelloJava()
// In Scala, null is to wrap with option type.
val string1 = Option(helloJava.get1()) // Option[String]
val string2 = Option(helloJava.get2()) // Option[String]
val string3 = Option(helloJava.get3()) // Option[String]
val string4: String = helloJava.getNull() // null 型検査エラーなし
val string5: String = None // コンパイル前に警告がでて、型検査エラーあり
println(string1) // Some(1)
println(string2) // Some(2)
println(string3) // None
println(string4) // null
println(string4.toInt)
// Exception in thread "main" java.lang.NumberFormatException: null
// 実行時エラー
println(string5)
}
Scalaの場合
String型にnullを代入してもエラーにならないので
null.toIntで、null Pointer Exceptionの危険性
nullをOption型でラップして、None型に変換すると
コンパイル前に警告が出るので、事前に気づいて
コンパイル時間を節約できる
scala> var string = Option(null)
string: Option[Null] = None
scala> string match {
| case Some(string) => println(string)
| case None => println("string is None")
| }
string is None
Scalaの場合、
コンパイル前に
String型は、Noneを代入できない警告がでる
String型は、Option(null)を代入できない警告が出る
Kotlinの場合
HelloKotlin.kt
fun main(args: Array<String>) {
val string1: String? = HelloJava().get1() // String!
val string2: String? = HelloJava().get2() // String!
val string3: String? = HelloJava().getNull() // String!
val string4 = HelloJava().getNull() // String!
println(string1) // String?
println(string2) // String?
println(string3) // String?
println(string4) // null
println(string4.toInt())
// java.lang.NumberFormatException: null
// 実行時エラー
println(string5)
val string5: String = HelloJava().getNull() // String!
// Exception in thread "main" java.lang.IllegalStateException: HelloJava().getNull() must not be null
// 警告なし、コンパイル後に型検査エラーが出る
}
Kotlinの場合
JavaからくるnullをStringに代入しても、
コンパイル前に警告はでない、
コンパイル後に、代入できないエラーが出るので、コンパイルを待つ時間が必要です。
(注)Javaではなく、Kotlinのnullであれば、
String型に代入するとコンパイル前に警告が出ます
Javaからくるnullは必ずString?みたいに?をつけて
Option型にして、安全にアンラップしましょう!
ScalaとKotlinのOptionのアンラップの違い
Scalaの場合
HelloScala.scala
import scala.util.{Success, Try}
object HelloScala {
def main(args: Array[String]): Unit = {
val helloJava = new HelloJava()
// In Scala, null is to wrap with option type.
val string1 = Option(helloJava.get1()) // Option[String]
val string2 = Option(helloJava.get2()) // Option[String]
val string3 = Option(helloJava.getNull()) // Option[String]
// val string4 = helloJava.getNull() // String
// Expression of type None.type doesn't confirm to expected type String
// In Scala,null of Java wraps with Option, converts null to None,
// and checks None before run,but Kotlin need run
// In Scala, if you assign None to String type, the IDE checks it before executing it.
println(string1) // Some(1)
println(string2) // Some(2)
println(string3) // None
// println(string4) // null
// println(string4.toInt)
// Exception in thread "main" java.lang.NumberFormatException: null
try { // 例1
(string1, string2, string3) match {
case (Some(string1), Some(string2), Some(string3)) =>
println(string1) // String
println(string2) // String
println(string3) // String
println(s"$string1 + $string2 + $string3 = ${string1.toInt + string2.toInt + string3.toInt}") // Int
case _ => throw new Exception("string1 is None or string2 is None or string3 is None")
}
} catch {
case numberFormatException: NumberFormatException =>
println(numberFormatException.getMessage)
case exception: Exception => println(exception.getMessage)
}
def toIntTry(option: Option[String]): Option[Try[Int]] = option.map{string => Try(string.toInt)}
try { // 例2
for {
try1 <- toIntTry(string1) // Try[Int] <- Option[Try[Int]]
try2 <- toIntTry(string2) // Try[Int] <- Option[Try[Int]]
try3 <- toIntTry(string3) // Try[Int] <- Option[Try[Int]]
} yield {
(try1, try2, try3) match {
case (Success(string1), Success(string2), Success(string3)) =>
println(s"string1 + string2 + string3 = ${string1 + string2 + string3}")
case _ =>
throw new Exception("string1 is None or string2 is None or string3 is None")
}
}
} catch {
case exception: Exception =>
println(exception.getMessage)
}
try { // 例3 ただの条件分岐(アンラップなし) getはオススメしない
if (string1 != None && string2 != None && string3 != None) {
// get is Not Recommend
println(s"string1 + string2 + string3 = ${string1.get.toInt + string2.get.toInt + string3.get.toInt}")
} else {
throw new Exception("string1 is None or string2 is None or string3 is None")
}
} catch {
case numberFormatException: NumberFormatException =>
println(numberFormatException.getMessage)
case exception: Exception =>
println(exception.getMessage)
}
}
}
try { // 例1 Tupleのパターンマッチで、アンラップ
(string1, string2, string3) match {
case (Some(string1), Some(string2), Some(string3)) =>
println(string1) // String
println(string2) // String
println(string3) // String
println(s"$string1 + $string2 + $string3 = ${string1.toInt + string2.toInt + string3.toInt}") // Int
case _ => throw new Exception("string1 is None or string2 is None or string3 is None")
}
} catch {
case numberFormatException: NumberFormatException =>
println(numberFormatException.getMessage)
case exception: Exception => println(exception.getMessage)
}
def toIntTry(option: Option[String]): Option[Try[Int]] = option.map{string => Try(string.toInt)}
try { // 例2 for comprehensionでアンラップして、toIntの結果を、TryでSuccessとFailureを分ける
for {
try1 <- toIntTry(string1) // Try[Int] <- Option[Try[Int]]
try2 <- toIntTry(string2) // Try[Int] <- Option[Try[Int]]
try3 <- toIntTry(string3) // Try[Int] <- Option[Try[Int]]
} yield {
(try1, try2, try3) match {
case (Success(string1), Success(string2), Success(string3)) =>
println(s"string1 + string2 + string3 = ${string1 + string2 + string3}")
case _ =>
throw new Exception("string1 is None or string2 is None or string3 is None")
}
}
} catch {
case exception: Exception =>
println(exception.getMessage)
}
try { // 例3 ただの条件分岐(アンラップなし) Option#getはオススメしない
if (string1 != None && string2 != None && string3 != None) {
// get is Not Recommend
println(s"string1 + string2 + string3 = ${string1.get.toInt + string2.get.toInt + string3.get.toInt}")
} else {
throw new Exception("string1 is None or string2 is None or string3 is None")
}
} catch {
case numberFormatException: NumberFormatException =>
println(numberFormatException.getMessage)
case exception: Exception =>
println(exception.getMessage)
}
Kotlinの場合
HelloKotlin.kt
fun main(args: Array<String>) {
val string1: String? = HelloJava().get1()
val string2: String? = HelloJava().get2()
val string3: String? = HelloJava().getNull()
// val string4: String = HelloJava().getNull()
// Exception in thread "main" java.lang.IllegalStateException: HelloJava().getNull() must not be null
// at HelloKotlinKt.main(HelloKotlin.kt:5)
println(string1) // 1
println(string2) // 2
println(string3) // null
// println(string4) // string
try { // 例1 ?.let
string1?.let { string1 ->
string2?.let { string2 ->
string3?.let { string3 ->
println(string1) // String
println(string2) // String
println(string3) // String
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}") // Int
} ?: run {
throw Exception("string3 is null")
}
} ?: run {
throw Exception("string2 is null")
}
} ?: run {
throw Exception("string1 is null")
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
try { // 例2 パターンマッチ
when (string1) {
null -> throw Exception("string1 is null")
else -> when (string2) {
null -> throw Exception("string2 is null")
else -> when (string3) {
null -> throw Exception("string3 is null")
else ->
// string1: String?
// string2: String?
// string3: String?
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}")
}
}
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
try { // 例3 スマートキャスト
if (string1 != null && string2 != null && string3 != null) {
println(string1) // String?
println(string2) // String?
println(string3) // String?
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}") // Int
} else {
throw Exception("string1 is null or string2 is null or string3 is null")
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
}
try { // 例1 ?.let
string1?.let { string1 ->
string2?.let { string2 ->
string3?.let { string3 ->
println(string1) // String
println(string2) // String
println(string3) // String
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}") // Int
} ?: run {
throw Exception("string3 is null")
}
} ?: run {
throw Exception("string2 is null")
}
} ?: run {
throw Exception("string1 is null")
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
try { // 例2 パターンマッチ
when (string1) {
null -> throw Exception("string1 is null")
else -> when (string2) {
null -> throw Exception("string2 is null")
else -> when (string3) {
null -> throw Exception("string3 is null")
else ->
// string1: String?
// string2: String?
// string3: String?
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}")
}
}
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
try { // 例3 スマートキャスト
if (string1 != null && string2 != null && string3 != null) {
println(string1) // String?
println(string2) // String?
println(string3) // String?
println("$string1 + $string2 + $string3 = ${string1.toInt() + string2.toInt() + string3.toInt()}") // Int
} else {
throw Exception("string1 is null or string2 is null or string3 is null")
}
} catch(numberFormatException: NumberFormatException) {
println(numberFormatException.message)
} catch(exception: Exception) {
println(exception.message)
}
まとめ
KotlinのJavaからのnullの殺し方は
String?などのOption型に代入して
null安全にアンラップすると
無駄なコンパイル時間を減らせる
ScalaのJavaからのnullの殺し方は
Option(null)などOption型でラップして、
Noneに変換して安全にアンラップすると
無駄なコンパイル時間を減らせる
ScalaのOptionのget、Kotlinの!!などで、
Option型を強制アンラップすると
null,Noneの時に、実行時エラーになって、
クラッシュする可能性があるので
安全にアンラップして
適切なエラーハンドリングしましょう!
エラーハンドリング
ユーザーが離脱しないように、
発生したエラーに応じて、
ユーザーや開発者に現在の状態を示し、
必要に応じて次の行動を示してあげること
オフライン
(例)Free Wifiに繋がっているけど認証してない場合
× ユーザーに何も伝えずリロードボタンだけ表示
ユーザーがオフラインに気づかないと、
クレームされたり、離脱してしまう
○ ユーザーにネットワークの接続がないことを伝えて、ネットに接続すれば使えることを伝える
不正な入力データ
× ユーザーに何も伝えず、全てのサービスがクラッシュする
× ユーザーに何も伝えず、不正な入力データを受け付けない
○ 不正な入力データであることを伝えて、正常な入力を促す
null安全,None安全な言語で
?のオプショナルチェーンや安全なアンラップで、
バグになる仕様の場合にバグっても動くだけだと
障害に気づかないでバグを出し続けるので
必要に応じて、エラーハンドリングしたり、
エラー検知しましょう!
null安全な言語でも、バグ検知を怠れば安心ではない