覚えたら書く

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

Kotlin - 名前付き引数

Javaで例えば、Book というクラスを定義するとして属性には以下を持つものとします。

  • Book
    • title : 本のタイトル
    • author : 著者
    • price : 価格(円)
    • isbn : ISBN-13
    • remarks : 備考。(あれば何か書く。特に無いかもしれない)

これを単純にJavaのクラスとして表現すると以下の様になります。

public final class Book {

    private final String title;

    private final int price;

    private final String author;

    private final String isbn;

    private final String remarks;

    public Book(String title, int price, String author, String isbn, String remarks) {
        this.title = title;
        this.price = price;
        this.author = author;
        this.isbn = isbn;
        this.remarks = remarks;
    }

    public String getTitle() {
        return title;
    }

    public int getPrice() {
        return price;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    public String getRemarks() {
        return remarks;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", price=" + price +
                ", author='" + author + '\'' +
                ", isbn='" + isbn + '\'' +
                ", remarks='" + remarks + '\'' +
                '}';
    }
}

これを呼び出すコードを書くと以下の様になります

Book book =
        new Book("Kotlinイン・アクション", 4115, "Dmitry Jemerov", "978-4839961749", "サイズ: 23.6x18.2x3.6 cm");

System.out.println(book);

◾️実行結果

Book{title='Kotlinイン・アクション', price=4115, author='Dmitry Jemerov', isbn='978-4839961749', remarks='サイズ: 23.6x18.2x3.6 cm'}


コンストラクタの引数が多すぎて、与えてるパラメータの順番が正しいのかが呼び出し側のコードからは良く分かりません。


こういったケースでは、一般的にBuilderパターンを利用して以下の様に書きます。

public final class Book {

    private final String title;

    private final int price;

    private final String author;

    private final String isbn;

    private final String remarks;

    private Book(Builder builder) {
        this.title = builder.title;
        this.price = builder.price;
        this.author = builder.author;
        this.isbn = builder.isbn;
        this.remarks = builder.remarks;
    }

    public String getTitle() {
        return title;
    }

    public int getPrice() {
        return price;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    public String getRemarks() {
        return remarks;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", price=" + price +
                ", author='" + author + '\'' +
                ", isbn='" + isbn + '\'' +
                ", remarks='" + remarks + '\'' +
                '}';
    }

    public static class Builder {
        private String title;
        private int price;
        private String author;
        private String isbn;
        private String remarks;

        Builder() {
        }

        public Builder title(String title) {
            this.title = title;
            return this;
        }

        public Builder price(int price) {
            this.price = price;
            return this;
        }

        public Builder author(String author) {
            this.author = author;
            return this;
        }

        public Builder isbn(String isbn) {
            this.isbn = isbn;
            return this;
        }

        public Builder remarks(String remarks) {
            this.remarks = remarks;
            return this;
        }

        public Book build() {
            return new Book(this);
        }
    }

    public static Builder builder() {
        return new Builder();
    }
}

これを呼び出すコードは以下の様になり、当初のものと比較して何のパラメータを与えているのかが明確になります

Book book = Book.builder()
        .title("Kotlinイン・アクション")
        .author("Dmitry Jemerov")
        .price(4115)
        .isbn("978-4839961749")
        .remarks("サイズ: 23.6x18.2x3.6 cm")
        .build();

System.out.println(book);  // ->  結果はもとのものと同じ

Builderを登場させる事で、呼び出し時に分かりやすさが確保されるのですが、
いかんせんコードが長くなります。いわゆるBuilderクラスを登場させるがゆえのボイラープレートコードが増えたと言えます。

これを楽にするためには、Lombok の @Builderを利用する方法などがあります。


名前付き引数

Kotlin には関数呼び出し時に、その引数の名前を指定する事が可能です。

KotlinでBookクラスを定義して呼び出すと以下の様になります。

class Book(
    val title: String,
    val price: Int,
    val author: String,
    val isbn: String,
    val remarks: String
) {
    override fun toString(): String {
        return "Book(title='$title', price=$price, author='$author', isbn='$isbn', remarks='$remarks')"
    }
}


呼び出し側のコード

val book = Book(
    title = "Kotlinイン・アクション",
    price = 4115,
    author = "Dmitry Jemerov",
    isbn = "978-4839961749",
    remarks = "サイズ: 23.6x18.2x3.6 cm")

println(book)

◾️実行結果

Book(title='Kotlinイン・アクション', price=4115, author='Dmitry Jemerov', isbn='978-4839961749', remarks='サイズ: 23.6x18.2x3.6 cm')

Builderを持ち出す事なく、明確なパラメータの設定でBookのインスタンス生成ができています。


デフォルト引数

Kotlinでは関数宣言で引数にデフォルト値を指定可能です。

先ほど定義したBookの remarks のデフォルト値を ''-" にしてみます。

class Book(
    val title: String,
    val price: Int,
    val author: String,
    val isbn: String,
    val remarks: String = "-"
) {
    override fun toString(): String {
        return "Book(title='$title', price=$price, author='$author', isbn='$isbn', remarks='$remarks')"
    }
}

呼び出し側のコードは以下の通りで、remarksを明示的に指定するケースと指定しないケースの2パターンで呼び出します。

val book1 = Book(
    title = "Kotlinイン・アクション",
    price = 4115,
    author = "Dmitry Jemerov",
    isbn = "978-4839961749",
    remarks = "サイズ: 23.6x18.2x3.6 cm")

println(book1)


val book2 = Book(
    title = "Kotlinイン・アクション",
    price = 4115,
    author = "Dmitry Jemerov",
    isbn = "978-4839961749")

println(book2)

◾️実行結果

Book(title='Kotlinイン・アクション', price=4115, author='Dmitry Jemerov', isbn='978-4839961749', remarks='サイズ: 23.6x18.2x3.6 cm')
Book(title='Kotlinイン・アクション', price=4115, author='Dmitry Jemerov', isbn='978-4839961749', remarks='-')

いずれのケースも正常に動作し、remarksを指定しなかった場合デフォルト値が利用されている事が分かります。


Java にはメソッド等に、このデフォルト値を持たせる機構がないため、オーバーロードで代替する必要があります。
が、オーバーロードメソッドが増えすぎると、メソッドごとの違いが良くわからなくなってきて混乱の元になります。

Kotlinのデフォルト引数はその様な混乱を無くしてくれるものとなります。



関連エントリ

Kotlin - コレクションの生成

Javaでのコレクションの初期化とKotlinの関数でのコレクションの初期化を比較してみます。
コレクションの内容と、実クラスを表示してみます。


Javaのコレクション

JavaにおけるList生成

List<Integer> list1 = Arrays.asList(1, 2, 3);
System.out.printf("Data -> %s, Class -> %s %n", list1, list1.getClass());

List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);

System.out.printf("Data -> %s, Class -> %s %n", list2, list2.getClass());

// Java 9 からこの書き方もできる
List<Integer> list3 = List.of(1, 2, 3);
System.out.printf("Data -> %s, Class -> %s %n", list3, list3.getClass());

◾️実行結果

Data -> [1, 2, 3], Class -> class java.util.Arrays$ArrayList 
Data -> [1, 2, 3], Class -> class java.util.ArrayList 
Data -> [1, 2, 3], Class -> class java.util.ImmutableCollections$ListN


JavaにおけるSet生成

Set<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(2);
set1.add(3);
System.out.printf("Data -> %s, Class -> %s %n", set1, set1.getClass());

// Java 9 からこの書き方もできる
Set<Integer> set2 = Set.of(1, 2, 3);
System.out.printf("Data -> %s, Class -> %s %n", set2, set2.getClass());

◾️実行結果

Data -> [1, 2, 3], Class -> class java.util.HashSet 
Data -> [1, 2, 3], Class -> class java.util.ImmutableCollections$SetN 


JavaにおけるMap生成

Map<String, Integer> map1 = new HashMap<>();
map1.put("key1", 1);
map1.put("key2", 2);
map1.put("key3", 3);
System.out.printf("Data -> %s, Class -> %s %n", map1, map1.getClass());

// Java 9 からこの書き方もできる
Map<String, Integer> map2 = Map.of("key1", 1, "key2", 2, "key3", 3);
System.out.printf("Data -> %s, Class -> %s %n", map2, map2.getClass());

◾️実行結果

Data -> {key1=1, key2=2, key3=3}, Class -> class java.util.HashMap 
Data -> {key1=1, key2=2, key3=3}, Class -> class java.util.ImmutableCollections$MapN 


Kotlinのコレクション

Kotlin ではコレクションを簡単に生成して初期化する関数がいくつも用意されています。

KotlinにおけるList生成

val list1 = listOf(1, 2, 3)
println("Data -> $list1, Class -> ${list1.javaClass}")

val list2 = arrayListOf(1, 2, 3)
println("Data -> $list2, Class -> ${list2.javaClass}")

◾️実行結果

Data -> [1, 2, 3], Class -> class java.util.Arrays$ArrayList
Data -> [1, 2, 3], Class -> class java.util.ArrayList


KotlinにおけるSet生成

val set1 = setOf(1, 2, 3)
println("Data -> $set1, Class -> ${set1.javaClass}")

val set2 = hashSetOf(1, 2, 3)
println("Data -> $set2, Class -> ${set2.javaClass}")

◾️実行結果

Data -> [1, 2, 3], Class -> class java.util.LinkedHashSet
Data -> [1, 2, 3], Class -> class java.util.HashSet


KotlinにおけるMap生成

val map1 = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
println("Data -> $map1, Class -> ${map1.javaClass}")

val map2 = hashMapOf("key1" to 1, "key2" to 2, "key3" to 3)
println("Data -> $map2, Class -> ${map2.javaClass}")

◾️実行結果

Data -> {key1=1, key2=2, key3=3}, Class -> class java.util.LinkedHashMap
Data -> {key1=1, key2=2, key3=3}, Class -> class java.util.HashMap

Kotlin のコレクション生成関数によって作成されたインスタンスは、Java標準のコレクションが利用されている事がわかります。
これにより、Javaのコレクションに関する知識がそのまま流用できるということになります。

Kotlin の独自コレクションではなく Javaの標準コレクションを使用する事で、Javaコードとの相互運用が簡単になります。


拡張された操作が行える

Kotlin のコレクションは Java のコレクションと全く同じクラスであるにもかかわらず、Kotlin ではより多くの操作が可能です。

以下例では、Listの先頭要素を取得する first という関数と 最終要素を取得する last という関数を呼び出しています。
もちろん、この様な関数は Javaの標準コレクションには存在していません。

val list = listOf(1, 2, 3, 4, 5)
val firstElement = list.first()
val lastElement = list.last()
println("Data -> $list, 1st-element -> $firstElement, last-element -> $lastElement")

◾️実行結果

Data -> [1, 2, 3, 4, 5], 1st-element -> 1, last-element -> 5

List の先頭要素の値と、最終要素の値が取得できています。


これらにとどまらず、Kotlin ではコレクションに対して多くの操作を行う事が可能です。

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 は便利なので積極的に利用できるようになっておきたいです