まくまくKotlinノート
クラスを定義する (class)
2019-04-24

クラス定義の基本

Kotlin のクラス定義は Java と同様に class キーワードを使用しますが、デフォルトで public final 扱いという違いがあります。 これは、多くのケースで public の方が都合がよいことと、意図しない継承を防ぐことを意図した仕様です。

  • デフォルトでは全クラスからアクセス可能 (public)
  • デフォルトでは継承できない (final)

次の Book クラスは、リードオンリーな title プロパティ持つ、シンプルなクラスの実装例と使用例です。

リードオンリーなプロパティを持つクラス

class Book(val title: String)

val b = Book("Title1")
println(b.title)  //=> Title1

詳しくは後述しますが、Kotlin にはメソッドを簡潔に記述するための仕組みがたくさん用意されています。 メソッド実装などの記述が必要ない場合は、上記のようにクラス本体部分を示す { } ブロックすら省略して記述することができます。 Java とは異なり、コンストラクタを呼び出すときの new キーワードも省略できます(というより new は存在しません)。

上記は title プロパティをリードオンリーとして定義していますが、プロパティの値を書き換え可能にするには、val キーワードを var キーワードに置き換えるだけで済みます。

書き換え可能なプロパティを持つクラス

class Book(var title: String)

val b = Book("Title1")
b.title = "Title2"
println(b.title)  //=> Title2

上記のように b.title という形で title フィールドにアクセスできるのは、内部で getter/setter メソッドが定義されて呼び出されているからです。 title フィールドそのものが public になっているわけではなく、あくまで public な getter/setter メソッドが暗黙的に呼び出されています。 この Book クラスを Java のコードから使用する場合、title フィールドへのアクセスは、b.getTitle()b.setTitle("...") のように記述することになります。

コンストラクタを定義する

コンストラクタは 2 種類ある

Kotlin のクラスのコンストラクタには、プライマリ・コンストラクタ (primary constructor) とセカンダリ・コンストラクタ (secondary constructor) の 2 種類があります。

プライマリ・コンストラクタ
インスタンス生成時に必ず呼び出されるコンストラクタ。class 本文の外で定義する。
セカンダリ・コンストラクタ
コンストラクタのバリエーション。プライマリ・コンストラクタが定義されている場合、セカンダリ・コンストラクタから直接的、あるいは間接的にプライマリ・コンストラクタを呼び出す必要がある(プライマリ・コンストラクタはいかなる場合にも呼び出される)。class 本文で定義する。

例えば、プライマリ・コンストラクタと、セカンダリ・コンストラクタを 2 つ持つクラスがあった場合、コンストラクタの呼び出しは下記のような感じで、プライマリ・コンストラクタによる初期化が起点となってオブジェクトの構築が行われます。 コード上はセカンダリ・コンストラクタからプライマリ・コンストラクタを呼び出しているかのように見えるかもしれませんが、あくまで実行順序はプライマリ・コンストラクタが先です。

  • プライマリ(+初期化ブロック)
  • プライマリ(+初期化ブロック) → セカンダリA
  • プライマリ(+初期化ブロック) → セカンダリA → セカンダリB

上記のセカンダリ・コンストラクタの説明では、「バリエーション」という言葉を使いましたが、コンストラクタのパラメータとしてデフォルト引数の仕組みが使えるので、プライマリ・コンストラクタだけでもある程度の生成の「バリエーション」を持たせることは可能です。

プライマリ・コンストラクタと初期化ブロック

プライマリ・コンストラクタは、インスタンスの生成時に必ず呼び出されます。 省略記法がいろいろありますが、一番冗長な書き方から順番に見ていきます。

プライマリ・コンストラクタが受け取るパラメータは、クラス名の後ろに続けて constructor(...) という形で宣言します。 プライマリ・コンストラクタが呼び出されると、(1) プロパティの定義部分 (property initializer)、(2) init で囲まれた初期化ブロック (initializer block) が順番に実行されます。 (1) と (2) の中では、プライマリ・コンストラクタに渡されたパラメータを直接参照することができます。 下記の Book クラスは、プライマリ・コンストラクタで 1 つの値を受け取り、それをリードオンリーなプロパティとして保持しています。

class Book constructor(title: String) {
    // (1) プロパティ定義 (property initializer)
    val title: String

    // (2) 初期化ブロック (initializer block)
    init {
        this.title = title
    }
}

// 使用例
val b = Book("タイトル")
println(b.title)

この例のように、初期化ブロック内で単純なプロパティ代入しか行っていない場合は、初期化ブロック (init) の記述を省略して、プロパティの定義部分で値の設定まで済ませてしまうことができます。 プロパティ定義部分では型推論が働くので、型の指定を省略することができます。

class Book constructor(title: String) {
    val title = title
}

さらに、プライマリ・コンストラクタに、アノテーションや可視性の指定がない場合は、constructor キーワードを省略することができます。

class Book(title: String) {
    val title = title
}

さらに、パラメータで受け取った値を、プロパティの定義部分で単純に代入しているだけであれば、パラメータ名の前に val、あるいは var キーワードを付けることによって、パラメータとプロパティの定義を同時に行ってしまうことができますval を付けた場合はリードオンリーなプロパティとなり、コンストラクタで設定された値から変更することができなくなります。 最終的に Book クラスは下記のようにシンプルに記述できることになります。

class Book(val title: String)

コンストラクタの(パラメータの)定義を省略した場合は、パラメータを取らないプライマリ・コンストラクタが自動的に生成されます。

class Book {
    var title: String = "Unknown"
    init {
        println("初期化ブロックはいつでも書けるよ")
    }
}

val b = Book()
b.title = "ぴよぴよ"
println(b.title)  //=> ぴよぴよ

デフォルト値と名前付き引数

コンストラクタのパラメータには、デフォルト値を持たせることができます(通常の関数と同様です)。 下記の例では、2 つのパラメータにデフォルト値を設定しています。

class Book(val title: String = "無題", val author: String = "著者不明") {
    override fun toString() = "$title, $author"
}

コンストラクタの呼び出し時に引数を省略すると、デフォルト値として指定した値が使用されます。

println(Book())  //=> 無題, 著者不明
println(Book("ああ"))  //=> ああ, 著者不明
println(Book("ああ", "まく"))  //=> ああ, まく

また、コンストラクタに引数を渡す時に パラメータ名=値 という形で指定すると、任意の順序でパラメータを指定することができます (名前付き引数)。

println(Book(author = "まく", title="ああ"))  //=> ああ, まく

型が同じパラメータが複数ある場合、名前付き引数の仕組みを使うと、引数の順番を間違えるといったミスを防ぐことができます。

セカンダリ・コンストラクタ

プライマリ・コンストラクタだけではカバーしきれないような、パラメータのバリエーションを持たせたい場合は、セカンダリ・コンストラクタ (secondary constructor) を定義します。 Kotlin にはデフォルト値や名前付き引数の仕組みがあるので、多くの場合はプライマリ・コンストラクタだけで十分ですが、フレームワークで定義されているクラスを継承するようなケースで必要になったりします(親クラスのコンストラクタに合わせてパラメータ定義する必要があったりするため)。

セカンダリ・コンストラクタは、クラス本体部分で constructor キーワードを使って定義します。 下記の Indenter クラスは、テキストの前にインデントを入れて出力するためのクラスです。 コンストラクタで渡した文字数分のスペース、あるいは、渡された文字列そのものをインデントとして出力します。 パラメータに応じて異なる初期化処理を行う必要があるため、2 つのセカンダリ・コンストラクタを作成して、それぞれの初期化処理を定義しています。

class Indenter {
    val text: String
    constructor(size: Int) {
        text = " ".repeat(size)
    }
    constructor(text: String) {
        this.text = text
    }
    fun puts(message: String) {
        println("$text$message")
    }
}

fun main() {
    Indenter(4).puts("Hello")  //=> "    Hello"
    Indenter("----").puts("Hello")  //=> "----Hello"
}

上記のように、パラメータ付きのセカンダリ・コンストラクタのみを定義した場合、パラメータなしのプライマリ・コンストラクタが自動生成されることはありません。

Indenter()  // NG(パラメータなしのコンストラクタはない)

プライマリ・コンストラクタとセカンダリ・コンストラクタの両方を定義する場合、セカンダリ・コンストラクタから this を使って、間接的、あるいは、直接的にプライマリ・コンストラクタを呼び出しておく必要があります。 プライマリ・コンストラクタはいかなる場合にも呼び出されるからです。

下記の例では、Int 値を受け取るセカンダリ・コンストラクタから、String 値を受け取るプライマリ・コンストラクタを呼び出しています(実装部分は空なので後ろの {} を省略しています)。

class Indenter(val text: String) {
    // プライマリ・コンストラクタを呼び出す
    constructor(size: Int) : this(" ".repeat(size))
}

this キーワードは、プライマリ・コンストラクタの呼び出しだけでなく、別のセカンダリ・コンストラクタの呼び出しにも使用できます。 下記の例では、1 つ目のセカンダリ・コンストラクタから、2 つ目のセカンダリ・コンストラクタを呼び出しています。 結果的にプライマリ・コンストラクタの呼び出しにつながるため、このようなコンストラクタ定義も正しいものとなります(これを「間接的」なプライマリ・コンストラクタの呼び出しと呼んでいます)。

class Indenter(val text: String) {
    // 別のセカンダリ・コンストラクタを呼び出すセカンダリ・コンストラクタ
    constructor(size: Int) : this(size, " ")

    // プライマリ・コンストラクタを呼び出すセカンダリ・コンストラクタ
    constructor(size: Int, text: String) : this(text.repeat(size))
}

セカンダリ・コンストラクタを呼び出した場合でも、先にプライマリ・コンストラクタの処理が実行されることに注意してください。 下記のような順番で実行されていきます。

  1. プライマリ・コンストラクタによるフィールドの初期化
  2. 初期化ブロック (init)
  3. セカンダリ・コンストラクタの本文部分

下記のようなテストコードを実行してみれば、処理の流れを理解できると思います。

class Book(title: String) {
    // (1) プライマリ・コンストラクタによるフィールドの初期化
    val title = title
    var author = "作者不明"

    // (2) プライマリ・コンストラクタの初期化ブロック
    init {
        println("---- init ----")
        println(author)
    }

    // (3) セカンダリ・コンストラクタの中身は最後に実行される
    constructor(title: String, author: String) : this(title) {
        println("---- secondary ----")
        println(this.author)
        this.author = author
    }
}

fun main() {
    // セカンダリ・コンストラクタを呼び出し
    val b = Book("タイトル", "まく")
    println("---- main ----")
    println(b.author)  //=> 作者
}

実行結果

---- init ----
作者不明
---- secondary ----
作者不明
---- main ----
まく

データクラスにセカンダリ・コンストラクタを定義する場合は、データクラスによって自動生成されるコードが、プライマリ・コンストラクタで定義されたフィールドのみを参照するという点に注意してください。 そもそも、セカンダリ・コンストラクタのパラメータでフィールドを定義することはできませんが、セカンダリ・コンストラクタ内の実装で、クラス本体部分で定義したフィールドを初期化するということをやってしまいがちです。 クラスが保持するべき値は、できるだけプライマリ・コンストラクタのパラメータとしてフィールド定義してしまうのが安全です。

データクラスに関する詳しい説明は下記を参照してください。

コンストラクタの private 化

プライマリ・コンストラクタをクラスの外部から呼び出せないようにするには、constructor の前に private キーワードを付けます。 このように可視性を付加する場合、constructor の記述は省略できなくなります。

class Book private constructor() {}

private なコンストラクタしか存在しないクラスのインスタンスを生成するには、クラスのコンパニオン・オブジェクト (companion object) からコンストラクタを呼び出す必要があります。 コンパニオン・オブジェクトに定義した関数は、インスタンスがなくても呼び出すことができるため、外部から間接的に private なコンストラクタを呼び出すための入り口として使用できます。

コンストラクタのパラメータが複雑な場合、直観的な名前のついたファクトリ・メソッドをコンストラクタの代わりに提供すると可読性を向上させることができます。 下記の Book クラスは、インスタンスの生成をファクトリ・メソッド経由で行うことを強制しています。

class Book private constructor(val title: String, val price: Int) {
    companion object {
        fun newFreeBook(title: String) = Book(title, 0)
        fun newDamnedBook(title: String) = Book(title, -1)
    }
}

fun main() {
    val b = Book.newFreeBook("はじめてのKotlin")
    println(b.title)
    println(b.price)
}

この例だとコンストラクタのパラメータがシンプルすぎて、ファクトリ・メソッドを導入するメリットは感じられないかもしれませんが、こういった設計パターンがあることを覚えておくといつか役に立つでしょう。 こういった抽象度の高いデザインパターンは、Kotlin に限らず、一般的なベストプラクティスとして受け入れられています。

プライマリ・コンストラクタを private にする目的が、シングルトンを作成したいということであれば、代わりに Kotlin の object 宣言を使用すると簡潔な記述が可能です。

単なる静的なユーティリティ関数を集めただけのユーティリティ・クラスを作りたいということであれば、パッケージのトップレベルに関数を定義してしまうのが手っ取り早いです。

2019-04-24