JavaにはあるけれどKotlinにないものの1つに、クラスメソッドやクラス変数があります。 この記事では、そのクラスメソッドとクラス変数をKotlinではどう定義すべきかという話をします。
より詳細に言えば、メソッドや定数*1をtop-levelで宣言するのとcompanion objectに宣言するのとどちらが良いかという話です。
Java -> Kotlin 自動変換
JavaからKotlinへの自動変換を使ったことのある人は、Javaのクラスメソッドや定数がcompanion objectに変換されることに気付いたでしょう。
こういうJavaクラスがあったとして、自動変換をすると、
package shape; public class Circle { public static final double PI = 3.14159265358979323846; private final double r; private Circle(double r) { this.r = r; } public static Circle newCircle(double r) { return new Circle(r); } public double area() { return PI * r * r; } }
つぎのようになります*2。
package shape class Circle(private val r: Double) { fun area(): Double { return PI * r * r } companion object { const val PI = 3.14159265358979323846 fun newCircle(r: Double): Circle { return Circle(r) } } }
これはKotlinコードとしては(多少不自然さはあるものの)まったく正しいコードです。
呼び出し方も Circle.newCircle()
もしくは Circle.PI
とすればよいのでJavaに馴れた目にも違和感がありません。
そのせいか、自分が目にするKotlinコードの多くは、Javaのクラスメソッドやクラス変数に相当するものをcompanion objectで実装する傾向があるように思います。
ただ、個人的な感覚からすると、このcompanion objectは余計な印象があります。この印象を分析すると、2つの理由があります。
- 作らなくてもいいオブジェクトを生成している
- このcompanion objectがどういう振る舞いを期待されているか分からない
2について補足すると、ファクトリーメソッドを持ちつつ、そのインスタンス生成に関係ないPI
という変数を持っているのがやや気持ち悪く感じます。
またPI
という定数の性質も相まってCircleクラスに関連付いているというのも微妙な気分になります。
top-level関数および定数
一方、Kotlinではtop-levelで関数およびプロパティを宣言することができます。 これは特定のクラスに属さない関数や定数を定義するのに自然な選択です。
さきほどの例だと、こうなるでしょう。
package shape const val PI = 3.14159265358979323846 fun newCircle(r: Double): Circle { return Circle(r) } class Circle(private val r: Double) { fun area() = PI * r * r }
いかがでしょうか?クラス設計という点からは、Circle
クラスのみになり、理解しやすい気がします。ただ後述するように使う側からすると使いにくさがありそうです。
なお、この例ではCircle
クラスのコンストラクタがpublicですが、こういったnewCircle
メソッドのようなファクトリーメソッドがある場合は、コンストラクタをprivateにするケースが大半です(さもないとせっかくのファクトリーメソッドを迂回されてしまいます)。
その場合、このtop-level版のコードはコンパイルできません。なぜならnewCircle
はCircle
クラスのprivateコンストラクタにアクセスできないからです。
しかし、ここでは、どちらがいいか、という議論はやめておきましょう。
かわりに、両者を比較する上での材料を集めてみます。
バイトコードを見てみる
まずは、両者の書き方によって、バイトコードに差が出るか見てみましょう。
まずはcompanion object版です(関係のなさそうなところは端折っています)。
public final class shape/Circle { // access flags 0x1A public final static D PI = 3.141592653589793 // access flags 0x19 public final static Lshape/Circle$Companion; Companion } public final class shape/Circle$Companion { // access flags 0x11 public final newCircle(D)Lshape/Circle; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 12 L0 NEW shape/Circle DUP DLOAD 1 INVOKESPECIAL shape/Circle.<init> (D)V ARETURN L1 LOCALVARIABLE this Lshape/Circle$Companion; L0 L1 0 LOCALVARIABLE r D L0 L1 1 MAXSTACK = 4 MAXLOCALS = 3 }
Circle
クラスがCircle$Companion
型のクラス変数Companion
を持ち、newCircle()
はその変数経由でアクセスします。
Companion
という名前がクラス名と変数名両方に使われていてちょっと紛らわしいですね。
とはいえ、おおむね予想通りだったのではないかと思います。
ではtop-level版はどうなるでしょうか。
public final class shape/Circle { ... } public final class shape/CircleKt { // access flags 0x1A public final static D PI = 3.141592653589793 // access flags 0x19 public final static newCircle(D)Lshape/Circle; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 6 L0 NEW shape/Circle DUP DLOAD 0 INVOKESPECIAL shape/Circle.<init> (D)V ARETURN L1 LOCALVARIABLE r D L0 L1 0 MAXSTACK = 4 MAXLOCALS = 2 }
CompanionObject
がなくなったかわりにCircleKt
クラスが登場し、そのクラス変数およびクラスメソッドになっています。
こちらも公式ドキュメントにある通りなのでびっくりはないと思います。
バイトコード的には、companion object版にあったCompanion
変数がなくなったので、すこしだけオーバーヘッドはなくなったかもしれません。
結論として、両者をバイトコードから比較したときに、どちらが良い、とは言えなさそうです。わずかにtop-level版の方がオーバーヘッドは小さい程度でしょうか。
使うときのメリット・デメリット
では使う側からはどういう違いがあるでしょうか。 それぞれのバージョンを呼び出すKotlinコードはつぎのようになります。
companion object版はこうなります。
import shape.Circle fun main(args: Array<String>) { val c = Circle.newCircle(1.0) println("PI = $Circle.PI") println(c.area()) }
つぎにtop-level版です。
import shape.PI import shape.newCircle fun main(args: Array<String>) { val c = newCircle(1.0) println("PI = $PI") println(c.area()) }
これは好みが分かれそうです。おそらく、Javaに馴れた目から見るとcompanion object版の方が自然でしょう。
とくにCircle
というクラス名で修飾するのに比べてshape
パッケージでしか名前空間を定義できないのは心許なく感じるかもしれません。
ここで、自分の好みを言うと、自分は実はtop-level版でも大して気になりません。
ただ、AndroidのIntent生成メソッドのnewIntent
やFragment生成メソッドのnewFragment
のように慣習的に同名のメソッドが大量にあると、それらに別名を付けるのは面倒そうです。
使う側の観点からは好み次第といったところでしょうか。
教科書やブログを調べてみる
それでは、ここで教科書やブログはどう言っているか見てみましょう。
まずはKotlin in Actionです。つい最近邦訳が出ましたが、残念ながら英語版しかないので、そこからの引用です。ちょっと長いですが、まさにドンピシャな部分があったので全パラグラフを載せます。後ろに拙訳を載せました(繰り返しになりますが日本語版を持っていないのです 🙇🏻)
Classes in Kotlin can’t have static members; Java’s static keyword isn’t part of the Kotlin language. As a replacement, Kotlin relies on package-level functions (which can replace Java’s static methods in many situations) and object declarations (which replace Java static methods in other cases, as well as static fields). In most cases, it’s recommended that you use top-level functions. But top-level functions can’t access private members of a class, as illustrated by figure 4.5. Thus, if you need to write a function that can be called without having a class instance but needs access to the internals of a class, you can write it as a member of an object declaration inside that class. An example of such a function would be a factory method.
Kotlinのクラスはスタティックメンバーを持てません。JavaのstaticキーワードはKotlinの言語仕様にないのです。代わりに、Kotlinはpackageレベルの関数(Javaの一般的なスタティックメソッドの代替)とobject宣言(Javaの特殊なスタティックメソッドとスタティックフィールドの代替)があります。 一般に、top-levelの関数を使うことをおすすめします。しかしtop-level関数は図4.5に示すようにクラスのプライベートメンバーにアクセスできません。そのため、クラスインスタンスは持つ必要はないけれど、クラス内部にアクセスしたい場合には、そのクラスのobjectを宣言し、関数をそのobject内に宣言します。そのような関数の典型例はファクトリーメソッドでしょう。
4.4.2. Companion objects: a place for factory methods and static members, Kotlin in Actions
いかがでしょうか?関数については明示的にtop-levelにすべきとありますね。一方でクラス変数(定数)については明言はされていません。どちらかというとobjectに定義することを想定しているようにも読めます。また、objectにメソッドを定義するのはクラス内部にアクセスしたい場合すなわちファクトリーメソッドの場合であると書いてあります。
巷間のブログではどうでしょうか?1つ参考になるブログ記事にWhere Should I Keep My Constants in Kotlin?という記事があります。 これは余計なオブジェクトを生成しないという観点からobjectに定数を定義する方法とtop-levelに定義する方法を比較し、後者の方がよいとしています。
一方、コメントを見るとobjectに名前を付けることで定数のグルーピングが出来て便利という意見もあるようです。
標準ライブラリをgrepしてみる
最後にやや飛び道具的ですが、標準ライブラリの実装を見てみます。 標準ライブラリに従わなければならないという法はありませんが、それらがcompanion objectをどう使っているか調べれば、きっとヒントが得られるでしょう。
ここでは https://github.com/JetBrains/kotlin の現時点での最新のコード*3でつぎのコマンドを実行してみます。
$ find ./libraries/stdlib -name '*.kt' | xargs grep -l 'companion object'
結果は15件と意外と少ないことに気付きます。テストを除いた .kt
ファイルは131ファイルあるので、それと比べても少なめなことが分かります。
一方、top-level関数が定義されているファイルは適当に egrep -l '^public inline fun'
とやっても52件見つかります。ただし、これは標準ライブラリという性質および拡張関数も含まれることを考えると公平な比較ではありません。参考程度にしておきます。
さて、ではcompanion objectは実際にどういう箇所で使っているのでしょうか。
たとえば、このへんなどは参考になりそうです。一種のファクトリーメソッドですが、キャッシュ用のプロパティを持っています。
public enum class CharCategory(public val value: Int, public val code: String) { ... public companion object { private val categoryMap by lazy { CharCategory.values().associateBy { it.value } } public fun valueOf(category: Int): CharCategory = categoryMap[category] ?: throw IllegalArgumentException("Category #$category is not defined.") } }
あるいは、ちょっと変わったものだとこのあたりでしょうか。
expect class Regex { ... companion object { fun fromLiteral(literal: String): Regex fun escape(literal: String): String fun escapeReplacement(literal: String): String } }
public interface ContinuationInterceptor : CoroutineContext.Element { companion object Key : CoroutineContext.Key<ContinuationInterceptor> ... }
前者は、Regex
クラスのインタフェース(expect class)を定義しているものですが、companion object
にファクトリーメソッドだけでなくescape
メソッドも定義しています。
これは、実装をこの場で提供できないがためにこうなっているのかもしれません。
後者は、coroutineの実装です。
詳細は述べませんが、CoroutineContext.Element
を継承しているインタフェースはどれもcompanion objectがCoroutineContext.Key
を継承しており、
クラス名をキーにしてContextからそのクラスのシングルトンを取得できるようになっています。
一方で、publicな定数はあまりcompanion objectに定義されていないようでした。せいぜいKotlinVersionVersion.ktに定義されていたMAX_COMPONENT_VALUE
くらいでしょうか。これはその直後の変数CURRENT
のために必要だったようですが。
むしろ、const val
でgrepしてみるとtop-levelに多くの定数が定義されています。あるいは、Typographyのように名前のあるobject内に定数を定義してグルーピングしているものはありました。
まとめ
以上の調査結果をもとにpublic/internalなクラスメソッドや定数をどう定義するのがよいか考えてみましょう。
まず、バイトコード的には大差ありませんでした。しかし、使う側からすると、companion object版の方が名前の衝突に気を遣わなくてよい分、有利そうです。
一方、Kotlin in Actionや巷のブログ記事を読むと、ファクトリーメソッドはcompanion object、それ以外はtop-levelというのが主流のようです。定数はtop-levelがやや有利、グルーピングしたいならobjectに定義、という感じでしょうか。
最後に標準ライブラリの実装を調べてみたところ、そもそもcompanion objectを使っているファイルが少なかったものの、companion objectに定義されたpublic/internalなメソッドと定数は、top-levelのそれに比較してあまり多くない印象でした。これは標準ライブラリという性格も影響しているかもしれませんが、興味深い結果です。
結論としては、メソッドはクラス内部にアクセスするファクトリーメソッドはcompanion object、それ以外はtop-level、定数はグルーピングが必要ならobject、そうでないならtop-level、というあたりが落としどころのように思います。
なにかこれ以外の観点や見落としがあったら、ぜひご指摘ください。
最後になりましたが、companion objectの定数についてヒントを下さり、kotlinalang-jp Slackで議論の相手をして下さった @shaunkawanoさんに感謝いたします。
*1:なお、この記事ではpublicもしくはinternalなメソッドや定数について考えます。privateなものについてはどちらも大差ないという考えからです。
*2:http://shaunkawano.hatenablog.com/entry/2017/11/07/101611にもあるように、通常の自動変換ではconstが付かないのですが、ここでは付けています。これによって無駄なメソッド呼び出しが減ります
*3:https://github.com/JetBrains/kotlin/commit/a39f2f82718dd278eba9a82df4a5632abb1f4044