はじめに
この記事はKotlinアドベントカレンダーの参加記事です。
皆さん、Kotlin使っていますか?
コアの設計はJavaに大きな影響を受けたKotlinですが、その言語仕様はJavaと大きく異なる部分が少なくありません。
今回はそんなKotlinの特徴の一つ、エントリポイントについてお話しようと思います。
TL;DR
- Kotlinの
mainメソッドはコンパイル時特殊クラスに突っ込まれる -
mainメソッドをそのままクラスの静的メソッドとして書くことはできない -
companion objectはそれ自体が独立クラスとしてコンパイルされる -
@JvmStaticでクラスの静的メソッドを登録する
前提知識
- KotlinはJava同様にJVM用Javaバイトコード(.classファイル)にコンパイルされる。
- JavaもKotlinも
main()というメソッドをエントリポイントとして起動する。 - Javaは全てのメソッドや変数が必ず何かしらのクラスに属している必要がある。無論
mainメソッドも同様 - Kotlinのメソッド・変数はクラスに属している必要がない。特に**
mainメソッドはトップレベルに記述されるのが慣例である**。
ちなみに
もう一つ慣例として、エントリポイントを含むファイルの名称をKotlinは`main`、Javaは`メインクラス名`とするものがあります。今回あまり重要ではありません。fun main(args:Array<String>){
println("Hello Kotlin")
}
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello Java");
}
}
この3と4、互換性のあるバイトコードにコンパイルされたとき矛盾が生じますね。Kotlinでトップレベルに書かれるmainメソッドはそのままではJavaに受け入れられません。
これはどういうことでしょう。
Kotlinのmainメソッドもコンパイル時にクラスに収められる
実はKotlinのmainメソッドもコンパイル時にクラスの中に放り込まれるんです。
Kotlinソースコードをコンパイルすると、[mainメソッドがあるファイル名の先頭を大文字にした文字列]Kt という特殊なクラスが作られ、mainメソッドはその静的メソッドとして登録されます。
慣例的にKotlinのmainメソッドを含むファイルはmain.ktとされるため、出来るクラス名は**MainKt**となります。
Kotlinのmainメソッドをクラスメソッドとして書いてみる
さて、Kotlinをコンパイルすると特殊クラスが作られてJavaバイトコードにしたとき矛盾が発生しない仕組みになっていることはわかりました。
これで枕を高くして寝られる方は帰って大丈夫です。
ここからは、KotlinのmainをJavaのそれのようにクラス内に記述できないかを試みていきます。
環境
- Kotlin JVM 1.5.21
- gradle 7.3
- shadowJar 7.1.0
- IntelliJ IDEA 2021.1.1(Community)
書いたコードをshadowJarにて単一jarファイル化したものをコンソールから実行し、jarのエントリポイントmain()が実行されることをゴールとします。
まずは素直にJavaライクに書く
Javaではmainメソッドはクラスの静的メソッドです。Kotlinにはstatic宣言がないので、代わりにcompanion objectとしてクラス内にシングルトンを作り、その中に記述したメソッド・プロパティを静的メソッド・プロパティとして使うことになります。
class HelloWorld{
companion object{
fun main(args:Array<String>){
println("Hello Kotlin")
}
}
}
このときmainはHelloWorldクラスの静的メソッドになる予定なので、ビルドスクリプトも修正しておきましょう。
//前略
application {
mainClass.set("HelloWorld") //mainメソッドを含むクラスの設定を変更しておく
}
エラー: メイン・メソッドがクラスHelloWorldで見つかりません。次のようにメイン・メソッドを定義してください。
public static void main(String[] args)
HelloWorldクラスに定義したはずのmainメソッドが読み込まれません。これはなぜでしょう。
companion objectはコンパイル時別クラス扱いされる
staticの代用として使われるcompanion objectの実態はシングルトンクラス。この中に書かれたメソッドはそれを持つクラスのメソッドではなく、独立した別クラスのメソッド扱いなのです。
もう少し詳しく言うと、Kotlinコンパイル時ソースにcompanion objectを含むクラスが見つかると、MainKtと同じように、[クラス名]$Companionというクラスが作られ、クラス内のcompanion object内のメソッドはこちらに収められるのです。今回であればmainメソッドはHelloWorld$Companionクラスのメソッドとして登録されたのです。
JvmStatic
これを解決するために、@JvmStaticというアノテーションをメソッドに付与します。
こうすることで付与されたメソッドを[クラス名]$Companionのとは別に、companion objectを抱えるクラスのメソッドとして登録してくれるのです。
class HelloWorld{
companion object{
@JvmStatic
fun main(args:Array<String>){
println("Hello Kotlin")
}
}
}
Hello Kotlin
できました。
感想
KotlinはJavaに比べ構文も設計も洗練されていて大好きなのですが、staticの書き方に限ってはJavaのほうが完結で好きです。
物は試しでmain in classやってみましたが、冗長になる上メリットも(私の調査の範囲では)無いので、やる必要はおそらく皆無でしょう。