読者です 読者をやめる 読者になる 読者になる

算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

ことりんのクラスと継承とオーバライドのお話

Kotlin

今回の記事からKotlinのオブジェクト指向的な話をしていきます。
Kotlinは高次関数や関数リテラルなど関数型言語的な側面を持ちますが、Kotlinはオブジェクト指向言語であると主張されています。

※下記URLのサイトを参考にしました。英語、または技術的な知識が至らず、内容に誤りが含まれるおそれがありますので、ご了承ください。
※本エントリの8割は参考サイトの翻訳です。残りの2割は私の解釈で加筆や変更を施した構成です。

参考サイト http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritance

クラスと主要コンストラクタ

Kotlinでは次のようにクラスを宣言します。

class Circle(x : Double, y : Double, radius : Double) {
  val x = x
  val y = y
  val radius = radius
}

最初の行に注目してください。これはCircleクラスの宣言です。このクラスは、Double型のパラメータを3つ取るコンストラクタを持っています。このようなクラスヘッダの直後で宣言されているコンストラクタを主要コンストラクタ(primary constructor)と呼びます。
Circleクラスの新しいインスタンスを生成するために、そのコンストラクタを通常の関数のように呼び出します。

val circle = Circle(0.0, 0.0, 1.0)

(注意:newキーワードは不要。)

コンストラクタの宣言がないクラスはインスタンス化できません。

class MyClass{}
val myClass = MyClass() //コンパイルエラー

注意:コンストラクタを持たないクラスは、いかなる状態(不可視フィールドを含むプロパティ)も宣言できない。また、コンストラクタを持つクラスを継承することもできない。

主要コンストラクタは、プロパティのイニシャライザを宣言順に実行します。このイニシャライザはコンストラクタの引数を使用できます。上記の例でコンストラクタが行う唯一のことは、x, y, radius の初期化です。
さらに、クラスの本体に「匿名のイニシャライザ」(コードを中括弧でくくったもの)を置くことができます。

class Hoge(param : String) {
  val list = ArrayList<String>()
  {
    list.add(param)
  }
}

ここで、主要コンストラクタはArrayListを生成し、それを list に割り当て、 param の値をそのリストに追加します。

その場で対応するプロパティを宣言するために、主要コンストラクタのパラメータには接頭辞として val または varキーワードを付けられます。

class Circle(val x : Double, val y : Double, val radius : Double)

クラス本体は任意です。上記の例は、x と y と radius という3つのプロパティを持ち、それらを明白に初期化するための3つのパラメータを含む主要コンストラクタを持ったクラスの完全な宣言です。
そのためKotlinでは、Beanスタイルのデータクラスを宣言するのにたくさんのコードを書かなくて済みます。少なくともJavaよりかは。

クラスメンバ

クラスは次のような種類のメンバを持ちます

  • 関数
  • プロパティ
  • その他のクラス
  • Object declaration

継承

Kotlinでは、すべてのクラスがAnyクラスをスーパークラスとして持ちます。これはデフォルト(スーパークラスが宣言されていないとき)のスーパークラスです。Javaで言うObjectクラスのようなものです。
でも Any は java.lang.Object ではありません。Anyはいかなるメンバも持っていません(equals(), hashCode(), toString()でさえも)。任意のオブジェクトでtoString()を呼べない、という意味ではありません。呼び出すことはできますが、それは拡張関数によるものでしょう。(詳細についてはまた今度)
明示的なスーパータイプ(基底型)を宣言するには、クラスヘッダのコロンの後にその型を置きます。次のコードを見てください*1

open class Base(p : Int)

class Derived(p : Int) : Base(p)

ご覧のとおり、基底型はその場で、主要コンストラクタの引数を使って初期化できます(というか、初期化しなければなりません)。

openアノテーションはJavafinalとは逆です。つまり、そのクラスを他のクラスで継承することを許可するのです。Kotlinではすべてのクラスがデフォルトでfinalです。これはEffective Javaの項目17「Design and document for inheritance or else prohibit it*2」に対応しています。

メンバのオーバライド

これまで述べてきたとおり、Kotlinは物事を明示的にすることに固執しています。Javaとは異なり、Kotlinではメンバをオーバライドするためには明示的なアノテーションが必須です。openアノテーションとoverrideアノテーションです。次のコードを見てください*3

open class Base {
  open fun v() {}
  fun nv() {}
}
class Derived() : Base {
  override fun v() {}
}

Derived.v()にはoverrideアノテーションが必須です。もしそれが見つからないと、コンパイラは文句を言ってきます。Base.nv()のように関数にopenアノテーションが付いていない場合、サブクラスで同じシグネチャを持つメソッドは宣言できません。overrideが付いていようが、いまいが。finalクラス(openアノテーションが付いていないクラス)ではopenメンバは宣言できません。

overrideが付いたメンバはオープンです。つまり、サブクラスでオーバライドされるかも知れません。再オーバライドを禁止したい場合はfinalを使います。次のコードを見てください*4

open class AnotherDerived() : Base {
  final override fun v() {}
}
待った!どうやってライブラリをハックすればいいんだ?!

Kotlinにおけるオーバライディングのアプローチ(デフォルトでクラスやメンバがfinalであること)の問題は、ライブラリ設計者の意図していないオーバライドが困難であることと、そういったオーバライドが汚いハックをもたらしてしまうことです。
これは次の理由で不利ではありません。

  1. これらのハックをとにかく許可すべきではない、というベストプラクティスがある
  2. みんな、同様のアプローチを持つ他の言語(C++, C#)をうまく使っている
  3. 本当にハックしたいなら、まだ方法はある:場合によってはJavaで書けるし、アスペクト・フレームワークは常にこれらを目的として働く

*1:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用

*2:邦訳は「継承のために設計および文書化する、でなければ継承を禁止する」

*3:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用

*4:http://confluence.jetbrains.net/display/Kotlin/Classes+and+Inheritanceより引用