Kotlin のオブジェクト初期化
コンストラクタとか 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)
は引数に var
や val
が付いていない。
この場合、その引数はプロパティとして保持されない。つまり下記のように 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 つの対応方法がある。
-
nickname
の setter の可視性をprivate
にして、変更用の関数を提供する。class Employee(val id: Int, nickname: String) { var nickname: String = nickname private set // セッターの可視性を private に設定 fun updateNickname(newNickname: String) { this.nickname = newNickname } }
-
nickname
をprivate 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()
で呼び出す。
Employee
に age
を追加する場合。
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