はじめに こんにちは、新卒2年目の菊池(akikuchi_rks)です。 近年、 Android アプリ開発 のみならず、サーバーサイドの開発言語としてもKotlinが急速に注目を集めています。私自身もKotlinを使ってサーバーサイドの開発を行っており、豊富な機能やシンプルな文法に魅力を感じています。 Kotlinを使用していて特に感じるのは、そのコレクション関数の充実性です。コレクション操作はプログラミングにおいて頻繁に行われるため、これらの関数が豊富であることはKotlinの特長のひとつと言えると思います。 また、これに ラムダ式 を組み合わせることで、よりシンプルで効率的なコーディングが可能となります。 この記事では、Kotlinでコーディングをする際に欠かせないコレクション関数に焦点を当て、その中からいくつかピックアップして紹介していきます。 初心者の方でもわかりやすいように、具体的なコード例も交えながら説明していきますので、ぜひ最後までご覧ください。 はじめに コレクションとは、ラムダ式とは コレクションとは ラムダ式とは コレクション関数の活用例 map filter any, all, none associate, associateBy, associateWith groupBy partition コレクション関数を使うもう一つのメリット ミュータブルな変数を使わないほうがいい理由 1. 可読性が低下する 2. 予期せぬバグを生む まとめ コレクションとは、 ラムダ式 とは まずはそもそもコレクションとは何なのか、 ラムダ式 とは何なのか簡潔に説明します。 コレクションとは まず、Kotlinにおけるコレクションは複数の要素をまとめて管理するためのデータ構造です。 主な種類としては、 List 、 Set 、 Map などがあります。 List : 要素の順序付き集合。同じ要素を複数持つことができる。 Set : 要素の順序がない集合。重複する要素を持たない。 Map : Keyと値 Value のペアの集合。Keyは一意で重複しない。 val sampleList = listOf( 1 , 2 , 3 ) val sampleSet = setOf( 1 , 2 , 3 ) val sampleMap = mapOf( 1 to "one" , 2 to "two" , 3 to "three" ) println(sampleList) // [1, 2, 3] println(sampleSet) // [1, 2, 3] println(sampleMap) // {1=one, 2=two, 3=three} 上記のようにコレクションを宣言する際には listOf 、 setof 、 mapOf を用いて インスタンス を生成します。 これらのコレクションは読み取り専用のコレクションでそれぞれ宣言した後に要素の追加、削除、更新はできません。 後で要素の変更を行いたい場合にはミュータブル(変更可能な) MutableList 、 MutableSet 、 MutableMap を使います。 しかし、詳細は後述しますがコレクションを扱う場合は、書き換える必要がないものは変更不可のコレクションを使用するのが望ましいです。 そして、これらのコレクションを操作するための関数がコレクション関数です。 Kotlinの標準ライブラリとして用意されており、コレクションの変換、フィルタリング、グルーピングなど様々な操作を行うための関数が揃っています。 ラムダ式 とは ラムダ式 は、無名関数(名前を定義しない関数)をさらにコンパクトにし、最小限の記述量で定義できるようにしたものです。 val square: ( Int ) -> Int = { x -> x * x } val result = square( 5 ) // 25 この例では、 square という名前の変数に、 ラムダ式 が代入されています。 変数自体には名前が付ける必要がありますが ラムダ式 は Int を受け取り、Int を返す無名関数です。 { x -> x * x } という部分が ラムダ式 であり、 -> の左側に引数、右側に実際の関数の処理を記述します。 ラムダ式 は他の関数の引数として直接渡すこともできます。 fun operateNumber(x: Int , operation: ( Int ) -> Int ): Int { return operation(x) } val result = operateNumber( 5 ) { it * it } println(result) // 25 この例では、 operateNumber という関数が定義されています。 この関数は、整数値とそれを操作する関数を受け取ります。 このような関数を引数として受け取る関数に ラムダ式 を直接記述することで関数定義の手間を省くことができます。 ラムダ式 内で使われている it という引数は受け取るパラメータが1つしかない ラムダ式 を記述する際に使うことができる暗黙の引数で -> の左側に引数を宣言をする手間をさらに省くことができます。 前述したコレクション関数は関数を引数にとるものが多いため、 ラムダ式 と組み合わせて使用されることが多く、これらのコレクション関数と ラムダ式 を組み合わせることでシンプルで効率的なコーディングが可能となります。 コレクション関数の活用例 コレクション関数を ラムダ式 と組み合わせて活用することによりコードをシンプルにする具体例をいくつか紹介していきます。 map 以下は整数値のListの各要素を2倍にする際のコーディング例です。 val numbers = listOf( 1 , 2 , 3 ) val result = mutableListOf< Int >() for (i in numbers) { result.add(i * 2 ) } println(result) // [2, 4, 6] このようなListの各要素の値に何らかの変換処理を加えて別のリストを生成する際には map が利用できます。 map は ラムダ式 で各要素に加えたい変換処理を指定することで、その変換処理を加えた結果のリストを生成することができます。 val numbers = listOf( 1 , 2 , 3 ) val result = numbers.map { it * 2 } println(result) // [2, 4, 6] filter 以下は整数値のListから奇数のものだけを取り出す際のコーディング例です。 val numbers = listOf( 1 , 2 , 3 ) val result = mutableListOf< Int >() for (i in numbers) { result.add(i % 2 == 1 ) } println(result) // [1, 3] このようなListから特定の条件を満たす要素のみを抽出して取り出すような処理は filter を使うことで、シンプルに記述することができます。 filter では ラムダ式 に条件式を記述し、その式が true に判定される要素のみを抽出したListが生成されます。 val numbers = listOf( 1 , 2 , 3 ) val result = numbers.filter { it % 2 == 1 } println(result) // [1, 3] filter の逆で ラムダ式 の条件式が false に判定される要素のみを抽出したい場合には filterNot が使えます。 any, all, none 以下は整数値のListの要素に偶数か含まれているかを判定する例です。 val numbers = listOf( 1 , 2 , 3 ) var result = false for (i in numbers) { if (i % 2 == 0 ) { result = true break } } println(result) // true こちらは any を使うことでシンプルに記述することができます。 any はコレクションの要素に対して、指定した条件式が true になるものが存在するかどうかを判定します。 val numbers = listOf( 1 , 2 , 3 ) var result = numbers.any { it % 2 == 0 } println(result) // true any と同じようにコレクションの要素がある条件に合致しているかどうかを判定するコレクション関数に all や none があるので併せて紹介します。 any : 一つでも条件を満たす要素があれば true (空の場合は false ) all : 全ての要素が条件を満たせば true (空の場合は true ) none : 一つも条件を満たさなければ true (空の場合は true ) いずれも使い方は any と同様です。 associate, associateBy, associateWith 以下は文字列のリストから元の文字列をKey、その文字列の長さを Value としたMapを生成する例です。 val colors = listOf( "red" , "yellow" , "blue" ) val result = mutableMapOf< String , Int >() for (color in colors) { result[color] = color.length } println(result) // {red=3, yellow=6, blue=4} このようにListからMapを生成する際は associate が使えます。 associate は Key to Value といった形式のPair型を返す ラムダ式 を指定することで、指定したKeyと Value のMapを生成することができます。 val colors = listOf( "red" , "yellow" , "blue" ) val result = colors.associate { it to it.length } println(result) // {red=3, yellow=6, blue=4} associate と似たコレクション関数に associateBy , associateWith があります。 associate : ラムダ式 でKeyと Value をPair型で指定 associateBy : ラムダ式 でKeyを指定、 Value は各要素の元の値となる associateWith : ラムダ式 で Value を指定、Keyは各要素の元の値となる Keyをカスタムしたいか、 Value をカスタムしたいか、あるいは両方カスタムしたいかで使い分けます。 例として associateWith を用いて上記の処理を書き換えると以下のようなコードになります。 val colors = listOf( "red" , "yellow" , "blue" ) val result = colors.associate { it.length } println(result) // {red=3, yellow=6, blue=4} groupBy 以下は整数値のListの要素を偶数と奇数に分類する際のコード例です。 val numbers = listOf( 1 , 2 , 3 , 4 , 5 ) val evens = mutableListOf< Int >() val odds = mutableListOf< Int >() for (i in numbers) { if (i % 2 == 0 ) { evens.add(i) } else { odds.add(i) } } val result = mapOf( "even" to evens, "odd" to odds) このようなコレクションの要素をある特徴によって分類したい場合は groupBy を使用することでシンプルに記述することができます。 groupBy は ラムダ式 が返した値をKeyとしてその値ごとに各要素を分類し、Mapとして返します。 val numbers = listOf( 1 , 2 , 3 , 4 , 5 ) val result = numbers.groupBy { if (it % 2 == 0 ) "even" else "odd" } println(result) // {odd=[1, 3, 5], even=[2, 4]} ここでは、偶数の場合に返される文字列 even と奇数の場合に返される文字列 odd をKeyとしているため、リストの値が偶数の場合は even をKeyとする Value のリストに値が格納され、リストの値が奇数の場合は odd をKeyとする Value のリストに値が格納されます。 ちなみに、 groupBy の第二引数にさらに ラムダ式 を指定すると、各要素に任意の変換処理を加えた値をMapに詰めることができます。 groupBy と前述した map の処理を同時に行うことができ、かなりコードがコンパクトになります。 val numbers = listOf( 1 , 2 , 3 , 4 , 5 ) val result = numbers.groupBy({ if (it % 2 == 0 ) "even" else "odd" }, { it * 3 }) println(result) // {odd=[3, 9, 15], even=[6, 12]} partition groupBy の例のように、単純にリストの要素を2つのグループに分割するだけであれば partition も利用することができます。 val numbers = listOf( 1 , 2 , 3 , 4 , 5 ) val result = numbers.partition { it % 2 == 0 } println(result) // ([2, 4], [1, 3, 5]) partition は groupBy と同様にコレクションの要素を ラムダ式 で指定した条件で分割する関数です。 groupBy と異なるのは partition は ラムダ式 に true , false で判定される条件式を指定する必要がある点と、返される値がMapではなくPairであるという点です。 partition では ラムダ式 の条件が true となった要素がfirstのリストに格納され、 false となった要素がsecondのリストに格納されます。 コレクション関数を使うもう一つのメリット 上記の活用例であげたコードを見ていて気付かれた方もいるかと思いますが、コレクション関数を活用することで var や mutableList といったミュータブルな変数の使用を減らすことができます。 この変数の可変性を減らせることがコレクション関数を使用する重要なメリットの1つであると思っています。 ミュータブルな変数を使わないほうがいい理由 ミュータブルな変数をなるべく使わないほうがいい理由を以下に挙げます。 1. 可読性が低下する 変数が再代入できるようになっていると、その変数の値がどのように変化するかを追跡するのが難しくなります。 その結果、コードの理解やメンテナンスが難しくなります。 2. 予期せぬバグを生む 変数の再代入は、その変数を参照している他の部分に影響を与える可能性があります。 特に、大規模なアプリケーションでは変数の書き換えが思わぬ箇所に影響を与え、予期しないバグや振る舞いを発生させる可能性が高くなります。 これらのデメリットを踏まえると、ミュータブルな var や mutableList といった注意深く使用する必要があり、使用は最小限に抑えたほうが良いです。 以上の理由から、コレクション関数を活用してミュータブルな変数の使用を抑えることはコードの可読性、堅守性を上げることにも繋がると言えます。 まとめ Kotlinのコレクション関数と ラムダ式 はコードをシンプルで効率的にするための強力なツールです。 これらを使いこなすことで、コードの可読性を向上させ、より効率的に開発を進めることができます。 さらに、コレクション関数と ラムダ式 を活用することでミュータブルな変数の使用を抑えられ、コードの堅牢性向上も期待できます。 Kotlinには本記事で紹介したコレクション関数以外にも便利なものが数多く揃っているので、今後もこれらのコレクション関数を活用して効率的な開発を行っていきたいです。