Kotlin - Javaとの相互運用
Kotlin 文法 - アノテーション、リフレクション、型安全なビルダー、動的型の続き。
Kotlin Referenceの Interop 章の大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。
※この記事は Qiita の記事 と同じものです。
Kotlin から Java のコードを呼ぶ
Kotlin は Java との相互運用を念頭において設計されている。既存の Java コードは Kotlin から自然な方法で呼び出すことができる。そして Kotlin のコードも同様にかなりスムーズに Java から利用することができる。この節では Kotlin から Java のコードを呼ぶ場合の詳細を記述する。
ほとんど全ての Java コードは何の問題もなく利用できる。
import java.util.*
fun demo(source: List<Int>) {
val list = ArrayList<Int>()
// Javaコレクションでのforループが動作する
for (item in source)
list.add(item)
// 演算子置換ルールも同様に機能する
for (i in 0..source.size() - 1)
list[i] = source[i] // get と set が呼ばれる
}
Getter と Setter
Java の慣習に従った getter と setter(get で始まる名前の引数なしメソッドと、set で始まる1引数のメソッド)は Kotlin ではプロパティとして解釈される。例えば以下のように。
import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // getFirstDayOfWeek()を呼ぶ
calendar.firstDayOfWeek = Calendar.MONDAY // setFirstDayOfWeek()を呼ぶ
}
}
もし Java クラスが setter しか持たなかったら、Kotlin ではプロパティとして見えないことに注意。今のところ Kotlin では set しかできないプロパティはサポートしていないので。
void を返すメソッド
もし Java メソッドが void を返す場合、Kotlin から呼び出されるときは Unit を返す。ひょっとして誰かがその値を使うなら、Kotlin コンパイラによって呼び出し箇所で代入される。値そのものは事前に(Unit になると)わかっているので。
Kotlin でキーワードになっている Java 識別子のエスケープ
幾つかの Kotlin キーワードは Java では有効な識別子。つまり in, object, is など。もし Java ライブラリが Kotlin キーワードをメソッドに使っていたら、バッククォートでエスケープして呼び出すことができる。
foo.`is`(bar)
Null 安全とプラットフォーム型
Java での全ての参照は null になりうる。なので Java から来るオブジェクトに対しては、Kotlin の厳格な null 安全要求は実現困難になる。Java 宣言の型は Kotlin では特別扱いされており、プラットフォーム型 と呼ばれている。この型に対しての Null チェックは緩和され、安全保証は Java の場合と同じになる(下記参照)。
次の例を考えてみよう。
val list = ArrayList<String>() // nullでない (コンストラクタの結果)
list.add("Item")
val size = list.size() // nullでない (プリミティブ型のint)
val item = list[0] // プラットフォーム型と推論される(普通のJavaオブジェクト)
プラットフォーム型の変数に対してメソッドを呼ぶ時、Kotlin はコンパイル時に null 可能性エラーを報告しない。しかし null ポインタ例外や null の伝搬を防ぐために Kotlin が生成するアサーションによって、実行時に呼び出しが失敗するかもしれない。
item.substring(1) // 許可される。them == null で例外が投げられるかも。
プラットフォーム型は言語で明示的に書き下すことができないという意味で 表記できない 。プラットフォーム型の値が Kotlin の変数に代入されるとき、型推論(変数は上記例の item のようにプラットフォーム型と推論される)に頼るか、期待する型(nulllable も null でない型も許される)を選ぶことができる1。
val nullable: String? = item // 許可されるし、常に動作する
val notNull: String = item // 許可されるけど、実行時に失敗するかもしれない
null でない型を選べば、コンパイラが代入前にアサーションを挿入する。これにより Kotlin の null でない型が null を持つことを防ぐ。アサーションは、プラットフォーム型を null でない型を期待する関数に渡すときなどにも挿入される。全体として、コンパイラはプログラムを通して null が遠くに伝搬するのを防ぐようベストを尽くす(ジェネリクスがあるので完全に排除できないことがあるけれど)。
プラットフォーム型の表記
上で言及した通り、プラットフォーム型はプログラム中で明示的に示すことはできない。つまり言語にそのための表記法はない。それでもコンパイラや IDE は時々それらを表示する必要がある(エラーメッセージやパラメータ情報など)。そのためこれらのための簡略表示法を用意している。
- T! は ”T または T? ” を表す
- (Mutable)Collection<T>! は「 T の Java コレクションは mutable またはそうでないかもしれず、nullable またはそうでないかもしれない」を意味する。
- Array<(out) T>! は「 T (または T のサブ型)の Java 配列は nullable またはそうでない」を意味する。
Null 可能性アノテーション
Null 可能性アノテーションを持った Java 型は、プラットフォーム型ではなく実際の Kotlin の nullable または null でない型として表現される。今のところ、コンパイラはJetBrains 風の Null 可能性アノテーション(org.jetbrains.annotations パッケージにある @Nullable と @NotNull)をサポートしている。
マッピングされる型
Kotlin は幾つかの Java 型を特別に扱う。これらの型は Java から “そのまま” はロードされず、Kotlin の対応する型にマッピングされる。このマッピングはコンパイル時にだけ重要であり、実行時の表現は変化しないままである。Java のプリミティブ型は対応する Kotlin 型にマッピングされる(プラットフォーム型のことは心に留めておいて)。
Java 型 | Kotlin 型 |
---|---|
byte | kotlin.Byte |
short | kotlin.Short |
int | kotlin.Int |
long | kotlin.Long |
char | kotlin.Char |
float | kotlin.Float |
double | kotlin.Double |
boolean | kotlin.Boolean |
幾つかのプリミティブでないビルトインクラスもマッピングされる。
Java 型 | Kotlin 型 |
---|---|
java.lang.Object | kotlin.Any! |
java.lang.Cloneable | kotlin.Cloneable! |
java.lang.Comparable | kotlin.Comparable! |
java.lang.Enum | kotlin.Enum! |
java.lang.Annotation | kotlin.Annotation! |
java.lang.Deprecated | kotlin.Deprecated! |
java.lang.Void | kotlin.Nothing! |
java.lang.CharSequence | kotlin.CharSequence! |
java.lang.String | kotlin.String! |
java.lang.Number | kotlin.Number! |
java.lang.Throwable | kotlin.Throwable! |
コレクション型は Kotlin では読み取り専用か可変のどちらかになる。なので Java コレクションは次のようにマッピングされる(この表の Kotlin 型は全て kotlin パッケージにある)。
Java 型 | Kotlin 読取専用型 | Kotlin 可変型 | ロードされるプラットフォーム型 |
---|---|---|---|
Iterator<T> | Iterator<T> | MutableIterator<T> | (Mutable)Iterator<T>! |
Iterable<T> | Iterable<T> | MutableIterable<T> | (Mutable)Iterable<T>! |
Collection<T> | Collection<T> | MutableCollection<T> | (Mutable)Collection<T>! |
Set<T> | Set<T> | MutableSet<T> | (Mutable)Set<T>! |
List<T> | List<T> | MutableList<T> | (Mutable)List<T>! |
ListIterator<T> | ListIterator<T> | MutableListIterator<T> | (Mutable)ListIterator<T>! |
Map<K, V> | Map<K, V> | MutableMap<K, V> | (Mutable)Map<K, V>! |
Map.Entry<K, V> | Map.Entry<K, V> | MutableMap.MutableEntry<K,V> | (Mutable)Map.(Mutable)Entry<K, V>! |
Java 配列のマッピングについては後述する。
Java 型 | Kotlin 型 |
---|---|
int[] | kotlin.IntArray! |
String[] | kotlin.Array<(out) String>! |
Kotlin での Java ジェネリクス
Kotlin のジェネリクスは Java とは少し違っている(ジェネリクス参照)。Java 型を Kotlin にインポートするとき、幾つかの変換が行われる。
-
Java のワイルドカードは型投影に変換される
- Foo<? extends Bar> は Foo<out Bar!>! になる
- Foo<? super Bar> は Foo<in Bar!>! になる
-
Java の原型(raw type)は星投影(star projection)に変換される
- List は List<*>! つまり List<out Any?>! になる
Java と同様に Kotlin のジェネリクスは実行時には保持されない。つまりオブジェクトはコンストラクタに渡された引数の実際の型情報を持ち運ばない。つまり ArrayList<Integer>() は ArrayList<Character>() と区別がつかない。これはジェネリクスを考慮した is チェックを不可能にする。Kotlin は星投影(star projection)されたジェネリック型に対してのみ is チェックを許している。
if (a is List<Int>) // Error: 本当にIntのリストかどうかチェックできない
// しかし
if (a is List<*>) // OK: リストの内容について何も保証しない
Java 配列
Kotlin での配列は Java と違って不変(invariant)である。これは Array<String> を Array<Any> に代入できないことを意味している。これにより実行時エラーを防ぐことができる。サブクラスの配列をスーパークラスの配列として Kotlin のメソッドに渡すことも禁止されている。しかし Java のメソッドはこれを許可する(Array<(out) String>! の形のプラットフォーム型を通して)。
Java プラットフォームではボクシングのコストを避けるため、プリミティブ型の配列が使われる。Kotlin はそれらの実装の詳細を隠すので、Java コードのインターフェースに対しては回避方法が必要になる。こういう場合のためにプリミティブ型の配列にはそれぞれ特別なクラスが用意されている(IntArray, DoubleArray, CharArrayなどなど)。これらは Array クラスとの関係はなく、最大のパフォーマンスを発揮する Java のプリミティブ配列にコンパイルされる。
int の配列を取る Java メソッドがあるとしよう。
public class JavaArrayExample {
public void removeIndices(int[] indices) {
// code here...
}
}
プリミティブ型の配列を渡すために、Kotlin では次のようにできる。
val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3) // IntArrayを返す
javaObj.removeIndices(array) // int[] をメソッドに渡す
JVM バイトコードにコンパイルされるとき、コンパイラは配列へのアクセスを最適化するので、オーバーヘッドは生じない。
val array = arrayOf(1, 2, 3, 4)
array[x] = array[x] * 2 // 実際にはget()やset()の呼び出しは生成されない
for (x in array) // イテレータは生成されない
print(x)
インデックスをループで回すときでさえ、オーバーヘッドは生じない。
for (i in array.indices) // イテレータは生成されない
array[i] += 2
最後に、in チェックもオーバーヘッドを生じない。
if (i in array.indices) { // (i >= 0 && i < array.size) と同じ
print(array[i])
}
Java 可変長引数
Java クラスはしばしば可変長引数(varargs)を使ってメソッドの引数を宣言している。
public class JavaArrayExample {
public void removeIndices(int... indices) {
// code here...
}
}
こういう場合、IntArray を渡すのに展開演算子 * を使う必要がある。
val javaObj = JavaArray()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)
可変長引数として宣言されているメソッドに null を渡すのは、今のところ不可能。
演算子
Kotlin の operator 修飾子のような、演算子として使うと分かるようにメソッドに印を付ける方法は Java にはない。なので Kotlin は、Java メソッドが正しい名前とシグネチャを持っていれば、演算子オーバーロードや他の変換ルール(invoke()など)に使うことを許す。接中辞記法で Java メソッドを呼び出すことはできない。
検査例外
Kotlin では全ての例外は検査されない。つまりコンパイラはどの例外もプログラマにキャッチすることを強制しない。なので Java で検査例外として宣言されているメソッドを呼び出す場合、Kotlin は何も強制しない。
fun render(list: List<*>, to: Appendable) {
for (item in list)
to.append(item.toString()) // JavaならIOExceptionをキャッチすることを要求するだろう
}
オブジェクトメソッド
Java 型が Kotlin にインポートされるとき、全ての java.lang.Object 型の参照は Any になる。Any はプラットフォーム依存でないので、そのメンバとして toString(), hashCode() と equals() しか宣言していない。他の java.lang.Object のメンバを有効にするのに、Kotlin は拡張関数を使う。
wait()/notify()
Effective Java の 69 項は、wait()と notify()よりもコンカレンシーユーティリティーを使うように提案している。それでこれらのメソッドは Any では有効になっていない。これらを本当に呼ぶ必要があるなら、java.lang.Object にキャストする。
(foo as java.lang.Object).wait()
getClass()
オブジェクトから型情報を引き出すには、javaClass 拡張プロパティを利用する。
val fooClass = foo.javaClass
Java の Foo.class の代わりに Foo::class.java を使う。
val fooClass = Foo::class.java
clone()
clone() をオーバーライドするには、kotlin.Cloneable を実装する必要がある。
class Example : Cloneable {
override fun clone(): Any { ... }
}
Effective Java の第11項「clone を注意してオーバーライドする」を忘れないこと。
finalize()
finalize() をオーバーライドするには、override キーワードなしで単に宣言するだけでいい。
class C {
protected fun finalize() {
// finalization logic
}
}
Java のルールに従い、finalize() は private であってはいけない。
Java クラスからの継承
最大1つの Java クラス(と好きなだけの数の Java インターフェース)が Kotlin クラスのスーパー型になれる。
static メンバへのアクセス
Java クラスの static メンバは “コンパニオンオブジェクト” の形になる。こういう “コンパニオンブジェクト” は値として渡すことはできない。しかしメンバに明示的にアクセスすることはできる。例えば以下のように。
if (Character.isLetter(a)) {
// ...
}
Java リフレクション
Java リフレクションは Kotlin でも動作するし、その逆も同様。java.lang.Class を通して Java リフレクションに入り込むのに、上述したように instance.javaClass や ClassName::class.java を利用できる。
他にも Java の getter/setter メソッドや Kotlin プロパティのバッキングフィールドを獲得することもできる。Java フィールドには KProperty 、Java メソッドやコンストラクタには KFunction を使う。
SAM 変換
Java 8 のように、Kotlin は SAM2変換をサポートする。つまり Kotlin の関数リテラルは、デフォルトでない1つだけのメソッドを持つ Java インターフェースの実装へと、自動的に変換される。インターフェースメソッドの引数の型と Kotlin 関数の引数の型が一致していれば。
SAM インターフェースのインスタンスをこんな風に作成できる。
val runnable = Runnable { println("This runs in a runnable") }
…で、メソッド呼び出しではこんな感じ。
val executor = ThreadPoolExecutor()
// Javaでのシグネチャ: void execute(Runnable command)
executor.execute { println("This runs in a thread pool") }
もし Java クラスが別の SAM インターフェースを取る複数のメソッドを持つなら、ラムダを指定 SAM 型に変換するアダプタ関数を使ってどれを使うか選択する。これらのアダプタ関数も必要に応じてコンパイラが生成する。
// Runnable以外のSAMインターフェースを取るexecuteがあるので、
// アダプタ関数 Runnable を使って、Rnnnableを取るexecuteを指定。
executor.execute(Runnable { println("This runs in a thread pool") })
SAM 変換はインターフェースに対してしか働かないことに注意。たとえ1つの抽象メソッドしか持たなかったとしても、抽象クラスには働かない。
またこの機能は Java との相互運用でしか働かないことにも注意。Kotlin はちゃんとした関数型を持ってるので、関数から Kotlin インターフェースへの自動変換は必要ないし、だからサポートされていない。
Java から Kotlin のコードを呼ぶ
Kotlin のコードは Java から簡単に呼び出すことができる。
プロパティ
プロパティの getter は get-メソッドに、setter は set-メソッドになる。
パッケージレベルの関数
パッケージ org.foo.bar 内の example.kt ファイルで宣言された全ての関数とプロパティは、org.foo.bar.ExampleKt という名前の Java クラスに所属する。
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.ExampleKt.bar();
生成される Java クラスの名前は @JvmName アノテーションで変更することができる。
@file:JvmName("DemoUtils")
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.DemoUtils.bar();
同じ Java クラス名(同じパッケージで同じ名前または同じ@JvmName アノテーション)で生成されるファイルが複数ある場合、通常はエラーになる。しかしコンパイラにはそれらを1つにまとめたファサードクラスを生成する機能がある。これを有効にするには全てのファイルに @JvmMultifileClass アノテーションを付ける。
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun foo() {
}
@file:JvmName("Utils")
@file:JvmMultifileClass
package demo
fun bar() {
}
// Java
demo.Utils.foo();
demo.Utils.bar();
フィールド
もし Kotlin のプロパティを Java のフィールドとして外に出す必要があるなら、@JvmField アノテーションを付ける。そのフィールドのアクセス制限はプロパティと同じになる。このアノテーションを付けられるのは、バッキングフィールドを持ち、private でなく、open でも override でも const でもなく、デリテートされたプロパティでもない場合。
class C(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(C c) {
return c.ID;
}
}
static なメソッドとフィールド
上述したように、Kotlin はパッケージレベルの関数を static メソッドとして生成する。またコンパニオンオブジェクトや名前付きオブジェクトに定義された関数に @JvmStatic アノテーションが付けられた場合も、static メソッドとして生成する。例を挙げよう。
class C {
companion object {
@JvmStatic fun foo() {} // コンパニオンオブジェクトの関数に@JvmStatic指定
fun bar() {}
}
}
これで foo() は Java で static になる。bar()はならない。
C.foo(); // うまく動作する
C.bar(); // error: staticメソッドでない
名前付きオブジェクトも同じ。
object Obj {
@JvmStatic fun foo() {}
fun bar() {}
}
Java からこう呼び出せる。
Obj.foo(); // うまくいく
Obj.bar(); // error
Obj.INSTANCE.bar(); // 動作する。シングルトンインスタンスを通して呼び出す。
Obj.INSTANCE.foo(); // 同様に動作する。
またオブジェクトやコンパニオンオブジェクトに定義された public プロパティも、const 修飾されたトップレベルプロパティと同様に、Java では static フィールドになる。
object Obj {
val CONST = 1
}
const val MAX = 239
Java からこう呼び出せる。
int c = Obj.CONST;
int d = ExampleKt.MAX;
@JvmName によるシグネチャ衝突の回避
ときどき Kotlin で、バイトコードでは異なる JVM 名が必要な名前付き関数を持つことがある。典型的な例は 型消去 によって起こる。
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
これら2つの関数は一緒には定義できない。なぜならこれらの JVM シグネチャは同じ(filterValid(Ljava/util/List;)Ljava/util/List;)だから。もし Kotlin で本当にこれらを同じ名前にしたいなら、片方(または両方)に @JvmName アノテーションで異なる JVM 名を付けてやる付けてやる。
fun List<String>.filterValid(): List<String>
// こっちはKotlinでは同じ名前で使うけど、JVMバイトコードではfilterValidIntって名前にする
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
これで Kotlin からはどちらも同じ名前 filterValid でアクセスできる。けど Java からはfilterValid と filterValidInt になる。
同じトリックは getX() があるのにプロパティ x が必要な時にも使える。
// getX()があるので、何も指定しないとxのgetterと名前が衝突する
val x: Int
@JvmName("getX_prop") // getterのJVM名に別の名前を指定する
get() = 15
fun getX() = 10
オーバーロードの生成
デフォルト値を持った Kotlin のメソッドを書く場合、通常 Java からは全てのパラメータを持った1つのメソッドとして見える。もし Java から複数のオーバーロードとして見えるようにしたいなら、@JvmOverloads アノテーションが使える。
@JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
...
}
デフォルト値を持つそれぞれのパラメータに対して、そのパラメータの右側にあるパラメータを除いたオーバーロードを1つずつ生成する。この例では、次のメソッドが生成される。
// Java
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }
このアノテーションはコンストラクタや static メソッドなどに対しても使える。インターフェースに定義されたものも含めて抽象メソッドに対しては使えない。
コンストラクタで記述したように、全てのコンストラクタ引数がデフォルト値を持つクラスでは、public な引数なしコンストラクタが生成されることに注意。これは @JvmOverloads を指定しなくてもそうなる。
検査例外
前述した通り、Kotlin に検査例外はない。なので通常 Kotlin 関数の Java シグネチャは例外を投げるとは宣言されない。こんな Kotlin の関数があるとする。
package demo
fun foo() {
throw IOException()
}
それを Java から呼んで例外をキャッチしたいとする。
try {
demo.Example.foo();
}
catch (IOException e) { // error: foo()は検査例外IOExceptionをthrowsリストの中に宣言していない
// ...
}
foo() は IOException を宣言していないので、Java コンパイラからエラーメッセージを受け取る。この問題に対処するには、Kotlin 側で @Throws アノテーションを使う。
@Throws(IOException::class)
fun foo() {
throw IOException()
}
Null 安全
Java から Kotlin の関数を呼ぶとき、null でないパラメータに null を渡せてしまう。なので Kotlin の方で、null でないことを期待する全ての public 関数に実行時チェックを挿入している。これによって Java コードは即座に NullPointerException を受け取ることになる。
ジェネリクスの変性
Kotlin のクラスが宣言側変性を使うとき、Java コードから見える利用方法には2つのオプションがある。次のクラスとそれを使う2つの関数を見てみよう。
// T型の値を格納する箱。Tを返すメソッドしか持たないので <out T> にできる。
// コンストラクタ引数にvalを付けているので、読み取り専用プロパティvalueが自動生成される。
class Box<out T>(val value: T)
// Boxに入れる型として、Baseインターフェースを実装したDerivedクラスを用意
interface Base
class Derived : Base
// Defived型の値をBoxに入れて返す
fun boxDerived(value: Derived): Box<Derived> = Box(value)
// BoxからBase型(Derivedじゃないよ)の値を取り出す
fun unboxBase(box: Box<Base>): Base = box.value
これらを Java に翻訳する自然な方法はこうなるだろう。
Box<Derived> boxDerived(Derived value) { ... }
Base unboxBase(Box<Base> box) { ... }
問題は Kotlin では unboxBase(boxDerived(“s”)) と書けても3、Java では書けないということ。なぜなら Java では Box は型 T に対して 不変(invariant) だから。つまり Box<Derived> は Box<Base> のサブ型ではない。これを Java で動作させるには unboxBase を次のように定義しなければならない。
Base unboxBase(Box<? extends Base> box) { ... }
ここでは宣言側変性を利用側変性でエミュレートするために、Java のワイルドカード( ? extends Base )を使っている。だって Java にはこれしかないから。
Kotlin の API を Java で動作させるために、パラメータとして現れる場所で、共変として定義された Box に対して Box<Super> を Box<? extends Super> として生成している。戻り値に対してワイルドカードは生成しない。そうしないと Java クライアントがそれらを扱わないといけなくなるから(そして Java のコーディングスタイルに反する)。そんなわけで、先ほどの例は実際は次のように翻訳される。
// 戻り値の型 - ワイルドカードは使わない
Box<Derived> boxDerived(Derived value) { ... }
// パラメータ - ワイルドカードを使う
Base unboxBase(Box<? extends Base> box) { ... }
注:パラメータの型が final の場合4、通常はワイルドカードを生成する箇所はない。なのでそれを取る場所に関係なく Box<String> は常に Box<String> になる。
デフォルトでは生成されない場所にワイルドカードが必要なら、@JvmWildcard アノテーションが使える。
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value)
// これは次のように翻訳される
// Box<? extends Derived> boxDerived(Derived value) { ... }
逆に生成される場所でワイルドカードがいらない場合は、@JvmSuppressWirldcard が使える。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
// これは次のように翻訳される
// Base unboxBase(Box<Base> box) { ... }
Nothing 型の翻訳
Nothing 型5は特別で、Java には自然に対応するものがない。実際、java.lang.Void を含む全ての Java 型の参照は値として null を受け入れるが、Nothing はそれさえも受け入れない。なのでこの型は Java の世界では正確に表現することはできない。これが引数の型に Nothing を使う場所で Kotlin が原型(raw type)を生成する理由である。
fun emptyList(): List<Nothing> = listOf()
// これは次のように翻訳される
// List emptyList() { ... }
終わりに
お疲れ様でした!これで Kotlin Reference の日本語訳は終わりです。 ざっと確認したい時や、忘れてしまって思い出したい時のために Kotlin 文法まとめ を用意しているので活用してください。