覚えたら書く

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

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 にすることなどが可能です。

Kotlin - getter/setter からのバッキングフィールドへのアクセス

Kotlin のクラスでは、フィールドを持つことができません。
しかし、カスタムアクセサを使用するときに バッキングフィールド が必要になることがあります。 この目的のために、Kotlinでは field という識別子を使用して自動的に作成されたバッキングフィールドにアクセスすることができます。


以下、商品を表現するクラスで、その商品の価格(price)を変更した場合はログ出力するようにします。

class Product(val serialNo: Int) {
    var price: Int = 10980
        set(value) {
            println("[Logging] Product price changed. [$field -> $value]")
            field = value
        }
}

price をミュータブルなプロパティで宣言して、カスタムの setter で 変更前の値と変更後の値を出力しています。

例えば以下のような実行コードが書けます

fun main() {
    val product1 = Product(100001)
    println("Product(S/N: ${product1.serialNo}) price = ${product1.price}")

    product1.price = 9980

    println("Product(S/N: ${product1.serialNo}) price = ${product1.price}")
}

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

Product(S/N: 100001) price -> 10980
[Logging] Product price changed. [10980 -> 9980]
Product(S/N: 100001) price -> 9980

product1.price = 9980 の 部分でプロパティの値を変更しています。
この際、内部的には setter が呼び出されています。
このクラスでは setter が再定義されていてログ出力をするようになっているので結果的に値の変更とともにログ出力も行われます。

先に書いた通りに、setterの中で、field という識別子を使ってバッキングフィールドの値にアクセスしています。


getterの場合は、値を読むことしかできませんが、setterの内部では値の読出しと変更の両方が可能です。と「Kotlinイン・アクション」には書かれていますが、

例えば以下のよなコードはコンパイルが通ります。 get() の中で field の値を書き換えできちゃってる気がするのですが、
私の理解不足なだけですかね。よくわかってない。。。

class Product(val serialNo: Int) {
    var price: Int = 10980
        set(value) {
            println("[Logging] Product price changed. [$field -> $value]")
            field = value
        }

        get() {
            field = field * 100
            return field
        }
}


まとめ

どの程度の頻度で利用することになるのかわかっていませんが、
field 識別子を利用することでカスタムアクセサの実装でバッキングフィールドにアクセスできる事がわかりました。