Kotlin のコレクション

1. 始める前に

この Codelab では、Kotlin のコレクション、ラムダ、高階関数について学習します。

前提条件

  • 以前の Codelab で紹介されている Kotlin のコンセプトに関する基本的な知識がある。
  • Kotlin のプレイグラウンドを使用した Kotlin プログラムの作成と編集に慣れている。

学習内容

  • セットやマップなどのコレクションの使用方法
  • ラムダの基本
  • 高階関数の基本

必要なもの

2. コレクションの詳細

コレクションとは、単語リストや従業員記録などの関連する項目のグループです。コレクションには、項目が順序付けされているものも、順序付けされていないものも、また一意なものも、一意でないものもあります。リストという種類のコレクションは学習済みです。リストには、項目に順序がありますが、一意である必要はありません。

リストと同様に、Kotlin では可変コレクションと不変コレクションを区別します。Kotlin には、項目の追加または削除、コレクションの表示、操作を行う多数の関数が用意されています。

リストを作成する

このタスクでは、数値のリストを作成して、それを並べ替える方法を説明します。

  1. Kotlin のプレイグラウンドを開きます。
  2. コードを次のコードで置き換えます。
fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
}
  1. 緑色の矢印をタップしてプログラムを実行し、表示された結果を確認します。
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
  1. リストには 0 から 9 までの 10 個の数値が入っています。複数回表示される数値もあれば、まったく表示されない数値もあります。
  2. リストでの項目の順序は重要です。最初の項目は 0、2 番目は 3 というようになっています。項目の順序は、変更しない限り保持されます。
  3. これまでの Codelab から、リストには、昇順で並べ替えられたリストのコピーを返す sorted() のような組み込み関数が多数あったことを思い出してください。println() の後に、並べ替えられたリストのコピーを表示するプログラムの行を追加します。
println("sorted: ${numbers.sorted()}")
  1. プログラムを再度実行して、結果を確認します。
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]

数値を並べ替えると、リスト内での各数値の出現回数や一切出現しなかったかどうかを簡単に把握できます。

セットの詳細

Kotlin には、セットという種類のコレクションもあります。関連する項目のグループです。ただし、リストとは異なり、重複させることができず順序もありません。項目はセットに入っている場合と入っていない場合があり、セットにある場合は 1 つだけです。集合という数学の概念に似ています。読んだ本の集合がその例です。同じ本を何度読んでも、それが読んだ本の集合に含まれるという事実は変わりません。

  1. 以下の行をプログラムに追加して、リストをセットに変換します。
val setOfNumbers = numbers.toSet()
println("set:    ${setOfNumbers}")
  1. プログラムを実行して結果を確認します。
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]
set:    [0, 3, 8, 4, 5, 9, 2]

結果には、元のリストにあったすべての数値がありますが、それぞれの数値は 1 回だけ出現します。元のリストと同じ順序で並んでいますが、この順序はセットでは重要ではありません。

  1. 次の行を追加して、可変セットと不変セットを定義し、順序が異なる同じ数の集合で初期化します。
val set1 = setOf(1,2,3)
val set2 = mutableSetOf(3,2,1)
  1. 等しいかどうかを表示する次の行を追加します。
println("$set1 == $set2: ${set1 == set2}")
  1. プログラムを実行して、新しい結果を確認します。
[1, 2, 3] == [3, 2, 1]: true

片方は可変で、もう片方は不変な上、項目の順序も異なりますが、同じ項目の集合を含んでいるため、等しいとみなされます。

セットに対して実行する主な操作には、contains() 関数を用いて特定の項目がセットに含まれるかどうかを確認するというものがあります。contains() は以前に使用しましたが、そのときはリストに対して使用しました。

  1. 次の行をプログラムに追加して、7 がセットにあるかどうかを表示します。
println("contains 7: ${setOfNumbers.contains(7)}")
  1. プログラムを実行して、追加された結果を確認します。
contains 7: false

セットに存在する値を使用してテストすることもできます。

上記のコード全体を次に示します。

fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
    println("sorted: ${numbers.sorted()}")
    val setOfNumbers = numbers.toSet()
    println("set:    ${setOfNumbers}")
    val set1 = setOf(1,2,3)
    val set2 = mutableSetOf(3,2,1)
    println("$set1 == $set2: ${set1 == set2}")
    println("contains 7: ${setOfNumbers.contains(7)}")
}

数学の集合と同様に、Kotlin でも、intersect()union() を使用して、2 つの集合に対する積集合(∩)や和集合(∪)のような操作を行うこともできます。

マップの詳細

この Codelab で学習する最後のコレクションは、マップ(辞書とも呼ぶ)です。マップは、特定のキーを指定して値を簡単に検索できるように設計された、Key-Value ペアの集まりです。キーは一意であり、各キーはちょうど 1 つの値にマッピングされますが、値は重複する場合があります。マップ内の値は、文字列、数値、オブジェクトだけでなく、リストやセットなどの別のコレクションとすることもできます。

b55b9042a75c56c0.png

マップは、データのペアがある場合に便利です。キーに基づいて各ペアを識別できます。キーは対応する値に「マッピング」されます。

  1. Kotlin のプレイグラウンドでコードすべてを、人の名前と年齢を格納する可変マップを作成する次のコードで置き換えます。
fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    println(peopleAges)
}

これにより、String(キー)から Int(値)への可変マップが作成され、マップが 2 つのエントリで初期化されて、項目が表示されます。

  1. プログラムを実行して結果を確認します。
{Fred=30, Ann=23}
  1. マップにエントリを追加するために、put() 関数を使用し、その際にキーと値を渡します。
peopleAges.put("Barbara", 42)
  1. 次のように、省略記法を使用してエントリを追加することもできます。
peopleAges["Joe"] = 51

以上のコード全体を次に示します。

fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    peopleAges.put("Barbara", 42)
    peopleAges["Joe"] = 51
    println(peopleAges)
}
  1. プログラムを実行して結果を確認します。
{Fred=30, Ann=23, Barbara=42, Joe=51}

上記のように、キー(名前)は一意ですが、値(年齢)は重複する場合があります。同じキーを使用してアイテムを追加しようとすると、何が起きるでしょうか。

  1. println() の前に次のコードを追加します。
peopleAges["Fred"] = 31
  1. プログラムを実行して結果を確認します。
{Fred=31, Ann=23, Barbara=42, Joe=51}

キー "Fred" を再度追加することはできませんが、マッピングされる値が 31 に更新されます。

以上のように、マップは、コード内でキーを値にマッピングする簡単な方法として便利です。

3.コレクションの操作

さまざまな特性、種類のコレクションがありますが、共通の動作が多数あります。可変の場合、項目の追加と削除ができます。全項目の列挙や特定の項目の検索ができますが、別の種類のコレクションに変換できる場合もあります。以前に toSet() を使用して ListSet に変換しています。コレクションの操作に役立つ関数をいくつか紹介しましょう。

forEach

peopleAges に入っている項目を表示し、この際には人の名前と年齢を含めるとします。たとえば、"Fred is 31, Ann is 23,..." のようになります。前の Codelab で for ループを学んでいるので、for (people in peopleAges) { ... } でループを記述できると思います。

しかし、コレクション内の全オブジェクトを列挙することは共通の操作であることから、Kotlin には、全項目を走査して各項目になんらかの操作を行う forEach() が用意されています。

  1. プレイグラウンドで、println() の後に次のコードを追加します。
peopleAges.forEach { print("${it.key} is ${it.value}, ") }

for ループに似ていますが、もう少しコンパクトです。forEach では、現在の項目の変数を指定する代わりに、特別な識別子 it を使用します。

なお、forEach() メソッドを呼び出すときに丸かっこを追加する必要はありません。波かっこ「{}」で囲んだコードを渡します。

  1. プログラムを実行して、追加された結果を確認します。
Fred is 31, Ann is 23, Barbara is 42, Joe is 51,

望む出力とほとんど同じですが、末尾にカンマがあります。

コレクションを文字列に変換するのは共通の操作ですが、末尾に余分な区切り文字があるのも共通の問題です。その解決方法については、以降のステップで説明します。

map

map() 関数(コレクションのマップや辞書と混同しないでください)は、コレクション内の各項目に変換を適用します。

  1. プログラム内の forEach ステートメントを次の行で置き換えます。
println(peopleAges.map { "${it.key} is ${it.value}" }.joinToString(", ") )
  1. プログラムを実行して、追加された結果を確認します。
Fred is 31, Ann is 23, Barbara is 42, Joe is 51

正しい出力が表示されました。余分なカンマもありません。1 行で多くのことを行っているので、詳しく見てみましょう。

  • peopleAges.mappeopleAges 内の各項目に変換を適用し、変換された項目の新しいコレクションを作成しています。
  • 波かっこ「{}」内の部分では、各項目に適用する変換を定義しています。この変換では、Key-Value ペアを受け取って文字列に変換しています。たとえば、<Fred, 31>Fred is 31 に変換されます。
  • joinToString(", ") は、変換されたコレクション内の各項目を文字列に追加します。「,」で区切りますが、最後の項目には追加しません。
  • これらをすべて「.」(ドット演算子)で連結します。以前の Codelab で、関数呼び出しやプロパティ アクセスに行ったのと同様です。

filter

コレクションに共通する操作には、特定の条件に一致するアイテムを見つけるというものもあります。filter() 関数は、式に基づいて、コレクション内の一致する項目を返します。

  1. println() の後に、次の行を追加します。
val filteredNames = peopleAges.filter { it.key.length < 4 }
println(filteredNames)

ここでも、filter の呼び出しに丸かっこは不要です。it はリスト内の現在の項目を表します。

  1. プログラムを実行して、追加された結果を確認します。
{Ann=23, Joe=51}

この場合、式では、キー(String)の長さを取得し、その値が 4 未満かどうかを確認しています。一致する項目、つまり名前の長さが 4 文字未満の項目がすべて新しいコレクションに追加されます。

フィルタをマップに適用したときに返される型は、新しいマップ(LinkedHashMap)です。このマップに別の処理を行うことができます。またはリストなどの別の種類のコレクションに変換することもできます。

4. ラムダ式と高階関数の詳細

ラムダ

先ほどの例をもう一度見てみましょう。

peopleAges.forEach { print("${it.key} is ${it.value}") }

変数(peopleAges)に、それに対して呼び出される関数(forEach)が付いています。関数名の後のパラメータが入った丸かっこの代わりに、波かっこ「{}」に囲まれたコードがあります。同じパターンが、前のステップの map 関数と filter 関数を使用するコードにあります。forEach 関数は、peopleAges 変数に対して呼び出され、波かっこ内のコードを使用します。

波かっこ内で小さな関数を記述したようですが、関数名がありません。このように名前を使わずにその場で使用できる関数は大変便利な概念であり、ラムダ式(あるいは単にラムダ)と呼ばれています。

このことを導入として、Kotlin で関数を扱う強力な方法についての重要なトピックに移ります。関数は、変数やクラスに保存したり、引数として渡したり、返したりすることもできます。IntString などの型の変数と同じように扱うことができます。

関数型

Kotlin には、このような動作を実現するため、関数型と呼ばれるものが用意されていて、関数型では入力パラメータと戻り値に基づいて特定の種類の関数を定義できます。次の形式で表現されます。

関数型の例: (Int) -> Int

上記の関数型の関数は、Int 型のパラメータを受け取って、Int 型の値を返す必要があります。関数型の表記法では、パラメータは丸かっこ内に列挙されます(複数のパラメータがある場合はカンマで区切ります)。これに、矢印「->」、戻り値の型と続きます。

次の条件を満たすのはどんな関数型でしょう。次のように整数の入力値を 3 倍にするラムダ式です。ラムダ式の構文では、パラメータの最初の部分(赤い箱で強調)が最初にあり、その後に、関数の矢印、関数の本体(紫色の箱で強調)と続きます。ラムダの最後の式は戻り値です。

252712172e539fe2.png

次の図のように、ラムダを変数に保存することもできます。この構文は、Int のような基本的なデータ型の変数を宣言する方法と同様です。変数名(黄色の箱)、変数の型(青い箱)、変数の値(緑色の箱)があります。triple 変数には関数が保存されます。その型は関数型 (Int) -> Int で、値はラムダ式 { a: Int -> a * 3} です。

  1. プレイグラウンドで、このコードを試してみてください。triple 関数を定義して、5 のような数値を渡して呼び出します。4d3f2be4f253af50.png
fun main() {
    val triple: (Int) -> Int = { a: Int -> a * 3 }
    println(triple(5))
}
  1. 結果は次のようになります。
15
  1. 波かっこ内では、明示的なパラメータの宣言(a: Int)と関数の矢印(->)を省略して、関数の本体だけにできます。main 関数で宣言されている triple 関数を更新し、コードを実行します。
val triple: (Int) -> Int = { it * 3 }
  1. 出力は同じになりますが、ラムダで書いた方が簡潔です。ラムダに関するその他の例については、こちらの参考資料をご覧ください。
15

高階関数

Kotlin で関数を操作する方法の柔軟性がわかってきたので、今度は別の強力な概念である高階関数について説明します。これは単に関数(この場合はラムダ)を別の関数に渡すこと、または関数から別の関数を返すことを意味します。

map 関数、filter 関数、forEach 関数は、どれも関数をパラメータとして取るため、高階関数の例だということになります(この filter 高階関数に渡されるラムダでは、単一のパラメータと矢印記号を省略し、it パラメータを使用することもできます)。

peopleAges.filter { it.key.length < 4 }

高階関数の新しい例として sortedWith() を説明します。

文字列のリストを並べ替える場合は、組み込みの sorted() メソッドをコレクションに使用しますしかし、リストを文字列の長さで並べ替えるには、2 つの文字列の長さを取得して比較するコードを記述する必要があります。Kotlin では、ラムダを sortedWith() メソッドに渡すことで、これを実現できます。

  1. プレイグラウンドで、次のコードを使用して名前のリストを作成し、名前で並べ替えて表示します。
fun main() {
    val peopleNames = listOf("Fred", "Ann", "Barbara", "Joe")
    println(peopleNames.sorted())
}
  1. 今度は、ラムダを sortedWith() 関数に渡して、名前の長さで並べ替えられたリストを表示します。ラムダは同じ型の 2 つのパラメータを取って、Int を返す必要があります。main() 関数内の println() ステートメントの後に、次のコードを追加します。
println(peopleNames.sortedWith { str1: String, str2: String -> str1.length - str2.length })
  1. プログラムを実行して結果を確認します。
[Ann, Barbara, Fred, Joe]
[Ann, Joe, Fred, Barbara]

sortedWith() に渡されたラムダには 2 つのパラメータ str1String)と str2String)があります。その後に、関数の矢印、関数の本体と続きます。

7005f5b6bc466894.png

ラムダの最後の式は戻り値でした。この場合、最初の文字列の長さと 2 番目の文字列の長さとの差(Int)が返されます。これは並べ替えに必要な条件を満たしています。つまり、str1str2 より短い場合、0 より小さい値を返します。str1str2 が同じ長さの場合、0 を返します。str1str2 より長い場合、0 より大きい値を返します。sortedWith() 関数は、一連の 2 つの Strings の比較を一度に行って、長さの昇順で並べ替えられた名前のリストを出力します。

Android の OnClickListener と OnKeyListener

これまでに Android で学んだことに当てはめると、チップ計算アプリのボタンのクリック リスナーを設定するときなど、前の Codelab でラムダを使用していました。

calculateButton.setOnClickListener{ calculateTip() }

ラムダを使用してクリック リスナーを設定するのは、便利な省略記法です。比較のため、上記のコードの長い形式を上に、短縮版を下に示します。長いバージョンの詳細をすべて把握する必要はありませんが、2 つのバージョンにはパターンがあることがわかります。

29760e0a3cac26a2.png

ラムダが OnClickListeneronClick() メソッドと同じ関数型になっています(1 つの View 引数を取り、戻り値がないことを意味する Unit を返します)。

コードの短縮版は、Kotlin の SAM(単一抽象メソッド)変換と呼ばれるものを行うことで可能になります。Kotlin はラムダを、単一抽象メソッド onClick() を実装する OnClickListener オブジェクトに変換します。必要なのは、ラムダの関数型が抽象関数の関数型と一致していることだけです。

view パラメータはラムダで使用されないため、このパラメータは省略できます。そのため、ラムダ内は関数の本体だけになります。

calculateButton.setOnClickListener { calculateTip() }

これらの概念は難解で、十分に理解するには時間と経験が必要です。別の例を見てみましょう。チップ計算ツールの [Cost of service] テキスト フィールドにキーリスナーを設定して、Enter キーが押されたときに画面キーボードが非表示になるようにしていました。

costOfServiceEditText.setOnKeyListener { view, keyCode, event -> handleKeyEvent(view, keyCode) }

OnKeyListener を見ると、抽象メソッドはパラメータ onKey(View v, int keyCode, KeyEvent event) を取り、Boolean を返しています。Kotlin の SAM 変換により、setOnKeyListener() にラムダを渡すことができます。ラムダの関数型は (View, Int, KeyEvent) -> Boolean となります。

上のラムダ式を表す図を次に示します。パラメータは、view、keyCode、event です。関数の本体は、渡されたパラメータを使用して Boolean を返す handleKeyEvent(view, keyCode) で構成されます。

f73fe767b8950123.png

5. 単語リストを作成する

コレクション、ラムダ、高階関数について学んだことをすべて取り入れて、実際のユースケースに適用しましょう。

単語ゲームや語彙の学習に使用する Android アプリを作成するとしましょう。アプリには、アルファベット 1 文字ごとにボタンがあるとしましょう。

7539df92789fad47.png

文字 A をクリックすると、文字 A で始まる単語の短いリストが表示されます。

単語のコレクションが必要ですが、どんなコレクションでしょうか?アルファベットの各文字で始まる単語リストを使う場合、指定の文字で始まるすべての単語を検索または整理する方法が必要です。さらに難しくするため、ユーザーがアプリを起動するたびにコレクションから別の単語リストを選ぶことにします。

まず、単語のリストから始めます。実際のアプリでは、単語のリストを長くし、またアルファベットのすべての文字で始まる単語を追加すると思いますが、この課題には短いリストで十分です。

  1. Kotlin プレイグラウンドのコードを次のコードで置き換えます。
fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
}
  1. 文字 B で始まる単語のコレクションを取得するために、ラムダ式を添えて filter を使用します。次の行を追加します。
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
println(filteredWords)

startsWith() 関数は、文字列が指定された文字列で始まる場合は true を返します。「b」が「b」または「B」に一致するように、大文字小文字を無視するように指定することもできます。

  1. アプリを実行し、結果を確認します。
[balloon, best, brief]
  1. 単語はランダムに選ぶ必要があります。Kotlin のコレクションでは、shuffled() 関数を使用して、項目がランダムにシャッフルされたコレクションのコピーを作成できます。フィルタされた単語がシャッフルもされるように変更します。
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
  1. プログラムを実行して、新しい結果を確認します。
[brief, balloon, best]

単語はランダムにシャッフルされるため、異なる順番で表示される場合もあります。

  1. すべての単語は不要で(特に単語のリストが長い場合)、少しだけあれば十分です。take() 関数を使用すると、コレクションの先頭にある項目を取得できます。フィルタされた単語に、シャッフルされた単語の最初の 2 個だけが含まれるようにします。
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
  1. プログラムを実行して、新しい結果を確認します。
[brief, balloon]

ここでも、ランダムにシャッフルされるため、実行するたびに別の単語が表示される場合があります。

  1. 最後に、各文字の単語のランダムなリストを並べ替えます。前と同様に、sorted() 関数を使用して、並べ替えられた項目が入ったコレクションのコピーを返すことができます。
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
    .sorted()
  1. プログラムを実行して、新しい結果を確認します。
[balloon, brief]

以上のコード全体を次に示します。

fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
    val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
        .shuffled()
        .take(2)
        .sorted()
    println(filteredWords)
}
  1. コードを変更して、文字 c で始まるランダムな単語 1 つのリストを作成してみてください。どこを変更する必要があるでしょう?
val filteredWords = words.filter { it.startsWith("c", ignoreCase = true) }
    .shuffled()
    .take(1)

実際のアプリでは、アルファベットの各文字でフィルタを適用する必要がありますが、各文字の単語リストを生成する方法は学びました。

コレクションは強力で柔軟です。できることは多数あり、同じことを行うのに複数の方法があります。プログラミングを詳しく学習すると、発生している問題に適したコレクションの種類と、それを処理するための最適な方法がわかるようになります。

ラムダ関数と高階関数を使用すると、コレクションの使用が簡単かつ簡潔になります。こうした概念は大変便利なため、何度も目にすることになるでしょう。

6. まとめ

  • コレクションは関連する項目のグループです。
  • 変更可能なコレクションと、変更不可能なコレクションがあります。
  • 順序のあるコレクションと、順序のないコレクションがあります。
  • 項目が一意である必要のあるコレクションと、重複が許されるコレクションがあります。
  • Kotlin では、リスト、セット、マップなど、さまざまな種類のコレクションがサポートされています。
  • Kotlin には、forEachmapfiltersorted など、コレクションの処理と変換の関数が多数用意されています。
  • ラムダは、式としてその場で渡すことができる名前のない関数です。{ a: Int -> a * 3 } のような形です。
  • 高階関数とは、関数を別の関数に渡す、または関数から別の関数を返すことを意味します。

7. 詳細