覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - シングルトン

Java でプログラムを書いている場合に、対象クラスのインスタンスがただ1つだけ存在する状態にしたい場合があります。

そういった場合には、一般的に Singleton パターン(シングルトン・パターン) というデザインパターンを用いてクラスを宣言する事になります。

Java でシングルトンを実現する方法はいくつかありますが、例えば以下のような記述をすることでシングルトンのクラスとなります。
(以下コードでは import文等を一部省略しています)

public final class ProductList {

    public static final ProductList INSTANCE = new ProductList();

    public static ProductList getInstance() {
        return INSTANCE;
    }

    private final List<String> products =
            Arrays.asList("Product1", "Product1.2", "ProductB", "ProductC");

    public List<String> allProducts() {
        return products;
    }

    private ProductList() {
    }
}

クラスがシングルトンとなっているのは上記宣言の以下のようなコードの記述の組み合わせによります。

  • コンストラクタが private (外部からの通常のコンストラクタでのインスタンス化ができないようにする)
  • static final で自分自身のインスタンスを保持する。(これが対象クラスの唯一のインスタンス)
  • 保持しているインスタンスを外部から取得するためのメソッドを定義(ここでは getInstance)


Javaでは上記のようなコードの組み合わせによってシングルトンを実現するわけですが、
Kotlin では、シングルトンの定義をするためのサポートを第一級言語機能として提供しています。
それが オブジェクト宣言(object declaration)です。

オブジェクト宣言は、クラス宣言(class declaration)とそのクラスのシングルインスタンス(single instance)の宣言が組み合わさっています。

先ほどの Java で定義したシングルトンのクラスを Kotlin で記述すると以下のようになります。(Listのクラスが若干違いますが、そこは無視で。)

object ProductList {

    private val products = arrayListOf("Product1", "Product1.2", "ProductB", "ProductC")

    fun allProducts() : List<String> {
        return products
    }
}

オブジェクト宣言は、 object キーワードで始まります。
クラスと同様に、オブジェクト宣言は、プロパティ、メソッド、初期化ブロックなどの宣言を含めることも可能です。
コンストラクタ(プライマリコンストラクタ、セカンダリコンストラクタ)は含めることができません。
通常のクラスのインスタンスと異なり、オブジェクト宣言はコード中の他の場所からのコンストラクタ呼び出しではなく、定義の時点で即座にインスタンスが生成されます。

オブジェクト宣言した内容の呼び出しは、<オブジェクト名>.<メソッド名> という形式になります。
上記でオブジェクト宣言した内容以下のようになります

ProductList.allProducts()


オブジェクト宣言は、クラスやインターフェースを継承することも可能です。

java.util.Comparator インターフェースを実装するクラスは、compare メソッドで2つのオブジェクトを引数に取り、
いずれのオブジェクトが大きいかを示す整数値を返却します。 このクラスは、基本的にcompare メソッドの引数にのみ依存して動きが制御され、基本的にクラス自体が状態を持つことはほぼありません。
そのため、インスタンスは1つだけ存在すればよいことになります。

こういった場面で オブジェクト宣言 が利用できます。

import java.io.File

object FilePathCmparator : Comparator<File> {

    override fun compare(f1: File, f2: File): Int {
        return f1.path.compareTo(f2.path, ignoreCase = true)
    }
}

このオブジェクトを利用してソートを実行したコードは以下の通りです

val pathList = listOf(
    File("/tmp"), File("/storage1/dir001/f001"), File("/storage1/dir002"),
    File("/storage1/dir001"), File("/storage1"), File("/root")
)

val sortedList = pathList.sortedWith(FilePathCmparator)

println("$pathList\n->\n$sortedList")

実行結果以下の通りです

[/tmp, /storage1/dir001/f001, /storage1/dir002, /storage1/dir001, /storage1, /root]
->
[/root, /storage1, /storage1/dir001, /storage1/dir001/f001, /storage1/dir002, /tmp]


Java からKotlinのオブジェクトを呼び出す

Kotlin のオブジェクト宣言は、1インスタンスを静的フィールドに保持するようなクラスにコンパイルされます。
その静的フィールドの名前は INSTANCE となっています。
Javaでシングルトンのインスタンスを宣言したのとほぼ同じ状態と言えます。

そのためKotlinのオブジェクトをJavaのコードから使うためには、以下のようにINSTANCEフィールドにアクセスする方法となります。
(Kotlinでオブジェクト宣言した ProductList のメソッドを呼び出している例です)

public class Main {
    public static void main(String[] args) {
        System.out.println(ProductList.INSTANCE.allProducts());
    }
}


シングルトンの補足

デザインパターンの中でも シングルトンパターン は特殊な部類に入り、シングルトンパターンのクラスは必ず実装クラスとなります。
そのため、そのクラスに依存するクラスのユニットテスト時に他の実装に差し替えること等ができません。
シングルトンパターンを利用するクラスとは密結合することになります、また、使い方を誤るとグルーバル変数のような物体と化します。
そのため、シングルトンパターンで作成したオブジェクトを多用しすぎないように注意が必要です。


まとめ

Kotlin では シングルトンのクラスを宣言するためのサポートが 言語機能として提供されているため、簡単にシングルトンのオブジェクトの作成ができることが分かりました。