覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - 例外処理

「Kotlinイン・アクション」 を読みながらの写経まだまだ続いております。(Javaと比較しながら)

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


Kotlin の例外処理は Java のそれとよく似ています。

関数は、関数に記述された処理を何事もなく正常に完了するか、異常が起こった場合に例外をスローする事ができます。
関数の呼び出し元で、その例外をキャッチして処理する事ができます。その例外はスタックのより上位にスローすることもできます。

例えば、値が 0〜100の範囲に収まっていない場合は、IllegalArgumentExceptionをスローする関数は以下のように記述できます。

fun validation(percentage :Int) {
    if (percentage !in 0..100) {
        throw IllegalArgumentException("A percentage must be between 0 and 100 [parameter: $percentage]")
    }
}

Javaと違って、例外のインスタンスを生成するのに new キーワードは不要となっています。

一応以下のようなコードで実行してみると

package net.yyuki.sandbox.exception

import java.lang.IllegalArgumentException


fun validation(percentage :Int) {
    if (percentage !in 0..100) {
        throw IllegalArgumentException("A percentage must be between 0 and 100 [parameter: $percentage]")
    }
}

fun main() {
    validation(101)
}

当然のように例外がスローされました

Exception in thread "main" java.lang.IllegalArgumentException: A percentage must be between 0 and 100 [parameter: 101]
    at net.yyuki.sandbox.exception.ExceptionTrialKt.validation(ExceptionTrial.kt:8)
    at net.yyuki.sandbox.exception.ExceptionTrialKt.main(ExceptionTrial.kt:14)
    at net.yyuki.sandbox.exception.ExceptionTrialKt.main(ExceptionTrial.kt)


throw

Javaと違って、Kotlinでは throw構文は式となっているため、他の式の一部として利用する事ができます。

以下は、if の条件に当てはまる場合はその値を返し、そうでなければ例外がスローされます

fun validation(value :Int) =
    if (value in 0..100) {
        value
    } else {
        throw IllegalArgumentException("A percentage must be between 0 and 100 [parameter: $value]")
    }


try-catch-finally

Javaと同じく、例外を処理するためにcatch節, finally節 を伴ったtry構文を使用します。

以下は、ファイル操作(BufferedReader経由でのデータの読み取り)の処理を行う関数の例です

fun readLineCount(reader: BufferedReader): Int {
    try {
        val str = reader.readLine()    // <- IOException の可能性がある
        return str.toInt()
    } catch (e: NumberFormatException) {
        return 0
    } finally {
        reader.close()
    }
}

Java と違って throws 節が存在しません。
Javaでは、IOException は検査例外(checked exception)であるため、明示的な記述が必要です。
また、その関数の呼び出し側でも、検査例外を処理するか再スローするかといったコードの記述が必要となります。

が、他のモダンなJVM言語と同様に、Kotlin では検査例外と非検査例外を区別していません。
関数からスローされる例外を指定する必要はなく、利用側ではどの例外について処理してもいいし、しなくてもいいということになっています。

Java では検査例外の扱いに対するルールのために、例外を再スローしたり無視したりするための意味のないコードだらけになってしまいます。挙句に、実際に起こり得るエラーからは実装者を守ることになっていないことが経験的に分かっています。
そのため、Kotlinではこのような方針になっています。


式としてのtry

以下の関数を定義して実行してみます

fun printReadLineCount(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println("read value = $number")
}

この関数を以下で実行すると何も出力されません

val reader = BufferedReader(StringReader("not a number")
printReadLineCount(reader))

Kotlin の try キーワードは、if や when と同じように式として使用され、変数にその値を割り当てる事ができます。

この関数は、catch ブロックにおける return 文で処理が終わってしまい、それ以降の処理は実行されません。


関数を続行したい場合は、以下の様に catch 節でも値を持つ必要があります

fun printReadLineCount(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        0
    }

    println("read value = $number")
}

この関数を同様に呼び出すと

val reader = BufferedReader(StringReader("not a number")
printReadLineCount(reader))

結果は以下のようになります

read value = 0

Kotlin - 繰り返し処理

Kotlin における while ループは Java と同じですが、 for ループは、Javaにおける for-each に該当するものしか存在しません。
いわゆるインデックス指定での for ループはありません。


whileループ

Kotlin には while と do-while のループが存在していて、Javaでの記述方法と変わりありません

while (条件) {
  // do something
}
do {
  // do something
} while (条件)


レンジでの繰り返し処理

Kotlinでは、ループ用のインデックスを初期化し各ステップでそのインデックスの変数をインクリメントして、境界に達したらループを抜けるというというような、Javaの通常のforループが存在しません。

こういったケースでは、Kotlinではレンジ(range)を使用します。
例えば、1〜9の範囲は以下のように書けます。

val oneToNine = 1..9

Kotin のレンジは閉じて(closed)いて、包括的(inclusive)です。つまり2つ目の値がレンジの中に中に含まれるということになります。(上記の例だと 9 がレンジ内に含まれている)

例えば以下のコードを実行すると

fun main() {
    val oneToNine = 1..9

    for (value in oneToNine) {
        print("$value ")
    }
}

結果は以下のようになり、1 から 9 まで含まれていることがわかります

1 2 3 4 5 6 7 8 9 


step を利用すると間を飛ばすことも可能です

fun main() {
    val oneToNine = 1..9

    for (value in oneToNine step 2) {
        print("$value ")
    }
}

◾️実行結果

1 3 5 7 9 


レンジの .. の記法では、エンドポイントを必ず含んでしまいますが、until関数を使うことでエンドポイントを含まないレンジを作る事ができます。

for (value in 0 until max)

というループでは 0..max-1 のループを意味することになります。


fun main() {
    for (value in 0 until 5) {
        print("$value ")
    }
}

上記コードを実行すると結果は以下の通りとなり、 エンドポイントの 5 を含んでいない事がわかります。

0 1 2 3 4 


Listの繰り返し処理

List でもforループ処理は可能です。まー当たり前ですかね。

fun main() {
    val list = arrayListOf("Taro", "Jiro", "Saburo", "Hanako")

    for (value in list) {
        println("I am $value.")
    }
}

◾️実行結果

I am Taro.
I am Jiro.
I am Saburo.
I am Hanako.


Mapの繰り返し処理

key:名前、 value:持ち金 というマップをループさせる処理です。

fun main() {
    val map = mapOf("Ken" to 1000, "Jon" to 1500, "Hanako" to 10000)

    for ((name, money) in map) {
        println("$name has $money yen.")
    }
}


結果は以下の通りです

Ken has 1000 yen.
Jon has 1500 yen.
Hanako has 10000 yen.

for ループが繰り返し処理を行なっている要素を分解して、マップのキーと値にしていることがわかります。


コレクションの繰り返し処理でインデックスを取得

Mapをキーと値に分解して繰り返し処理したように、コレクションの現在の項目のインデックスを取得しながら繰り返し処理をすることができます。(withIndex を利用)

fun main() {
    val list = arrayListOf("Taro", "Jiro", "Saburo", "Hanako")

    for ((index, element) in list.withIndex()) {
        println("list[$index] -> $element")
    }
}

実行結果は以下の通りです。インデックスも取得できています。

list[0] -> Taro
list[1] -> Jiro
list[2] -> Saburo
list[3] -> Hanako

Kotlin - when式

Java で switch文を使って処理を分岐するケースは、 when 式で記述します。

例えば、数値型の値を条件にとって

  • 値が 1 なら ''Value is One" と出力する
  • 値が 2 なら ''Value is Two" と出力する
  • 値が 3から10 なら ''Value range in 3 .. 10 (value = xx)" と出力する (xxの部分は条件として与えた値)
  • 上記のいずれの条件にも当てはまらない場合は、 "Unknown value (value = xx)" と出力する (xxの部分は条件として与えた値)

これを実現する関数を when を利用して書くと以下のようになります

fun valuePrint(value: Int) {
    when(value) {
        1 -> println("Value is One")
        2 -> println("Value is Two")
        in 3..10 -> println("Value range in 3 .. 10 (value = $value)")  // 値範囲の一致チェックもできる
        else -> println("Unknown value (value = $value)")
    }
}


上記の関数を以下で実行したとすると、

fun main() {
    valuePrint(1)
    valuePrint(2)
    valuePrint(5)
    valuePrint(10)
    valuePrint(0)
}

結果は以下のようになります

Value is One
Value is Two
Value range in 3 .. 10 (value = 5)
Value range in 3 .. 10 (value = 10)
Unknown value (value = 0)


Java の switch文との大まかな違いは以下の通りです。

  • switch が whenになった
  • case: が ->になった
  • 各条件の処理の終わりにbreakを記述しなくて良い
  • default が elseになっている


when は式なので、その分岐結果から値を返す事が可能です。

例えば、先ほどの関数を変形して以下のような関数を定義することができます。(whenで分岐した結果を文字列とreturnする関数)

fun valueToString(value: Int) =
    when(value) {
        1 -> "One"
        2 -> "Two"
        in 3..10 -> "Value range in 3 .. 10 (value = $value)"
        else -> "Unknown value (value = $value)"
    }


上記の関数を以下で実行したとすると、

fun main() {
    val a1 = valueToString(1)
    val a2 = valueToString(10)
    val a3 = valueToString(-1)
}

実行結果は以下のようになります

a1 -> One
a2 -> Value range in 3 .. 10 (value = 10)
a3 -> Unknown value (value = -1)


引数なしでwhen

when は、引数なしで利用することも可能です。こうした場合は if-else ifを代替することができます。 分岐条件は単純なBooleanの式となります。

fun InsteadOfIf(value: Int) {
    when {
        value > 0 -> println("Value is Positive number")
        value < 0 -> println("Value is Negative number")
        value == 0 -> println("Value is Zero")
        else -> println("Unknown value (value = $value)")
    }
}


上記の関数を以下で実行したとすると、

fun main() {
    InsteadOfIf(1)
    InsteadOfIf(10)
    InsteadOfIf(-1)
    InsteadOfIf(0)
}

実行結果は以下のようになります

Value is Positive number
Value is Positive number
Value is Negative number
Value is Zero


型のチェックと組みあわせ

Kotlin では is を使うことで、ある変数が目的の型かどうかをチェックすることも可能です。
ある変数が目的の型かどうかをチェックすれば、チェック済みの型を持つ変数として使用できます。

when(value) {
        is Int -> println("$value + 100 = ${value + 100}")    // ->  対象が Int 型なら 100加算する演算を行う
        is String -> println("$value UpperCase -> ${value.toUpperCase()}")    // -> 対象が String 型 なら 大文字化する
        else -> println("Unknown Type")
    }


まとめ

Kotlin の when は便利なので積極的に利用できるようになっておきたいです

Kotlin - Intellij IDEAで REPL

Kotlin ではREPLの機能が備わっているので、わざわざmain関数書いたりしなくても、 ちょっとしたコードを試すときにはREPLを利用するのが便利なケースがあります。

REPLは Intellij IDEA からも実行できるようになっています。


「Tools」→「Kotlin」→「Kotlin REPL」と選択することで起動可能です。

f:id:nini_y:20190505214223p:plain


または、

Shift キー2回クリックでの どこでも検索で "Kotlin REPL" と入力して、Enterキーをクリックする事でも起動できます。

f:id:nini_y:20190505214713p:plain


起動するとこんな感じです。

f:id:nini_y:20190505214802p:plain

あとは実行したいコードを入力して Command + Enter で実行可能です。ここでは変数に数値を割り当てて、その変数の値を表示しています。

f:id:nini_y:20190505214910p:plain


":quit" と入力して Command + Enter でREPLを終了することです。

f:id:nini_y:20190505214923p:plain


これでIntellij IDEA上でのKotlinのREPLができるようになりました。簡単なコードの動作確認はこれでできますね。

Kotlin - クラスとプロパティ

「Kotlinイン・アクション」 を読みながらの写経続いております。(Javaと比較しながら)

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


値クラス

以下はJavaでのクラス宣言の例です。

public final class Person {
    private final String name;

    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

nameage のfinalなフィールドとそのフィールドに対するgetterのみを持つ単純なクラスとなっています。

このクラスを、 Kotlinへコンバートすると以下のようになります。

class Person(val name: String, val age: Int)

うん、とてもシンプル。

Javaのコードでは、コンストラクタ内で受け取った引数をフィールドに割り当てるコードを繰り返します。
まさにJavaの典型的なボイラープレートコードの一つと言えますが、
Kotlinではこれが無くなっており、すっきりしています。

このようなデータのみが存在して、実装コードが無いクラスは値オブジェクト(value object)と呼ばれる事があります。
多くのプログラミング言語では、これを宣言するための簡潔な構文を用意しています。

残念ながら Java ではこのような構文は提供されておらず、Lombok を利用する事で似たようなことを実現することはできます。


また、Kotlin ではデフォルトの可視性が public であるため、public の記述が無くなっています。


プロパティ

Java では、データを通常はprivateなフィールドに格納し、利用者がそのデータにアクセスする必要があるのであればアクセサメソッド(getterやsetter)を提供します。
このフィールドとアクセサの組み合わせはプロパティ(property)と呼ばれ、多くのライブラリやフレームワークに多用されています。

Kotlin では、Javaとは違い プロパティは第一級の言語機能となっています。これが、Javaにおけるフィールドとアクセサメソッドを置き換えるものとなっています。

Kotlin のクラス内ではプロパティを宣言する方法は、変数を宣言する方法と同じで valvar キーワードを使用します。各々の違いは以下の通りです。

  • val : 宣言されたプロパティは読み取り専用
  • var : 宣言されたプロパティはミュータブルで変更が可能

例えば以下のようなクラス宣言が可能です

class Person(
    val name: String,   // <- 読み取り専用。フィールドとgetter が生成される
    var age: Int        // <- 書き込み可能。フィールドとgetter, setter が生成される
)

プロパティを宣言するときには、それに対応するアクセサ(読み取り専用のプロパティではgetter, 書き込み可能なプロパティではgetter とsetterの両方)も宣言されたことになります。

上記の Person クラスは Java からも Kotlin からも同様に利用する事が可能です。

Java からは以下のように利用可能です。

public static void main(String[] args) {
        Person p = new Person("Taro", 20);

        System.out.println(p.getName());  // -> Taro
        System.out.println(p.getAge());   // -> 20

        p.setAge(25);                     // -> age の値の変更
        System.out.println(p.getAge());   // -> 25
    }

Koitlin からは以下のように利用します

fun main() {
    val p = Person("Taro", 20)

    println(p.name)  // -> Taro
    println(p.age)   // -> 20

    p.age = 25       // -> age の値の変更
    println(p.age)   // -> 25
}

Kotlin ではgetterを呼び出す代わりにプロパティを直接参照します。
ミュータブルなプロパティの値の書き換えもsetter経由ではなく直接プロパティに値を渡します。


カスタムアクセサ

矩形(rectangle)を表現するクラスを定義して、その矩形が正方形かどうかを判別できるようにする場合、以下のように記述できます。

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }
}

isSquare プロパティは、値をフィールドに格納する必要はなく、この例ではカスタムのgetterの実装を持つだけとなっています。
必要な値はプロパティから値を取得するたびに毎回計算(判定)されます。

呼び出しの例は以下の通りです

fun main() {
    val r1 = Rectangle(10, 20)
    println(r1.isSuqare)    // -> 長方形なので false

    val r2 = Rectangle(100, 100)
    println(r2.isSuqare)    // -> 正方形なので true
}

このプロパティに Java からアクセスする場合は、isSquare メソッドを呼び出すことになります。


引数のない関数とカスタムgetterを持つプロパティのどちらを使うべきかについて、
どちらの選択肢も似ていて実装内容や実行速度の面では違いがありません。

違いがあるのは 可読性 だけです。
一般に、あるクラスの特徴を表現したい場合には、それをプロパティとして宣言するべきです。


まとめ

Kotlin の場合 Java に比べてかなり簡単な記述でプロパティの取り扱いができることが分かりました。



関連エントリ