📝

Kotlin のオブジェクト初期化

2025/02/11に公開

コンストラクタとか init ブロックとかオブジェクト初期化に関するメモ。

シンプルなクラスの場合

class Employee {}

上記を省略しないで書くとこうなる。

class Employee() {}

もしくはこう。

class Employee constructor() {}

クラス名の右横のはプライマリコンストラクタという。
ちなみに IntelliJ など IDE からは冗長だよ、と注意されるし省略形で実装するのがフツー。

インスタンス化するときに new は不要。

val employee = Employee()

引数ありコンストラクタを持つクラスの場合

基本はこんな感じ。

class Employee(id: Int, nickname: String) {}

この構文は以下と同義だと思うと理解しやすいかも。

class Employee constructor(id: Int, nickname: String) {}

インスタンス化するとき。

val employee = Employee(1, "Momotaro")

Kotlin は named arguments 名前付き引数 が使えるので、こうもできる。

val employee = Employee(id = 1, nickname = "Momotaro")

引数に var / val なし

先ほどの例 class Employee(id: Int, nickname: String) は引数に varval が付いていない。
この場合、その引数はプロパティとして保持されない。つまり下記のように Employee オブジェクトの id を参照しようとするとコンパイルエラーになる。

val employee = Employee(1, "Momotaro")
println(employee.id) // コンパイルエラー

init ブロックなどで一時的に使用したい場合にはこの方法をとる。

引数に var / val あり

class Employee(val id: Int, var nickname: String) {}

こうすればプロパティとして保持されるので先ほどのコードは実行することができる。

val employee = Employee(1, "Momotaro")
println(employee.id) // コンパイルエラーにならない
employee.nickname = "Taro" // nickname に再代入が可能

var はミュータブルで val はイミュータブル。変数そのものについてはこのオフィシャルドキュメントを参照)

Java で書くとこうなる。nickname には public なセッターが実装される。

public class Employee {
    private final int id;
    private String nickname;

    public Employee(int id, String nickname) {
        this.id = id;
        this.nickname = nickname;
    }

    public int getId() {
        return id;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}

nickname の変更はさせたいが、クラス外部からそれをさせたくない場合、2 つの対応方法がある。

  1. nickname の setter の可視性を private にして、変更用の関数を提供する。
    class Employee(val id: Int, nickname: String) {
        var nickname: String = nickname
            private set // セッターの可視性を private に設定
    
        fun updateNickname(newNickname: String) {
            this.nickname = newNickname
        }
    }
    
  2. nicknameprivate var とする。この場合 getter は自動で提供されなくなるので、必要であれば実装しないといけない。
    class Employee(val id: Int, private var nickname: String) {
    
         fun updateNickname(newNickname: String) {
             this.nickname = newNickname
         }
    
         fun getNickname(): String {
             return nickname
         }
     }
    

1 と 2 の使い分け 🤔

  • 1 の private set はクラス外部からプロパティは読み取れるが変更は内部のみ行いたい場合。
  • 2 の private var はプロパティを完全に隠蔽したい場合。読み取りが必要な場合は getter を実装する。

プロパティへの代入時にバリデーションがしたい

nickname は 20 文字以内にしたい時。

class Employee(val id: Int, nickname: String) {
    var nickname: String
        private set

    init {
        validateNickname(nickname)
        this.nickname = nickname
    }

    fun updateNickname(nickname: String) {
        validateNickname(nickname)
        this.nickname = nickname
    }

    private fun validateNickname(nickname: String) {
        if (nickname.length > 20) {
            throw IllegalArgumentException("nickname must be 20 characters or less")
        }
    }
}

init ブロック

プライマリコンストラクタとともに使用される。その引数を受け取って、初期化や検証などを行うのに使える。インスタンス化時に自動的に実行され、複数定義することもできる。その実行順序は宣言された通りになる。

init {
    println("init 1")
}
init {
    println("init 2")
}

これは以下の通り上から実行される。

init 1
init 2

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

constructor キーワードを使って定義し、プライマリコンストラクタに加えて、異なる初期化ロジックを提供できる。セカンダリコンストラクタは、プライマリコンストラクタまたは他のセカンダリコンストラクタ(つまりセカンダリコンストラクタも複数定義が可能)を呼び出す必要があり、それを this() で呼び出す。
Employeeage を追加する場合。

class Employee(val id: Int, nickname: String) {
    var nickname: String
        private set

    var age: Int = 0 // age プロパティ追加
        private set // nickname と同じくクラス外部からの変更を許可しない

    init {
        validateNickname(nickname)
        this.nickname = nickname
    }

    // セカンダリコンストラクタ
    constructor(id: Int, nickname: String, age: Int) : this(id, nickname) {
        this.age = age
    }

    fun updateNickname(nickname: String) {
        validateNickname(nickname)
        this.nickname = nickname
    }

    private fun validateNickname(nickname: String) {
        if (nickname.length > 20) {
            throw IllegalArgumentException("nickname must be 20 characters or less")
        }
    }
}

Employee のインスタンス化は age アリでもナシでもできる。

val employee = Employee(1, "Momotaro") // age ナシ
val employee2 = Employee(2, "Kintaro", 30) // age アリ

init ブロックとセカンダリコンストラクタ両方が定義されているときの実行順序メモ。
セカンダリコンストラクタ → this() → プライマリコンストラクタが呼ばれる。それぞれに実装されているロジックは init ブロック → セカンダリコンストラクタのロジックの順序で実行される。

Discussion