Kotlinのエントリポイントの話

# はじめに

この記事は[Kotlinアドベントカレンダー](https://qiita.com/advent-calendar/2021/kotlin)の参加記事です。

皆さん、Kotlin使っていますか？

コアの設計はJavaに大きな影響を受けたKotlinですが、その言語仕様はJavaと大きく異なる部分が少なくありません。
今回はそんなKotlinの特徴の一つ、エントリポイントについてお話しようと思います。

## TL;DR
- Kotlinの`main`メソッドはコンパイル時特殊クラスに突っ込まれる
- `main`メソッドをそのままクラスの静的メソッドとして書くことはできない
- `companion object`はそれ自体が独立クラスとしてコンパイルされる
- `@JvmStatic`でクラスの静的メソッドを登録する


## 前提知識

1. KotlinはJava同様にJVM用Javaバイトコード(.classファイル)にコンパイルされる。
- JavaもKotlinも`main()`というメソッドをエントリポイントとして起動する。
- Javaは全てのメソッドや変数が**必ず何かしらのクラスに属している必要がある**。無論`main`メソッドも同様
- Kotlinのメソッド・変数はクラスに属している必要がない。特に**`main`メソッドはトップレベルに記述されるのが慣例である**。


<details><summary>ちなみに</summary>もう一つ慣例として、エントリポイントを含むファイルの名称をKotlinは`main`、Javaは`メインクラス名`とするものがあります。今回あまり重要ではありません。
</details>

```kotlin:main.kt
fun main(args:Array<String>){
    println("Hello Kotlin")
}
```
```java:HelloWorld.java
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`としてクラス内にシングルトンを作り、その中に記述したメソッド・プロパティを静的メソッド・プロパティとして使うことになります。

```kotlin:main.kt
class HelloWorld{
    companion object{
        fun main(args:Array<String>){
            println("Hello Kotlin")
        }
    }
}
```

このとき`main`は`HelloWorld`クラスの静的メソッドになる予定なので、ビルドスクリプトも修正しておきましょう。

```kotlin:build.gradle.kts
//前略
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`を抱えるクラスのメソッドとして登録してくれるのです。

```kotlin:main.kt
class HelloWorld{
    companion object{
        @JvmStatic
        fun main(args:Array<String>){
            println("Hello Kotlin")
        }
    }
}
```

```
Hello Kotlin
```

できました。

## 感想
KotlinはJavaに比べ構文も設計も洗練されていて大好きなのですが、`static`の書き方に限ってはJavaのほうが完結で好きです。

物は試しでmain in classやってみましたが、冗長になる上メリットも(私の調査の範囲では)無いので、やる必要はおそらく皆無でしょう。