Help us understand the problem. What is going on with this article?

Kotlinのクラスの継承とメソッドのオーバーライド、あとポエム

More than 3 years have passed since last update.

 どうも、第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のクラス継承とオーバーライドの失敗を紹介します。

DefaultExecutor
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クラスを継承しメソッドをオーバーライドし、実処理をカスタマイズしてみましょう。

実処理をカスタマイズするCustomizedExecutor(間違いあり)
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メソッドを間違えてオーバーライドしようとしたら、コンパイルエラーになって間違えに気づくことができますね!

コンパイルエラーになるCustomizedExecutor
package com.mrstar.extend_and_override;

public class CustomizedExecutor extends DefaultExecutor{
    // コンパイルエラー、executeはオーバーライドできない!!!
    @Override
    public void execute(){ /*中略*/ }
}

Kotlinのクラスの継承とメソッドのオーバーライド

 前置きが長くなってしまいましたが、お待ちかねKotlinです。

 Kotlinの継承とメソッドのオーバーライドはどうなっているのでしょうか?

 公式サイト、Classes and InheritanceのInheritanceOverriding Membersより

 Kotlinではもし継承及びオーバーライドをしたいのであれば、

  • 継承元のクラスにopen修飾子を必ずつける
  • メソッドをオーバーライドするには、継承元のスーパークラスでそのメソッドにopen修飾子をつける
  • メソッドをオーバーライドするには、継承するサブクラスでそのメソッドにoverride修飾子が必ずつける

となっています。

 Javaではfinalをつけることで継承・オーバーライドを禁止にしていました。Javaでは何もしなければ継承・オーバーライドができます。

 しかしKotlinでは明示的に修飾子をつけないと継承・オーバーライドができません。逆ですね。これは継承していいよ、オーバーライドしていいよ、というのを継承元のスーパークラスの提供者側が、明示的に示す必要があります。

 ではコードを見てましょう。

継承とオーバーライドの例、main.kt
// 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#でクラスの継承とメソッドのオーバーライドをしてみましょう。

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修飾子が付いているクラスを継承して見ましょう。

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")
}

 上記のOpenクラスを継承したJavaのコードを下記にしまします。

OpenClassを継承したExtendOpenClass
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継承できるか試してみましょう。

NotOpenClass
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のバイトコードを見てみましょう。

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クラスのバイトコードを見てみましょう。

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クラスName
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がつくようです。

RyotaMurohoshi
プログラミングが大好きで、 C#が大好きで、 .NETが大好きで、 LINQが大好きで、 JVM言語が大好きで、 ゲームで遊ぶことが大好きで、 ゲーム開発が大好きで、 頑張るのが大好きで、 Unityが大好きだったから...!
http://mrstar-memo.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした