Kotlinのオブジェクトキーワード:シングルトン、コンパニオンオブジェクト、オブジェクト式の使い方

Kotlinのobjectキーワードの基本概念

Kotlinのobjectキーワードは、状態を持たないユーティリティクラス、定数コレクション、または依存性注入フレームワーク内のモジュール定義に適しています。

objectキーワードの主な用途

オブジェクト宣言 クラスを定義すると同時にそのインスタンスを作成する、遅延初期化しないシングルトンパターンです。
コンパニオンオブジェクト クラスに付随して存在するオブジェクトで、クラスがロードされる際にインスタンス化され、複数のインスタンス間で共有されます。
オブジェクト式 匿名内部クラスとして機能し、複数のインターフェースを実装でき、外部関数のローカル変数にアクセスする際にfinal修飾子が不要です。

Javaの静的メソッドに類似したKotlinの機能

Kotlinでは静的メソッドの概念が大幅に弱められ、より便利な構文機能が提供されています。これらも【クラス名.関数名】の形式で呼び出せます。

object 「静的メソッドのみを持つユーティリティクラス」を作成するのに適しています。(Kotlin v1.9で追加されたdata objectは、シールドクラス/シールドインターフェースの使用シナリオ向けの最適化で、toString()がハッシュコードなどの不要な情報を出力せず、より意味のある出力を提供します)
companion object クラスに「専用の静的メソッド」を定義するのに適しています。
トップレベル関数 関数method()をHelper.ktファイルに定義すると、コンパイラはHelperKt.javaを作成し、JavaからはHelperKt.method()として呼び出せます。
@JvmStaticアノテーション 上記のシングルトンクラスやコンパニオンオブジェクトにこのアノテーションを追加すると、Javaから静的メソッドとして呼び出せます。

乱用によるリスク

シングルトンはグローバルアクセスポイントを提供し、どこからでも呼び出せるため非常に便利ですが、いくつかのリスクも伴います。状態を持たないユーティリティクラス、定数コレクション、または依存性注入フレームワーク内のモジュール定義として使用するのが最適です。代替案として、インターフェースと依存性注入の使用が推奨されます。

  • 隠れた依存関係:どのクラスも内部で静かにXXXManagerに依存している可能性があり、そのコンストラクタや公開APIからはこの点が分かりません。
  • グローバル状態:グローバルな可変状態は、どこからでも変更可能で、特にマルチスレッド環境では予測不可能な動作を引き起こします。
  • テストの困難さ:ユニットテストでUserManagerの動作を簡単に置換したりモックしたりできません。例えば、「トークンの有効期限切れ」のシナリオをどのようにテストしますか?
// インターフェースと依存性注入を使用した例
interface SessionManager {
    fun getToken(): String?
    fun saveToken(token: String)
}

class MemorySessionManager : SessionManager {
    private var userToken: String? = null
    // ... メソッドの実装
}

@Singleton // 依存性注入フレームワークでライフサイクルを宣言
class MemorySessionManager @Inject constructor() : SessionManager { ... }

// ViewModelや他のクラスに注入
class UserProfileViewModel @Inject constructor(
    private val sessionManager: SessionManager
) : ViewModel() {
    fun performAction() {
        val token = sessionManager.getToken()
        // ...
    }
}

シングルトン(オブジェクト宣言)

クラスを定義すると同時にそのインスタンスを作成します。クラスを継承したりインターフェースを実装したり、プロパティやメソッドを含めたりできますが、コンストラクタを手動で宣言することはできません。コンストラクタが使用できないため、init()ブロックを使用できます。コンストラクタ経由でのパラメータ渡しについては、後述のコンパニオンオブジェクトを参照してください。

トップレベルでもクラス内でも宣言できます(後述のコンパニオンオブジェクトとの比較を参照)。

object UtilitySingleton {
    val identifier = "シングルトン"
    fun display() = println(identifier)
}

UtilitySingleton.display()
println(UtilitySingleton.identifier)

Javaに逆コンパイルしたコード:(遅延初期化しないシングルトン)

public final class UtilitySingleton {
    // メンバー変数
    private static String identifier;
    // 静的フィールドを通じてインスタンスを提供
    public static final UtilitySingleton INSTANCE;
    // プライベートコンストラクタ
    private UtilitySingleton() { }
    // 静的コードブロックで初期化
    static {
        UtilitySingleton var0 = new UtilitySingleton();
        INSTANCE = var0;
        identifier = "シングルトン";
    }
    // メンバーメソッド
    public final void display() {
        String var1 = identifier;
        System.out.println(var1);
    }
    // 生成されたgetter、setter
    public final String getIdentifier() { return identifier; }
    public final void setIdentifier(@NotNull String var1) {
        Intrinsics.checkNotNullParameter(var1, "<set-?>");
        identifier = var1;
    }
}

Javaスタイルでパラメータ付きのKotlinシングルトンを作成:

class DataRepository private constructor(
    private var identifier: Long,
    private var title: String
) {
    companion object {
        private var instance: DataRepository? = null
        fun createInstance(id: Long, name: String): DataRepository {
            return instance ?: synchronized(this) {
                instance ?: DataRepository(id, name).also { instance = it }
            }
        }
    }
}

コンパニオンオブジェクト(静的呼び出し)

コンパニオンオブジェクトはクラスに付随して存在するオブジェクトで、クラスがロードされる際にインスタンス化され、複数のインスタンス間で共有されます。Kotlinにはstaticという静的概念がないため、トップレベル関数や定数を使用できます。トップレベル関数はクラスのプライベートメンバーにアクセスできず、コンパニオンオブジェクト内では外部インスタンスを使用してアクセスする必要があります(静的内部クラスは外部クラスの非静的メンバーに直接アクセスできません)、例えばOuter().valueのようにします。

コンパニオンオブジェクトの名前は省略可能です(デフォルトはCompanion)。コンパニオンオブジェクトの名前を手動で宣言するかどうかに関係なく、外部クラス名を使用してコンパニオンオブジェクト内のプロパティやメソッドを呼び出せます。

クラスのメンバーとコンパニオンオブジェクトのメンバーが同名の場合、クラス名で呼び出すとコンパニオンオブジェクト内のものが、インスタンスで呼び出すとクラス内のものが使用されます。

class Container {
    companion object Factory {    // Factoryという名前は省略可能
        var data: String = ""
        fun process(){}
    }
}

fun main() {
    Container.data = ""
    Container.process()
    Container.Factory.data = "" // コンパイラはFactoryが冗長であると警告
    Container.Factory.process() // コンパイラはFactoryが冗長であると警告
    val obj: Container.Factory = Container.Factory
    obj.data = ""
    obj.process()
}

コンパニオンオブジェクトとクラス内シングルトンの比較

コンパニオンオブジェクト クラス内シングルトンオブジェクト
共通点 どちらも静的内部クラスであり、(外部クラス名.内部クラス名.メンバー名)の形式で呼び出します。
コンストラクタはどちらもprivateです。
クラスを継承したりインターフェースを実装したり、プロパティや関数を持つことができます。
相違点 ①外部クラスがコンパニオンオブジェクトのインスタンスを作成し、保持します。
②定義されたプロパティは外部クラスのプライベート静的フィールドになります(privateとして宣言しない場合)。関数は内部クラスに残ります。
③1つのクラスに持てるコンパニオンオブジェクトは1つだけです。
①それ自体がインスタンスです。
②独自のプロパティと関数を保持します。
③1つのクラス/インターフェースに複数のシングルトンオブジェクトを持つことができます(定数のグループ化として使用可能)。
④ネストされたクラス(ネストされたオブジェクトと呼ばれる)であり、静的内部クラスです。

Kotlinコード:

class Example {
    // コンパニオンオブジェクト
    companion object First {    // 名前を付けない場合、デフォルト名はCompanion
        var value1 = 1    // カスタムgetter、setterを定義可能
        val value2 = 2
        fun firstMethod() = println("コンパニオンオブジェクト")
    }
    // シングルトン
    object Second {
        var value3 = 3    // カスタムgetter、setterを定義可能
        val value4 = 4
        fun secondMethod() = println("シングルトン")
    }
}

Javaに逆コンパイルしたコード:

public final class Example {
    // コンパニオンオブジェクトのメンバー変数(外部クラスが保持)
    private static int value1 = 1;
    private static final int value2 = 2;
    // コンパニオンオブジェクトインスタンスを保持(外部クラスが保持)
    public static final Example.First First = new Example.First((DefaultConstructorMarker)null);
    
    // コンパニオンオブジェクト
    public static final class First {
        // プライベートコンストラクタ
        private First() { }
        // $FF: synthetic method    合成メソッド
        public First(DefaultConstructorMarker $constructor_marker) {
            this();
        }
        // メンバーメソッド
        public final void firstMethod() {
            String var1 = "コンパニオンオブジェクト";
            System.out.println(var1);
        }
        // 生成されたgetter、setter
        public final int getValue1() { return Example.value1; }
        public final void setValue1(int var1) { Example.value1 = var1; }
        public final int getValue2() { return Example.value2; }
    }
    
    // シングルトン
    public static final class Second {
        // 静的フィールドを通じてインスタンスを提供
        public static final Example.Second INSTANCE;
        // プライベートコンストラクタ
        private Second() { }
        // 静的コードブロックで初期化
        static {
            Example.Second var0 = new Example.Second();
            INSTANCE = var0;
            value3 = 3;
            value4 = 4;
        }
        // メンバー変数
        private static int value3;
        private static final int value4;
        // メンバーメソッド
        public final void secondMethod() {
            String var1 = "シングルトン";
            System.out.println(var1);
        }
        // 生成されたgetter、setter
        public final int getValue3() { return value3; }
        public final void setValue3(int var1) { value3 = var1; }
        public final int getValue4() { return value4; }
    }
}

コンパニオンオブジェクト内で定義されたval/varは逆コンパイル後にprivateアクセス修飾子になるため、静的オブジェクトとgetter/setterが生成され、そのコストは単純な静的パラメータの価値をはるかに超えます。以下のように最適化できます:

  • 静的定数:valは逆コンパイルするとprivate static finalになるため、getterが生成されます。const valを使用すると逆コンパイルはpublic static finalになり、getterが生成されません。
  • 静的変数:varは逆コンパイルするとprivate staticになるため、getter&setterが生成されます。@JvmFieldを使用すると逆コンパイルはpublic staticになり、getter&setterが生成されません。

静的ファクトリメソッド

interface Vehicle {
    val manufacturer: String
    companion object {
        operator fun invoke(type: VehicleType): Vehicle {
            return when (type) {
                VehicleType.TESLA -> TeslaModel()    
                VehicleType.TOYOTA -> ToyotaModel()
            }
        }
    }
}

Vehicle(VehicleType.TOYOTA)

拡張メソッド

コンパニオンオブジェクトに拡張メソッドを追加すると、実際には外部クラスに静的メソッドが追加されたのと同じ効果があります。

fun Container.Companion.processData(){}
Container.processData()

Javaとの相互運用性

Javaコードに逆コンパイルすると、コンパニオンオブジェクトは外部クラスが保持する静的インスタンスであり、プロパティやメソッドはすべてOutter().Inner.XXXを通じて呼び出す必要があることがわかります。クラス名.XXXで呼び出したい場合は、@JvmStaticアノテーションを関数に、@JvmFieldアノテーションをプロパティに使用すると、外部クラスに対応するpublic staticフィールドとメソッドが追加されます。

// Kotlin
class UtilityClass {
    companion object Factory {
        @JvmField
        var counter = 1
        @JvmField
        val maxCount = 100
        @JvmStatic
        fun initialize() = println("初期化")
    }
}
// Java
class UtilityClass {
    public static int counter = 1;
    public static final int maxCount = 100;
    public static final void initialize() { Factory.initialize(); }
}

パラメータが必要なシングルトン

objectで修飾されたクラスはコンストラクタを持てないため、通常のクラスのコンストラクタをprivateで修飾し、コンパニオンオブジェクト内でインスタンスを作成して取得メソッドを提供します。以下はダブルチェックロッキングによる実装で、遅延初期化シングルトンより効率的です。

class ServiceManager private constructor(
    val serviceName: String
) {
    companion object {
        // volatileはマルチスレッド環境での可視性を確保
        @Volatile
        private var instance: ServiceManager? = null
        fun createInstance(name: String): ServiceManager {
            // 最初のチェック、不要な同期を避ける
            if (instance == null) {
                synchronized(this) {
                    // 2回目のチェック、インスタンスが1つだけ作成されるようにする
                    if (instance == null) instance = ServiceManager(name)
                }
            }
            return instance!!
        }
    }
}
// 上記のcreateInstance()は簡略化可能
fun createInstance(name: String) = instance ?: synchronized(this) {
    instance ?: ServiceManager(name).also { instance = it }
}

オブジェクト式(匿名内部クラス)

解決策1 Javaでは実行時に匿名内部クラスが継承/実装している親クラス/インターフェースとして扱われるため、匿名内部クラスに親クラス/親インターフェース以外の追加メソッドを定義しても正常に使用できません。Kotlinはオブジェクト式でJavaの匿名内部クラスを置き換え、この問題を解決しています。
解決策2 外部関数のローカル変数にアクセスする際にfinal修飾子が不要です(詳細:Lambdaクロージャ)。SAM(単一抽象メソッド)インターフェースの場合、通常はLambdaを使用する方が簡便ですが、複数の抽象メソッドをオーバーライドする必要がある場合は、匿名オブジェクトを使用するしかありません。

変数への代入

継承も実装もない場合
val anonymousObject = object {
    init { println("初期化") }
    val propertyName = ""
    fun execute() { println("実行") }
}
単一の親タイプを持つ場合
abstract class BaseClass{}
interface BaseInterface{}
val obj1: BaseClass = object : BaseClass() {}
val obj2: BaseInterface = object : BaseInterface {}
複数の親タイプを持つ場合
トップレベル変数
メンバー変数
privateとして宣言された変数のみ、その型が正常に認識されます(IDEでは型が<anonymous object : AA, BB>と表示されます)。
publicとして宣言された変数や関数の戻り値として使用される場合、Kotlinは親クラス/親インターフェースの型として認識します。複数の親タイプがある場合は、そのうちの1つとして明示的に宣言する必要があり、宣言がない場合はAny型として認識され、メンバー変数/メソッドを呼び出せません。
ローカル変数 可視性修飾子が存在しないため、正常に認識されます(IDEでは型が<anonymous object : AA, BB>と表示されます)。
abstract class BaseA{ abstract fun methodA() }
interface BaseB{ fun methodB() }
// トップレベル変数として使用
val obj1: BaseA = object: BaseA(), BaseB {
    override fun methodA() {}
    override fun methodB() {}
}
private val obj2 = object: BaseA(), BaseB {
    override fun methodA() {}
    override fun methodB() {}
}
// メンバー変数として使用
class DemoClass {
    val obj3: BaseA = object: BaseA(), BaseB {
        override fun methodA() {}
        override fun methodB() {}
    }
    private val obj4 = object: BaseA(), BaseB {
        override fun methodA() {}
        override fun methodB() {}
    }
}
// ローカル変数として使用
fun execute(){
    val obj = object : BaseA(), BaseB {
        override fun methodA() {}
        override fun methodB() {}
    }
}

匿名オブジェクトのパラメータ渡しとクロージャ

fun outerFunction() {
    var count = 0    // アクセスされるローカル変数はfinalのvalではなくvarとして宣言可能
    val obj = object {
        fun innerFunction() { count += 1 }
    }
}

// Androidでクリックリスナーを設定する際に外部ローカル変数にアクセスする例
fun setupClickHandler() {
    var clickCount = 0
    // オブジェクト式で記述
    button.setOnClickListener(object : View.OnClickListener{
        override fun onClick(v: View){ clickCount += 1 }
    })
    // Lambdaで簡略化(抽象メソッドが1つだけのインターフェースの場合のみ、複数メソッドのオーバーライドにはオブジェクト式のみ使用可能)
    button.setOnClickListener{ view -> clickCount += 1 }
}

タグ: Kotlin オブジェクト宣言 コンパニオンオブジェクト オブジェクト式 シングルトン

7月7日 00:40 投稿