🦜

Kotlinのプロパティ初期化処理、きちんと使い分けできてますか?

2023/08/01に公開

この記事は『blessing software 夏のブログリレー企画』の初日の記事です。

明日はエスツーさん@stg_techさんのGoogle Cloud についてのなにかが公開される予定です!お楽しみに!

TL;DR

Kotlin は、Java と同様にコンストラクターを使用してオブジェクトを生成し、プロパティを初期化します。

Kotlin のコンストラクターには、プライマリコンストラクター、セカンダリコンストラクター、イニシャライザブロック、プロパティの初期化の 4 つの種類があります。

では、この 4 つの初期化処理をどのように使い分けるのか正しく説明できますか?

自分はできなかったので、それぞれの初期化処理について調べた内容について説明し、サンプルコードを紹介します。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

1. プライマリコンストラクター

プライマリコンストラクターは、主にクラスのプロパティを宣言し、初期化するために使用されます。

クラスのインスタンス化の際に呼び出され、引数を指定してインスタンスを作成することができます。

プライマリコンストラクターを使用することで、クラスのプロパティの初期化が簡単に行えるため、冗長な初期化コードを省略できます。

class Person(val name: String, var age: Int)

上記の例では、Person クラスには 2 つのプロパティがあります。コンストラクターで宣言された name は読み取り専用のプロパティとして宣言され、age は書き込み可能なプロパティとして宣言されます。

プライマリコンストラクターの特徴は以下の通りです。

  • シンプルな構造でコードの可読性が向上します。
  • コードがスマートになり、コンストラクタの記述が簡略化されます。

シンプルな構造というのは、以下のコードを実行してみるとわかります。

package constructor

fun main(args: Array<String>) {
    val person = Person("Alice", 29)
    println(person.name)
    println(person.age)
}

結果は以下の通りです。

Alice
29

つまり、プライマリコンストラクターを利用すると,Java のコードでよく見る、以下のような記述する必要がないということです。(むしろできない)

class Person(val name: String, var age: Int){
    private val name;
    private var age;
}

2. セカンダリコンストラクター

セカンダリコンストラクターは、主にクラスのプロパティを追加したり、既存のプロパティを変更するために使用されます。

また、プライマリコンストラクターによって生成されたインスタンスを変更することもできます。

セカンダリコンストラクターは、プライマリコンストラクターに加えて追加の機能を提供するため、柔軟性が高く、オプションの機能を提供できます。

class Person{
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }

    constructor(name: String) {
        this.name = name
        this.age = 0
    }
}

セカンダリコンストラクターの特徴は以下の通りです。

  • 複数のコンストラクタを提供することで、異なる方法でインスタンス化することができます。
  • 引数の型や数を変更することで、クラスの柔軟性が向上します。

ちなみにですが、ここでの constructor(name: String, age: Int)と constructor(name: String)はどちらもセカンダリコンストラクターに分類されます。

あくまでプライマリコンストラクターに分類されるのは先に紹介した class Person(val name: String, var age: Int)ということになります。

つまり、ここで紹介したコードサンプルは、プライマリコンストラクターを持たず、セカンダリコンストラクターのみを持つクラスということです。

この理屈は、あとに紹介する"使い分け"の章を読んでいただくと納得できると思います。

3. イニシャライザブロック

イニシャライザブロックは、クラスのインスタンスが生成される前に、プロパティを初期化するために使用されます。

Kotlin では、クラスの本体の中で init キーワードを使用して、イニシャライザブロックを定義することができます。

class Person(name: String, age: Int) {
    var name: String
    var age: Int

    init {
        // イニシャライザブロック内で、引数からプロパティを初期化する
        this.name = "Mr/Ms. $name"
        this.age = age
    }
}

イニシャライザブロックの長所は以下の通りです。

  • クラスの初期化コードをよりシンプルに保つことができます。
  • 複数のコンストラクタで共通の初期化処理を行うことができます。
  • コンストラクタとは独立してプロパティを初期化できます。

4. プロパティの初期化

Kotlin では、クラスプロパティを宣言する際に、プロパティの初期化を直接行うことができます。

プロパティの初期化は、プライマリコンストラクターやセカンダリコンストラクターを使用しなくても、簡単に行うことができます。

ただしこの場合、決められた値しか初期値として持つことはできません。

class Person {
    val name: String = "John"
    val age: Int = 30
}

プロパティの初期化の長所は以下の通りです。

  • クラスの初期化コードをよりシンプルに保つことができます。
  • プライマリコンストラクターやセカンダリコンストラクターを使用しなくても、簡単にプロパティを初期化できます。
  • コンストラクタとは独立してプロパティを初期化できます。

使い分け

以上がインスタンス化の際にプロパティを初期化する 4 つの方法なのですが、いくつか疑問が浮かんできますよね?

1. プライマリコンストラクターとセカンダリコンストラクターの違いって?

「2. セカンダリコンストラクター」で以下のコードはプライマリコンストラクターを持たず、セカンダリコンストラクターのみを持つクラスだと言いました。

class Person{
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }

    constructor(name: String) {
        this.name = name
        this.age = 0
    }
}

ではそのことを理解するために以下のコードを書いてみます。

class Person(var name: String) {
    constructor(name: String, age: Int) {
        println("Person initialized $name, $age years old")
    }
}

そしてこのコードを実行する。

package constructor

fun main(args: Array<String>) {
    val person = Person("Alice", 29)
}

すると、"Primary constructor call expected"というエラーを吐きます。どうやら、「プライマリコンストラクターを呼ぶことが期待されている」そうです。

では、どうすればよいか?

以下のようにコードを修正します。

class Person(var name: String) {
    constructor(name: String, age: Int) : this(name) {
        println("Person initialized $name, $age years old")
    }
}

そしてまず、以下のようなコードを実行します。

package constructor

fun main(args: Array<String>) {
    val person = Person("Alice")
}

すると、出力には何も表示されません。

次に、main の中を以下のように修正します。

package constructor

fun main(args: Array<String>) {
    val person = Person("Alice",29)
}

そして実行すると、以下のように出力されます。

Person initialized Alice, 29 years old

このことから、以下のコードはプライマリコンストラクターを持たず、セカンダリコンストラクターのみを持つクラスだと言えるわけです。

class Person{
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }

    constructor(name: String) {
        this.name = name
        this.age = 0
    }
}

2. プロパティの初期化と イニシャライザブロック、どっちが先なの?

例えば以下のようなクラスがあったとします。

class Person {
    val name: String = "John"
    val age: Int = 30

    init{
        println(this.name)
        println(this.age)
    }
}

このクラスを、以下のように使うとします。

package constructor

fun main(args: Array<String>) {
    val person = Person()
}

main を実行すると、init で初期化処理が走るわけですが、そのときの println の出力はどうなるでしょうか?

答えは以下のようになります。

John
30

つまり、プロパティの初期化をおこなった場合は、init の処理よりも優先されているということです。

3. セカンダリコンストラクターと イニシャライザブロック、どっちが先なの?

例えば以下のようなクラスがあったとします。

package constructor

class Person {
    var name: String
    var age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
        println("A constructor")
    }

    constructor(name: String) {
        this.name = name
        this.age = 0
        println("B constructor")
    }

    init {
        println("Person initialized")
    }
}

このクラスを、以下のように使うとします。

package constructor

fun main(args: Array<String>) {
    val person = Person("Alice", 29)
}

この時の main を実行した際の出力結果はどうなるでしょうか?

答えは以下のようになります。

Person initialized
A constructor

つまり、init 処理はセカンダリコンストラクターによる初期化よりも先に動くということです。

では、このクラスを以下のように実行した場合はどうでしょうか?

class Person {
    var name: String
    var age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
        println("Primary constructor")
    }

    init {
        println("Person initialized $name")
    }
}
fun main(args: Array<String>) {
    val person = Person("Alice", 29)
    println(person.name)
    println(person.age)
}

先ほどとは違い、init の処理の際に name を使おうとしています。

これは、イニシャライザブロックはセカンダリコンストラクターよりも先に動くということを踏まえれば分かる気もするのですが、"Variable 'name' must be initialized"というエラーを吐きます。

4. プライマリコンストラクターと イニシャライザブロック、どっちが先なの?

ではこれはどうでしょうか?

先ほどの結果から考えるに、このコードを実行した際にも"Variable 'name' must be initialized"というエラーを吐きそうです。

package constructor

class Person(var name: String, var age: Int) {
    init {
        println("Person initialized $name")
    }
}
fun main(args: Array<String>) {
    val person = Person("Alice", 29)
}

しかし結果は…

Person initialized Alice

となる。

つまり、プライマリコンストラクターは init ブロックよりも優先されているということです。

以上のことから、初期化処理の順番を整理すると、プライマリコンストラクター or プロパティの初期化 > init ブロック > セカンダリコンストラクターということになっていると言えます。

おわりに

Kotlin のコンストラクターはシンプルに、そして多彩な表現があるとこが魅力的な反面、きちんと理解して使用しないと予期せぬ動きを引き起こしそうです。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion