- Kotlin
Kotlinクラス継承の書き方 open・override・abstract
Kotlin のクラス継承は、親クラスに open を付け、子クラスで「class 子 : 親(引数)」と書き、上書きするメンバーに override を付ける、の3点で成り立ちます。 Kotlin のクラスとメンバーは最初から final(継承・上書きできない状態)なので、Java と同じ感覚で書くと open の付け忘れでコンパイルエラーになります。
この記事では、open / final / abstract / override / final override をクラス・メソッド・プロパティ別にまとめた早見表を最初に置き、続いて親と子の init・プロパティ初期化子・コンストラクタが走る順番を println の出力で確かめます。親の init から open メンバーを呼ぶと null が見える現象と直し方、open 忘れ・override 忘れなどのエラー文も、kotlinc が出したままの形で載せています。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc-jvm)/ OpenJDK 21.0.10。掲載した出力とエラー文は、すべてこの環境でコンパイル・実行したものです。
Kotlin 継承の修飾子 早見表
| 修飾子 | クラスに付けたとき | メソッドに付けたとき | プロパティに付けたとき |
|---|---|---|---|
| (何も付けない) | final。継承できない | final。上書きできない | final。上書きできない |
open |
継承できる | 子で上書きできる(しなくてもよい) | 子で上書きできる(しなくてもよい) |
final |
継承できない(既定と同じ) | 上書きできない(既定と同じ) | 上書きできない(既定と同じ) |
abstract |
インスタンスを作れない。継承される前提 | 本体なし。子が必ず上書きする | 初期値なし。子が必ず上書きする |
override |
(クラスには付けない) | 親のメソッドを上書き。さらに下の子も上書きできる | 親のプロパティを上書き。さらに下の子も上書きできる |
final override |
(クラスには付けない) | 上書きしたうえで、下の子の上書きを止める | 上書きしたうえで、下の子の上書きを止める |
書き方の対応は次のとおりです。
| やりたいこと | 書き方 |
|---|---|
| 継承できるクラスを作る | open class Animal(val name: String) |
| 継承する(親のコンストラクタを呼ぶ) | class Dog(name: String) : Animal(name) |
| メソッドを上書きする | 親 open fun speak() / 子 override fun speak() |
| プロパティを上書きする | 親 open val legs = 4 / 子 override val legs = 2 |
| 親の実装を呼ぶ | super.speak() |
| 必ず上書きさせる | abstract class Shape { abstract fun area(): Double } |
| これ以上の上書きを止める | final override fun run() |
| セカンダリコンストラクタから親を呼ぶ | constructor(name: String) : super(name) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinクラス継承の書き方 open・override・abstract」
継承の基本形:open と override
まずは、コピーしてそのまま動く最小の例です。
open class Animal(val name: String) {
open val legs: Int = 4
open fun speak(): String = "..."
fun intro(): String = "${name}(足は${legs}本): ${speak()}"
}
class Dog(name: String) : Animal(name) {
override fun speak(): String = "ワン"
}
class Bird(name: String) : Animal(name) {
override val legs: Int = 2
override fun speak(): String = "ピヨ"
}
fun main() {
val animals: List<Animal> = listOf(Animal("なにか"), Dog("ポチ"), Bird("ピー"))
for (a in animals) println(a.intro())
}
実行コマンドと出力です。日本語を正しく表示するため、java に -Dstdout.encoding=UTF-8 を付けています。
kotlinc basic.kt -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -jar basic.jar
なにか(足は4本): ...
ポチ(足は4本): ワン
ピー(足は2本): ピヨ
ポイントは4つです。
- 親クラスに open:
open class Animal。付けないと継承できません。 - 継承はコロン:
class Dog(name: String) : Animal(name)。Java の extends にあたり、同時に親のコンストラクタを呼んでいます。 - 上書きされる側に open、上書きする側に override:どちらも省略できません。
- open を付けていない intro() は final:子クラスで上書きできず、どの子でも同じ動きになります。
Dog のコンストラクタ引数 name には val を付けていません。name プロパティは親の Animal が持っているので、子は受け取った値を親へ渡すだけにします。
open・override を忘れたときのエラー文
Kotlin 2.4.20 の kotlinc が出すエラー文を、原因と直し方とあわせてまとめます。エラー文で検索してたどり着いた場合は、この表から該当行を探してください。
| エラー文(kotlinc 2.4.20) | 原因 | 直し方 |
|---|---|---|
| this type is final, so it cannot be extended. | 親クラスに open がない | 親を open class にする |
| 'speak' hides member of supertype 'Animal' and needs an 'override' modifier. | 子に override がない | 子のメンバーに override を付ける |
| 'speak' in 'Animal' is final and cannot be overridden. | 親のメンバーに open がない | 親のメンバーに open を付ける |
| 'speek' overrides nothing. | 親に同じ名前・引数のメンバーがない | 名前と引数の型を親に合わせる |
| this type has a constructor, so it must be initialized here. | 親の後ろに () がない |
: Animal(name) のように呼ぶ |
| no value passed for parameter 'name'. | 親コンストラクタの引数が足りない | 必要な引数を渡す |
| class 'Rect' is not abstract and does not implement abstract base class member | abstract メンバーを上書きしていない | 子で override する |
| cannot create an instance of an abstract class. | 抽象クラスを直接生成した | 具体的な子クラスを生成する |
open 忘れ:this type is final
class Animal(val name: String)
class Dog(name: String) : Animal(name)
e1.kt:3:27: error: this type is final, so it cannot be extended.
class Dog(name: String) : Animal(name)
^^^^^^
親クラスの宣言を open class Animal に変えると通ります。
override 忘れ:hides member of supertype
open class Animal {
open fun speak() = "..."
open val legs = 4
}
class Dog : Animal() {
fun speak() = "ワン"
val legs = 4
}
e2.kt:7:9: error: 'speak' hides member of supertype 'Animal' and needs an 'override' modifier.
fun speak() = "ワン"
^^^^^
e2.kt:8:9: error: 'legs' hides member of supertype 'Animal' and needs an 'override' modifier.
val legs = 4
^^^^
Java では @Override は任意ですが、Kotlin の override は必須のキーワードです。親と同じ名前のメンバーを書いたつもりがなくても、偶然重なればこのエラーで知らせてくれます。
親のメンバーに open がない:is final and cannot be overridden
open class Animal {
fun speak() = "..."
val legs = 4
}
class Dog : Animal() {
override fun speak() = "ワン"
override val legs = 4
}
e3.kt:7:5: error: 'speak' in 'Animal' is final and cannot be overridden.
override fun speak() = "ワン"
^^^^^^^^
e3.kt:8:5: error: 'legs' in 'Animal' is final and cannot be overridden.
override val legs = 4
^^^^^^^^
クラスを open にしても、メンバーは final のままです。上書きを許したいメンバーごとに open を付けます。
名前の打ち間違い:overrides nothing
open class Animal {
open fun speak() = "..."
}
class Dog : Animal() {
override fun speek() = "ワン"
}
e5.kt:6:5: error: 'speek' overrides nothing.
override fun speek() = "ワン"
^^^^^^^^
override を付けたのに親に該当するメンバーがないと、このエラーになります。名前のつづりのほか、引数の型や個数が親と違う場合にも出ます。
親コンストラクタの呼び忘れ
open class Animal(val name: String)
class Dog(name: String) : Animal
class Cat : Animal()
e6.kt:3:10: error: no value passed for parameter 'name'.
class Dog(name: String) : Animal
^^^^^^^^^^^^^^
e6.kt:3:27: error: this type has a constructor, so it must be initialized here.
class Dog(name: String) : Animal
^^^^^^
e6.kt:5:13: error: no value passed for parameter 'name'.
class Cat : Animal()
^^^^^^
子クラスにプライマリコンストラクタがあるときは、親の型名のすぐ後ろで親のコンストラクタを呼びます。引数のない親でも : Animal() とかっこが必要です。
戻り値の型・可視性を変えたとき
上書きでは、戻り値の型を親と違う型にしたり、可視性を狭めたりできません。
open class Animal {
open fun speak(): String = "..."
protected open fun secret() {}
}
class Dog : Animal() {
override fun speak(): Int = 1
private override fun secret() {}
}
e8.kt:7:27: error: return type of 'fun speak(): Int' is not a subtype of the return type of the overridden member 'fun speak(): String' defined in 'Animal'.
override fun speak(): Int = 1
^^^
e8.kt:8:5: error: cannot weaken access privilege private for 'secret' in 'Animal'.
private override fun secret() {}
^^^^^^^
e8.kt:8:5: error: modifier 'private' is incompatible with 'override'.
private override fun secret() {}
^^^^^^^
e8.kt:8:13: error: modifier 'override' is incompatible with 'private'.
private override fun secret() {}
^^^^^^^^
エラー文に「not a subtype of」とあるとおり、戻り値は親の型のサブタイプであれば変えられます。可視性は、protected を public override で public に広げる向きなら通ります。
親と子の init・初期化子・コンストラクタが走る順番
継承で最もつまずきやすいのが初期化の順番です。親と子のすべての箇所に println を仕込んで確かめます。
fun log(msg: String): String {
println(msg)
return msg
}
open class Parent(name: String) {
val a = log("1. 親: プロパティ初期化子 a")
init {
log("2. 親: init ブロック(name=$name)")
}
val b = log("3. 親: プロパティ初期化子 b")
constructor() : this(log("0. 親: セカンダリが this(...) の引数を評価")) {
log("4. 親: セカンダリコンストラクタの本体")
}
}
class Child : Parent {
val c = log("5. 子: プロパティ初期化子 c")
init {
log("6. 子: init ブロック")
}
constructor() : super() {
log("7. 子: セカンダリコンストラクタの本体")
}
}
fun main() {
Child()
}
0. 親: セカンダリが this(...) の引数を評価
1. 親: プロパティ初期化子 a
2. 親: init ブロック(name=0. 親: セカンダリが this(...) の引数を評価)
3. 親: プロパティ初期化子 b
4. 親: セカンダリコンストラクタの本体
5. 子: プロパティ初期化子 c
6. 子: init ブロック
7. 子: セカンダリコンストラクタの本体
出力から読み取れる順番を表にします。
| 順番 | 走るもの | 補足 |
|---|---|---|
| 1 | 親コンストラクタへ渡す引数の評価 | 子の側に書いた式でも、最初に評価される |
| 2 | 親のプロパティ初期化子と init ブロック | ソースに書いた順(上から下)に、混ざって走る |
| 3 | 親のセカンダリコンストラクタの本体 | セカンダリ経由で生成したときだけ |
| 4 | 子のプロパティ初期化子と init ブロック | こちらも書いた順 |
| 5 | 子のセカンダリコンストラクタの本体 | セカンダリ経由で生成したときだけ |
「親がすべて終わってから子が始まる」「同じクラスの中では、初期化子と init が書いた順に走る」 の2点を押さえれば、どんな組み合わせでも順番を読めます。プライマリコンストラクタだけの場合も同じです。
open class Base(val name: String) {
init { println("親 init: name=$name") }
}
class Derived(name: String, val size: Int) : Base(name.also { println("親コンストラクタの引数を評価: $it") }) {
init { println("子 init: size=$size") }
}
fun main() {
Derived("box", 3)
}
親コンストラクタの引数を評価: box
親 init: name=box
子 init: size=3
Kotlin 公式ドキュメントの Inheritance のページにも、派生クラスの生成では基底クラスの初期化が最初の段階として行われ、その前にあるのは基底クラスのコンストラクタ引数の評価だけ、と書かれています。
親の init から open メンバーを呼ぶと null が見える
親が終わってから子が始まる、という順番から、次の現象が起きます。親の init やプロパティ初期化子が走っている時点では、子のプロパティはまだ初期化されていません。
open class Screen {
open val title: String = "親のタイトル"
open val width: Int = 100
init {
println("親 init: title=$title, width=$width, describe()=${describe()}")
}
open fun describe(): String = "Screen"
}
class DetailScreen : Screen() {
override val title: String = "詳細"
override val width: Int = 320
private val label: String = "ラベル"
override fun describe(): String = "DetailScreen(label=$label)"
}
fun main() {
val s = DetailScreen()
println("生成後 : title=${s.title}, width=${s.width}, describe()=${s.describe()}")
}
親 init: title=null, width=0, describe()=DetailScreen(label=null)
生成後 : title=詳細, width=320, describe()=DetailScreen(label=ラベル)
title は String(null を許さない型)なのに、親の init の中では null が、Int の width は 0 が見えています。「親のタイトル」でも「詳細」でもない点に注目してください。理由は次のとおりです。
- 親の init が title を読むと、上書きされた子のゲッターが呼ばれる
- 子のゲッターは、子のフィールドを返す
- 子のフィールドに "詳細" を入れる処理(子のプロパティ初期化子)は、親の初期化が終わってから走る
- そのため、まだ何も入っていない初期状態(参照型は null、Int は 0)が返る
describe() も同じで、子の実装が呼ばれ、その中で読んだ label がまだ null です。このコードは Kotlin 2.4.20 の kotlinc で警告なしにコンパイルできるため、コンパイル時には気づきにくい現象です。
文字列として表示するだけなら null と出て終わりますが、メソッドを呼ぶと実行時に止まります。
open class Screen {
open val title: String = "親のタイトル"
init {
println("文字数: ${title.length}")
}
}
class DetailScreen : Screen() {
override val title: String = "詳細"
}
fun main() {
DetailScreen()
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Screen.getTitle()" is null
at Screen.<init>(npe.kt:4)
at DetailScreen.<init>(npe.kt:8)
at NpeKt.main(npe.kt:13)
at NpeKt.main(npe.kt)
直し方:値はコンストラクタ引数で渡す
公式ドキュメントも、基底クラスを設計するときはコンストラクタ・プロパティ初期化子・init ブロックで open メンバーを使うのを避けるよう勧めています。直し方は2つあります。
// 直し方1: 上書きではなく、親のコンストラクタ引数で渡す
open class Screen(val title: String, val width: Int) {
init {
println("親 init: title=$title, width=$width")
}
}
class DetailScreen : Screen(title = "詳細", width = 320)
// 直し方2: フィールドを持たないゲッターで上書きする
open class Screen2 {
open val title: String get() = "親のタイトル"
init {
println("親 init: title=$title")
}
}
class DetailScreen2 : Screen2() {
override val title: String get() = "詳細"
}
fun main() {
DetailScreen()
DetailScreen2()
}
親 init: title=詳細, width=320
親 init: title=詳細
| 直し方 | 仕組み | 向いている場面 |
|---|---|---|
| コンストラクタ引数で渡す | 引数は親の初期化より前に評価される | 子ごとに値が決まっているとき(第一候補) |
| ゲッターで上書きする | フィールドがないので、初期化の順番に左右されない | 固定値や計算で返せる値のとき |
| 親の init で open メンバーに触れない | 生成が終わってから別のメソッドで使う | 初期化処理を後から呼べるとき |
親の実装を呼ぶ super と、セカンダリコンストラクタ
super で親の処理に付け足す
override した中から super.メソッド名() と書くと、親の実装を呼べます。親の処理を残したまま前後に処理を足すときに使います。
open class Task {
open fun run() = println("Task.run")
open val priority: Int = 0
}
open class TimedTask : Task() {
final override fun run() {
println("計測開始")
super.run()
println("計測終了")
}
override var priority: Int = 5
}
class NightlyTask : TimedTask() {
override var priority: Int = 9
}
fun main() {
val t = NightlyTask()
t.run()
t.priority = 10
println(t.priority)
}
計測開始
Task.run
計測終了
10
この例には3つの要素が入っています。
- super.run():親 Task の実装を呼んでいます。
- final override:TimedTask の run() は「計測開始〜終了」で必ず囲みたいので、孫クラスでの上書きを止めています。
- val を var で上書き:親の
open val priorityを、子でoverride var priorityにできます。override したメンバーは open のままなので、NightlyTask でもう一度上書きできています。
逆向き(var を val で上書き)と、final override したメソッドの上書きは、次のエラーになります。
e7.kt:8:14: error: 'var' property 'var priority: Int' defined in 'Task' cannot be overridden by 'val' property 'val priority: Int'.
override val priority: Int = 5
^^^
e7.kt:12:5: error: 'run' in 'TimedTask' is final and cannot be overridden.
override fun run() {}
^^^^^^^^
val はゲッターだけ、var はゲッターとセッターを持ちます。子でセッターを足すことはできても、親が持っているセッターを取り除くことはできない、と考えると覚えやすくなります。
セカンダリコンストラクタから親を呼ぶ
子クラスにプライマリコンストラクタがない場合は、それぞれのセカンダリコンストラクタから super(...) で親を呼びます。親のコンストラクタが複数あれば、呼び分けられます。
open class Item(val name: String, val price: Int) {
constructor(name: String) : this(name, 0)
override fun toString() = "Item(name=$name, price=$price)"
}
class Book : Item {
val pages: Int
constructor(name: String, price: Int, pages: Int) : super(name, price) {
this.pages = pages
}
constructor(name: String) : super(name) {
this.pages = 0
}
override fun toString() = "Book(${super.toString()}, pages=$pages)"
}
fun main() {
println(Book("Kotlin入門", 2800, 320))
println(Book("見本誌"))
}
Book(Item(name=Kotlin入門, price=2800), pages=320)
Book(Item(name=見本誌, price=0), pages=0)
| 子クラスの形 | 親コンストラクタの呼び方 |
|---|---|
| プライマリコンストラクタあり | クラス宣言で class Book(name: String) : Item(name) |
| セカンダリコンストラクタだけ | 各コンストラクタで constructor(...) : super(...) |
| 同じクラスの別コンストラクタへ回す | constructor(...) : this(...) |
toString() のように Any が持つメソッドは最初から open なので、どのクラスでも override できます。
abstract class:必ず上書きさせる
abstract は「このメンバーは子クラスが必ず用意する」という約束です。abstract class はインスタンスを作れず、abstract メンバーには本体や初期値を書きません。abstract を付けたクラスとメンバーには open を重ねて書く必要はありません。
abstract class Shape(val name: String) {
abstract val corners: Int
abstract fun area(): Double
open fun label(): String = name
fun describe(): String = "${label()}: 角=${corners}, 面積=${"%.2f".format(area())}"
}
class Rect(val w: Double, val h: Double) : Shape("長方形") {
override val corners = 4
override fun area() = w * h
}
class Circle(val r: Double) : Shape("円") {
override val corners = 0
override fun area() = Math.PI * r * r
override fun label() = "${super.label()}(r=$r)"
}
fun main() {
val shapes: List<Shape> = listOf(Rect(3.0, 4.0), Circle(1.0))
shapes.forEach { println(it.describe()) }
}
長方形: 角=4, 面積=12.00
円(r=1.0): 角=0, 面積=3.14
抽象クラスの中のメンバーは、次の3種類を使い分けます。
| 種類 | 例 | 子クラスでの扱い |
|---|---|---|
| abstract メンバー | abstract fun area(): Double |
必ず override する |
| open メンバー | open fun label(): String = name |
必要なら override する |
| 何も付けないメンバー | fun describe(): String |
上書きできない(共通の処理) |
describe() の中で corners や area() を呼んでいますが、これは生成が終わった後に呼ばれるメソッドなので、前の章の初期化順の影響は受けません。
abstract まわりのエラー文
abstract class Shape {
abstract fun area(): Double
}
class Rect(val w: Double, val h: Double) : Shape()
fun main() {
val s = Shape()
}
e4.kt:5:1: error: class 'Rect' is not abstract and does not implement abstract base class member:
fun area(): Double
class Rect(val w: Double, val h: Double) : Shape()
^^^^^^^^^^
e4.kt:8:13: error: cannot create an instance of an abstract class.
val s = Shape()
^^^^^^^
abstract の書き方そのものを間違えた場合のエラー文も載せておきます。
| 書いたコード | エラー文(kotlinc 2.4.20) |
|---|---|
abstract fun f() {} |
function 'f' with a body cannot be abstract. |
abstract val x: Int = 1 |
property with initializer cannot be abstract. |
通常のクラスの中に abstract fun g() |
abstract function 'g' in non-abstract class 'B'. |
open・abstract・final override の選び方
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 継承させる予定がない | 何も付けない(final) | 既定のままが最も読みやすく、意図しない上書きが起きない |
| 既定の動きがあり、一部の子だけ変えたい | open |
上書きは任意 |
| 既定の動きがなく、子ごとに必ず決める | abstract |
書き忘れをコンパイルエラーで知らせてくれる |
| 途中の階層で動きを確定させたい | final override |
それより下の子での上書きを止める |
| 共通の手順を親に置き、部品だけ子に任せる | 手順は何も付けない、部品は abstract |
手順は全員同じ、部品だけ差し替え |
| data class に共通の親を持たせたい | 親を open class か abstract class にし、data class を子にする |
data class に open を付けると「modifier 'open' is incompatible with 'data'.」になるため |
クラスを open にするかどうか迷ったら、まず final のままにしておき、継承が必要になった時点で open を付ける進め方が扱いやすいです。open を後から足すのは1語の変更で済みます。
よくある質問
Q. Kotlin のクラスはなぜ最初から final なのですか? A. 継承されることを想定していないクラスが上書きされて、思わぬ動きになるのを防ぐためです。継承を許すクラスとメンバーだけに open と書くので、「どこが上書きされうるか」がコードから読み取れます。公式ドキュメントにも、Kotlin のクラスは既定で final であり、継承可能にするには open を付けると書かれています。
Q. override したメンバーを、さらに下の子で上書きできますか? A. できます。override を付けたメンバーは open の状態を引き継ぎます。止めたいときは final override と書きます。本文の TimedTask と NightlyTask の例で、両方の動きを確かめています。
Q. 複数のクラスを同時に継承できますか? A. 親にできるクラスは1つだけです。複数の型を組み合わせたいときは、クラス1つに加えてインターフェースを複数実装します。
Q. 親の init で子のプロパティが null や 0 に見えるのは不具合ですか? A. 初期化の順番どおりの動きです。親の初期化が終わってから子のプロパティ初期化子が走るため、親の init の時点では子のフィールドに値が入っていません。子ごとの値はコンストラクタ引数で親へ渡すと、親の init でも正しい値を読めます。
まとめ
- Kotlin のクラスとメンバーは既定で final。継承させるクラスと、上書きさせるメンバーの両方に open を付ける
- 継承は「class 子 : 親(引数)」。親のコンストラクタは必ず呼び、セカンダリコンストラクタからは super(...) で呼ぶ
- override は必須のキーワード。付け忘れ・open 忘れ・打ち間違いは、それぞれ別のエラー文で知らせてくれる
- override したメンバーは open のまま。下の子での上書きを止めるには final override
- 初期化は「親の初期化子と init → 親のコンストラクタ本体 → 子の初期化子と init → 子のコンストラクタ本体」 の順。同じクラス内は書いた順
- 親の init から open メンバーを呼ぶと、子の値はまだ null や 0。値はコンストラクタ引数で渡すか、ゲッターで上書きする
- abstract は必ず上書きさせる約束。既定の動きがあるなら open、ないなら abstract を選ぶ
参考・出典
- Kotlin 公式ドキュメント「Inheritance」 https://kotlinlang.org/docs/inheritance.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlinクラス継承の書き方 open・override・abstract」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。