本記事は、Kotlin Advent Calendar 2021の12月21日付の記事です。
Javaの知識が使えるなら、うろ覚えでKotlinのコードを書いてもいい
Kotlinが登場して10年だそうですが、現在はKotlinの先祖であるJavaのプログラマがKotlinに進出したパターンがまだまだ主流なのではないかと思います。Kotlinを使いこなす上でJavaの知識と経験は大きなアドバンテージです。しかし、それにしてもJavaからKotlinになってかなりの部分が変化しており、Java経験者でも覚えることは少なくありません。特にコンストラクタの仕様をJavaと見比べながら本稿でまとめてみると、結構な長さになってしまいました。
ここで筆者が主に書きたいことは、Kotlinのコンストラクタは色々ややこしいが、まぁ何とかなりますよ、ということです。本稿を飽きずに最後まで読み進めるのはかなり大変と思いますので、適当に切り上げて頭の片隅にだけ置いていただき、いざコンストラクタを書いていて手が止まったときに読み直していただければ筆者は満足です。
プライマリコンストラクタ、セカンダリコンストラクタ
コンストラクタを使ってクラスのインスタンスを生成するという点はKotlinもJavaと変わりませんが、Kotlinでは「プライマリコンストラクタ」「セカンダリコンストラクタ」とコンストラクタが2種類あります。妙にややこしい、と感じているプログラマの方もおられると思いますが、わかりにくいという人はあえてプライマリコンストラクタを使わずセカンダリコンストラクタだけを定義すれば、従来のJavaとおおむね同じ感覚でコーディングできると思います。
プライマリもセカンダリも、クラス外から見れば区別なくコンストラクタであり、(後述するデータクラスをどうしても使いたい、などの理由がある場合を除けば)どちらを使って定義しても大丈夫です。
class ManyConstructors(intValue: Int, message: String) { // Class name and primary constructor
constructor(intValue: Int) : this(intValue, "") { println("in constructor(Int = $intValue)") } // Secondary constructor
constructor(floatValue: Float) : this(floatValue.toInt(), "") { println("in constructor(Float = $floatValue)") } // Secondary constructor
constructor(doubleValue: Double) : this(doubleValue.toInt(), "") { println("in constructor(Double = $doubleValue)") } // Secondary constructor
val intValue: Int // Property
init { // initialization
this.intValue = intValue
println("in init block" + if (message.isEmpty()) "" else ": $message")
}
// ...
}
1行目のクラス名宣言とプライマリコンストラクタの定義、2行目以降の constructor
によるセカンダリコンストラクタの定義、プロパティ(Javaの用語ではフィールド)宣言、init
ブロックと、インスタンス生成処理だけで4部構成になっていますが、クラス名宣言以外は省略する記法が使えます。すべてをフルに使わなければならないケースは多くないでしょう。最初のうちは必要なところだけ使って満足してOKです。
1行目の (intValue: Int, message: String)
がプライマリコンストラクタの定義です。1行では狭すぎて不自由ですが、init
ブロックの中が実行されますのでここに必要な処理を書くことができます。init
ブロックは、クラスで定義されたすべてのコンストラクタの呼び出しで実行されます。また、プライマリコンストラクタの引数はプロパティ宣言文と init
ブロックの内部で参照できます(セカンダリコンストラクタのブロック内では使えません)。
2〜4行目、クラスブロック内側の constructor
で3つのセカンダリコンストラクタが定義されています。プライマリコンストラクタが定義されている場合、this(<arguments 1>, <arguments 2>, ..., <arguments N>)
という記法でプライマリコンストラクタを最初に呼び出さなければなりません。
ここで、constructor(Double)
を呼び出した場合、プライマリコンストラクタを経由した init
ブロック-> constructor
のブロック、の順に呼び出されます。
val instance = ManyConstructors(1.0)
// =>
// in init block
// in constructor(Double = 1.0)
プロパティ intValue
は val
で宣言されており、これは再代入不可能なプロパティであることを示しています。再代入不可能なプロパティは、プロパティ宣言または init
ブロックのどちらか一方でのみ代入ができます(両方で代入することはできません)。intValue
は再代入不可能なプリミティブ型 (Int) プロパティですので、コンストラクタで生成されたインスタンスが破棄されるまで値は不変です。val
に代えて var
で宣言した場合には再代入可能なプロパティになります。また、プロパティに private
や protected
などのアクセス修飾子を付与してアクセス範囲を制御することができます。
init
ブロックでは、プライマリコンストラクタの引数を使用した代入や処理を書くことができます。
プライマリコンストラクタの省略
このようにプライマリコンストラクタの呼び出しが必須となる制約がわずらわしければ、以下のようにクラス名宣言の次の括弧を書かないことでプライマリコンストラクタを定義しないことも可能です。
class OnlySecondaryConstructors { // Class name and primary constructor
constructor(intValue: Int) {
println("in constructor(Int = $intValue)")
this.intValue = intValue
} // Secondary constructor
constructor(floatValue: Float) {
println("in constructor(Float = $floatValue)")
this.intValue = floatValue.toInt()
} // Secondary constructor
constructor(doubleValue: Double) {
println("in constructor(Double = $doubleValue)")
this.intValue = doubleValue.toInt()
} // Secondary constructor
val intValue: Int = intValue // Property
init { // initialization
println("in init block") // + if (message.isEmpty()) "" else ": $message"
}
// ...
}
プライマリコンストラクタがなければセカンダリコンストラクタからの呼び出しは不要ですが、この場合でも init
ブロックは実行されます。また、 this(...)
で呼び出すことができるコンストラクタはプライマリコンストラクタだけで、セカンダリコンストラクタを同じクラスの他のコンストラクタから呼び出す方法はありません。また、セカンダリコンストラクタの引数をプロパティ宣言文や init
ブロックに参照させることもできません。
セカンダリコンストラクタの省略
コンストラクタが1つで足りるのならば、セカンダリコンストラクタを書く必要はなく、以下のようにプライマリコンストラクタだけ定義すれば充分です。
class OnlyOneConstructor(intValue: Int, message: String) {
val intValue: Int
init {
this.intValue = intValue
println("in init block" + if (message.isEmpty()) "" else ": $message")
}
}
プライマリ・セカンダリコンストラクタの省略
以下のように、プライマリもセカンダリもかまわず省略してコンストラクタを1つも書かないことも可能です。この場合、引数なしのプライマリコンストラクタが1つ自動的に定義され、(抽象クラスでなくても)コンパイルエラーは生じません。
class NoConstructor {
val intValue: Int
init {
println("in init block")
intValue = 0
}
}
Javaのデフォルトコンストラクタとおおむね同じ仕様、といえます。
プロパティの省略
プロパティを使う必要のないクラスならば、当然プロパティを省略することが可能です。
init
ブロックの省略
init
ブロックで行わせる処理がない場合、すなわち init {}
と書ける場合は、以下のように init
ブロックを省略できます。
class NoInit(intValue: Int) { // Class name and primary constructor
constructor(floatValue: Float) : this(floatValue.toInt()) { println("in constructor(Float = $floatValue)") } // Secondary constructor
constructor(doubleValue: Double) : this(doubleValue.toInt()) { println("in constructor(Double = $doubleValue)") } // Secondary constructor
val intValue: Int = intValue // Property
// ...
}
ただしこの場合は、後述する原則外の場合を除いて、プロパティの定義において必ず何らかの値を代入して初期化しなければなりません。上記の = intValue
を削除するとコンパイルエラーになります。Javaなら、フィールド定義だけを書いておけば初期値を省略してもインスタンス生成時に適当なデフォルト値を代入して初期化してくれますが、Kotlinのプロパティではそれは許されません。Kotlinは「null安全」、すなわち null
を値として取り得ないプロパティや変数を使えることが大きな特長であり、「初期値はとりあえず null
」とすることができないのですから、デフォルトが存在しないのは当然です。
上記のように代入する定数や引数がプロパティ定義文の時点で確定しておらず、数式で表せない値の演算処理が必要である場合は、init
ブロックで演算処理を記述する必要があります。また、原則外についてはちょっとややこしいので、ひとまずコンストラクタ引数プロパティについて述べます。
コンストラクタ引数プロパティ
冒頭の ManyConstructors.kt は、以下のように val
ないし var
宣言をプライマリコンストラクタ引数内で行うことができ、この記法によってプライマリコンストラクタ引数の宣言とプロパティの宣言を兼ねることができます。
class ManyConstructors(val intValue: Int, message: String) { // Class name and primary constructor
constructor(intValue: Int) : this(intValue, "") { println("in constructor(Int = $intValue)") } // Secondary constructor
constructor(floatValue: Float) : this(floatValue.toInt(), "") { println("in constructor(Float = $floatValue)") } // Secondary constructor
constructor(doubleValue: Double) : this(doubleValue.toInt(), "") { println("in constructor(Double = $doubleValue)") } // Secondary constructor
init { // initialization
println("in init block" + if (message.isEmpty()) "" else ": $message")
}
// ...
}
ここでは、このように宣言することで、プライマリコンストラクタの第1引数の値をそのままプロパティ intValue
の初期値とします。 この場合は val
で宣言しているので intValue
の値はインスタンスが破棄されるまで不変ですが、クラスブロック内での宣言と同様 var
で宣言することで再代入による値の変更が可能になります。また private
や protected
などのアクセス修飾子を付与してアクセス範囲を制御できる点も、クラスブロック内での宣言と同様です。
引数でプロパティ宣言を兼ねることができるコンストラクタはプライマリコンストラクタのみで、セカンダリコンストラクタで同様のことはできません。
データクラス
コンストラクタ引数プロパティの文法を用いて、「データクラス」を宣言することができます。
data class Profile(val name: String, val email: String, private var age: Int)
「データクラス」として宣言することで、toString(), copy(...) などのメンバ関数が自動的に定義されるのが利点です。上記の通りクラスブロックを記述しなくてもクラス名宣言とコンストラクタ引数プロパティのみで定義することができ、データを集積したクラスとしての用途に適しています。一方、クラスブロックを記述して引数にないプロパティやセカンダリコンストラクタ、新たなメンバ関数などを追加することもできます。
データクラスとして宣言するには、クラス宣言キーワードを data class
とし、プライマリコンストラクタに1つ以上の引数を与え、かつプライマリコンストラクタのすべての引数をコンストラクタ引数プロパティとして記述することが必要です。ほか、継承等に制約があります。詳細な仕様は、Kotlin公式サイトのデータクラスをご参照ください。
スーパークラスを継承している場合
スーパークラスを継承しているクラスのすべてのコンストラクタは、何らかの形でスーパークラスのコンストラクタを呼び出している必要があります。
class ManyConstructors(val intValue: Int, message: String) : KtSuperClass(intValue) { // Class name, primary constructor and Super Class Constructor
constructor(intValue: Int) : this(intValue, "") { println("in constructor(Int = $intValue)") } // Secondary constructor
constructor(floatValue: Float) : this(floatValue.toInt(), "") { println("in constructor(Float = $floatValue)") } // Secondary constructor
constructor(doubleValue: Double) : this(doubleValue.toInt(), "") { println("in constructor(Double = $doubleValue)") } // Secondary constructor
init { // initialization
println("in init block" + if (message.isEmpty()) "" else ": $message")
}
// ...
}
上記では、1行目の KtSuperClass(intValue)
がスーパークラスのコンストラクタを呼び出している記述になります。Kotlinではこのように、クラス名宣言に続いて :
をはさんで継承するスーパークラスおよびインターフェイスを列挙し、スーパークラスには引数列を記述することでスーパークラスの呼び出しを表現しています。なお、インターフェイスのみを継承しクラスを継承していない場合はスーパークラスのコンストラクタ呼び出しを行いません。ここでは、プライマリコンストラクタは KtSuperClass(intValue)
を呼び出した後に init
ブロックを実行しています。セカンダリコンストラクタは、 this(...)
でプライマリコンストラクタを呼び出すことで間接的にスーパークラスのコンストラクタを呼び出しています。
class ManyConstructors : KtSuperClass { // Class name and primary constructor
constructor(intValue: Int) : super(intValue) { println("in constructor(Int = $intValue)") } // Secondary constructor
constructor(floatValue: Float) : super(floatValue) { println("in constructor(Float = $floatValue)") } // Secondary constructor
// ...
}
上記のように、スーパークラスを継承し、かつプライマリコンストラクタを記述しない場合には、すべてのセカンダリコンストラクタはスーパークラスのコンストラクタを直接呼び出さなければなりません。
なお、サブクラスのコンストラクタが呼び出すスーパークラスのコンストラクタは、プライマリ・セカンダリのいずれのコンストラクタでもかまいません。
プライマリコンストラクタにアクセス修飾子を付与する
以下のように constructor
キーワードの前に private
, internal
などのアクセス修飾子を付与することにより、コンストラクタ呼び出しのアクセス範囲を制御できます。
class ManyConstructors private constructor(val intValue: Int, message: String) { // Class name and primary constructor
constructor(intValue: Int) : this(intValue, "") { println("in constructor(Int = $intValue)") } // Secondary constructor
constructor(floatValue: Float) : this(floatValue.toInt(), "") { println("in constructor(Float = $floatValue)") } // Secondary constructor
internal constructor(doubleValue: Double) : this(doubleValue.toInt(), "") { println("in constructor(Double = $doubleValue)") } // Secondary constructor
init { // initialization
println("in init block" + if (message.isEmpty()) "" else ": $message")
}
// ...
}
1行目のプライマリコンストラクタの記述に constructor
キーワードを用いる記法はここまで紹介しておりませんでしたが、
class ManyConstructors(val intValue: Int, message: String) {
// ...
は
class ManyConstructors constructor(val intValue: Int, message: String) {
// ...
と書くこともできます。実はこれまでのプライマリコンストラクタの記法は constructor
キーワードを省略する記法でした。プライマリコンストラクタにアクセス修飾子を記述しなくてよい (=public
) 場合には constructor
は省略可能です。一方、アクセス修飾子が必要なときはそれを constructor
キーワードの前に置かなければならないので、省略はできなくなります。
ここでは、プライマリコンストラクタを private
とすることで、プライマリコンストラクタを呼び出せる処理をセカンダリコンストラクタやクラススコープ内の関数などに限ることができます。
デフォルト引数
コンストラクタの引数もKotlinの一般の関数と同様、デフォルト引数が使えます。デフォルト引数を使うことで、さまざまな引数の型や数のコンストラクタを要領よく定義することも可能です。特にプライマリコンストラクタは、仕様上1つしか定義できないものの、デフォルト引数の設定次第であたかも多数のプライマリコンストラクタが存在するかのように記述することもできるでしょう。
コンストラクタ内で初期化を完了しなくてもよい例
最後に、後回しにしていた「コンストラクタによるインスタンス生成の完了時にプロパティの初期化を終えていなければならない」という原則の適用外となる場合について、以下の3項を紹介します。
バッキングフィールドのないプロパティ
以下の realNumber
は、値を保持するバッキングフィールドが存在せず、初期化する対象がありませんので、初期値や初期化処理なしでもコンパイルエラーにはなりません。
class Fraction(numerator: Long, denominator: Long) {
val numerator = numerator
val denominator = denominator
val realNumber: Double get() = numerator.toDouble() / denominator
}
realNumber
は値を保持する領域を持つ変わりにカスタムゲッターで値を定義しています。カスタムゲッター、カスタムセッターの細かい説明は本稿の主旨にそぐわないため、ここでは省略します。
Javaではこのような計算値をフィールドとして表現することはできず、メソッド(Kotlinでは関数)で表現しなければなりませんが、Kotlinでは原則として引数のない関数をカスタムゲッターとして記述することによって、以下のように静的な値であるかのように扱うことができます。
val f = Fraction(3, 4)
println(f.realNumber)
// => 0.75
lateinit
プロパティ
以下のように lateinit
と修飾されたプロパティは、初期化原則の適用外とすることができます。
class InitializedAsynchronous(val callback: (Int) -> String) {
var initialized = false
private set
private lateinit var string: String
init {
Thread {
string = callback(3)
initialized = true
}.start()
}
}
lateinitプロパティは var
での宣言が必須で val
は使えません。また、null許容型やプリミティブ型(Int, Float, Booleanなど)プロパティをlateinitとすることはできません。また、lateinitプロパティに何らかの値が代入される前に値を参照しようとすると実行時例外が発生しますので、確実に初期化が完了した後で値を参照するように(上記の場合なら initialized
が true になった後に)注意深くコーディングしましょう。
コンストラクタの実行完了時には初期化すべき値が得られないもののすぐに初期化される、というケースで使うと便利ですが、初期化されない期間があまりにも長いようなケースでは使用を避けた方がよいでしょう。初期化忘れは発見しにくいバグの温床ですので。
by lazy
、もしくは by
による遅延初期化
by lazy
を使って以下のように記述することで、プロパティの値が最初に参照されたときにブロック内の処理を実行させ、得られた値を以後のプロパティの値とする「遅延初期化」を行うことができます。
class InitializedAsynchronous(val callback: (Int) -> String?) {
val string: String? by lazy { callback(3) }
}
この場合は初期化後の値がキャッシュされて不変のため lateinit
とは逆に val
での宣言が必須で、var
は使えません。また、null許容型/非許容型、参照型/プリミティブ型に関わらず使えます。値が参照される時期にはブロック内の処理で初期値が得られることを保証できる、というケースでは、lateinit
より使いやすい場合もあるでしょう。
ここで、lazy
は関数オブジェクトを引数にとり kotlin.Lazy
型の値を返すKotlinの組み込み関数です。by
の後に同様の任意の関数を置いて遅延初期化を行わせることもできます。
参考文献
- Kotlin公式サイト
- Kotlinのソースコード