覚えたら書く

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

Kotlin - 移譲

Java(オブジェクト指向言語)を用いて大規模なシステムを設計・開発した場合に、実装の継承 によってシステムが壊れていくことが多々あります。

共通化という名目(DRYの達成)のために、実装クラスを継承してしまっていると、
クラスを拡張し、いくつかのメソッドをオーバーライドするときに、拡張元の基底クラスの実装の詳細に依存するようになり、極端な密結合が発生します。

対象のシステムが発展する中で、基底クラスの実装内容が変更されたり、新しいメソッドが追加されたりすると
継承した側の子クラスのコードが正しく動かなくなる事があります。

実装クラスの継承を行うと、コードを共通化して綺麗に整理できた気分になれるので、陥りがちな状況な気がします・・・。
対象システムの改修作業のために、後から参画したメンバーやコードを保守するメンバーが痛い目に逢います。
後任者からは、「結局、親クラスの親クラスまで見ないとコードの意味わからないじゃん、影響範囲わからないじゃん。」とかいう声が上がる事でしょう。


Kotlin では、上記のような問題への対処のために、デフォルトでクラスは final の状態(継承不可)となっています。
拡張性を考えて設計されたクラスのみを継承可能としており、あえて open という修飾子を指定する必要があります。


クラスを拡張したいケースでは、実装の継承を行う代わりに、
拡張元のクラスと同じインターフェースを実装して、元の実装クラスをフィールドに持つという方法が取られます。
拡張が必要ないメソッドはそのまま元の実装クラスに処理を転送、拡張が必要なメソッドは自前の実装でオーバーライドする
というやり方になります。

ただし、Java でこのアプローチを行なった場合には元の実装クラスへ処理を転送する大量のボイラープレートコードが発生します。

java.util.List を実装したクラスを作成して、フィールドに持ったArrayList のインスタンスに処理を転送しようとすると以下のようなコードになってしまいます。

public class ExArrayList<String> implements List<String> {

    // 処理の転送先となる実装クラス
    private final List<String> internalList = new ArrayList<>();

    // 拡張したメソッド
    @Override
    public boolean add(String string) {
        System.out.println("Try add " + string);
        return internalList.add(string);
    }

    @Override
    public int size() {
        return internalList.size();
    }

    @Override
    public boolean isEmpty() {
        return internalList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return internalList.contains(o);
    }

    @Override
    public Iterator<String> iterator() {
        return internalList.iterator();
    }

    @Override
    public Object[] toArray() {
        return internalList.toArray();
    }

    @Override
    public boolean remove(Object o) {
        return internalList.remove(o);
    }

    @Override
    public void replaceAll(UnaryOperator<String> operator) {
        internalList.replaceAll(operator);
    }

    @Override
    public void sort(Comparator<? super String> c) {
        internalList.sort(c);
    }

    @Override
    public void clear() {
        internalList.clear();
    }

    @Override
    public boolean equals(Object o) {
        return internalList.equals(o);
    }

    @Override
    public int hashCode() {
        return internalList.hashCode();
    }

    @Override
    public String get(int index) {
        return internalList.get(index);
    }

    @Override
    public String set(int index, String element) {
        return internalList.set(index, element);
    }

    @Override
    public void add(int index, String element) {
        internalList.add(index, element);
    }

    @Override
    public String remove(int index) {
        return internalList.remove(index);
    }

   // 以下省略
}


ほとんどの場面で、実装の継承をするよりもより良い方法とはなりますが、移譲(delegation)というものを Java が言語的にサポートしていないために、
ボイラープレートコードが生まれてしまうという状況に陥ってしまいます。


Kotin では、この 移譲 を第一級言語機能としてサポートしています。
インターフェースを実装している場合、 by キーワード を利用する事によってインターフェースの実装を別のオブジェクトに移譲できます。


Kotlin でさきほどの実装と同様のクラスの宣言は以下のように記述できます。
自分で拡張したい add メソッド以外は登場しておらず、ボイラープレートコードが激減しています。

class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

このクラスを実行するサンプルコードで実行してみると

val list = ExArrayList<Any>()
list.add("Kotlin")
list.add("Java")

println("$list : List size = ${list.size}")

実行結果は以下の通りです。addメソッドが予定通り拡張されていることが分かります。

Try add Kotlin
Try add Java
net.yyuki.sandbox.list2.ExArrayList@59a6e353 : List size = 2


by キーワードはあくまで対象インターフェースを転送してくれるだけのようなので、それとは関係ない toString などはオーバーライドされていないようです。

toString までオーバーライドまでしたければ、データクラス として宣言しちゃえばいいわけですが、こんなことしていいのかは良く分かってないです笑

data class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

こうすると toString の結果も意味のある文字列となります

Try add Kotlin
Try add Java
ExArrayList(innerList=[Kotlin, Java]) : List size = 2


まとめ

"継承よりコンポジションを選ぶ" という記述が「Effective Java」に存在していますが、残念ながらJavaでは移譲(delegate)は、第一級言語機能となっていません。
そのためか、Javaで作られたシステムでは、実装の継承 というものが氾濫していることが少なくありません。

Kotlin では by キーワードによって簡単に移譲を実現できます。これによって、クラス間の不要な密結合が生まれにくくなると思われます。



関連エントリ

Kotlin - データクラス

前回エントリで データを保持するクラスの toString, equals, hashCode のオーバーライドについて簡単に書きました。

これらの実装は、Intellij IDEA などのIDEを使う事で簡単に自動生成することが可能です。


Kotlin では、データクラスを利用することで、これらをわざわざコード上に書く必要すらありません。

前回記述した以下クラス Person については、

class Person(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Person

        if (name != other.name) return false
        if (age != other.age) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + age
        return result
    }

    override fun toString(): String {
        return "Person(name='$name', age=$age)"
    }

}

以下のように記述可能です

data class Person(val name: String, val age: Int)

data という修飾子を付けるだけです。非常に簡単!

これによって、以下のようにJavaの標準的なメソッドをオーバライドしたクラスを作成したのと同じ状態になります。

  • インスタンス同士を比較するための equals
  • HashMapなどのハッシュベースのコンテナのキーに利用される hashCode
  • クラスの文字列表現を生成する(フィールドの各内容を出力する)ための toString

equals と hashCode は、プライマリコンストラクタで宣言された全てのプロパティが考慮されます。
equals は全てのプロパティの値が等しい事をチェックします。
hashCode は全てのプロパティのハッシュ値に依存した値を返します。


各メソッドの動きを確認してみました

val person1 = Person("KotlinTaro", 25)
val person2 = Person("KotlinTaro", 25)
val person3 = Person("KotlinTaro", 26)

// 文字列表現の確認
println(person1)

// 等価性の確認
println("person1 == person2 -> ${person1 == person2}")
println("person1 == person3 -> ${person1 == person3}")

// ハッシュコードの確認
println("person1 hashCode -> ${person1.hashCode()}")
println("person2 hashCode -> ${person2.hashCode()}")
println("person3 hashCode -> ${person3.hashCode()}")

結果は以下の通りです

Person(name=KotlinTaro, age=25)
person1 == person2 -> true
person1 == person3 -> false
person1 hashCode -> -3502258
person2 hashCode -> -3502258
person3 hashCode -> -3502257


不変性の推奨

データクラスのプロパティは val で宣言が必須ではなく、var での宣言可能です。
しかし、読み取り専用のプロパティのみを利用して、データクラスのインスタンスがイミュータブル(immutable)となることが強く推奨されているようです。

よく知られた事実ではありますが、イミュータブルなオブジェクトはマルチスレッド環境下での強さがあります。
一度生成されたオブジェクトは、変更される事がないため複数スレッドからは参照する操作しかできず、特定のスレッドが変更をしてしまい 意図しない複雑な問題が発生する心配をする必要がなくなります。


copy メソッド

データクラスには、さらに copy というメソッドが実装されています。
データクラスをイミュータブルとして利用することを容易にしてくれます。

copy メドッドはクラスのプロパティの値を変更しながらインスタンスをコピーしてくれます。
ちなみにプロパティの値を変更なしでのコピーも可能です。(コピーコンストラクタのようなイメージに近いと思います)


サンプルコード と実行結果は以下の通りです

プロパティの変更なしでのコピー

◾️サンプルコード

val person1 = Person("KotlinTaro", 25)

// 値の変更なしでコピー
val person2 = person1.copy()

println("person1 == person2 -> ${person1 == person2}")  // 等価
println("person1 === person2 -> ${person1 === person2}")  // 参照は別物

println(person2)

◾️実行結果

person1 == person2 -> true
person1 === person2 -> false
Person(name=KotlinTaro, age=25)

単純にコピーしたので、等価なインスタンスですが参照は異なっています


プロパティの値を変更しながらのコピー

◾️サンプルコード

val person1 = Person("KotlinTaro", 25)

// 値を変更しながらコピー
val person2 = person1.copy(age = 40)

println("person1 == person2 -> ${person1 == person2}")
println("person1 === person2 -> ${person1 === person2}")

println(person2)

◾️実行結果

person1 == person2 -> false
person1 === person2 -> false
Person(name=KotlinTaro, age=40)

プロパティを変更しながらコピーしたので、インスタンスは等価でもなく参照も異なっています


まとめ

data 修飾子の指定により、ボイラープレートコード無しで有用な データクラス を宣言することができることが分かりました。


関連エントリ

Kotlin - toString, equals, hashCode

Java で以下データを保持するクラスを定義する場合、以下のようになります。

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

多くの場合にこれで終わらず toStringをオーバーライドすることが多いです。
さらに、equals, hashCode メソッドをオーバーライドすることも少なくなくありません。

これらメソッドを手動で実装するのはほとんどなく、基本的にIDEによって自動的に生成する事がほとんどです。

たとえば Intellij IDEA で自動生成すると以下のようになります

import java.util.Objects;

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

IDEによって自動生成しているので手数はすくなくてすみますが、コードだけ見るとボイラープレートコードの塊でしかないです。


Kotlin で同様のコードを書くと以下のようになります(各種関数は Intellij IDEA で自動生成しました)

class Person(val name: String, val age: Int) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Person

        if (name != other.name) return false
        if (age != other.age) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + age
        return result
    }

    override fun toString(): String {
        return "Person(name='$name', age=$age)"
    }
    
}


toString

toString は、そのクラスの内容の文字列表現を取得するための手段となります。
基本的にはデバッグやログ出力時などに有効な情報となります。
デフォルトでは役に立つ情報が出力されないため、toString はオーバーライドされる方が望ましいです。


equals

クラスの内部で同じデータを保持しているかの等価性を評価するための equals をオーバーライドします。

fun main() {
    val taro1 = Person("taro", 10)
    val taro2 = Person("taro", 10)

    println("taro1 == taro2 -> ${taro1 == taro2}")

    val jiro1 = Person("jiro", 10)
    val jiro2 = Person("jiro", 11)

    println("jiro1 == jiro2 -> ${jiro1 == jiro2}")
}

上記コードの実行結果は以下の通りです

taro1 == taro2 -> true
jiro1 == jiro2 -> false

Java では、 == 演算子はプリミティブ型では値の比較をしますが、参照型の場合は参照の比較をします。
そのため、Javaでは基本的にクラス同士の比較では equals メソッドを呼び出す必要があります。Javaの初学者が必ずはまる罠です。

Kotlin では、 == 演算子は内部的に equals を呼び出して値のの比較をします。そのため、対象のクラスで equals をオーバーライドすることで == によるインスタンスの比較が行えます。
あえて参照の比較を行いたい場合は、 === 演算子を使用します。


hashCode

hashCode は equals と一緒にオーバーライドされる必要があります。
例えば、HashSetに格納のされる値の比較は、まずハッシュコードを比較して、ハッシュコードが等しい場合のみ equals で比較を行います。

これらの動作を正しくするためにも hashCode のオーバーライドも必須となります。


まとめ

Javaと同等のデータを格納するクラスを宣言してみました。
実際には、ここで宣言したようなクラスはデータクラスを利用する事でさらに少ないコード量で記述する事が可能です。
それは次のエントリで。



関連エントリ

Kotlin - アクセサの可視性変更

Kotlin では、プロパティのアクセサの可視性は、デフォルトではそのプロパティの可視性と同じです。

例えば以下のようなクラスを定義したとします。シャンプーを意味するクラスで変更可能な価格(price)をプロパティとして持っています。

class Shampoo {
    var price: Int = 1200
}

このプロパティのアクセサの可視性は特に指定おらず、 public になっています。

そのため、以下のように値を変更可能です

fun main() {
    val shampoo = Shampoo()
    println("Shampoo price ->  ${shampoo.price}(yen)")

    shampoo.price = 100 // 値を変更可能
    println("Shampoo new price ->  ${shampoo.price}(yen)")
}

実行結果は以下の通りです。

Shampoo price ->  1200(yen)
Shampoo new price ->  100(yen)


アクセサの可視性は、 get または set キーワードの前に可視性修飾子を記述する事で、可視性を変更可能です。


先ほどの Shampoo クラスの価格の変更は内部からのみに限定(private)にしたい場合はクラス定義は以下のようになります。

class Shampoo {
    var price: Int = 1200
        private set
}

そうするとクラスを外部から利用するコードにおける以下部分がエラーとなり、コンパイルが通らなくなります

shampoo.price = 100


上記の例は、 publicの可視性のプロパティを、setに関しては可視性をprivateに狭めるものでしたが、逆はどうなるのか。。。

以下のクラスを定義して price プロパティの可視性に private を指定します。

class Shampoo {
    private var price: Int = 1200
}

このコードを以下のように書き換えようとしてもコンパイルエラーとなります。

class Shampoo {
    private var price: Int = 1200
        public get // コンパイルエラー 
        public set // コンパイルエラー
}

private のプロパティのアクセサの可視性を広げるようなことはできないようです。
そもそも、こんなことをする状況が生まれないのかな。。。


まとめ

プロパティのアクセサの可視性を変更する方法を学びました。
public の可視性のプロパティのアクセサを private にすることなどが可能です。