Edited at

JavaプログラマがKotlinで便利だと感じること

More than 1 year has passed since last update.

Kotlin が Android の公式言語になることが Goole I/O 2017 で発表されました。本投稿では、 Java プログラマを対象に、 Java にはない Kotlin 便利な機能について説明します。

本投稿は単独で理解できるように書いていますが、↓の連載の第三弾です。 Kotlin の基礎的な構文は理解していることを前提としているので、 Kotlin の構文自体を知らない方は以前の投稿を先に御覧下さい。


  1. Javaとほぼ同じところ

  2. 新しい考え方が必要でつまづきがちなところ

  3. 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 のように文字列を + で結合する必要はありません。


複数行文字列リテラル

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)

listOfmapOf などの利用頻度の高い関数は、このようにクラス名なしで呼び出せると便利です( 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

コードが圧縮できてずいぶんとすっきりします。


関数の型と代入

また、関数を変数に代入することもできます。

// 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"


演算子オーバーロード

実は 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)

その他詳細については公式ドキュメントを御覧下さい。

僕が特に便利だと思うのが getset です。それぞれ( Java でいう配列の) [] に対応します。

// Kotlin

val a = list[i] // list.get(i)
list[i] = b // list.set(i, b)

Java の ListMapget / set ( put ) は煩わしかったですが、 Kotlin では演算子オーバーロードのおかげで [i] のようにしてコレクションの要素にアクセスすることができます( Kotlin では Map でも set メソッドでエントリーを登録できます)。


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

ただし、 PairTriple の乱用は型の意味を失い、可読性を損ねるので注意して下さい。特に、戻り値として一時的に使うだけでなく、 PairTriple のまま値を取り回し始めたら、独自のクラスに置き換えた方がよくないか検討してみましょう。

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>component1component2 を実装しているので、↑のようにして値を分解しながら取り出すことができます。

この他にも 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>AB には別に A to B という関係があるわけではないので、この tomapOf と合わせて使うことが想定されていると思います。大抵の言語では Map のための特殊なリテラルや構文が用意されることが多いですが、 Kotlin はできるだけ言語の構文で解決しようとする傾向があり、 to はその代表的なものの一つです。


Data class

プライマリコンストラクタを使えば、コンストラクタ、フィールド、 getter 、 setter の宣言と実装をまとめて済ませることができて楽でした。しかし、 Data Class を使えばもっと楽ができます。

getter, setter だけを持っている、ただデータとして値を保持するだけのクラスはよくあります。そういうクラスを Kotlin では Data Class と呼びます。

そのようなクラスでは、コンストラクタとプロパティに加えて equalshashCode, 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 で変更するのと大して変わりません。 イミュータビリティと変更の容易さを同時に実現 できるなんて素晴らしいですね。


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 のうちどれが呼ばれるかはコンパイル時に決定されます。 animalAnimal 型なので、その実体が Animal だろうと Cat だろうと、どの foo が呼ばれるかは引数に渡された値の型(この場合は Animal )から決定されます。

どうして Extension はクラスそのものを書き変えずに、こんなややこしいことをするのでしょうか?もしクラス自体を書き換えてしまうと、異なるライブラリ間でたまたま同じ名前のメソッドを後付してしまったらメソッドの実装が衝突してしまいます。ライブラリ A と B が両方同じクラスに foo というメソッドを追加したら、ライブラリ A の foo メソッドがライブラリ B の中で予期せず呼ばれるというようなことが起こりえます。それでは怖くて Extension を使えません。コンパイル時に静的に解決されるのであれば、そのような混同は起こり得ません。

Extension Function をメソッドの形式で呼び出せるのは便利ですが、利用者側からすると見た目は同じなのに、本物のメソッドなのか Extension Function なのかで挙動が変わるのはややこしいとも言えます。スーパークラスとサブクラスで同じ名前の Extension Function を追加するのはややこしい事態を招きかねないのでやめましょう。


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 はクラスでありインスタンスです。その証拠に、 FooFoo 型の変数に代入することもできます。

// 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 ->
...
}


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, JSONNullclass ではなく 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 -> { ... }{ ... } の部分では jsonJSONObject 型として扱えます。

なお、 Sealed Class はクラスで、インタフェースではありません。ただし、 Sealed Class は自動的に抽象クラスになるので、 Sealed Class 自体のインスタンスを生成することはできません。 Sealed Class 本体には abstract メソッドのみを定義し、インタフェース的に使うこともできますし、共通の実装を書くこともできます。


スコープ関数

Kotlin にはスコープ関数と呼ばれる便利な関数群があります。 also, apply, let, run, with です。ただし、スコープ関数という呼称は日英問わず使われているものの、公式ドキュメントでは確認できませんでした。→(追記)公式ブログが元のようです。

スコープ関数については @ngsw_taro さんが詳しく解説してくれていますので、ここでは僕が特に便利だと思う letapply についてのみ紹介します。


let

x + 1 / x のような計算をしたいとしましょう。今、 xfoo(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 }xnull でない場合だけ実行されます。 x3 なら square9 になります。一方で、 xnull だった場合には x?. なので let 自体が実行されず結果( square )は null になります。これは、 Java の Optional でいう mapflatMap に相当する操作になります。


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 のレシーバーである primaryStagethis 扱いになることによります。こんなことができるのは、 Kotlin が関数リテラルにレシーバーを this として紐付ける仕組みがあるからです。

また、 apply の戻り値はそのレシーバー自身( primaryStage.apply { ... } なら primaryStage )なので、 Java のコードにあった btnroot などの一時変数もなくすことができます。これがわかりやすいかはケース・バイ・ケースですが、一時変数を作りたければ Kotlin でも作ることができます。僕は、名前を付けておかないと何をやっているかわかりにくくてコードの可読性を損ねそうな場合だけ一時変数に代入するようにしています。

apply は次のように実装されています。

// Kotlin

inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

この T.() -> UnitT. がによってレシーバーを this に紐付けています。


今後の Kotlin の学び方

本投稿は Java プログラマがスムーズに Kotlin を始められることを目的としており、網羅的な説明はしていません。より網羅的に Kotlin を学びたい場合は、公式サイトのドキュメントがよくできているのでそちらをオススメします。英語ですが、 Android アプリ開発者であれば普段から英語を読んでいると思いますし、平易な英語なので大きな問題はないと思います。