LoginSignup
25

More than 5 years have passed since last update.

kotlinのcompanionとcompanion objectをちゃんとまとめる。

Posted at

javaでよくあるユーティリティクラスをkotlinに移植しようと思ったときに気づきがあったのでまとめておく。

javaユーティリティクラスのkotlin convert

事の発端はjavaで記述された以下のユーティリティクラス(staticなメンバーのみをもつクラス)をkotlinにconvertしたとき、object SampleUtil で定義されていたこと。kotlinでobjectを定義するとsingletonになるはずで、本来インスタンスを生成しないはずのものがsingletonに変換されていることを不思議に思った。

例えばaws-android-sdkにあったこのSampleUtil、
これをkotlinにconvertすると、

public class SampleUtil {
    private static final String PropertyFile = "aws-iot-sdk-samples.properties";

    public static class KeyStorePasswordPair {
        public KeyStore keyStore;
        public String keyPassword;

        public KeyStorePasswordPair(KeyStore keyStore, String keyPassword) {
            this.keyStore = keyStore;
            this.keyPassword = keyPassword;
        }
    }
...
}

SampleUtilはobjectで定義されていて、staticだったものがsingletonに変換される。

object SampleUtil {
    private val PropertyFile = "aws-iot-sdk-samples.properties"

    class KeyStorePasswordPair(var keyStore: KeyStore, var keyPassword: String)

    fun getConfig(name: String): String? {
        val prop = Properties()
        val resource = SampleUtil::class.java.getResource(PropertyFile) ?: return null
        try {
            resource.openStream().use { stream -> prop.load(stream) }
        } catch (e: IOException) {
            return null
        }

        val value = prop.getProperty(name)
        return if (value == null || value.trim { it <= ' ' }.length == 0) {
            null
        } else {
            value
        }
    }
...
}

kotlinでobjectで定義するsingletonになることは知識としては前から知っていたので、javaにおけるユーティリティクラス(staticメンバのみを持つクラス)をkotlinにconvertすると class SampleUtil内のcompanion object にメンバーを定義する形になると想像していたが結果は違った。また多くのピュアkotlinで書かれたプロジェクトのユーティリティクラスも同じようにobjectでシングルトンを生成していたことも混乱する原因となった。

objectとcompanion objectのおさらい

詳細は以下にまとまっているが、改めてobjectcompanion objectのおさらいをする。
[https://kotlinlang.org/docs/reference/object-declarations.html:title]

objectで定義したDataProviderManagerはシングルトンで生成され、

object DataProviderManager {
  fun registerDataProvider(provider: DataProvider) {
    // ...
  }

  val allDataProviders: Collection<DataProvider>
    get() = // ...
}

DataProviderManager.registerDataProvider(...) のように使える。

companion object についても同じように、

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create() のように使える。

objectがsingletonということはcompanion objectはstaticだろうと思っていたのが勘違いの始まりでちゃんとドキュメントを読むと以下のようにstaticに見えるメンバーもオブジェクトインスタンスのメンバーであるという記述がある。

Note that, even though the members of companion objects look like static members in other languages, at runtime those are still instance members of real objects, and can, for example, implement interfaces:

objectとcompanion objectはjavaでどう解釈されるか

公式ドキュメントによればobjectcompanion objectも非staticでありインスタンスは生成されているということだが、実際にjavaではどのように解釈されるのか、
理解を深めるために実際に以下のktクラスをdecompileした。

object
object SampleUtil1 {
    fun test() {
        Log.e("SampleUtil1", "test")
    }
}
@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
   d2 = {"Lcom/hoge/util/asset/SampleUtil1;", "", "()V", "test", "", "production sources for module app"}
)
public final class SampleUtil1 {
   public static final SampleUtil1 INSTANCE;

   public final void test() {
      Log.e("SampleUtil2", "test");
   }

   static {
      SampleUtil1 var0 = new SampleUtil1();
      INSTANCE = var0;
   }
}
companion object
class SampleUtil2 {
    companion object {
        fun test() {
            Log.e("SampleUtil2", "test")
        }
    }
}
@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u0018\u0000 \u00032\u00020\u0001:\u0001\u0003B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0004"},
   d2 = {"Lcom/hoge/util/asset/SampleUtil2;", "", "()V", "Companion", "production sources for module app"}
)
public final class SampleUtil2 {
   public static final SampleUtil2.Companion Companion = new SampleUtil2.Companion((DefaultConstructorMarker)null);

   @Metadata(
      mv = {1, 1, 11},
      bv = {1, 0, 2},
      k = 1,
      d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
      d2 = {"Lcom/hoge/util/asset/SampleUtil2$Companion;", "", "()V", "test", "", "production sources for module app"}
   )
   public static final class Companion {
      public final void test() {
         Log.e("SampleUtil2", "test");
      }

      private Companion() {
      }

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

metadataとかがあって弱化読みにくいが、SampleUtil1については想定どおりでstaticイニシャライザでSampleUtil1をシングルトンで生成している。
一方companion object のほうはというと、companion object 内で定義したメンバーはsitaticインナークラスで定義されたCompanionクラス内に定義されている。
SampleUtil2自体はインスタンス化されていないが内部のCompanionクラスがシングルトンで生成されるのでやっていることとしてはシングルトンインスタンスを作るということでobject定義のほうと大きな差はないように思う。

結論

  • kotlinではstaticを許容しない
  • companion objectobject もシングルトンインスタンスを生成する

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25