Kotlin が Android の公式言語になることが Goole I/O 2017 で発表されました。本投稿では、 Java プログラマを対象に、 Java にはない Kotlin 便利な機能について説明します。
本投稿は単独で理解できるように書いていますが、↓の連載の第三弾です。 Kotlin の基礎的な構文は理解していることを前提としているので、 Kotlin の構文自体を知らない方は以前の投稿を先に御覧下さい。
- Javaとほぼ同じところ
- 新しい考え方が必要でつまづきがちなところ
- Kotlinならではの便利なこと ←この投稿で扱う内容
Kotlinならではの便利なこと
Java にはない Kotlin の便利な機能について説明します。
「Kotlinならでは」と書いていますが、あくまで Java と比較してです。本投稿は Java プログラマ向けです。( Java にはないけど)それ他の言語にあるよ、というものも多いです。
色々挙げていますが、僕のイチオシは Data Class と最後に挙げた apply
です。本当に便利です。
String Template
他の言語でよく文字列補間と呼ばれるものです。
// Kotlin
val a = 2
val b = 3
println("$a + $b = ${a + b}") // 2 + 3 = 5
$foo
の形で変数 foo
の値を文字列に埋め込むことができます。また、 ${...}
のように書くと、 ...
の部分に任意の式を書くことができます。 Java のように文字列を +
で結合する必要はありません。
- String Templates: https://kotlinlang.org/docs/reference/basic-types.html#string-templates
複数行文字列リテラル
Kotlin では複数行の文字列リテラルが使えます。↓のように、改行や " を伴う文字列リテラルを作るときに便利です。また、複数行文字列リテラルの中でも $title
のように String Template が使えます。
// Kotlin
val title = "Hello, World!"
val headerId = "header"
val header = "Hello, World!"
val html = """<!DOCTYPE html>
<html>
<title>$title</title>
<body>
<h1 id="$headerId">$header</h1>
</body>
</html>
"""
println(html)
↓は出力結果です。
<!DOCTYPE html>
<html>
<title>Hello, World!</title>
<body>
<h1 id="header">Hello, World!</h1>
</body>
</html>
関数
Kotlin では、 Java と違ってクラスから独立した 関数( Function ) を作ることができます。 Java しか経験のない人は関数という言葉を聞きなれないかもしれませんが、クラスやインスタンスに属さない独立したメソッドのようなものです。役割としては Java の static
メソッドと似ています。
たとえば、 Kotlin の listOf
は関数です。クラスに属していないので、 static
メソッドである Arrays.asList
と違って Arrays.
の部分を必要としません。
// Java
List<Integer> list = Arrays.asList(2, 3, 5);
// Kotlin
val list = listOf(2, 3, 5)
listOf
や mapOf
などの利用頻度の高い関数は、このようにクラス名なしで呼び出せると便利です( Java でも import static
でできなくはないですが、繰り返し使う場合でないと import
すること自体が面倒です)。
関数は static
メソッドと違ってクラス名で名前空間が分けられていないので、なんでもかんでも関数にしてしまうと名前衝突してしまいます。しかし、ファイル内に限定して private
な関数を作ったり、あるモジュール内限定の internal
な関数を作ったり、乱用しなければ便利です。
Single-Expression function
式一つで実装できる関数は(関数に限らずメソッドの場合もですが)↓のように簡潔に記述することもできます。
// Kotlin
// 普通の書き方
fun square(number: Int): Int {
return number * number
}
// Kotlin
// 簡潔な書き方
fun square(number: Int): Int = number * number
コードが圧縮できてずいぶんとすっきりします。
- Single-Expression functions: https://kotlinlang.org/docs/reference/functions.html#single-expression-functions
関数の型と代入
また、関数を変数に代入することもできます。
// Kotlin
val square: (Int) -> Int = { it * it }
square(3) // 9
square
の型が (Int) -> Int
なことに注目して下さい。これは Int
一つを引数に受けて Int
を返す関数という意味です。 (Foo, Bar) -> Baz
と書けば、第一引数に Foo
、第二引数に Bar
を受けて Baz
を返す関数の型となります。
変数に入れられるということは、引数に渡すこともできます。
val list = listOf(2, 3, 5)
list.map(square) // [4, 9, 25]
デフォルト引数
Kotlin では関数やメソッドのオーバーロードの変わりにデフォルト引数を与えることができます。
// Java
static double log(double x, double base) {
...
}
static double log(double x) {
return log(Math.E);
}
// Kotlin
fun log(x: Double, base: Double = Math.E): Double {
...
}
複数のデフォルト引数を与えて、その内の一つだけを指定するようなこともできます。
// Kotlin
fun foo(x: Int = 42, y: Boolean = true, z: String = "xyz") {
...
}
foo(y = false) // x = 42, z = "xyz"
- Default Arguments: https://kotlinlang.org/docs/reference/functions.html#default-arguments
演算子オーバーロード
実は Kotlin の演算子はメソッドです。たとえば +
は plus
というメソッドで、 1 + 2
と書くのは 1.plus(2)
と書くのと等価です。演算子として働くメソッドを実装するには operator
修飾子をつけます。
// Kotlin
class Vector2(val x: Double, val y: Double) {
operator fun plus(vector: Vector2): Vector2 {
return Vector2(x + vector.x, y + vector.y)
}
}
Vector2(2.0, 3.0) + Vector2(4.0, 5.0) // Vector2(6.0, 8.0)
四則演算の演算子とメソッドの対応は次の通りです。
演算子 | メソッド |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
その他詳細については公式ドキュメントを御覧下さい。
僕が特に便利だと思うのが get
と set
です。それぞれ( Java でいう配列の) []
に対応します。
// Kotlin
val a = list[i] // list.get(i)
list[i] = b // list.set(i, b)
Java の List
や Map
の get
/ set
( put
) は煩わしかったですが、 Kotlin では演算子オーバーロードのおかげで [i]
のようにしてコレクションの要素にアクセスすることができます( Kotlin では Map
でも set
メソッドでエントリーを登録できます)。
- Operator overloading: https://kotlinlang.org/docs/reference/operator-overloading.html
-
Int.plus
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/plus.html -
List.get
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/get.html -
MutableList.set
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-mutable-list/set.html -
MutableMap.set
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/set.html
Destructuring Declaration
一時的に二つの値をペアにして扱いたいけど、そのためにクラスを作るほどでもないということがあります。 Java ではメソッドの戻り値は常に一つしか返せませんが、二つ戻り値を返したいと思ったことが一度はあるんじゃないでしょうか。そんなときは泣く泣く配列にするか、専用のクラスを作るしかありませんでした。
Kotlin でも戻り値は常に一つですが、代わりに Pair<out A, out B>
という汎用的なクラスが用意されています。
// Kotlin
val x = Pair(42, "xyz")
この Pair
が便利なのは、↓のように代入できることです。
// Kotlin
val (a, b) = x
println(a) // 42
println(b) // "xyz"
これを Destructuring Declaration と言います。これなら、 Pair
を返すメソッドを作れば、まるで戻り値が二つあるかのように振る舞わせることができます。
// Kotlin
fun foo(): Pair<Int, String> {
return Pair(42, "xyz")
}
val (a, b) = foo()
実はこの機能は Pair
の専売特許ではありません。 component1
, component2
などの operator fun
を持つクラスを使えば、どんなクラスでもこの機能を実装することができます。
// Kotlin
class Foo(val a: Int, val b: String) {
operator fun component1(): Int {
return a
}
operator fun component2(): String {
return b
}
}
val (a, b) = Foo(42, "xyz")
もちろん、二つ以上の値に分解して代入することもできます。 Kotlin の標準ライブラリは Pair
に加えて Triple
という三つの値を保持するクラスも提供しています。
// Kotlin
val triple = Triple(42, "xyz", true)
val (a, b, c) = triple
ただし、 Pair
や Triple
の乱用は型の意味を失い、可読性を損ねるので注意して下さい。特に、戻り値として一時的に使うだけでなく、 Pair
や Triple
のまま値を取り回し始めたら、独自のクラスに置き換えた方がよくないか検討してみましょう。
Destructuring Declarations は for
ループと組み合わせて使うこともできます。たとえば、 Kotlin では for
ループで List
の要素を取り出すときに、次のようにしてインデックスも一緒に取り出すことができます。
// Kotlin
val list = listOf("a", "b", "c")
for ((index, element) in list.withIndex()) {
println("$index : $element")
}
0 : a
1 : b
2 : c
withIndex
メソッドの戻り値の型は Iterable<IndexedValue<T>>
です。 IndexedValue<T>
は component1
と component2
を実装しているので、↑のようにして値を分解しながら取り出すことができます。
この他にも Map
をイテレートしてキーと値を取り出すようなこともできます。
// Kotlin
val map = mapOf(
"a" to 2,
"b" to 3,
"c" to 5
)
for ((key, value) in map) {
println("$key : $value")
}
a : 2
b : 3
c : 5
これは Map<K, V>
の iterator
メソッドが Iterator<Map.Entry<K, V>>
を返し、 Map.Entry<K, V>
の component1
がキーを、 component2
が値を返すからです。イテレータの next
で返される Map.Entry<K, V>
が一つずつ (key, value)
に代入されてループが実行されるわけです。
さらに、実は上記の to
が生成しているのも Pair
です。 "a" to 2
は "a".to(2)
と書くのと同じです。 Kotlin では infix
修飾子を付けて宣言したメソッドはこのように中置記法でコールすることができます。 "a" to 2
= "a".to(2)
= Pair("a", 2)
なので、 mapOf
の部分は Pair
のコンストラクタを使って次のように書くこともできます。
// Kotlin
val map = mapOf(
Pair("a", 2),
Pair("b", 3),
Pair("c", 5)
)
Pair<A, B>
の A
と B
には別に A
to B
という関係があるわけではないので、この to
は mapOf
と合わせて使うことが想定されていると思います。大抵の言語では Map
のための特殊なリテラルや構文が用意されることが多いですが、 Kotlin はできるだけ言語の構文で解決しようとする傾向があり、 to
はその代表的なものの一つです。
- Destructuring Declarations: https://kotlinlang.org/docs/reference/multi-declarations.html
-
Pair
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-pair/ -
Triple
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-triple/index.html -
withIndex
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/with-index.html -
IndexedValue
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-indexed-value/index.html -
Entry
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/-entry/index.html -
to
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/to.html
Data class
プライマリコンストラクタを使えば、コンストラクタ、フィールド、 getter 、 setter の宣言と実装をまとめて済ませることができて楽でした。しかし、 Data Class を使えばもっと楽ができます。
getter, setter だけを持っている、ただデータとして値を保持するだけのクラスはよくあります。そういうクラスを Kotlin では Data Class と呼びます。
そのようなクラスでは、コンストラクタとプロパティに加えて equals
や hashCode
, toString
の定型的な実装をすることが多いです。 Java ではそれらのコードを IDE の機能で生成したりするわけですが、 Kotlin ではクラス宣言に修飾子として data
を付けるだけです。そうすると、 hashCode
, equals
, toString
の実装が裏側で作られ、それらが存在するものとして扱えます。
// Java
public final class Person {
private final String firstName;
private final String lastName;
private final int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
@Override
public int hashCode() {
return ...;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof Person)) {
return false;
}
Person person = (Person)object;
return firstName.equals(person.firstName) && lastName.equals(person.lastName) && age == person.age;
}
@Override
public String toString() {
return "Person(firstName=" + firstName + ", lastName=" + lastName + ", age=" + age + ")";
}
}
// Kotlin
data class Person(val firstName: String, val familyName: String, val age: Int)
さらに、 Data Class では各プロパティのための component1
, component2
などのメソッドも自動生成してくれます。そのため、 Data Class のインスタンスに対して Destructuring Declaration を使うこともできます。
// Kotlin
val person = Person("Albert", "Einstein", 28)
val (firstName, familyName, age) = person
そして、僕が Data Class について一番便利だと思うのが copy
メソッドです。
現代のプログラミングでは、イミュータブルにできるものはできるだけイミュータブルにした方が良いという考え方があります。ミュータブルなインスタンスは共有された場合に問題を起こしがちなため、用途に応じてコピーを返すなどしなければいけません。しかし、インスタンスのコピーを返しても、そのインスタンスが保持している値がミュータブルなインスタンスであれば、連鎖的にそれもコピーしなければいけません(シャローコピーではなくディープコピーをしないと共有されたミュータブルなインスタンスが問題を起こさないことを保証できない)。そのような複雑さを避けるためには、イミュータブルにできる部分とどうしてもミュータブルでなければならない部分を分離し、イミュータブルな世界はイミュータブルで完結させてしまった方がシンプルになります。
しかし、イミュータブルな世界でも、インスタンスの一部だけを変更したいようなケースがあります。イミュータブルなクラスのインスタンスを変更したいときには、変更を加えた新しいインスタンスを作り直すことによって変更を表現します。
// Kotlin
val person = Person("Albert", "Einstein", 28)
val newPerson = Person(person.firstName, person.lastName, person.age + 1)
↑のように、一つのプロパティを変更するためだけに、コンストラクタに全部のプロパティを渡すのは面倒です。プロパティが 3 個くらいなら耐えられるかもしれませんが、もしプロパティが 20 個あったらどうでしょうか? Data Class の copy
メソッドを使えばもっと簡単に書けます。
Data Class には次のような copy
メソッドが自動生成されます。
// Kotlin
fun copy(firstName: String = this.firstName, lastName: String = this.lastName,
age: Int = this.age) = User(firstName, lastName, age)
デフォルト引数が設定されているので、変更したいプロパティだけ指定すれば OK です。
// Kotlin
val person = Person("Albert", "Einstein", 28)
val newPerson = person.copy(age = person.age + 1)
これなら、ミュータブルなインスタンスを setter で変更するのと大して変わりません。 イミュータビリティと変更の容易さを同時に実現 できるなんて素晴らしいですね。
- Data Classes: https://kotlinlang.org/docs/reference/data-classes.html
Extension
オブジェクト指向言語のプログラムを書いていると、既存クラスにメソッドを追加したくなることがあります。
// Kotlin
(3 + 2) * (3 + 2) // 計算結果を 2 乗したいが `+` が二度行われて無駄
(3 + 2).square() // `Int` に `square` メソッドがあればいいのに
もちろん、メソッドの代わりに関数を作ってオブジェクトを第一引数に渡すことはできます。
// Kotlin
fun square(value: Int): Int = value * value
square(3 + 2)
しかし、関数だと複数の処理を連結したい場合、処理の順序とコード上の順番が反転してしまい不便です。
// Kotlin
// foo → bar → baz → qux の順に実行
qux(baz(bar(foo(x))))
メソッドなら、順番通りにチェーンすることができます。
// Kotlin
// foo → bar → baz → qux の順に実行
x.foo().bar().baz().qux()
Kotlin では、クラスに後付でメソッド(のようなもの)を追加することができます。この仕組みを Extension と言います。
// Kotlin
fun Int.square(): Int = this * this
(3 + 2).square()
ただし、 Extension で追加されたメソッドは普通のメソッドとは異なります。
普通のメソッドは実際にどの実装がコールされるか実行時に動的に解決されます。
// Kotlin
open class Animal {
open fun foo(): Int = 2
}
class Cat: Animal() {
override fun foo(): Int = 3
}
val animal: Animal = if (Math.random() < 0.5) Animal() else Cat()
animal.foo() // `animal` が `Animal` なら `2` 、 `Cat` なら `3`
しかし、 Extension で追加されたメソッドはコンパイル時に静的に解決されます。ポリモーフィズムはできません。
// Kotlin
open class Animal
class Cat: Animal()
fun Animal.foo(): Int = 2
fun Cat.foo(): Int = 3
val animal: Animal = if (Math.random() < 0.5) Animal() else Cat()
animal.foo() // `animal` が `Animal` でも `Cat` でも `2`
Extension はクラス自体を書き変えているわけではありません。 Extension で追加されるメソッドの実体は関数です。このようなメソッドのように見える関数を Extension Function と呼びます。↑のコードの挙動は↓のように普通の関数で考えるとわかりやすいです。
// Kotlin
open class Animal
class Cat: Animal()
fun foo(animal: Animal): Int = 2
fun foo(cat: Cat): Int = 3
val animal: Animal = if (Math.random() < 0.5) Animal() else Cat()
foo(animal) // `animal` が `Animal` でも `Cat` でも `2`
オーバーロードされた foo
のうちどれが呼ばれるかはコンパイル時に決定されます。 animal
は Animal
型なので、その実体が Animal
だろうと Cat
だろうと、どの foo
が呼ばれるかは引数に渡された値の型(この場合は Animal
)から決定されます。
どうして Extension はクラスそのものを書き変えずに、こんなややこしいことをするのでしょうか?もしクラス自体を書き換えてしまうと、異なるライブラリ間でたまたま同じ名前のメソッドを後付してしまったらメソッドの実装が衝突してしまいます。ライブラリ A と B が両方同じクラスに foo
というメソッドを追加したら、ライブラリ A の foo
メソッドがライブラリ B の中で予期せず呼ばれるというようなことが起こりえます。それでは怖くて Extension を使えません。コンパイル時に静的に解決されるのであれば、そのような混同は起こり得ません。
Extension Function をメソッドの形式で呼び出せるのは便利ですが、利用者側からすると見た目は同じなのに、本物のメソッドなのか Extension Function なのかで挙動が変わるのはややこしいとも言えます。スーパークラスとサブクラスで同じ名前の Extension Function を追加するのはややこしい事態を招きかねないのでやめましょう。
- Extensions: https://kotlinlang.org/docs/reference/extensions.html
- Extension Functions: https://kotlinlang.org/docs/reference/extensions.html#extension-functions
object
シングルトンのように、あるクラスにインスタンスは一つしか必要ないというケースがあります。 Kotlin では class
の代わりに object
というキーワードを使うことで簡単にシングルトンが作れます。
// Java
class Foo {
private static final Foo INSTANCE = new Foo();
private Foo() {}
public static Foo getInstance() {
return INSTANCE;
}
public int bar() {
return 42;
}
}
Foo.getInstance().bar(); // 42
// Kotlin
object Foo {
fun bar(): Int = 42
}
Foo.bar() // 42
Foo.bar()
としているので Java でいう static
メソッドのように見えるかもしれませんが、 Foo
はクラスでありインスタンスです。その証拠に、 Foo
を Foo
型の変数に代入することもできます。
// Kotlin
val foo: Foo = Foo
foo.bar() // 42
Foo.getInstance()
などとしなくていいのは楽ですし、 static
メソッドだけを持ったクラスと異なり、 object
はインスタンスでもあるので他のクラスを継承することもできます。ポリモーフィズムだってできるので便利です。
// Kotlin
interface Foo {
fun bar(): Int
}
object Baz : Foo {
override fun bar(): Int = 42
}
Baz.bar() // 42
val foo: Foo = Baz
foo.bar() // 42
なお、 Kotlin ではこの object
を使って匿名内部クラスを作ります。なぜなら、匿名内部クラスのインスタンスは常に一つしか存在しないからです。
// Java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
...
}
});
// Kotlin
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(view: View) {
...
}
})
ちなみに、 Kotlin も Java 8 のように SAM 変換が使えるので、実際には↑のコードは次のように簡潔に書けます。
// Kotlin
button.setOnClickListener { view ->
...
}
- Object Expressions and Declarations: https://kotlinlang.org/docs/reference/object-declarations.html
- SAM Conversions: https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
Sealed Class
クラスやインタフェースを作っていて、それを継承/実装できるのはこのクラスだけと限定したい場合があります。
たとえば、 JSON をマッピングする型を作るとします。 JSON の仕様はこちらで明確に定められていて、 JSON の値として認められるものは object, array, string, number, true, false, null しかありません。これを型で表してみましょう。
それぞれ、 JSONObject
, JSONArray
, JSONString
, JSONNumber
, JSONTrue
, JSONFalse
, JSONNull
で表すとしましょう。そして、それらのスーパータイプとして JSONValue
インタフェースを考えましょう。
// Kotlin
interface JSONValue
class JSONObject(val value: Map<String, JSONValue>) : JSONValue
class JSONArray(val value: List<JSONValue>) : JSONValue
class JSONString(val value: String) : JSONValue
class JSONNumber(val value: Double) : JSONValue
object JSONTrue : JSONValue
object JSONFalse : JSONValue
object JSONNull : JSONValue
JSONTrue
, JSONFalse
, JSONNull
が class
ではなく object
なことに注意して下さい。これらの型のインスタンスはそれぞれ JSON の true
, false
, null
に対応し、一つしか存在しないので object
になっています。
しかし、これでは誰かが class JSONFoo : JSONValue
のようなものを後から追加できてしまいます。 JSON の仕様で JSON の値として認められるのは上記の 7 種類だけなので、他のクラスが JSONValue
を実装できないようにしたいです。 Java ではそのような制約を実現することができませんが、 Kotlin の Sealed Class を使えばできます。
// Kotlin
sealed class JSONValue
class JSONObject(val value: Map<String, JSONValue>) : JSONValue()
class JSONArray(val value: List<JSONValue>) : JSONValue()
class JSONString(val value: String) : JSONValue()
class JSONNumber(val value: Double) : JSONValue()
object JSONTrue : JSONValue()
object JSONFalse : JSONValue()
object JSONNull : JSONValue()
Sealed Class は自身が宣言されたファイルと同一ファイル内からしか継承することができません。そのため、第三者が勝手にサブクラスを実装することができません。
サブクラスの種類が限られているので、 when
を使った型による網羅的な分岐と相性が良いです。
// Kotlin
val json: JSONValue = ...
when (json) {
is JSONObject -> { ... }
is JSONArray -> { ... }
is JSONString -> { ... }
is JSONNumber -> { ... }
is JSONTrue -> { ... }
is JSONFalse -> { ... }
is JSONNull -> { ... }
}
is
で分岐すると Smart Cast によって { ... }
の部分では json
をそれぞれチェックした型として扱うことができます。たとえば、 is JSONObject -> { ... }
の { ... }
の部分では json
は JSONObject
型として扱えます。
なお、 Sealed Class はクラスで、インタフェースではありません。ただし、 Sealed Class は自動的に抽象クラスになるので、 Sealed Class 自体のインスタンスを生成することはできません。 Sealed Class 本体には abstract
メソッドのみを定義し、インタフェース的に使うこともできますし、共通の実装を書くこともできます。
- Sealed Classes: https://kotlinlang.org/docs/reference/sealed-classes.html
スコープ関数
Kotlin にはスコープ関数と呼ばれる便利な関数群があります。 also
, apply
, let
, run
, with
です。ただし、スコープ関数という呼称は日英問わず使われているものの、公式ドキュメントでは確認できませんでした。→(追記)公式ブログが元のようです。
スコープ関数については @ngsw_taro さんが詳しく解説してくれていますので、ここでは僕が特に便利だと思う let
と apply
についてのみ紹介します。
let
x + 1 / x
のような計算をしたいとしましょう。今、 x
が foo(42)
で求められるとしたら、 x + 1 / x
を計算するコードは次のように書けます。
// Kotlin
foo(42.0) + 1 / foo(42.0)
しかし、このコードには問題があります。それは、 foo(42)
を 2 回実行してしまっていることです。もし foo
がとても重い処理だったら二度同じ計算をするのは無駄です。もしくは、 Math.random()
のように実行する度に結果が変わる処理だったら計算結果が変わってしまいます。それを避けるには一度 foo(42)
の結果を変数に代入するなどしなければなりません。
// Kotlin
val x = foo(42.0)
x + 1 / x
しかし、変数への代入を挟むと一つの式で書けなくなってしまい不便です。そんなときに役に立つのが let
です。 let
は標準ライブラリで任意の型に対して実装されている Extension Function で、次のように使えます。
// Kotlin
foo(42.0).let { x -> x + 1 / x }
let
の実装は次のようになっています。
// Kotlin
inline fun <T, R> T.let(block: (T) -> R): R = block(this)
つまり、自分自身を引数に渡された関数に渡し、その関数の戻り値を let
自体の戻り値にするということです。
let
は上記のように、計算結果を一つの式の中で複数回使いたいに便利です。
またその他に、前回紹介したように ?.
と組み合わせて使うと便利です。
// Kotlin
val x: Int? = ...
val square: Int? = x?.let { it * it }
上記のラムダ式 { it * it }
は x
が null
でない場合だけ実行されます。 x
が 3
なら square
は 9
になります。一方で、 x
が null
だった場合には x?.
なので let
自体が実行されず結果( square
)は null
になります。これは、 Java の Optional
でいう map
や flatMap
に相当する操作になります。
apply
最後になりましたが、僕のイチオシの apply
です。
設定すべきパラメータの多いクラスの場合、インスタンスを生成してから繰り返し setter を読んでセットアップすることがあると思います。そういうケースで役立つのが apply
です。
たとえば、↓は JavaFX の Oracle のチュートリアルから抜粋したコードです。
// Java
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
繰り返し setter
を呼び出していてコードが冗長な感じですし、階層もわかりづらくてごちゃっとしてますね。
これを apply
を使って Kotlin で書き直してみると次のようになります。
// Kotlin
primaryStage.apply {
title = "Hello World!"
scene = Scene(StackPane().apply {
children.add(Button().apply {
text = "Say 'Hello World'"
onAction = EventHandler<ActionEvent> { println("Hello World!") }
})
}, 300.0, 250.0)
show()
}
apply
に渡されたラムダ式の中では title = "Hello World!"
というように、 primaryStage
を介さずに title
にアクセスできているところがポイントです。これによって primaryState.
のような形で繰り返しメンバにアクセスすることが避けられています。これは、 apply
に渡されたラムダ式の中では、 apply
のレシーバーである primaryStage
が this
扱いになることによります。こんなことができるのは、 Kotlin が関数リテラルにレシーバーを this
として紐付ける仕組みがあるからです。
また、 apply
の戻り値はそのレシーバー自身( primaryStage.apply { ... }
なら primaryStage
)なので、 Java のコードにあった btn
や root
などの一時変数もなくすことができます。これがわかりやすいかはケース・バイ・ケースですが、一時変数を作りたければ Kotlin でも作ることができます。僕は、名前を付けておかないと何をやっているかわかりにくくてコードの可読性を損ねそうな場合だけ一時変数に代入するようにしています。
apply
は次のように実装されています。
// Kotlin
inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
この T.() -> Unit
の T.
がによってレシーバーを this
に紐付けています。
-
apply
: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html - Function Literals with Receiver: https://kotlinlang.org/docs/reference/lambdas.html#function-literals-with-receiver
今後の Kotlin の学び方
本投稿は Java プログラマがスムーズに Kotlin を始められることを目的としており、網羅的な説明はしていません。より網羅的に Kotlin を学びたい場合は、公式サイトのドキュメントがよくできているのでそちらをオススメします。英語ですが、 Android アプリ開発者であれば普段から英語を読んでいると思いますし、平易な英語なので大きな問題はないと思います。