はじめに
こんにちは、新卒2年目の菊池(akikuchi_rks)です。
近年、Androidアプリ開発のみならず、サーバーサイドの開発言語としてもKotlinが急速に注目を集めています。私自身もKotlinを使ってサーバーサイドの開発を行っており、豊富な機能やシンプルな文法に魅力を感じています。
Kotlinを使用していて特に感じるのは、そのコレクション関数の充実性です。コレクション操作はプログラミングにおいて頻繁に行われるため、これらの関数が豊富であることはKotlinの特長のひとつと言えると思います。 また、これにラムダ式を組み合わせることで、よりシンプルで効率的なコーディングが可能となります。
この記事では、Kotlinでコーディングをする際に欠かせないコレクション関数に焦点を当て、その中からいくつかピックアップして紹介していきます。 初心者の方でもわかりやすいように、具体的なコード例も交えながら説明していきますので、ぜひ最後までご覧ください。
コレクションとは、ラムダ式とは
まずはそもそもコレクションとは何なのか、ラムダ式とは何なのか簡潔に説明します。
コレクションとは
まず、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には本記事で紹介したコレクション関数以外にも便利なものが数多く揃っているので、今後もこれらのコレクション関数を活用して効率的な開発を行っていきたいです。