LoginSignup
3
4

More than 5 years have passed since last update.

JavaからのnullをScalaとKotlinで 安全に扱う方法

Last updated at Posted at 2017-10-15

null Pointer Exception!


皆さん嫌いですよね?


僕も嫌いです


なぜなら実行時にしか気づかないエラーだから


無駄にコンパイル時間と手動テストの時間を増やしたくない


そんなあなたにnull安全な言語をオススメします。


Kotlinは、null安全な言語です。
Scalaは、null安全じゃないですが、
Scalaでは、nullは、Option型でラップして、
Noneに変換することにより、
コンパイル前に警告が出て時間を節約できます

スクリーンショット 2017-10-13 3.46.41.png

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型に変換すると
コンパイル前に警告が出るので、事前に気づいて
コンパイル時間を節約できる

スクリーンショット 2017-10-13 3.46.41.png


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を代入できない警告がでる
スクリーンショット 2017-10-13 3.43.46.png

String型は、Option(null)を代入できない警告が出る
スクリーンショット 2017-10-13 3.46.41.png


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型に代入するとコンパイル前に警告が出ます

スクリーンショット 2017-10-13 3.47.42.png

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安全な言語でも、バグ検知を怠れば安心ではない


Fin


3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4