コンパニオンオブジェクトとは、一言で言えばクラスのstatic(静的)メンバのようなものを定義する構文、あるいはクラスの中で定義するオブジェクト宣言のような存在です。
オブジェクト宣言と同じく、シングルトンを作るのがコンパニオンオブジェクトですが、両者の違いはどこにあるのでしょうか?
今回はコンパニオンオブジェクトの使い方、オブジェクト宣言との違い、staticメンバについてお話します。
コンパニオンオブジェクトの定義方法
例によって、簡単なコンパニオンオブジェクトを定義します。
class Outer(){
val str = "アウタークラスのプロパティ"
companion object{ //「companion object」で定義する
val x = 1
var y = 2
fun innerMethod(): Int{
return x+y
}
}
}
外側のOuterクラスにネストされたクラスがコンパニオンオブジェクトです。他のクラスと同様、プロパティとメソッドが確認できます。
名前はあっても無くてもかまわない
companion object Inner{...}
コンパニオンオブジェクトのヘッダに「Inner」という名前を付けてみます。これも正しい構文。コンパニオンオブジェクトには名前があっても無くてもかまいません。
呼び出しに名前は使わない
定義したコンパニオンオブジェクトに、main関数からアクセスしてみます。
fun main(){
println(Outer.x) //1
println(Outer.innerMethod()) //3
}
ここにコンパニオンオブジェクトの名前は表記されていないことに注意してください。名前があっても無くても呼び出しは変わりません。
ちょうどOuterクラスのstaticメンバのようにアクセスできるのが、コンパニオンオブジェクトの特徴です。呼び出しに名前を利用しないため、同じクラスにコンパニオンオブジェクトは1つしか定義できません。
オブジェクト宣言との違い
上で示したように、コンパニオンオブジェクトはオブジェクト宣言と比べて、
- 名前の扱い
- 複数定義が可能かどうか
- 呼び出し方
といった点が異なリます。さらにもう1つ、決定的な違いは初期化のタイミングです。
ここで言う「初期化」は、設定された値をメモリ上に配置し、それを使える状態にすることを指します。
コンパニオンオブジェクトが初期化されるタイミングは、
- 自身を囲むクラスが初期化されたとき
- 自身のメンバが参照されたとき
の2つです。オブジェクト宣言は1の場合、初期化されません。オブジェクト宣言に初期化ブロックを置いて試してみましょう。
class Outer(){
object Inner{ //オブジェクト宣言
init{
println("初期化されたよ")
}
val x = 1
}
}
fun main(){
val o = Outer()
println("---")
println(Outer.Inner.x)
}
//---
//初期化されたよ
//1
Outerクラスの初期化はInnerオブジェクトの初期化に影響を与えていません。対してコンパニオンオブジェクトでの実行結果は、
//初期化されたよ
//---
//1
Outerクラスが初期化されたタイミングと同時に初期化されているのが分かります。
コンパニオンオブジェクトはより外側のクラスと密接に関係していると言えますが、どちらがいいというものでもありません。それぞれに適した場面で使い分けるようにしましょう。
コンパニオンオブジェクトとstaticメンバ
Kotlinは言語レベルで、Javaと同じstaticメンバをサポートしていません。しかしコンパニオンオブジェクトによって、その値をあたかもstaticメンバであるかのようにアクセスすることができます。
「Kotlinでstaticメンバを定義するなら、コンパニオンオブジェクトを使う」という認識は、ほぼ正しいと言えます。
ただしJVMは、コンパニオンオブジェクトのメンバをstaticとして認識していません。
fun main(){
println(Outer.x) //1
Outer.printStr() //コンパニオン
}
class Outer(){
companion object{
val x = 1
fun printStr() = println("コンパニオン")
}
}
このコードの実行部分は、Javaで見るとこのようになります。
System.out.println(Outer.Companion.getX());
Outer.Companion.printStr();
Outer.Companion
と、あくまでコンパニオンオブジェクトのメンバとして、xプロパティが参照されているのが分かると思います。メソッドも同様です。
これらをOuterクラスのstaticメンバとしてJVMに解釈させたいなら、アノテーションを使うのがよりベターでしょう。
@JvmStaticと@JvmField
Kotlinのコードにアノテーションを付け加えてみます。
class Outer(){
companion object{
//アノテーション。大文字小文字に注意
@JvmField val x = 1
@JvmStatic fun printStr() = println("コンパニオン")
}
}
プロパティには@JvmField
、メソッドには@JvmStatic
と記述することで、Javaではこのように解釈されます。
System.out.println(Outer.x);
Outer.printStr();
アノテーションによってget
メソッドやCompanion
への参照が無くなり、より自然な形になりました。
トップレベルで宣言する
もう1つの方法はトップレベルでの宣言です。Kotlinではどのクラスにも属さない関数や変数を記述することができますが、これを使うことでJVMにstaticメンバとして参照させることができます。
main関数が含まれたファイル(ここではMain.kt)と、別の.ktファイル(ここではSub.kt)を同じパッケージとして登録し、Sub.ktファイルに変数と関数を記述します。
//Sub.ktファイル側
package net.pouhon
@JvmField val x = 1
fun printStr() = println("トップレベル")
//main関数側
package net.pouhon
fun main(){
println(x) //1
printStr() //トップレベル
}
Kotlinではこのように呼び出しますが、Javaに変換してみると、
public final class MainKt {
public static final void main() {
int var0 = SubKt.x; //変数はSubKt.xでアクセス
boolean var1 = false;
System.out.println(var0);
SubKt.printStr(); //関数はSubKt.printStr()でアクセス
}
//ここから下はJavaの実行部分
public static void main(String[] var0) {
main();
}
}
Javaのコードではファイル名.変数/関数名
とファイル名を1つのクラスとして、そのstaticなメンバを参照しています。