プログラマー向け Kotlin ブートキャンプ 6: 機能の操作

この Codelab は、プログラマー向け Kotlin ブートキャンプ コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。理解度によっては、特定のセクションの概要を読むだけで済む場合があります。このコースは、オブジェクト指向言語の知識があり、Kotlin を学習したいプログラマーを対象としています。

はじめに

これは Kotlin ブートキャンプの最後の Codelab です。この Codelab では、アノテーションとラベル付きの break について学習します。Kotlin の重要な部分であるラムダ式と高階関数について確認します。また、インライン関数と単一抽象メソッド(SAM)インターフェースについても詳しく学習します。最後に、Kotlin 標準ライブラリについて詳しく学習します。

このコースのレッスンは、1 つのサンプルアプリを作成するのではなく、知識を深めるように設計されています。また、各レッスンは半独立しているため、よく知っているセクションは読み飛ばすことができます。これらの例を関連付けるため、多くは水族館をテーマにしています。水族館の物語の全体像を確認したい場合は、Udacity の Kotlin ブートキャンプ(プログラマー向け)コースをご覧ください。

前提となる知識

  • Kotlin の関数、クラス、メソッドの構文
  • IntelliJ IDEA で新しいクラスを作成してプログラムを実行する方法
  • ラムダ式と高階関数の基本

学習内容

  • アノテーションの基本
  • ラベル付きブレークの使用方法
  • 高階関数に関する注意点
  • 単一抽象メソッド(SAM)インターフェースについて
  • Kotlin 標準ライブラリについて

演習内容

  • シンプルなアノテーションを作成します。
  • ラベル付きの改行を使用します。
  • Kotlin のラムダ関数を確認します。
  • 高階関数を使用および作成する。
  • 単一抽象メソッド インターフェースを呼び出します。
  • Kotlin 標準ライブラリの関数を使用します。

アノテーションは、コードにメタデータを付加する方法であり、Kotlin 固有のものではありません。アノテーションはコンパイラによって読み取られ、コードまたはロジックの生成に使用されます。KtorKotlinx などの多くのフレームワークや Room では、アノテーションを使用して、コードの実行方法やコードとのやり取りの方法を構成します。フレームワークの使用を開始するまでアノテーションに遭遇することはほとんどありませんが、アノテーションの読み方を理解しておくと便利です。

Kotlin 標準ライブラリを通じて利用できるアノテーションもあり、コードのコンパイル方法を制御します。これらは Kotlin を Java コードにエクスポートする場合に非常に便利ですが、それ以外の場合はあまり必要ありません。

アノテーションは、アノテーションを付けるものの直前に記述します。ほとんどのもの(クラス、関数、メソッド、制御構造など)にアノテーションを付けることができます。一部のアノテーションは引数を取ることができます。

以下にアノテーションの例を示します。

@file:JvmName("InteropFish")
class InteropFish {
   companion object {
       @JvmStatic fun interop()
   }
}

これは、このファイルのエクスポートされた名前が JvmName アノテーション付きの InteropFish であることを示しています。JvmName アノテーションは "InteropFish" の引数を取ります。コンパニオン オブジェクトでは、@JvmStatic は Kotlin に interop()InteropFish の静的関数にするよう指示します。

独自のアノテーションを作成することもできますが、これは主に、実行時にクラスに関する特定の情報を必要とするライブラリ(リフレクション)を作成する場合に役立ちます。

ステップ 1: 新しいパッケージとファイルを作成する

  1. [src] で、新しいパッケージ example を作成します。
  2. example で、新しい Kotlin ファイル Annotations.kt を作成します。

ステップ 2: 独自の注釈を作成する

  1. Annotations.kt で、2 つのメソッド(trim()fertilize())を持つ Plant クラスを作成します。
class Plant {
        fun trim(){}
        fun fertilize(){}
}
  1. クラス内のすべてのメソッドを出力する関数を作成します。::class を使用して、実行時にクラスに関する情報を取得します。declaredMemberFunctions を使用して、クラスのメソッドのリストを取得します。(これにアクセスするには、kotlin.reflect.full.* をインポートする必要があります)。
import kotlin.reflect.full.*    // required import

class Plant {
    fun trim(){}
    fun fertilize(){}
}

fun testAnnotations() {
    val classObj = Plant::class
    for (m in classObj.declaredMemberFunctions) {
        println(m.name)
    }
}
  1. テストルーチンを呼び出す main() 関数を作成します。プログラムを実行して出力を確認します。
fun main() {
    testAnnotations()
}
⇒ trim
fertilize
  1. 簡単なアノテーション ImAPlant を作成します。
annotation class ImAPlant

これは、アノテーションが付けられていることを示す以外には何も行いません。

  1. Plant クラスの前にアノテーションを追加します。
@ImAPlant class Plant{
    ...
}
  1. testAnnotations() を変更して、クラスのすべてのアノテーションを印刷します。クラスのすべてのアノテーションを取得するには、annotations を使用します。プログラムを実行して結果を確認します。
fun testAnnotations() {
    val plantObject = Plant::class
    for (a in plantObject.annotations) {
        println(a.annotationClass.simpleName)
    }
}
⇒ ImAPlant
  1. testAnnotations() を変更して ImAPlant アノテーションを見つけます。findAnnotation() を使用して、特定のアノテーションを検索します。プログラムを実行して結果を確認します。
fun testAnnotations() {
    val plantObject = Plant::class
    val myAnnotationObject = plantObject.findAnnotation<ImAPlant>()
    println(myAnnotationObject)
}
⇒ @example.ImAPlant()

ステップ 3: ターゲット設定されたメモを作成する

アノテーションはゲッターまたはセッターをターゲットにできます。その場合は、@get: または @set: 接頭辞を使用して適用できます。これは、アノテーションを含むフレームワークを使用する場合によく発生します。

  1. プロパティのゲッターにのみ適用できる OnGet と、プロパティのセッターにのみ適用できる OnSet の 2 つのアノテーションを宣言します。それぞれに @Target(AnnotationTarger.PROPERTY_GETTER) または PROPERTY_SETTER を使用します。
annotation class ImAPlant

@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class OnGet
@Target(AnnotationTarget.PROPERTY_SETTER)
annotation class OnSet

@ImAPlant class Plant {
    @get:OnGet
    val isGrowing: Boolean = true

    @set:OnSet
    var needsFood: Boolean = false
}

アノテーションは、実行時とコンパイル時の両方で検査を行うライブラリを作成するうえで非常に強力です。ただし、一般的なアプリケーション コードでは、フレームワークが提供するアノテーションのみが使用されます。

Kotlin には、フローを制御する方法がいくつかあります。関数からその関数を囲む関数に返る return については、すでにご存じでしょう。break の使用は return と同様ですが、for ループ用です。

Kotlin では、ラベル付き break と呼ばれるものを使用して、ループをさらに制御できます。ラベルで修飾された break は、そのラベルでマークされたループの直後の実行ポイントにジャンプします。これは、ネストされたループを扱う場合に特に便利です。

Kotlin の任意の式にラベルを付けることができます。ラベルは、識別子の後に @ 記号が続く形式です。

  1. Annotations.kt で、内部ループから抜け出すことでラベル付きブレークを試します。
fun labels() {
    outerLoop@ for (i in 1..100) {
         print("$i ")
         for (j in 1..100) {
             if (i > 10) break@outerLoop  // breaks to outer loop
        }
    }
}

fun main() {
    labels()
}
  1. プログラムを実行して出力を確認します。
⇒ 1 2 3 4 5 6 7 8 9 10 11 

同様に、ラベル付きの continue を使用できます。ラベル付き continue は、ラベル付きループを中断するのではなく、ループの次の反復処理に進みます。

ラムダは匿名関数です。つまり、名前のない関数です。変数に代入したり、関数やメソッドの引数として渡したりできます。非常に便利です。

ステップ 1: 簡単なラムダを作成する

  1. IntelliJ IDEA で REPL を起動します([Tools] > [Kotlin] > [Kotlin REPL])。
  2. 引数 dirty: Int を使用して、dirty を 2 で割る計算を行うラムダを作成します。ラムダを変数 waterFilter に割り当てます。
val waterFilter = { dirty: Int -> dirty / 2 }
  1. waterFilter を呼び出して、値 30 を渡します。
waterFilter(30)
⇒ res0: kotlin.Int = 15

ステップ 2: フィルタ ラムダを作成する

  1. REPL で、1 つのプロパティ name を持つデータクラス Fish を作成します。
data class Fish(val name: String)
  1. Flipper、Moby Dick、Dory という名前の 3 つの Fish のリストを作成します。
val myFish = listOf(Fish("Flipper"), Fish("Moby Dick"), Fish("Dory"))
  1. 文字「i」を含む名前をチェックするフィルタを追加します。
myFish.filter { it.name.contains("i")}
⇒ res3: kotlin.collections.List<Line_1.Fish> = [Fish(name=Flipper), Fish(name=Moby Dick)]

ラムダ式では、it は現在のリスト要素を指し、フィルタは各リスト要素に順番に適用されます。

  1. ", " を区切り文字として使用して、結果に joinString() を適用します。
myFish.filter { it.name.contains("i")}.joinToString(", ") { it.name }
⇒ res4: kotlin.String = Flipper, Moby Dick

joinToString() 関数は、フィルタされた名前を結合して、指定された文字列で区切られた文字列を作成します。これは、Kotlin 標準ライブラリに組み込まれている多くの便利な関数の 1 つです。

ラムダ関数や他の関数を引数として関数に渡すと、高階関数が作成されます。上記のフィルタは、その簡単な例です。filter() は関数であり、リストの各要素を処理する方法を指定するラムダを渡します。

拡張ラムダを使用した高階関数の記述は、Kotlin 言語の最も高度な部分の 1 つです。書き方を覚えるには時間がかかりますが、とても便利です。

ステップ 1: 新しいクラスを作成する

  1. example パッケージ内に、新しい Kotlin ファイル Fish.kt を作成します。
  2. Fish.kt で、1 つのプロパティ name を持つデータクラス Fish を作成します。
data class Fish (var name: String)
  1. 関数 fishExamples() を作成します。fishExamples() に、"splashy" という名前の魚(すべて小文字)を作成します。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
}
  1. fishExamples() を呼び出す main() 関数を作成します。
fun main () {
    fishExamples()
}
  1. main() の左にある緑色の三角形をクリックして、プログラムをコンパイルして実行します。まだ出力はありません。

ステップ 2: 高階関数を使用する

with() 関数を使用すると、オブジェクトまたはプロパティへの 1 つ以上の参照をよりコンパクトに作成できます。this を使用しています。with() は実際には高階関数であり、ラムダでは指定されたオブジェクトをどのように処理するかを指定します。

  1. with() を使用して、fishExamples() の魚の名前を大文字にします。波かっこ内の this は、with() に渡されたオブジェクトを参照します。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        this.capitalize()
    }
}
  1. 出力がないため、println() を追加します。this は暗黙的で不要なため、削除できます。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    with (fish.name) {
        println(capitalize())
    }
}
⇒ Splashy

ステップ 3: 高階関数を作成する

内部的には、with() は高階関数です。この仕組みを確認するには、文字列のみを処理する with() の簡略版を作成します。

  1. Fish.kt で、2 つの引数を取る関数 myWith() を定義します。引数は、操作するオブジェクトと、操作を定義する関数です。関数での引数名の慣例は block です。この場合、その関数は何も返しません。これは Unit で指定されています。
fun myWith(name: String, block: String.() -> Unit) {}

myWith() 内で、block()String の拡張関数になりました。拡張されるクラスは、多くの場合、レシーバ オブジェクトと呼ばれます。この場合、name はレシーバー オブジェクトです。

  1. myWith() の本体で、渡された関数 block() をレシーバ オブジェクト name に適用します。
fun myWith(name: String, block: String.() -> Unit) {
    name.block()
}
  1. fishExamples() で、with()myWith() に置き換えます。
fun fishExamples() {
    val fish = Fish("splashy")  // all lowercase
    myWith (fish.name) {
        println(capitalize())
    }
}

fish.name は名前引数、println(capitalize()) はブロック関数です。

  1. プログラムを実行すると、以前と同じように動作します。
⇒ Splashy

ステップ 4: その他の組み込み拡張機能を試す

with() 拡張ラムダは非常に便利で、Kotlin 標準ライブラリの一部です。run()apply()let() など、他にも便利なものがいくつかあります。

run() 関数は、すべての型で動作する拡張機能です。引数として 1 つのラムダを取り、ラムダの実行結果を返します。

  1. fishExamples() で、fishrun() を呼び出して名前を取得します。
fish.run {
   name
}

これは name プロパティを返すだけです。この値を変数に割り当てたり、出力したりできます。これは実際には有用な例ではありません。プロパティにアクセスするだけで済むためです。しかし、run() はより複雑な式で役立ちます。

apply() 関数は run() と似ていますが、ラムダの結果ではなく、適用された変更後のオブジェクトを返します。これは、新しく作成されたオブジェクトでメソッドを呼び出す場合に便利です。

  1. fish のコピーを作成し、apply() を呼び出して新しいコピーの名前を設定します。
val fish2 = Fish(name = "splashy").apply {
     name = "sharky"
}
println(fish2.name)
⇒ sharky

let() 関数は apply() と似ていますが、変更を含むオブジェクトのコピーを返します。これは、操作を連鎖させる場合に役立ちます。

  1. let() を使用して fish の名前を取得し、大文字に変換して別の文字列を連結し、その結果の長さを取得して、その長さに 31 を加えて結果を出力します。
println(fish.let { it.name.capitalize()}
.let{it + "fish"}
.let{it.length}
.let{it + 31})
⇒ 42

この例では、it が参照するオブジェクト タイプは、FishStringStringInt の順になります。

  1. let() を呼び出した後に fish を出力すると、変更されていないことがわかります。
println(fish.let { it.name.capitalize()}
    .let{it + "fish"}
    .let{it.length}
    .let{it + 31})
println(fish)
⇒ 42
Fish(name=splashy)

ラムダ式と高階関数は非常に便利ですが、知っておくべきことがあります。ラムダ式はオブジェクトです。ラムダ式は Function インターフェースのインスタンスであり、Function インターフェース自体は Object のサブタイプです。前述の myWith() の例について考えてみます。

myWith(fish.name) {
    capitalize()
}

Function インターフェースには、ラムダ式を呼び出すためにオーバーライドされるメソッド invoke() があります。コードは次のようになります。

// actually creates an object that looks like this
myWith(fish.name, object : Function1<String, Unit> {
    override fun invoke(name: String) {
        name.capitalize()
    }
})

通常、オブジェクトの作成と関数の呼び出しにはメモリと CPU 時間のオーバーヘッドがほとんど発生しないため、これは問題になりません。ただし、myWith() のようにどこでも使用するものを定義する場合は、オーバーヘッドが大きくなる可能性があります。

Kotlin には、このケースを処理して、コンパイラに少しだけ処理を追加することで実行時のオーバーヘッドを削減する手段として inline が用意されています。(inline については、具現化された型について説明した前のレッスンで少し学習しました)。関数を inline としてマークすると、関数が呼び出されるたびに、コンパイラはソースコードを変換して関数を「インライン化」します。つまり、コンパイラはコードを変更して、ラムダをラムダ内の命令に置き換えます。

上記の例の myWith()inline でマークされている場合:

inline myWith(fish.name) {
    capitalize()
}

直接呼び出しに変換されます。

// with myWith() inline, this becomes
fish.name.capitalize()

大きな関数をインライン化するとコードサイズが増加するため、myWith() のように何度も使用されるシンプルな関数に最適です。先ほど学習したライブラリの拡張関数には inline が付いているため、余分なオブジェクトが作成される心配はありません。

単一抽象メソッドは、メソッドが 1 つだけ含まれるインターフェースを意味します。Java プログラミング言語で記述された API を使用する際に頻繁に使用されるため、SAM という頭字語があります。たとえば、単一の抽象メソッド run() を持つ Runnable や、単一の抽象メソッド call() を持つ Callable などがあります。

Kotlin では、SAM をパラメータとして受け取る関数を常に呼び出す必要があります。以下の例をお試しください。

  1. example 内に Java クラス JavaRun を作成し、次のコードをファイルに貼り付けます。
package example;

public class JavaRun {
    public static void runNow(Runnable runnable) {
        runnable.run();
    }
}

Kotlin では、型の前に object: を付けることで、インターフェースを実装するオブジェクトをインスタンス化できます。これは、SAM にパラメータを渡す場合に便利です。

  1. Fish.kt に戻り、object: を使用して Runnable を作成する関数 runExample() を作成します。オブジェクトは "I'm a Runnable" を出力して run() を実装する必要があります。
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
}
  1. 作成したオブジェクトで JavaRun.runNow() を呼び出します。
fun runExample() {
    val runnable = object: Runnable {
        override fun run() {
            println("I'm a Runnable")
        }
    }
    JavaRun.runNow(runnable)
}
  1. main() から runExample() を呼び出して、プログラムを実行します。
⇒ I'm a Runnable

印刷するのに多くの作業が必要ですが、SAM の仕組みを示す良い例です。もちろん、Kotlin にはこれをより簡単に実現する方法があります。オブジェクトの代わりにラムダを使用すると、コードを大幅に簡潔にできます。

  1. runExample の既存のコードを削除し、ラムダで runNow() を呼び出すように変更して、プログラムを実行します。
fun runExample() {
    JavaRun.runNow({
        println("Passing a lambda as a Runnable")
    })
}
⇒ Passing a lambda as a Runnable
  1. 最後のパラメータ呼び出し構文を使用すると、さらに簡潔に記述でき、かっこを削除できます。
fun runExample() {
    JavaRun.runNow {
        println("Last parameter is a lambda as a Runnable")
    }
}
⇒ Last parameter is a lambda as a Runnable

これが SAM(Single Abstract Method)の基本です。パターン
Class.singleAbstractMethod { lambda_of_override } を使用すると、1 行のコードで SAM をインスタンス化、オーバーライド、呼び出しできます。

このレッスンでは、Kotlin の重要な部分であるラムダ式と高階関数について詳しく説明しました。アノテーションとラベル付きの break についても学習しました。

  • アノテーションを使用して、コンパイラに指定します。例:
    @file:JvmName("Foo")
  • ラベル付き break を使用すると、ネストされたループ内からコードを終了できます。例:
    if (i > 10) break@outerLoop // breaks to outerLoop label
  • ラムダ式は、高階関数と組み合わせると非常に強力になります。
  • ラムダはオブジェクトです。オブジェクトの作成を回避するには、関数を inline でマークします。コンパイラはラムダの内容をコードに直接配置します。
  • inline は慎重に使用する必要がありますが、プログラムによるリソース使用量を削減するのに役立ちます。
  • SAM(Single Abstract Method)は一般的なパターンであり、ラムダを使用すると簡素化できます。基本的なパターンは
    Class.singleAbstractMethod { lamba_of_override } です。
  • Kotlin 標準ライブラリには、複数の SAM を含む、多くの便利な関数が用意されています。ライブラリの内容を把握しておきましょう。

Kotlin には、このコースで説明した以外にも多くの機能がありますが、独自の Kotlin プログラムの開発を開始するための基本は身につきました。この表現力豊かな言語に興味を持ち、コードの記述量を減らしながら(特に Java プログラミング言語を使用している場合)より多くの機能を作成できることを楽しみにしていただければ幸いです。Kotlin のエキスパートになるには、実践と学習を繰り返すのが一番です。引き続き、Kotlin を独自に探求し、学習してください。

Kotlin ドキュメント

このコースのトピックについてさらに詳しい情報が必要な場合や、行き詰まった場合は、https://kotlinlang.org を参照することをおすすめします。

Kotlin のチュートリアル

https://try.kotlinlang.org には、Kotlin Koans という豊富なチュートリアル、ウェブベースのインタープリタ、例を含む完全なリファレンス ドキュメントが用意されています。

Udacity コース

このトピックに関する Udacity コースについては、プログラマー向け Kotlin ブートキャンプをご覧ください。

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトで確認できます。

Kotlin 標準ライブラリ

Kotlin 標準ライブラリには、便利な関数が多数用意されています。独自の関数やインターフェースを作成する前に、標準ライブラリを必ず確認して、すでに作成されているものがないか確認してください。新しい機能は頻繁に追加されるため、定期的にご確認ください。

Kotlin のチュートリアル

https://try.kotlinlang.org には、Kotlin Koans という豊富なチュートリアル、ウェブベースのインタープリタ、例を含む完全なリファレンス ドキュメントが用意されています。

Udacity コース

このトピックに関する Udacity コースについては、プログラマー向け Kotlin ブートキャンプをご覧ください。

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトで確認できます。

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

Kotlin では、SAM は次の意味を表します。

▢ 安全な引数の一致

▢ Simple Access Method

▢ 単一抽象メソッド

▢ 戦略的アクセス方法

問題 2

次のうち、Kotlin 標準ライブラリの拡張関数ではないものはどれですか?

elvis()

apply()

run()

with()

問題 3

Kotlin のラムダの説明として正しくないものは、次のうちどれですか。

▢ ラムダは匿名関数です。

▢ ラムダはインライン化されない限りオブジェクトです。

▢ ラムダはリソースを大量に消費するため、使用しないでください。

▢ ラムダは他の関数に渡すことができます。

問題 4

Kotlin のラベルは、識別子の後に次の文字を付けて示します。

:

::

@:

@

これで、プログラマー向け Kotlin ブートキャンプの Codelab は終了です。

他の Codelab へのリンクを含むコースの概要については、「プログラマー向け Kotlin ブートキャンプ: コースへようこそ」をご覧ください。