普段から当たり前のように使ってはいたものの、あまりきちんと理解していなかったcompanion objectについてざっと調べた内容をまとめました。
companion objectとは
companion objectはクラス内に作成されるSingletonのことです。
companion objectの宣言方法
Kotlinでは、classの代わりに objectキーワードを使用するだけでSingletonが作成できます。objectキーワードはパッケージレベルから使用可能です。
詳しくは公式ドキュメントをご参照ください。
クラス内では、objectキーワードの前にcompanion修飾子を付与することでクラスに属するSingletonが作成できます。これをcompanion objectと呼びます。
companion objectは1クラス内に1つだけ宣言可能です。また、オブジェクト名は省略可能です。
なお、companion修飾子を付与しないobjectは1クラス内に複数宣言可能です。
class Hoge {
  object A {
    val fizz = "fizz"
    fun foo() { ... }
  }
  companion object {
    val buzz = "buzz"
    fun bar() { ... }
  }
}
クラス内で作成されるcompanion objectとobjectに機能的な違いはないと思っています(間違っていたらご指摘ください)。
companion objectの使用方法
Kotlinから使用するとき
companion objectはクラスに属することが明白なので、Kotlin側から使用するときは以下のような簡潔な記述になります。
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側から使用するときは以下のような記述になります。
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ボタンを押すことによって、デコンパイルされたコードを確認することができます。
// @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 objectはcompanion objectのことを意味しています。
私はこの当時まだKotlinを使用したことがなかったので知りませんでしたが、M11で名称やコンセプトに関して変更があったことが公式ブログに記載されていました。
companion objectのプロパティ・メソッドにJava側から直接アクセスする方法
@JvmField/@JvmStaticアノテーション
companion objectがJava側からどのように見えるかについて以下のように書きました。
companion object内に宣言したプロパティ・メソッドは、Java側からは「Companion」というSingletonに属するフィールドのgetter/setter・メソッドとして見えます。
これらをJava側からstaticなフィールド・メソッドとして扱えるようにする方法が提供されています。それが@JvmField/@JvmStaticアノテーションです。
詳しくは公式ドキュメントをご参照ください。
使い方は以下のようにcompanion object内のプロパティとメソッドに各アノテーションを付与するだけです。
class Hoge {
  companion object {
    @JvmField
    val buzz = "buzz"
    @JvmStatic
    fun bar() { ... }
  }
}
その結果、以下のような見え方になります。
// @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側から使用するときと同じように記述可能となります。
public class Main {
  public static void main(String[] args) {
    String buzz = Hoge.buzz;
    Hoge.bar();
  }
}