[Kotlin]メソッドやプロパティのオーバーライド

今回はKotlinの継承その2。メソッドやプロパティのオーバーライドを行います。

使用するクラス

前回作成したスーパークラスとサブクラスです。今回もこのクラスを使用します。

open class Company(val name: String, _fund: Int){   //openで継承可能にする
    var fund = _fund    //アンダースコア付きは一時的な変数

    fun sell(): Int{
        fund += 10
        return fund
    }
}
//以下はサブクラス
class MyCompany(name: String, _fund: Int): Company(name, _fund)

メソッドのオーバーライド

今のところMyCompanyクラスはCompanyクラスのコピーでしかありません。CompanyクラスのメソッドをMyCompanyクラス独自のメソッドとして定義するには、メソッドをオーバーライドする(覆す)必要があります。それでは始めていきましょう。

スーパークラスの準備

まずはスーパークラス側の準備です。Companyクラスのsellメソッドに、クラスを継承可能にしたときと同じように「open」を追加します。

open fun sell(): Int{     //「open」を頭に付ける
        fund += 10
        return fund
}

クラスにopenを付けるとそのクラスは継承可能になりますが、中に含まれるメソッドやプロパティは、デフォルトの状態ではオーバーライドを許可していません。オーバーライドしたい場合はそのメソッドに個別に「open」を記述しましょう。

なお可視性修飾子が設定されている場合は、その後に書き入れます。

internal open fun sell(): Int

メソッドをオーバーライドする

次にMyCompany独自のsellメソッドを定義していきます。

class MyCompany(name: String, _fund: Int): Company(name, _fund){
    override fun sell(): Int{     //「override」を頭に付ける
        fund += 100
        return fund
    }
}

関数定義のキーワード「fun」の前に「override」を付けてください。これによってこのメソッドはスーパークラスのメソッドをオーバーライドした、サブクラス固有のメソッドとして定義されます。

これを使用してみましょう。

fun main() {
    val sony = MyCompany("SONY",1000)
    println(sony.sell())
}
//1100

sellメソッドはオーバーライドされ、資金に100を追加するメソッドに変更されました。

オーバーライドの制約

メソッドのオーバーライドでは、変更不可能な項目が複数存在するということはチェックしておいてください。例えば上のコードをこのように変更したとします。

class MyCompany(name: String, _fund: Int): Company(name, _fund){
    override fun sell(): String{     //戻り値をString型に変更
        fund += 100
        return "$fund"
    }
}

この状態でコードを実行してみると、

//Kotlin: Return type of 'sell' is not a subtype of the return type 
//of the overridden member 'public open fun sell(): Int defined in net.pouhon.Company'
sellメソッドの戻り値(String)は、Companyクラスのsellメソッドの戻り値(Int)と継承関係にない

戻り値の型が違うため、エラーになりました。オーバーライドして定義されたメソッドは、元のメソッドと

  • メソッド名
  • 引数の有無や数

が同じでなければなりません。さらに

  • 引数の型
  • 戻り値の型

が同じか、オーバーライド元のメソッドで指定した型のサブクラスでなければなりません。要は「これらの点が異なるメソッドなら、それはオーバーライドして作るべきものではない」ということです。

プロパティのオーバーライド

メソッドと同様、プロパティもオーバーライドすることができます。メソッドのときと同じように、継承可能プロパティには「open」が必要です。
今回はnameプロパティ、fundプロパティどちらもオーバーライドしてみます。

open class Company(open val name: String, _fund: Int){ //nameをopenに
    open var fund = _fund      //fundをopenに
}

コンストラクタで初期化されているプロパティにもopenを付けることが可能です。

次にサブクラス側。例によって「override」を頭に付けましょう。

class MyCompany(override var name: String, _fund: Int): Company(name, _fund){
    override var fund = 0
}

ここではこのクラスとプロパティが抽象クラス/抽象プロパティでない限り、初期化が必須であることに注意です。
そしてもう1つ、スーパークラスではvalで初期化されるプロパティを、varで初期化していることにも注目してください。これについては最後にお話したいと思います。

オーバーライド その他の原則

メソッドとプロパティのオーバーライドの基本的な方法については以上ですが、この他にも原則が存在します。ここではその他の原則を押さえておきましょう。

overrideが付いたメンバは、それ自体がopenである

スーパークラスを継承し、スーパークラスのメンバ(メソッドやプロパティ)をオーバーライドして作ったメンバは、それ自体がデフォルトでopenになります。
MyCompanyクラスと、新たに作成したMyCompanyを継承するクラス、MyShopを使って確認してみましょう。

//MyCompanyクラスにopenを付けて継承可能にする
open class MyCompany(name: String, _fund: Int): Company(name, _fund){
    override fun sell(): Int{     //Companyクラスから継承したメソッド
        fund += 100
        return fund
    }
}
class MyShop(name: String, _fund: Int): MyCompany(name, _fund){
    override fun sell(): Int{    //MyCompanyクラスから継承したメソッド
        return 100
    }
}

MyShopクラスのsellメソッドはMyCompanyクラスから継承したメソッドですが、MyCompanyクラスのsellメソッドには「open」がありません。
これはsellメソッドが、元々はCompanyクラスから継承されたメソッドだからです。このように一度他のクラスからオーバーライドして作られたメンバは、デフォルトで継承可能になります。

継承不可能にしたい場合、対象のメンバに「final」を付けましょう。

final override fun sell(): Int{     //「final」を頭に付ける
        fund += 100
        return fund
    }

これでsellメソッドは、MyCompanyを継承したクラスであってもオーバーライドできなくなります。

スーパークラスのメンバは「super」で参照する

「super」を使うことで、サブクラスからスーパークラスのメンバを参照できます。

open class Company(val name: String, _fund: Int){
    open var fund = _fund
    open fun sell(): Int{
        fund += 10
        return fund
    }
}

class MyCompany(name: String, _fund: Int): Company(name, _fund){
    //sellメソッド内でスーパークラスのメンバを参照
    override fun sell() = super.sell() + 10
    override var fund = super.fund
}

これ自体は単純なことです。ではもう少し複雑にして、複数のクラスから同名のメンバを継承した場合を考えてみましょう。

//継承元のクラス
open class Company(val name: String, _fund: Int){
    open var fund = _fund
    open fun sell(): Int{
        fund += 10
        return fund
    }
}

//継承元のインターフェイス
interface Company2{
    val fund: Int     //fundプロパティ
        get() = 0
    fun sell(): Int = 0   //sellメソッド
}

今回の継承元は2つ。1つは通常のクラス、もう1つはインターフェイスです。
この2つを継承し、プロパティはCompanyクラス、メソッドはCompany2インターフェイスのものを参照する場合、このような記述になります。

//MyCompanyはCompanyとCompany2を継承するクラス
class MyCompany(name: String, _fund: Int): Company(name, _fund), Company2 {
    override var fund = super<Company>.fund
    override fun sell() = super<Company2>.sell() + 10
}

参照先はsuper<Company>のようにsuperの後に<>を追加し、その中で「どのスーパークラスを参照するか」を指定します。

なおスーパークラスのどちらか片方に、参照するような「値が無い」のならこの記述は不要です。

valプロパティをvarとして継承できるのはなぜか?

最後にこの問題を検証しておきます。

open class Company(open val name: String, _fund: Int){...}
class MyCompany(override var name: String, _fund: Int): Company(name, _fund){...}

「スーパークラスでvalとして初期化されるプロパティを、サブクラスではvarとして初期化する」。これは一見すると非常におかしな挙動ですが、実際何が行われているのでしょうか?細かく見ていきましょう。

まずMyCompanyクラスのインスタンスを生成します。するとMyCompanyコンストラクタで引数にとったname引数は、スーパークラスのコンストラクタに渡されます。

Companyコンストラクタはnameを初期化します。nameは読み取り専用プロパティです。つまり自動的にゲッターが付与されます。

しかしここで、コンストラクタは大事なことに気が付きます。

openが付いたプロパティは、サブクラスに対してオープンでなければなりません。サブクラスでoverride指定がされているので、Companyコンストラクタはサブクラスのコンストラクタへnameプロパティを開示します。

MyCompanyが受け取ったnameはこの時点でvalです。しかしMyCompanyではvarとして初期化しなければなりません。コンストラクタはいとも簡単にこの仕事をやってのけます。

コンストラクタがやったことは、引き取ったnameにセッターを追加しただけ。これでこのプロパティの値はvarになります。

「逆もまた真なり」ではない

この逆、つまりスーパークラスでvarとして初期化されるプロパティを、サブクラスでvalに変更することはできません。セッターを後から付け加えることはできますが、元々設定されているセッターは削除できないというわけです。

日本語に訳されたリファレンスでは「逆もまた真なり」と書かれていますが、そうではありません。気を付けましょう。

You can also override a val property with a var property, but not vice versa.

valプロパティはvarプロパティにオーバーライドできるが、「逆もまた真なり」ではない

Kotlin Programming Languageより)

オーバーライドのまとめ

少々長くなってしまったので、今回の要点をまとめておきます。

  1. クラスのメンバをオーバーライドするには、スーパークラスに「open」、サブクラスに「override」が必要である
  2. オーバーライドしたメソッドは、元のメソッドと名前、引数の数と型、戻り値の型が同じか、互換性のある型でなければならない
  3. スーパークラスからオーバーライドしたメンバは、それ自体がopenである。それ以上の継承を防ぐには「final」を設定する
  4. サブクラス内からスーパークラスのメンバを参照するには「super」を使う
  5. スーパークラスでvalとして初期化されたプロパティは、サブクラスでvarに設定し直すことができるが、逆はできない
タイトルとURLをコピーしました