Kotlinのcompanion objectとは

6 min read読了の目安(約5800字

本記事はQiitaからの転載となります。基本的な内容はこちらの記事と同様です。

companion objectとは

companion objectクラス内に作成されるSingletonのことです。

companion objectの宣言方法

Kotlinでは、classの代わりに objectキーワードを使用するだけでSingletonが作成できますobjectキーワードはパッケージレベルから使用可能です。
詳しくは公式ドキュメントをご参照ください。

https://kotlinlang.org/docs/reference/object-declarations.html#object-declarations

クラス内では、objectキーワードの前にcompanion修飾子を付与することでクラスに属するSingletonが作成できます。これをcompanion objectと呼びます。
companion objectは1クラス内に1つだけ宣言可能です。また、オブジェクト名は省略可能です。
なお、companion修飾子を付与しないobjectは1クラス内に複数宣言可能です。

Hoge.kt
class Hoge {

  object A {
    val fizz = "fizz"
    fun foo() { ... }
  }

  companion object {
    val buzz = "buzz"
    fun bar() { ... }
  }

}

クラス内で作成されるcompanion objectobjectに機能的な違いはないと思っています(間違っていたらご指摘ください)。

companion objectの使用方法

Kotlinから使用するとき

companion objectはクラスに属することが明白なので、Kotlin側から使用するときは以下のような簡潔な記述になります。

Main.kt
fun main() {
  // objectの場合。
  val fizz = Hoge.A.fizz
  Hoge.A.foo()

  // companion objectの場合。
  val buzz = Hoge.buzz
  Hoge.bar()
}

Javaから使用するとき

companion object内に宣言したプロパティ・メソッドは、Java側からは「Companion」というSingletonに属するフィールドのgetter/setter・メソッドとして見えます
また、object内に宣言したプロパティ・メソッドは、Java側からは「INSTANCE」というSingletonに属するフィールドのgetter/setter・メソッドとして見えます。

したがって、Java側から使用するときは以下のような記述になります。

Main.java
public class Main {
  public static void main(String[] args) {
    // objectの場合。
    String fizz = Hoge.A.INSTANCE.getFizz();
    Hoge.A.INSTANCE.foo();

    // companion objectの場合。
    String buzz = Hoge.Companion.getBuzz();
    Hoge.Companion.bar();
  }
}

Android Studioでは、Kotlinプラグインをインストールした状態でTools > Kotlin > Show Kotlin Bytecodeを選択して表示されるペインでDecompileボタンを押すことによって、デコンパイルされたコードを確認することができます。

Hoge.decompiled.java
// @Metadataなどは省略しています。 
public final class Hoge {
  @NotNull
  // privateなので、Companionインスタンスのgetterを介してアクセス可能。
  private static final String buzz = "buzz";
  public static final Hoge.Companion Companion = new Hoge.Companion((DefaultConstructorMarker)null);

  public static final class A {
    @NotNull
    // privateなので、INSTANCEインスタンスのgetterを介してアクセス可能。
    private static final String fizz = "fizz";
    public static final Hoge.A INSTANCE;

    // Aクラス内に宣言されるので、INSTANCEインスタンスを介してアクセス可能。
    @NotNull
    public final String getFizz() {
      return fizz;
    }

    public final void foo() {
      ...
    }

    private A() {
    }

    static {
      Hoge.A var0 = new Hoge.A();
      INSTANCE = var0;
      fizz = "fizz";
    }
  }

  public static final class Companion {
    // Hogeクラス内に宣言されるので、Companionインスタンスを介してアクセス可能。
    @NotNull
    public final String getBuzz() {
      return Hoge.buzz;
    }

    public final void bar() {
      ...
    }

    private Companion() {
    }

    // $FF: synthetic method
    public Companion(DefaultConstructorMarker $constructor_marker) {
      this();
    }
  }
}

Kotlinにstatic修飾子がない理由

Kotlinにはstatic修飾子がないので、companion objectはstaticなフィールドやメソッドが必要なときの代替手段として利用されることがほとんどだと思います。

2013年のものですが、「“Static constants” in Kotlin」という記事でAndrey BreslavさんがKotlinにおけるstaticに対する考えについて言及していました。

Kotlin is designed so that there’s no such thing as a “static member” in a class.
If you have a function in a class, it can only be called on instances of this class.
If you need something that is not attached to an instance of any class, you define it in a package, outside any class (we call it package-level functions):

But sometimes you need static constants in your class: for example, to comply with requirements of some framework or to use serialization.
How do you do this in Kotlin? There are two things in Kotlin that resemble Java’s statics: aforementioned package-level functions and class objects.

内容としては、

  • Javaにおけるstaticな定数(final修飾子の付与されたフィールド)は、Kotlinではパッケージレベルに宣言するかclass objectを利用すればよい
  • Javaにおけるstaticなメソッドは、Kotlinではパッケージレベルに宣言すればよい

このため、Kotlinにおいてstaticは不要であるといった意味だと理解しています。

上記で登場するclass objectcompanion objectのことを意味しています。
私はこの当時まだKotlinを使用したことがなかったので知りませんでしたが、M11で名称やコンセプトに関して変更があったことが公式ブログに記載されていました。

companion objectのプロパティ・メソッドにJava側から直接アクセスする方法

@JvmField/@JvmStaticアノテーション

companion objectがJava側からどのように見えるかについて以下のように書きました。

companion object内に宣言したプロパティ・メソッドは、Java側からは「Companion」というSingletonに属するフィールドのgetter/setter・メソッドとして見えます。

これらをJava側からstaticなフィールド・メソッドとして扱えるようにする方法が提供されています。それが@JvmField/@JvmStaticアノテーションです。
詳しくは公式ドキュメントをご参照ください。

https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html

使い方は以下のようにcompanion object内のプロパティとメソッドに各アノテーションを付与するだけです。

Hoge.kt
class Hoge {

  companion object {
    @JvmField
    val buzz = "buzz"
    @JvmStatic
    fun bar() { ... }
  }

}

その結果、以下のような見え方になります。

Hoge.decompiled.java
// @Metadataなどは省略しています。 
public final class Hoge {
  @JvmField
  @NotNull
  // Companionクラス外にpublicかつstaticで宣言されるので、直接アクセス可能。
  public static final String buzz = "buzz";
  public static final Hoge.Companion Companion = new Hoge.Companion((DefaultConstructorMarker)null);

  @JvmStatic
  // Companionクラス外にpublicかつstaticで宣言されるので、直接アクセス可能。
  public static final void bar() {
    Companion.bar();
  }

  public static final class Companion {
    @JvmStatic
    public final void bar() {
      ...
    }

    private Companion() {
    }

    // $FF: synthetic method
    public Companion(DefaultConstructorMarker $constructor_marker) {
      this();
    }
  }
}

したがって、Java側から使用するときもKotlin側から使用するときと同じように記述可能となります。

Main.java
public class Main {
  public static void main(String[] args) {
    String buzz = Hoge.buzz;
    Hoge.bar();
  }
}