覚えたら書く

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

Kotlin - コンストラクタ

今日も相変わらず 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


Java のクラスは1つ以上のコンストラクを持ちます。
Kotlin も似ていますが プライマリ(primary)コンストラクタとセカンダリ(secondary)コンストラクタを区別している点が異なります。
また、初期化ブロック(initializer block)に追加の初期化用ロジックを記述する事ができます。


プライマリコンストラクタ

以下はKotlinのクラス宣言になります。

class Person(val fullName: String, val age: Int)

このクラスには波括弧なしで、丸括弧に宣言があるだけとなっています。
このような丸括弧で囲まれたコードブロックは、プライマリコンストラクタ と呼ばれます。

プライマリコンストラクタは以下の2つの役割を持ちます

  • コンストラクタの引数を指定する
  • 引数で初期化されるプロパティを定義する

当然ですがコンストラクラタにより以下のようにインスタンスを生成できます

val person = Person("Kotlin Taro", 20)

先ほどのプライマリコンストラクの内容をもっと明示的に記述すると以下のようになります

class Person constructor(_fullName: String, _age: Int) {
    val fullName: String
    val age: Int
    init {
        fullName = _fullName
        age = _age
    }
}

constructor というキーワードは、プライマリコンストラクタもしくはセカンダリコンストラクタの宣言のはじめに付けられます。
init は、初期化ブロック(initializer block)を実装するためのキーワードとなります。
初期化ブロックは、クラスが生成された時に実行される初期化コードを含んでいて、プライマリコンストラクタと一緒に利用されることが意図されています。
プライマリコンストラクタの構文では、初期化コードを含めることができないためです。


初期化ブロックを用いた例を上述しましたが、上記例であれば初期化ブロックに初期化用のコードを書く必要はありません。
fullName, age プロパティの宣言と組み合わせる事ができるためです。
プライマリコンストラクタに対してのアノテーションや可視性修飾子が付かないのであれば constructorキーワードを省略可能です。
これらの内容を適用すると以下のように書く事ができます。

class Person(_fullName: String, _age: Int) {
    val fullName = _fullName
    val age = _age
}

クラス本体で val キーワードでプロパティを宣言していますが、プロパティが対応するコンストラクタ引数で初期化されるのであれば、
コンストラクタの引数の前に val キーワードを付ける事で、コードをシンプルにする事が可能です。
それが以下になります

class Person(val fullName: String, val age: Int)

最初に書いたクラス宣言と同じ記述となりました。これがもっとも簡潔な構文になります。


コンストラクタ引数は、関数の引数と同じようにデフォルト値を設定する事が可能です。

// age プロパティにデフォルト値を設定した
class Person(val fullName: String, val age: Int = 10)
val person1 = Person("Kotlin Taro", 20)
println("${person1.fullName} is ${person1.age} years old.")    //  -> Kotlin Taro is 20 years old.

// デフォルト値が利用される
val person2 = Person("Kotlin Jiro")
println("${person2.fullName} is ${person2.age} years old.")    //  -> Kotlin Jiro is 10 years old.


ちなみに、コンストラクタ引数の全てがデフォルト値を持つ場合、コンパイラは引数を持たずに全てデフォルト値を使用するコンストラクタを追加で生成します。
こうすることで、引数なしコンストラクタでのインスタンス化を行うようなライブラリでKotlinを利用しやすくなります。

// コンストラクの引数の全てにデフォルト値を持つ
class Person(val fullName: String = "Unknown", val age: Int = 10)

fun main() {
    val person = Person()  //  デフォルトコンストラクタの実行
    println("${person.fullName} is ${person.age} years old.")    // -> Unknown is 10 years old.
}


クラスがスーパークラスを継承している場合、プライマリコンストラクタはスーパークラスも初期化する必要があります。
基底クラスのリストで、スーパークラスの参照に続けてそのコンストラクタ引数を与える事で、初期化できます。

open class Animal(val name: String)

class Cat(name: String) : Animal(name)


コンストラクタを宣言しないクラスでは、何も引数をとらないデフォルトコンストラクタが生成されます

open class Shape

// デフォルトコンストラクタが生成されている
val shape = Shape()

コンストラクタを宣言していないクラス継承し、なおかつコンストラクタを持たないクラスの場合は、
スーパークラスが引数を持たなかったとしても、スーパークラスのコンストラクタを明示的に呼び出す必要があります。

class Circle : Shape()

ちなみに、インターフェースを実装する場合は、インターフェースはコンストラクタを持たないため、
親の型のリスト内のインターフェース名の後ろに丸括弧は必要ありません。


あるクラスを他のコードからインスタンス化できないように保証するには、コンストラクタを private にするひつようがあります。
以下はプライマリコンストラクタを private にしたものです。

class SecreteClazz private constructor() {}

このクラスは、privateなコンストラクタしか持たないため、このコードの外側からはインスタンス化することはできません。


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

Java に比べて、Kotlin では複数のコンストラクタを持つクラスは一般的ではありません。
なぜならば、Javaでオーバーロードによって複数のコンストラクタを必要とする状況は、Kotlin では引数のデフォルト値と名前付き引数によってカバーされるからです。


それでも、複数のコンストラクタが必要となる状況は残されていて、複数の異なる方法でクラスを初期化するコンストラクタを持つフレームワークのクラスを拡張するような状況です。
Javaで宣言された2つのコンストラクタを持つ View クラスを想像した場合に、Kotlinでは以下のように宣言されます

open class View {
    constructor(ctx: Context) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) {
        // initialize code
    }
}

このクラスには、プライマリコンストラクタの宣言はありません(クラス宣言のクラス名の後ろに括弧がないことがそれを意味しています)
セカンダリコンストラクタは、constructor キーワードを使って導入し、必要な数だけ宣言可能です。

このクラスを拡張する場合、同じコンストラクタを宣言可能です。
以下では super() キーワードを使って、対応するスーパークラスのコンストラクタを呼び出しています。

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}

Javaと同じく this() キーワードを利用することも可能です

class MyView : View {
    constructor(ctx: Context) : this(ctx, DEFAULT_ATTRIBUTE) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}


クラスにプライマリコンストラクタがなければ、それぞれのセカンダリコンストラクタが基底クラスを初期化するか、
基底クラスを初期化するための別のコンストラクタを呼び出す必要があります。

Javaとの互換性が、セカンダリコンストラクタが登場する主なユースケースとなります。


まとめ

Java にはなかった、プライマリコンストラクタとセカンダリコンストラクタがKotlinには存在している事がわかりました。
主に使うのはプライマリコンストラクタなのだと思いますが、両方の使い分け等について把握しておく必要があるかなと感じました。