どうも、第3回 かわいいKotlin勉強会 #jkugで「普段C#を使っている僕から見たKotlin」ってタイトルで発表したら、予想以上にAndroiderの方が多くて聞いてくださる層を間違えて、申し訳ない気持ちと焦りで冷や汗かきまくったむろほしです。
この投稿は2015年、Kotlinアドベントカレンダー 9日目の投稿です。前日8日目はhkurokawaさんのKotlin Inline Functionsです。
本投稿では、Kotlinの継承とメソッドのオーバーライドについて紹介します。あと、Javaしか知らなかった昔の自分、それと同じような方向けのポエムな内容も含んでいます。
本投稿は、Kotlinのバージョン、beta3_/1.0.0-bata-3595-U143-26で検証を行いました。
はじめに
4年前とちょっと前の2011年12月1日、プログラマとして働き始めた私はJavaしか知りませんでした。当時のそんな私は
「オブジェクト指向言語のクラスの継承とメソッドのオーバーライド」
というのは、
「サブクラスで継承元のスーパークラスを指定し、メソッドをオーバーライドすれば、スーバークラスで定義したメソッドの挙動を書き換えることができる」
という風に思っていました。(当時読んだある本もそんな感じで書いてあった気がします。)
この投稿では、
『それは"Javaにおける"のクラスの継承とメソッドのオーバーライドであり、他の言語では異なる』
ということを、4年前の自分に、そして同じことを思っている(かもしれない)Javaしかしらない学生さんやプログラマの方に、Kotlin(とついでにC#も)を通じてお伝えできればと思います。
あなたが当たり前だと思っていることは、限られた範囲の当たり前なだけで、当たり前ではないかもしれませんよ。
自分がやってしまったJavaの継承・オーバーライドの失敗
この節ではだいぶ昔に私がやってしまったJavaのクラス継承とオーバーライドの失敗を紹介します。
package com.mrstar.extend_and_override;
public class DefaultExecutor {
public void execute(){
prepare();
executeImpl();
cleanup();
}
protected void executeImpl(){/*中略*/}
private void prepare(){ /*中略*/ }
private void cleanup(){ /*中略*/ }
/* 中略 */
}
DefaultExecutorクラスは、前処理(prepareで行う)と後処理(cleanupで行う)が必要な、ある処理を行うクラスです。executeImplメソッドをオーバーライドすることで行わせたい実処理をカスタマイズすることが可能です。
では、DefaultExecutorクラスを継承しメソッドをオーバーライドし、実処理をカスタマイズしてみましょう。
package com.mrstar.extend_and_override;
public class CustomizedExecutor extends DefaultExecutor{
@Override
public void execute(){ /*中略*/ }
}
ダメな点に気がつきましたか?
何のメソッドをオーバーライドしていますか?executeメソッドをオーバーライドしていますね。これだと、executeメソッドを呼び出した際、prepareメソッドとcleanupメソッドが呼ばれずに困ってしまいます。本来ならexecuteImplメソッドをオーバーライドしなければいけないのに!
ハードな日々が続くとこういうアホなミスをやらかして数時間無駄にしてしまいます。アホらし。
蛇足な捕捉
蛇足ですが、「DefaultExecutorクラスを抽象クラス、executeImplメソッドを抽象メソッドにすればいいのでは?」と突っ込みたくなった方もいるかと思います。記憶が定かじゃないのですが、たしか継承元のexecutImplの実装を、継承した側で使う必要があったりしたのかな?まぁサンプルのためのあれだと思って甘く見てくれると嬉しいです。
こうだったらよかったのに
さて前の節では、継承してはいけないメソッドを誤って継承して、思わぬ不具合を生んでしまいました。このミスを防ぐにはどうしたらいいでしょうか?
解決策の一つは、executeをオーバーライドできなくすることですね。executeメソッドにfinal修飾子をつけてみましょう。
package com.mrstar.extend_and_override;
public class DefaultExecutor {
// final修飾子がついた!
public final void execute(){
prepare();
executeImpl();
cleanup();
}
/* 中略 */
}
こうすればうっかりexecuteメソッドを間違えてオーバーライドしようとしたら、コンパイルエラーになって間違えに気づくことができますね!
package com.mrstar.extend_and_override;
public class CustomizedExecutor extends DefaultExecutor{
// コンパイルエラー、executeはオーバーライドできない!!!
@Override
public void execute(){ /*中略*/ }
}
Kotlinのクラスの継承とメソッドのオーバーライド
前置きが長くなってしまいましたが、お待ちかねKotlinです。
Kotlinの継承とメソッドのオーバーライドはどうなっているのでしょうか?
公式サイト、Classes and InheritanceのInheritanceとOverriding Membersより
Kotlinではもし継承及びオーバーライドをしたいのであれば、
- 継承元のクラスにopen修飾子を必ずつける
- メソッドをオーバーライドするには、継承元のスーパークラスでそのメソッドにopen修飾子をつける
- メソッドをオーバーライドするには、継承するサブクラスでそのメソッドにoverride修飾子が必ずつける
となっています。
Javaではfinalをつけることで継承・オーバーライドを禁止にしていました。Javaでは何もしなければ継承・オーバーライドができます。
しかしKotlinでは明示的に修飾子をつけないと継承・オーバーライドができません。逆ですね。これは継承していいよ、オーバーライドしていいよ、というのを継承元のスーパークラスの提供者側が、明示的に示す必要があります。
ではコードを見てましょう。
// open修飾子が絶対必要
public open class Super() {
// open修飾子が絶対必要
public open fun printMessage() = println("This is Super.")
}
public class Sub():Super() {
// override修飾子が絶対必要
public override fun printMessage() = println("This is Sub.")
}
fun main(args: Array<String>) {
val subInSuperVariable: Super = Sub()
subInSuperVariable.printMessage()
}
私が失敗してしまったコードはどうかけるでしょうか?
public open class DefaultExecutor() {
public fun execute():Unit{
prepare();
executeImpl();
cleanup();
}
protected open fun executeImpl(){/*中略*/}
private fun prepare(){ /*中略*/ }
private fun cleanup(){ /*中略*/ }
/* 中略 */
}
public class CustomizedExecutor() :DefaultExecutor() {
protected override fun executeImpl(){/*中略*/}
/* 中略 */
}
- DefaultExecutorは継承前提ですので、open修飾子を明示的に付与します
- executeImplもオーバーライドされること前提ですので、open修飾子をつけます
- JavaではOverrideアノテーションは必須ではありませんでしたが、kotlinではoverride修飾子が必須ですのでexecuteImplにoverride修飾子を付与します
JavaとKotlinのクラス継承とメソッドのオーバーライド
Javaではメソッドをオーバライドするためにスーパークラス側では特に何も指定する必要がありません。(可視性的に可視ならば)何もしない状態で『標準』で、クラスの継承もメソッドのオーバライドも可能です。逆に修飾子を付与することでそれらを抑制します。Javaではfinalをクラスにつけることで継承を禁止に、finalをメソッドにつけることでオーバライドを禁止にします。
一方Kotlinはどうでしょうか?
Kotlinではメソッドをオーバライドするためにスーパークラス側で、クラスとメソッド両方にopen修飾子をつける必要があります。そしてオーバライドするサブクラス側にもoverride修飾子をつける必要があります。Kotlinでは、修飾子がなにも付与されていなければ、クラスは継承禁止ですしメソッドはオーバライド禁止になります。
継承してほしくないクラスをうっかり継承させてしまうことも、うっかりオーバーライドして欲しくないメソッドのオーバライドを許してしまうこともありません。意図的openをスーパクラス側につけないといけませんから。
Kotlinのこの仕様、もしかしたら「めんどくせぇよ」と思う方もいるかもしれませんね。
C#のクラスの継承とメソッドのオーバーライド
いきなりですが、ここでC#の登場です。C#でクラスの継承とメソッドのオーバーライドをしてみましょう。
class Base{
public void printMessge(){
Console.WriteLine ("This is Base.");
}
}
class Sub:Base{
public void printMessge(){
Console.WriteLine ("This is Sub.");
}
}
class MainClass
{
public static void Main (string[] args)
{
Base baseInBaseVariable = new Base ();
baseInBaseVariable.printMessge ();
Sub subInSubVariable = new Sub ();
subInSubVariable.printMessge ();
Base subInBaseVariable = new Sub ();
subInBaseVariable.printMessge ();
}
}
実行結果は以下のようになります。
This is Base.
This is Sub.
This is Base.
Javaしか知らない方は、3個目の結果は想定外ではないでしょうか?「Base型の変数にはいっているのはSub型のインスタンスなのだから、This is Sub.
と表示されるので?」と思ったのではないでしょうか?Javaしか知らなかった自分も初めてC#を触った時にはこの挙動に驚きました。(実はさっきのコードは警告が出ています。「Base型のメンバ、スーパークラスのメンバかくしてるのいいん?」って感じの警告です。)
この挙動のポイントは仮想メソッドです。
さて、3個目の出力でThis is Base.
でなくて、This is Sub.
と出力されるようにコードを変更してみましょう。
class Base{
// 注目: virtual修飾子が加わった
public virtual void printMessge(){
Console.WriteLine ("This is Base.");
}
}
class Sub:Base{
// 注目: override修飾子が加わった
public overrude void printMessge(){
Console.WriteLine ("This is Sub.");
}
}
class MainClass
{
public static void Main (string[] args)
{
Base baseInBaseVariable = new Base ();
baseInBaseVariable.printMessge ();
Sub subInSubVariable = new Sub ();
subInSubVariable.printMessge ();
Base subInBaseVariable = new Sub ();
subInBaseVariable.printMessge ();
}
}
BaseクラスのprintMessageメソッドにvirtual修飾子が、SubクラスのprintMessgeメソッドにoverride修飾子が加わりましたね。実行結果は以下の通りになります。
This is Base.
This is Sub.
This is Sub.
この投稿はKotlinアドベントカレンダーなので詳細は省略しますが、ポイントは仮想メソッドです。
そして大切なポイントは、「Javaではインスタンスメソッドが標準で仮想メソッドになっている」ということです。そのため、Javaではスーパークラスで何も修飾子をつけないでも、メソッドをオーバーライドすることができます
ちなみにMSDN 仮想メンバーにはこのような記述があります。
十分な理由があり、仮想メンバーのデザイン、テスト、および保守に関連するすべてのコストを認識している場合を除き、メンバーを仮想メンバーにしないようにします。
どうでしょうか?Javaだと当たり前にだと思っていた挙動、C#では当たり前ではないんです。
まとめ
Kotlinアドベントカレンダー8日目。この投稿では、Kotlinの継承とメソッドのオーバーライドを紹介しました。Kotlinの継承とメソッドのオーバーライドを理解していただけたでしょうか?
そしてもう一個、どちらかというとこっちの方がメインなのですが、この投稿を通じて、
「オブジェクト指向のクラス継承とオーバーライド」
とは
「サブクラスで継承元のスーパークラスを指定し、メソッドをオーバーライドすれば、スーバークラスで定義したメソッドの挙動を書き換えることができる」
と思っている方が、実はそれが
「Javaでのオブジェクト指向のクラス継承とオーバーライド」
であり、
「他のプログラミング言語では必ずしもそうではない」
ということに一人でも多く気づくきっかけになればと思います。
また、「あなたが当たり前」と思っいることは、「あなたが使っている言語」で当たり前なだけかもしれません。それがその言語の強みだと気づかなかったり、逆に実は弱みがあるかもしれません。他の言語を触ってみて初めて、その言語の特徴がわかることもあるのではないでしょうか。いろいろな言語を使ってみて、自分のメイン言語の理解を深めてはいかがでしょうか?
Kotlin成分薄めでしたので、補足として
- KotlinのクラスをJavaで継承・オーバーライドしてみよう
- バイトコードはopenの有無でどうなる?
をどうぞ。
補足1 KotlinのクラスをJavaで継承・オーバーライドしてみよう
Kotlinで作ったクラスをJavaで継承・オーバーライドしてみましょう。
Openなクラス
下記のようなopen修飾子が付いているクラスを継承して見ましょう。
package com.mrstar.kotlin_example.inheritance_override
public open class OpenClass() {
public open fun openMethod() = println("OpenClass.openMethod")
public fun notOpenMethod() = println("OpenClass.notOpenMethod")
}
上記のOpenクラスを継承したJavaのコードを下記にしまします。
package com.mrstar.kotlin_example.java;
import com.mrstar.kotlin_example.inheritance_override.OpenClass;
public class ExtendOpenClass extends OpenClass{
@Override
public void openMethod() {
System.out.println("ExtendOpenClass.openMethod");
}
// 下記はコンパイルエラー
// @Override
// public void notOpenMethod() {
// System.out.println("ExtendOpenClass.openMethod");
// }
}
クラスは継承可能で、open修飾子がついているopenMethodはオーバーライド可能です。しかしopen修飾子がついてないnotOpenMethodメソッドはオーバーライドができません。
KotlinのクラスをKotlinで継承・オーバーライドとする時と同様に、open修飾子がないとメソッドはオーバーライドできません。
Openでないクラス
次は、下記のようなopen修飾子が付いていないクラスをJava継承できるか試してみましょう。
package com.mrstar.kotlin_example.inheritance_override
public class NotOpenClass() {
public open fun openMethod() = println("NotOpenClass.openMethod")
public fun notOpenMethod() = println("NotOpenClass.notOpenMethod")
}
残念ながら、クラスが継承できませんでした。
package com.mrstar.kotlin_example.java;
import com.mrstar.kotlin_example.inheritance_override.OpenClass;
//public class ExtendNotOpenClass extends NotOpenClass{
//}
Kotlinで作ったopenが付けられていないクラスは、Javaでも継承ができないのですね。
またNotOpenClassクラスは、openでないクラスにopenなメソッドがついています。これ自体はコンパイルエラーにはなりませんが、次のような警告がでます。
Kotlin: "open" has no effect in a final class
補足2 バイトコードはopenの有無でどうなる?
Kotlinのopen修飾子の有無で、バイトコードはどのようになるのでしょうか?
Kotlinのバイトコードは、IntelliJ IDEAのメニューからTools > Kotlin > Show Kotlin Bytecodeと辿ると見ることができます。
まずは先ほども使った、NotOpenClassのバイトコードを見てみましょう。
package com.mrstar.kotlin_example.inheritance_override
public class NotOpenClass() {
public open fun openMethod() = println("NotOpenClass.openMethod")
public fun notOpenMethod() = println("NotOpenClass.notOpenMethod")
}
NotOpenClassクラスのバイトコードは以下の通りです。クラスの前とnotOpenMethodメソッドの前にfinalが付いていますね。
// ================com/mrstar/kotlin_example/inheritance_override/NotOpenClass.class =================
// class version 50.0 (50)
// access flags 0x31
public final class com/mrstar/kotlin_example/inheritance_override/NotOpenClass {
// access flags 0x1
public openMethod()V
L0
LINENUMBER 4 L0
LDC "NotOpenClass.openMethod"
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/NotOpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x11
public final notOpenMethod()V
L0
LINENUMBER 6 L0
LDC "NotOpenClass.notOpenMethod"
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/NotOpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/NotOpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
@Lkotlin/jvm/internal/KotlinClass;(version={1, 0, 0}, abiVersion=32, data={"\u0013\u0015\u0009A\"A\u0003\u0002\u0011\u0001)\u0011\u0001B\u0001\u0006\u0003!\u0009Q\u0001\u0001\u0007\u00013\u0005A\n!)\u0002R\u0007\u0005A\u0011!J\u0002\u0009\u00045\u0009\u0001DA\u0013\u0005\u0009-A)!D\u0001\u0019\u0005\u0001"}, strings={"Lcom/mrstar/kotlin_example/inheritance_override/NotOpenClass;", "", "()V", "notOpenMethod", "", "openMethod"}, moduleName="production sources for module HelloKotlin")
// compiled from: NotOpenClass.kt
}
次はOpenClassクラスのバイトコードを見てみましょう。
package com.mrstar.kotlin_example.inheritance_override
public open class OpenClass() {
public open fun openMethod() = println("OpenClass.openMethod")
public fun notOpenMethod() = println("OpenClass.notOpenMethod")
}
NotOpenClassクラスのバイトコードと異なり、クラスにfinalは付与されていませんね。
// ================com/mrstar/kotlin_example/inheritance_override/OpenClass.class =================
// class version 50.0 (50)
// access flags 0x21
public class com/mrstar/kotlin_example/inheritance_override/OpenClass {
// access flags 0x1
public openMethod()V
L0
LINENUMBER 4 L0
LDC "OpenClass.openMethod"
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/OpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x11
public final notOpenMethod()V
L0
LINENUMBER 6 L0
LDC "OpenClass.notOpenMethod"
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/OpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/mrstar/kotlin_example/inheritance_override/OpenClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
@Lkotlin/jvm/internal/KotlinClass;(version={1, 0, 0}, abiVersion=32, data={"\u0013\u0015\u0009A\"A\u0003\u0002\u0011\u0001)\u0011\u0001B\u0001\u0006\u0003!\u0009Q\u0001\u0001\u0003\u000c\u0019\u0001I\u0012\u0001'\u0001\"\u0006E\u001b\u0011\u0001C\u0001&\u0007!\rQ\"\u0001\r\u0003K\u0011!1\u0002#\u0002\u000e\u0003a\u0011\u0001"}, strings={"Lcom/mrstar/kotlin_example/inheritance_override/OpenClass;", "", "()V", "notOpenMethod", "", "openMethod"}, moduleName="production sources for module HelloKotlin")
// compiled from: OpenClass.kt
}
openとバイトコードに関しては、クラス・メソッドともに、Kotlinのコードにopenがついていなければバイトコードにfinalが付与され、逆にKotlinのコードにopenがついたらバイトコードにfinalがつかなくなるようですね。この結果は、先ほどの「補足1 KotlinのクラスをJavaで継承・オーバーライドしてみよう」と比較した場合、納得できますね。
脱線になりますが、
data class Name(val firstName:String, val lastName:String)
というようなdataクラスも
// ================Name.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Name {
//以下略
というように、バイトコードにfinalがつくようです。