おはようエンジニア

まともなエンジニアになりたい

『Kotlinイン・アクション』第4章を読む (その2)

Kotlinイン・アクション第4章「クラス、オブジェクト、インターフェイス」の続きです。

いろいろなコンストラク

// プライマリコンストラクタを持つクラス
class User(val name: String)

// デフォルトコンストラクタを持つクラスとそれを継承するクラス
open class OpenUser
class TwitterUser: User() // 明示的にコンストラクタの呼び出しが必要

// privateなコンストラクタを持つクラス
class SecretUse private constructor() { }

// セカンダリコンストラクタを持つクラス (※1)
open class SecondUser {
    constructor(age: Int) { }
}

インターフェイスのプロパティ

  • インターフェイスでプロパティを宣言できる
    • その場合、値を取得する手段を提供する必要がある
interface User {
    val nickname: String
}

// 手段1: プロパティを直接宣言
class PrivateUser(override val nickname: String) : User

// 手段2: カスタムgetterを宣言
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

// 手段3: プロパティの初期化処理
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

カスタムアクセサの実装

  • setterの実装
    • setter内では field という特別な識別子でバッキングフィールドの値にアクセスする
class User(val name: String) {
    val address: String = "unspecified"
        set(value: String) {
            /* チェックやログ出力などの追加処理 */
            field = value
        }
}
  • 可視性の変更
    • get/set前に可視性修飾子をつける
val counter: Int = 0
    private set

データクラス

  • データクラスの宣言
    • プロパティに応じて toString, equals, hashCode が適切にオーバーライドされている
  • データクラスの不変性
    • データクラスはプロパティを全てvalで宣言してimmutableにすることを強く推奨
    • 変更する場合はコピー元に影響を及ぼさないcopyメソッドが便利
data class Client(val name: String, val postalCode: Int)

等価性チェック

Kotlinではequalsを正しく実装している場合 == を使えば良い

演算子 Kotlin Java
== 構造等価性を比較(equals) 参照等価性を比較
=== 参照等価性を比較 厳密等価性を比較

クラス委譲

  • 委譲とは

    • あるクラスを拡張する方法の一つ
      • 他の方法に「継承」がある
    • 委譲はデコレーターパターンによる実装が一般的
      • 元のクラスをフィールドとして保持し、振る舞いの変更がないメソッドをもとのクラスのメソッドへ転送するパターン
    • デメリットはボイラープレートコードが増えること
  • Kotlinでの実装

    • byキーワードを使うと振る舞いの変更がないメソッドはコンパイラが自動生成する
    • 変更がある場合のみオーバーライドする
class DelegatingCollection<T>(
        innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {} 

objectの使い方

1. オブジェクト宣言

  • オブジェクト宣言とは
  • 使いどころ
    • シングルトンパターンの実装
object Payroll { ... }

>>> Payroll.calculateSalary()
  • コンストラクタを宣言できないこと以外はクラスと同じ書き方
  • クラス内オブジェクト宣言も可能
    • 特定のクラス専用のオブジェクトで使うと良い
    • 例えばPersonクラスの比較のためのNameComparatorPersonクラス内オブジェクトとする

2. コンパニオンオブジェクト

  • コンパニオンオブジェクトとは
    • クラスのprivateメンバにアクセスできるオブジェクト
    • クラス内オブジェクト宣言の特殊な例
  • 使いどころ
    • ファクトリパターンの実装など
class A {
    companion object { // 名前を省略できる
        fun bar() { /* ... */ }
    }
}
>>> A.bar() // オブジェクト名を指定せず静的メソッドのように呼び出せる
  • Javaのstaticメソッドの代替
    • Kotlinはstaticメソッドがないが、ほとんどの場合トップレベル関数で代替できる
    • ただし、トップレベル関数ではprivateメンバにアクセスできないのでそういう場合はコンパニオンオブジェクトを使う
  • インターフェイスを実装可能
    • 様々なコンパニオンオブジェクトに共通のファクトリメソッドを実装する場合など
  • 拡張関数も宣言可能
    • コンパニオンオブジェクトに拡張関数を宣言することでクラスから呼び出すメソッドを拡張できる

3. オブジェクト式

  • オブジェクト式とは
    • クラス内部で使うための無名オブジェクトを宣言する式
  • 使い所
    • イベントリスナ内の実装
window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) { /* ... */ }
        ...
    }
)
  • 無名オブジェクトはシングルトンではないのでオブジェクト宣言時に生成される
  • オブジェクト式で生成したオブジェクトは変数に格納できる

おわりに

クラスやオブジェクトについては用意されている仕組みが多く、実装時には柔軟な選択ができそうです。 その一方で、それらを使いこなすには構文の理解だけでなく、良い設計のパターンをある程度知っていなければいけないと感じました。