ザクっと理解するKotlin Inheritance
Kotlinにおける継承やメソッド・プロパティの上書き、そして継承先クラスの初期化順序などは、オブジェクト指向プログラミングにおいて非常に重要な概念です。
初心者にとっては、これらの概念をしっかりと理解することが、Kotlinを使ったプログラミングの基礎となります。
本記事では、Kotlinにおける継承や上書きについて詳しく解説し、上書きのルールについても解説していきます。
Kotlinのオブジェクト指向プログラミングに興味がある方は、ぜひこの記事を参考にしてみてください。
この記事はKotlin公式サイトを翻訳しながらわかりやすくまとめています。
継承
Kotlinでは、スーパークラスが明示的に宣言されている場合を除いて、全てのクラスは暗黙的にAnyという共通のスーパークラスを継承しています。
// 暗黙的にAnyというスパークラスを継承している
class Person
Anyはequals()、hashCode()、toString()の3つのメソッドを持っています。
そのためこれらのメソッドは全てのKotlinクラスで定義されていることになります。
デフォルトでKotlinのクラスはfinalに設定されています。
要するにデフォルトで継承をしない設定にされています。
継承を可能にするにはopenキーワードをつける必要があります。
// 継承不可
class Person1
// 継承可能
open class Person2
明示的にスーパータイプを宣言するには、クラスのヘッダーの後にコロンと型を書きます。
class BlogOwner(name: String) : Person2(name)
open class Person2(name: String)
継承先のクラス(上記ではPerson2)にプライマリコンストラクタがある場合は、継承元クラス(上記ではBlogOwner)の初期化を継承先クラスのプライマリコンストラクタのパラメータを使ってしなければなりません。
継承先クラスにプライマリコンストラクタがない場合、セカンダリコンストラクタでsuperキーワードを使い初期化するか、superキーワードが設定されたセカンダリコンストラクタに値を渡す必要があります。
// 番号順に見たらわかりやすいかもしれません
fun main() {
// 5. BlogOwnerの最初のセカンダリコンストラクタを使用
val blogOwnerSato = BlogOwner("Sato", 50)
blogOwnerSato.showName()
blogOwnerSato.showAge()
// 6. BlogOwnerの2番目のセカンダリコンストラクタを使用
val blogOwnerYoshida = BlogOwner("Yoshida")
blogOwnerYoshida.showName()
blogOwnerYoshida.showAge()
}
// 2. Personの継承先であるBlogOwnerクラスはプライマリコンストラクタを持たない
class BlogOwner: Person {
// 3. Personのプライマリコンストラクを使用
constructor(name: String, age: Int): super(name, age)
// 4. BlogOwnerのセカンダリコンストラクを使用
constructor(name: String): this(name, 20)
}
// 1. Personクラスはプライマリコンストラクタを持っている
open class Person(val name: String, var age: Int) {
fun showName() = println("name: $name")
fun showAge() = println("age: $age")
}
例えばsuperを入れ忘れるとExpecting a ‘this’ or ‘super’ constructor callとエラーが表示されます。
このようにsuperを入れることで継承元に値を渡すことを明確にします。
メソッドの上書き
Kotlinでは上書き可能な関数などに対して、もしくは上書きすることに対して修飾子を明示的に指定する必要があります。
class BlogOwner(name: String, age: Int): Person(name, age) {
override fun greet() = println("Hi, I'm $name.")
// PersonクラスでshowName関数にopenがないため上書き(override)不可能でエラー表示
// override fun showName() = println("now overridable")
}
open class Person(val name: String, var age: Int) {
fun showName() = println("name: $name")
fun showAge() = println("age: $age")
open fun greet() = println("Hi")
}
また、finalが設定されているクラスやopenキーワードがついていないクラスの関数などにopenをつけたとしても効果がありません。
overrideキーワードをつけた関数などはそれ自体もopenになります。
そのためサブクラスで上書きされる可能性があります。
上書きを防ぎたい場合はfinalキーワードを使用する必要があります。
class Class03: Class02() {
override fun method01() = println("Class03.method01")
// Class02のmethod02はfinalがついているため上書き(override)不可のためエラー
// override fun method02() = println("Class03.method02")
}
open class Class02: Class01() {
override fun method01() = println("Class02.method01")
final override fun method02() = println("Class02.method02")
}
open class Class01 {
open fun method01() = println("Class01.method01")
open fun method02() = println("Class01.method02")
}
プロパティの上書き
プロパティの上書きはメソッドの上書きと同じようにできます。
上書きする際は継承先クラスでoverrideをつける必要があり、互換性のある型を宣言する必要があります。
初期化、もしくはgetメソッドで上書きができます。
fun main() {
val class02 = Class02()
println(class02.property01) // Class02.property01
println(class02.property02) // Class02.property02
}
class Class02: Class01() {
override var property01 = "Class02.property01"
override var property02: String = "Class02.property02"
get() = "Class02.property02"
}
open class Class01 {
open var property01 = "Class01.property01"
open var property02: String = "Class01.property02"
get() = "Class01.property02"
}
加えて、valで宣言したプロパティをvarに宣言しなおすことができます。
しかし逆はできません。
Kotlinにおいて、valプロパティは本質的にgetメソッドを持つプロパティであり、varプロパティはgetとsetメソッドの両方を持つプロパティです。
要するにvalプロパティはgetメソッドの宣言が必須になっており、これをvarで上書きするとプラスでsetメソッドを宣言するため可能になっています。
逆のvarからvalを考えるとvarで宣言されているgetとsetをvalでgetのみにする必要があります。
そのため不可能となっています。
fun main() {
val class02 = Class02()
println(class02.property01)
}
class Class02: Class01() {
override val property01 = "Class02.property01"
}
open class Class01 {
open var property01 = "Class01.property01"
}
上記の場合、Class02でClass01のvar宣言をvalに変えようとしているため下記エラーが表示されます。
Val-property cannot override var-property
訳: Val宣言のプロパティはVar宣言を上書きできません。
overrideキーワードはプライマリコンストラクタのプロパティ宣言でも使用できます。
fun main() {
val class02 = Class02("override in constructor")
println(class02.property01) // override in constructor
}
class Class02(override val property01: String): Class01() {}
open class Class01 {
open val property01 = "Class01.property01"
}
継承先クラスの初期化の順番
継承先クラスのインスタンス化の最中に、最初のステップとして継承元のクラスの初期化が行われます。
これは継承元クラスのコンストラクタの引数の評価のために実施されます。
そのため継承先クラスの初期化ロジックが実行される前に行われます。
詳しくは下の通りです。
fun main() {
println("継承先クラスの初期化の順番 start")
Class02("初期化")
println("継承先クラスの初期化の順番 end")
}
// 継承元のコンストラクタへの引数の評価(渡してる値が正しいか)を実施
// そのため継承元の内容が先に実行される
class Class02(name: String): Class01(name.also {println("1番目")}) {
init {
println("4番目")
}
val test = "test".also {println("5番目")}
}
open class Class01(val name: String) {
init {
println("2番目")
}
val property01 =
name.also {println("3番目")}
}
/*
* 実行結果
* 継承先クラスの初期化の順番 start
* 1番目
* 2番目
* 3番目
* 4番目
* 5番目
* 継承先クラスの初期化の順番 end
*/
これは継承元のクラスのコンストラクタが実行される時、継承先で宣言されている、もしくは上書きされているプロパティはまだ初期化されていません。
よって継承元でこれらのプロパティを使おうとすると予想に反した動きや実行時エラーを引き起こす可能性があります。
継承元クラスの設計をする際は、コンストラクタやプロパティの初期化、initブロック内でopenメンバーを使うべきではありません。
下記コードは実際に予想に反した動きをするコードです。
fun main() {
val class02 = Class02(5)
println("main thread: ${class02.getNumber()}")
}
class Class02(value: Int): Class01(value) {
override fun getNumber() = value * 2
}
open class Class01(val value: Int) {
init {
println("init block in Class01: ${getNumber()}")
}
open fun getNumber() = value
}
/*
* 実行結果
* init block in Class01: 10
* main thread: 10
*/
このコードではClass02がClass01を継承しています。
先ほどの注意を当てはめると継承元であるClass01ではコンストラクタやプロパティの初期化、initブロック内でopenメンバーを使うべきではありません。
しかしClass01の初期化ブロックでopenで宣言されたgetNumberメソッドを使用しています。
またClass02ではClass01から継承したgetNumberメソッドを上書きしています。
クラスの実行順からClass01の初期化ブロックのgetNumberメソッドは5を、Class02のgetNumberでは上書き後の2倍した数である10を表示するのが予想する動作になります。
しかし実行結果では両方とも10になってしまいます。
このように継承元を設計する際には、コンストラクターやプロパティの初期化子、initブロックでオープンメンバーを使用することを避けるべきです。
スーパークラスの実装の呼び出し
継承先のクラスではsuperキーワードを使用することで継承元の関数やプロパティにアクセスすることができます。
fun main() {
Class02().showSuperClassProperty() // 実行結果: Class01.property01
}
class Class02: Class01() {
fun showSuperClassProperty() = println(super.property01)
}
open class Class01 {
val property01 = "Class01.property01"
}
内部クラスで外部クラスのスーパークラスにアクセスする場合は、superキーワードの後に「@外部クラス名」と書く必要があります。
fun main() {
OuterClass().method01()
// 実行結果
// BaseClass property01
// OuterClass Property02
}
class OuterClass: BaseClass() {
val property02 = "OuterClass Property02"
fun method01() {
val innerClass = InnerClass()
innerClass.showSuperClassProperty()
innerClass.showOuterClassProperty()
}
inner class InnerClass {
fun showSuperClassProperty() = println(super@OuterClass.property01)
fun showOuterClassProperty() = println(property02)
}
}
open class BaseClass {
val property01 = "BaseClass property01"
}
上書きのルール
Kotlinでは実装の継承に下記の制限があります。
あるクラスが複数の親クラスから同じ名前のメソッドや変数を継承する場合、どの親クラスのメソッドや変数を使うべきかわからなくなってしまうため、子クラスでそれらを上書きしなければなりません。
例とともに見ていきます。
fun main() {
Class03().method01()
}
class Class03: BaseClass(), Interface {
override fun method01() = println("Class03 method01")
}
open class BaseClass {
open fun method01() = println("BaseClass method01")
}
interface Interface {
fun method01() = println("Interface method01")
}
method01を上書きしない場合, 下記エラーが表示されます。
Class ‘Class03’ must override public open fun method01(): Unit defined in BaseClass because it inherits many implementations of it
訳: method01は複数箇所から継承しているため、Class03ではBaseClassで定義されているmethod01を上書きしてください。
まとめると、もしmethod01をClass03で上書きしない場合、コンパイラはmain()で指定されているmethod01がBaseClassのものを指しているのか、Interfaceのものを指しているのかわかりません。
それを防ぐために、複数箇所から同じ名前のメンバーを継承する際は継承元で上書きするルールがあります。
また特定の継承元のメソッドを使用したい場合は下記のようにsuper<Class名>で指定できます。
fun main() {
Class03().method01()
// 実行結果
// Interface method01
}
class Class03: BaseClass(), Interface {
override fun method01() = super<Interface>.method01()
}
open class BaseClass {
open fun method01() = println("BaseClass method01")
}
interface Interface {
fun method01() = println("Interface method01")
}
どちらにせよ上書きは必須です。