覚えたら書く

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

Kotlin - 可変長引数を扱う

Javaのメソッドで可変長引数を扱う場合は、 <型>... 仮引数名 という記述で引数の部分を記述します。

例えば、与えられた可変長の文字列群をList化して出力するメソッドなら以下の様になります。

import java.util.Arrays;

void convToListAndPrint(String... values) {
    System.out.println((Arrays.asList(values)));
}

呼び出し側のコードは以下の様になります

convToListAndPrint("Str1", "Str2", "Str3");

実行結果は以下の通りです

[Str1, Str2, Str3]

可変長引数で渡されたパラメータは 配列 として扱われています。


以下の様に可変長引数部分に 配列を渡してメソッドを呼び出すことも可能です

convToListAndPrint(new String[] {"Str1", "Str2", "Str3"});    // -> [Str1, Str2, Str3]


Kotlinにおける可変長引数

Kotlinでは可変長引数として取る引数には、vararg 修飾子をつけます。

さきほどJavaのメソッドとして書いた convToListAndPrint をKotlinで書き直すと以下の様になります。
関数のパラメータにvararg 修飾子が付いている部分も当然異なっていますが、
Arrays.asList に可変長引数として受け取ったパラメータに * を付与している部分も違います。

fun convToListAndPrint(vararg values: String) {
    println(Arrays.asList(*values))
}

Kotlinでは、可変長引数をとる関数に 配列を渡す事ができません。可変長引数で受け取った values は、配列として格納されています。
これを、可変長引数を必要とする Arrays.asList には直接渡せません。
引数の前に* を置く スプレット演算子 というもので、配列の全要素を展開した状態にします。
これにより受け渡しが可能となっています。


以下の呼び出しは可能ですが、

convToListAndPrint("Str1", "Str2", "Str3")

以下の呼び出しはできません。コンパイルエラーになります

// コンパイルエラー
val array: Array<String> = arrayOf("Str1", "Str2", "Str3", "Str4")
convToListAndPrint(array)


スプレット演算子を利用した以下の呼び出しなら可能です

val array: Array<String> = arrayOf("Str1", "Str2", "Str3", "Str4")
convToListAndPrint(*array)


また、Javaでは可変長引数のパラメータは、一番最後の引数としてしか指定できません。
以下の様なコードはコンパイルエラーになります

import java.time.LocalDate;
import java.util.Arrays;

// コンパイルエラー(可変長引数の位置が不適切)
void convToListAddDateAndPrint(String... values, LocalDate date) {
    System.out.println(Arrays.asList(values).add(date.toString()));
}


ただし、Kotlinではこの縛り無いようで?、以下のようなコードは問題ありません。

import java.time.LocalDate;
import java.util.Arrays;

fun convToListAddDateAndPrint(vararg values: String, date: LocalDate) {
    println(Arrays.asList(*values).add(date.toString()))
}

呼び出す場合は、以下のように可変長引数以外のパラメータには名前付き引数を利用する必要があるようです。
(この辺りは、私の調べが足りてないだけかも?)

convToListAddDateAndPrint( "Str1", "Str2", "Str3", date = LocalDate.now())


まとめ

KotlinでもJavaと同様に可変長引数を扱える事が分かりました。
ただし、いくつかJavaとは異なる部分があり、意識する必要があります。

Kotlin - 拡張関数・拡張プロパティ

前回のエントリ で、文字列の前後に prefixとpostfixの文字列を付与する decorate というしょうもない関数を定義してみました。

この関数の呼び出しは以下のようになるのですが

decorate({ベースの文字列}, {prefix}, {postfix})

Kotlinの 拡張関数(extention function)という機能により、Stringのメンバ関数の様にして扱う事ができます。 結果的に以下の様に呼び出せるようになります。

<ベースの文字列>.decorate(<prefix>, <postfix>)


もともと定義した decorate 関数は以下の様なものでした

fun decorate(src: String, prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(src)
        .append(postfix)
        .toString()
}

これを拡張関数として定義し直すと以下の様になります

fun String.decorate(prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(this)
        .append(postfix)
        .toString()
}


拡張関数の構文は、以下ようになります。

fun <レシーバ型>.関数名: <戻り値型> {
    // ここに処理を記述。 ここでの this は レシーバオブジェクトを意味する
}

追加する関数の名前の前に、クラスまたはインターフェースの名前を置くだけです。このクラス名を レシーバ型(receivcer type)と呼び、拡張関数を呼び出す値は、レシーバーオブジェクト(receiver object)と呼ばれます。


実際に 定義してみた拡張関数を呼び出してみると以下の様になります。(Stringのメンバの様に呼び出している事が分かります)

val decoratedStr = "Kotlin".decorate("<b>", "</b>")
println("Kotlin -> $decoratedStr")

実行結果は以下の通りです。

Kotlin -> <b>Kotlin</b>

この例では、 String がレーシバ型で、"Kotlin"という文字列がレシーバオブジェクト ということになります。


拡張関数を定義する時の制限事項の一つに、カプセル化を破れない。というのがあります。
拡張関数では、拡張しているクラスのメソッドやプロパティに、そのクラス自身に定義されているメソッドのように直接アクセス可能です。
しかし、拡張関数はクラスに定義されたメソッドとは異なり、クラスの private または protected なメンバにアクセスすることは許されません。

また、拡張関数を定義した時に、自動的にプロジェクト全体を横断して利用可能になるわけではありません。
他のクラスや関数と同様にインポートが必要になります。


メンバが優先される

Sample というクラスを定義してメンバに hello という関数を定義します。
その上で同名の hello という拡張関数を定義します。

package net.yyuki.sandbox.expand

class Sample {
    fun hello() {
        println("Hello World")
    }
}

fun Sample.hello() {
    println("Hello World Expand")
}

このSampleのhello関数を呼び出してみます

import net.yyuki.sandbox.expand.hello
import net.yyuki.sandbox.expand.Sample

fun main() {
    val sample = Sample()
    sample.hello()
}

実行結果は以下の通りです

Hello World

メンバの関数の実行が優先されている事が分かります


ただし、ここから私の理解が追いついていないだけなのですが、
String には文字列を大文字化する toUpperCase というメソッドが存在しています。

例えば、これと同名の toUpperCase という拡張関数を String に定義します。
文字列の先頭を取り出すという関数名とは全く無関係な処理内容にしています。

package net.yyuki.sandbox.expand

fun String.toUpperCase(): String = this.first().toString()

で、これを以下のように呼び出すと

import net.yyuki.sandbox.expand.toUpperCase

fun main() {
    println("Kotlin".toUpperCase())
}

実行結果は以下の通りです。(通常の String の toUpperCase が実行されていない)

K

元のメンバの関数が実行されずに、拡張関数が呼ばれます。
この動きはいまいち何でこうなのかは理解できていないです。


拡張プロパティ

関数の構文ではなく、プロパティの構文を使ってクラスを拡張する方法も提供されています。
ただし、いかなる状態も持つことはできません。状態を格納するための場所がないためです。
つまり、Javaオブジェクトの既存インスタンスに付加的なフィールドを追加するのは不可能ということになります。

文字列の末尾の文字を取り出す拡張プロパティ lastChar を定義すると以下の様になります

val String.lastChar: Char
    get() = get(lastIndex)

上記では読み取り専用のプロパティの例でしたが、ミュータブルな書き込み可能な拡張プロパティの定義も可能です

var StringBuilder.lastChar: Char
    get() = get(lastIndex)
    set(value) {
        this.setCharAt(lastIndex , value)
    }

拡張プロパティを呼び出すコードは以下の通りです

import net.yyuki.sandbox.expand.lastChar
import java.lang.StringBuilder

fun main() {
    val lastCh = StringBuilder()
        .append("Abcdefg")
        .lastChar

    println("Last char = $lastCh")


    val sb = StringBuilder()
        .append("Abcdefg")
    sb.lastChar = '@'

    println("StringBuilder = $sb")
}

実行結果は以下の通りです

Last char = g
StringBuilder = Abcdef@


Javaライブラリ拡張

Kotlinで利用されている コレクションは Javaのコレクションライブラリです。
それにも関わらず、以下の様なListの先頭要素の取得、最終要素の取得する関数や、Setの最大値を取得する関数 が用意されています

val list = listOf("1st", "2nd", "3rd", "4th")

val firstElement = list.first() // Listの先頭要素を取得
val lastElement = list.last()   // Listの最終要素を取得

println("$list -> first=$firstElement, last=$lastElement")    // [1st, 2nd, 3rd, 4th] -> first=1st, last=4th


val set = setOf(10, 30, -1, 100, 1000, 5)
val maxNumber = set.max()    // コレクションの最大値を取得

println("$set -> max=$maxNumber")    // [10, 30, -1, 100, 1000, 5] -> max=1000

これらのfirst, last, max といった関数は、まさに拡張関数として宣言されたものです。
これら以外にも多くの拡張関数がKotlinの標準ライブラリに宣言されています。


まとめ

Kotlin では、Javaと違ってクラスを継承したりしなくても、クラスを拡張する能力が提供されていることが分かりました。



関連エントリ

blog.y-yuki.net

Kotlin - 静的なユーティリティクラスなしで関数を定義する

Javaの場合、メソッドは必ずクラスに属する必要があります。
対象のクラスの操作を提供するという意味ではこの形は良いものであると思います。

ただし、業務アプリケーションなどを作成していくうちに、どうしても特定のクラスに属さないユーティリ的なメソッドというものも登場してきます。
その様な場合でも何らかのクラスに属する必要があります。
こういった際には、一般的に xxxUtils という名称のクラスが登場することになります。

例えば、特定の文字列の前後をprefixとpostfixの文字列で装飾する decorate というメソッドを定義するとした場合、以下の様なコードを書くことになります。

package net.yyuki.sandbox.string;

public final class StringUtils {

    public static String decorate(String src, String prefix, String postfix) {
        return new StringBuilder(prefix)
                    .append(src)
                    .append(postfix)
                    .toString();
    }

    private StringUtils() {
    }
}

呼び出し側コードは以下の様になります

import net.yyuki.sandbox.string.StringUtils;

public class Main {
    public static void main(String[] args) {
        String baseStr = "something";
        String result = StringUtils.decorate(baseStr, "<", ">");

        System.out.println(baseStr + " -> " + result);
    }
}

実行結果は以下の通りです

something -> <something>


Kotlinの場合はトップレベルに関数を配置できる

Kotlinでは上記の様な意味の薄いクラスを作成する必要なしに、ソースファイルのトップレベル(=クラスの外側)に関数を置くことができます。
この関数は、ファイルのトップに宣言されたパッケージのメンバとなります。それら関数を他パッケージから呼び出す場合は、今まで通りインポートは必要ですが、不要なレベルのネストは存在しなくなります。


Javaで定義した StringUtils#decvorateStrings.ktというファイルを作成して定義し直すと以下の様になります

package net.yyuki.sandbox.string

import java.lang.StringBuilder

fun decorate(src: String, prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(src)
        .append(postfix)
        .toString()
}

(Kotlinでの)呼び出し側のコードは以下の様になります

import net.yyuki.sandbox.string.decorate

fun main() {
    val baseStr = "kotlin is jvm lang"
    val result = decorate(src = baseStr, prefix = "{", postfix = "}")

    println("$baseStr -> $result")
}

実行結果は以下の通りです

kotlin is jvm lang -> {kotlin is jvm lang}


Kotlinで定義した関数をJavaから呼び出す

Kotlinで定義した関数をKotlinから呼び出す場合はクラスは登場しなくなっていますが、
Kotlinで定義した関数をJavaから呼び出す場合はクラス名を意識する必要があります。

さきほどStrings.kt というファイルに decorate というメソッドを定義しましたが、Javaからはこの関数は StringsKtクラスの decorate メソッドとして呼び出す事ができます。

実際のJavaからの呼び出しのコードは以下の様になります

import net.yyuki.sandbox.string.StringsKt;

public class Main {
    public static void main(String[] args) {
        String baseStr = "something";
        String result = StringsKt.decorate(baseStr, "(", ")");

        System.out.println(baseStr + " -> " + result);
    }
}


Kotlinのトップレベル関数を含む、生成されたクラスの名前を変更する場合は、ファイルに@JvmNameアノテーションを追加します。
ファイルの冒頭(=パッケージ名の前)にアノテーションを配置する必要があります。

例えば、Java向けのクラス名を StringsOperation にしたい場合は以下の様になります

@file:JvmName("StringsOperation")

package net.yyuki.sandbox.string

import java.lang.StringBuilder

fun decorate(src: String, prefix: String, postfix: String): String {
    return StringBuilder(prefix)
        .append(src)
        .append(postfix)
        .toString()
}

Javaからの呼び出しのコードは以下の様になります

import net.yyuki.sandbox.string.StringsOperation;

public class Main {
    public static void main(String[] args) {
        String baseStr = "something";
        String result = StringsOperation.decorate(baseStr, "(", ")");

        System.out.println(baseStr + " -> " + result);
    }
}

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 ではコレクションに対して多くの操作を行う事が可能です。