覚えたら書く

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

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 識別子を利用することでカスタムアクセサの実装でバッキングフィールドにアクセスできる事がわかりました。

Kotlin - インターフェースで宣言したプロパティの実装

Kotlin ではインターフェースに抽象的なプロパティをを宣言する事が可能です。
例えば以下のようなものです。

interface Cat {
    val kind: String
}

Cat インターフェースを実装するクラスにおいて、kind の値を取得する手段を提供しなければならないという意味になります。


インターフェースは、その値がバッキングフィールドに格納されるか、getterを使って取得されるかの指定などはしていません。
インターフェースそのもは状態を持ちません。
インターフェースを実装するクラスのみが必要に応じて値を保持可能です。


先ほどのインターフェースに対する実装クラスは以下のようにいくつも可能性があります

// 猫種不明
class UnknownCat(override val kind: String) : Cat
// 雑種
class HybridCat : Cat {
    override val kind = "Hybrid"
}
// アメリカンショートヘア
class AmericanShortHair : Cat {
    override val kind: String
        get() = this::class.java.simpleName
}


UnknownCat の場合は、プライマリコンストラクタないでプロパティを直接宣言する構文を使っています。
このプロパティは、Catの抽象プロパティを実装しているため override と指定する必要があります。

HybridCat は初期化時に kind プロパティに値を代入しています。

AmericanShortHair はカスタムプロパティで、自身のクラス名を kindプロパティの値として返すようにしています。


interface Cat {
    val kind: String    // 猫種
    val name: String    // 名前
        get() = "${kind.toLowerCase()}-Miko"
}

このインターフェースの場合、抽象プロパティ kind とカスタムgetter を持った name プロパティを宣言しています。
kind プロパティは実装クラスによってオーバーライドされる必要がありますが、 name プロパティをそのまま使用できます。


まとめ

本エントリで対象とした記述範囲の区切りが変な感じもするのでまとめを書きにくいですが、
Kotolin でインターフェース内で宣言されたプロパティを実装する方法がわかりました。