//Animalを継承したCatクラス。「extends」ではなく「:」で基底クラスを指定する
class Cat: Animal(){}
リスト1 基本的な継承の書き方
ポイントは2つで、まずKotlinのクラスはデフォルトでJavaのfinal(継承不可)の扱いとなっているため、基底クラスでは「open」というキーワードを付けて継承を許可しなければなりません。そして、継承するクラスではJavaの「extends」キーワードではなく、「:」(コロン)で基底クラスを指定します。C++やC#っぽい書き方ですね。
基底クラスのコンストラクタ呼び出し
続いて基底クラスのコンストラクタ呼び出しについて、リスト2を見てみましょう。
//基底クラス
open class Person(val firstName: String, val lastName: String = ""){……}
//継承クラス。継承元のコンストラクタを指定
class BussinessPerson(firstName: String, lastName: String = "", val age: Int) : Person(firstName, lastName){……}
リスト2 基底クラスのコンストラクタ呼び出し
「Person」クラスのプライマリコンストラクタでは「firstName」「lastName」の2つのプロパティが定義されています。それを継承したBussinessPersonクラスのコンストラクタでは、firstName、lastNameの2つの引数に加えてageプロパティを定義しています。そして、firstName、lastName引数については、基底クラスであるPersonクラスのコンストラクタに渡しています。
なお、プライマリコンストラクタが存在せず、セカンダリコンストラクタだけの場合はリスト3のように「super」キーワードを使って基底クラスのコンストラクタを呼び出します。
//プライマリコンストラクタのないクラス
class Derived : Base{
//セカンダリコンストラクタはsuperキーワードで直接基底クラスのコンストラクタを呼び出す
constructor(a: Any) : super(a){
}
//同上
constructor(a: Any, b: Any) : super(a, b){
}
}
リスト3 プライマリコンストラクタが存在しないケース
繰り返しとなりますが、セカンダリコンストラクタから直接基底クラスのコンストラクタを呼び出すのは、そのクラスにプライマリコンストラクタを定義しない場合のみです。プライマリコンストラクタがある場合は、前回解説した通り、セカンダリコンストラクタは最終的に必ずプライマリコンストラクタを呼び出す必要があります。
メソッドのオーバーライド
メソッドのオーバーライドについては、リスト4のようになります。
//基底クラス
open class Person(val firstName: String, val lastName: String = ""){
//「open」キーワードでオーバーライド可能に
open fun print(){
println("お名前: ${firstName} ${lastName}")
}
}
//継承クラス。継承元のコンストラクタを指定
class BussinessPerson(firstName: String, lastName: String = "", val age: Int) : Person(firstName, lastName){
//メソッドのオーバーライド
override fun print(){
println("お名前: ${firstName} ${lastName}, 年齢: $age")
}
}
……
var bussinessPerson = BussinessPerson("Tanaka", "Taro", 22)
bussinessPerson.print() //「お名前: Tanaka Taro, 年齢: 22」
リスト4 基底クラスのコンストラクタ呼び出しとメソッドのオーバーライド
Kotlinのメソッドはクラスと同様にデフォルトではオーバーライド不可のため、基底クラスで「open」キーワードを使ってメソッドをオーバーライド可能にしておく必要があります。また、継承先では「override」キーワードで、オーバーライドすることを明示的に宣言しなければなりません。
「デフォルトでクラスもメソッドも継承不可になっている」点がJavaとちょっと違う部分ですが、デフォルトを厳しめにすることで、継承に関わるさまざまなトラブルを避けられるようになっています。例えば、オーバーライドしたつもりがミスタイプしていた、なんてことも避けられますね。
Javaでも最近は@Overrideアノテーションを付けることで、同様のチェックが行えるようになっていますが、Kotlinでは言語レベルでサポートされています*1。
*1)C#なども同様の仕組みを取っています。
『Effective Java』(柴田芳樹 訳、丸善出版)というJavaプログラマー必見の名著がありますが、その「Item 17」では「継承のために設計および文書化する、そうではないなら継承を禁止する」という項目があり、オーバーライド可能なメソッドを絞り込むことなどが提案されています。
Kotlinでは、この原則に従い、意図せず継承可能なクラス、オーバーライド可能なメソッドを作り込んでしまわないよう、デフォルトで継承不可になっているようです。
抽象クラス
抽象クラスと抽象メソッドについてはリスト5のようになります。
//抽象クラス
abstract class Vehicle{
//抽象メソッド
abstract fun run()
}
//抽象クラスの具象化
class Car : Vehicle(){
//抽象メソッドのオーバーライド
override fun run(){
println("Running!")
}
}
……
//抽象クラスはインスタンス化できない
val vehicle = Vehicle() //→コンパイルエラー
//継承したクラスのインスタンス化と呼び出し
val car = Car()
car.run()
リスト5 抽象クラス
オーバーライド時にoverrideキーワードが必要な以外は、Javaとほぼ同様なので、細かな解説は省略します。
インタフェース
Kotlinのインタフェースの使い方はリスト6のようになります。
//インタフェースの定義例
interface Flyable{
fun fly()
}
//インタフェースの実装例
class Airplane: Vehicle(), Flyable{
override fun run(){
println("Running on runway!")
}
//インタフェースのメソッドを実装(実装しないとコンパイルエラー)
override fun fly(){
println("Flying!")
}
}
……
//インタフェースを実装したクラスの使用
val airplane = Airplane()
airplane.run() //「Running on runway!」
airplane.fly() //「Flying!」
リスト6 インタフェースの使用法
インタフェースを実装する際に、Javaの「implements」キーワードではなく、クラス継承と同様に「:」の後に記述する点に注意してください。また、インタフェースのメソッドを実装する際もoverrideキーワードを付けてオーバーライドを明示的に宣言する必要があります。
また、Java 8でインタフェースのデフォルト実装がサポートされましたが、Kotlinでもリスト7のように実装を持つインタフェースを使用できます。
//実装を持つインタフェースの定義例
interface Stoppable{
fun stop(){ print("Stopped slowly...") }
}
//実装を持つインタフェースの実装例
class StoppableVehicle: Vehicle(), Stoppable{
override fun run(){
println("Running!")
}
}
リスト7 実装を持つインタフェース
さて、Javaのインタフェースは基本的にメソッドを記述するのが中心で、フィールドについては全て定数で、自動的に「final public static」が指定されることになっています。つまり、インスタンスごとのフィールドを宣言することはできません。
一方Kotlinでは、リスト8のようにインタフェースにもプロパティを持つことができます。
//プロパティを持つ(!)インタフェース
interface Limiter{
//プロパティを定義できるが、実際には「abstract」として扱われる
val maxSpeed: Int
//デフォルト実装からプロパティを参照できる
fun boost(speed : Int){
if(speed > maxSpeed){
println("Overspeed!")
}else{
println("Boosted to $speed")
}
}
}
//プロパティを持つインタフェースの実装例
class CarWithLimiter: Vehicle(), Limiter{
//インタフェースのプロパティを明示的に実装する必要あり
override var maxSpeed : Int = 100
override fun run(){
println("Running!")
}
}
……
//プロパティを持つインタフェースを実装したクラスの使用
val carWithLimiter = CarWithLimiter()
carWithLimiter.run() //「Running!」
carWithLimiter.boost(120) //「Overspeed!」
//プロパティの内容がインスタンスごとであることを確認するため、もう1つインスタンスを作成
val carWithLimiter2 = CarWithLimiter()
//1つ目のインスタンスのmaxSpeedプロパティを書き換える
carWithLimiter.maxSpeed = 200
carWithLimiter.boost(120) //「Boosted to 120」maxSpeedが書き換わっているので問題なくブースト可能
carWithLimiter2.boost(120) //「Overspeed!」2つ目のインスタンスのmaxSpeedは書き換わっていない
リスト8 プロパティを持つインタフェース
ここでは、乗り物の制限速度機能を表す「Limiter」インタフェースに最高速度の「maxSpeed」プロパティを宣言しています。加速するための「boost」メソッドではmaxSpeedプロパティを参照して、指定速度まで加速可能かどうかを判定しています。
もちろん、Kotlinのインタフェースの仕組みもJavaと同様、実際には状態を持つことはできませんので、インタフェースに宣言するプロパティはアクセサ(getter、setter)を持つか、あるいは「abstract」でなければなりません。
LimiterインタフェースのmaxSpeedプロパティにはアクセサを指定していないため、abstractとして扱われます。そのため、Limiterインタフェースを実装する「CarWithLimiter」クラスで、maxSpeedプロパティをoverrideキーワード付きで継承することで、実際に値を保存する場所を確保しています。
このサンプルでは、maxSpeedプロパティがインスタンスごとに値を持っていることを確認するため、2つのCarWithLimiterインスタンスを使っています。
デフォルト実装の解決
さて、インタフェースのデフォルト実装はとても便利な機能ですが、複数のインタフェースを実装する際、リスト9のようにデフォルト実装したメソッド名がぶつかることがあります。
interface Stoppable{
fun stop(){ print("Stopped slowly...") }
}
//同名の「stop」メソッドを持つインタフェース
interface Stoppable2{
//名前のぶつかるメソッド
fun stop(){ print("Stopped suddenly!!!") }
//名前のぶつからないメソッド
fun crash(){ print("Crash!!!") }
}
……
//同じ名前のメソッドを持つインタフェースを複数実装した例
class StoppableVehicle2: Vehicle(), Stoppable, Stoppable2{
override fun run(){
println("Running!")
}
//stopメソッドを定義しないとコンパイルエラーになる
override fun stop(){
print("Original stop implementation!")
super<Stoppable>.stop() //Stoppableのデフォルト実装を呼び出す
super<Stoppable2>.stop() //Stoppable2のデフォルト実装を呼び出す
}
}
……
//デフォルト実装の競合解決例
var stoppableVehicle2 = StoppableVehicle2()
stoppableVehicle2.stop() //「Original stop implementation!Stopped slowly...Stopped suddenly!!!」
リスト9 デフォルト実装の競合解決
このサンプルでは「Stoppable」インタフェースと「Stoppable2」インタフェースで同名の「stop」というメソッドが両方ともデフォルト実装を持ち、名前が競合しています。通常インタフェースにデフォルト実装があれば、インタフェースを実装したクラスでそのメソッドをオーバーライドしなくても問題ありませんが、名前が競合している場合は、このサンプルのように、明示的にメソッドをオーバーライドする必要があります。
ここでは、「StoppableVehicle2」クラスでstopメソッドを明示的にオーバーライドし、その中でsuperキーワードを使って2つのインタフェースのデフォルト実装を呼び出しています。「super<インタフェース名>.メソッド名()」とすることで、実装しているインタフェースのうち、呼び出すインタフェースを指定できます*2。
*2)Java 8でもデフォルト実装のために競合解決が必要ですが、同様のケースでJavaで呼び出すインタフェースを指定する場合は「インタフェース名.super.メソッド名()」となる点が少し異なります。
デフォルト実装によってインタフェースの活用範囲が広がりますが、このような競合解決の必要性も出てきますので、使用に際してはメリット、デメリットを考慮したいですね。
スマートキャスト
クラスの継承に関する話題が出てきたので、キャストについても考えておきましょう。
KotlinはJavaと同様に静的な型付け言語なので、データ型はコンパイル時点で確定されます。そのため、Javaと同様に型のキャストが必要になる場面があります。Kotlinではリスト10のように「as」演算子を使ってキャストします。C/C++/C#のように「(型名)変数」の形式ではキャストできないので、ご注意ください。
val cat = Cat() //「Cat」型(「Animal」型を継承)のインスタンス
val animal = cat as Animal //Animal型にキャスト
//val animal2 = (Animal)cat //この形式はエラー
リスト10 「as」を使ったキャスト
キャストに失敗した場合、as演算子は例外を送出します。as演算子の代わりに「as?」演算子を使うと、キャストに失敗した場合にnullが返ります。
型チェックとスマートキャスト
キャストが必要になるケースにおいては、多くの場合、事前の型チェックがセットになります。Javaであればリスト11のような「型をチェックしてマッチしたらキャストして使う」というコードを書いたことがあるかもしれません。
void action(Object something){
//引数がColor型だったら
if(something instanceof Color){
//いったんキャストする
Color c = (Color)something;
……
}else if(something instanceof Point){
Point p = (Point)something;
……
}
}
リスト11 Javaでのキャストの例
Kotlinであれば、同じコードをリスト12のように書けます。Javaの「instanceof」の代わりにKotlinでは「is」を使ってオブジェクトの型をチェックします。
//引数としてAny
fun action(something: Any){
//when文で型ごとに分岐
when(something){
//X,YはPoint型のプロパティ、R,G,BはColor型のプロパティとする
//somethingをキャストせずに使っている↓
is Point -> println("Point: {something.X}, {something.Y}")
is Color -> println("Color: [{something.R},{something.G},{something.B}]")
}
}
リスト12 Kotlinでのスマートキャストの例
注目したいのは「when」の中でisを使って分岐した中のコードです。「something.X」「something.R」のように、キャストしていないのに、「something」引数を「Point」型や「Color」型のオブジェクトとして参照しています。ここでは「事前にisで型をチェックしているのだから、自動的にその型として扱う」という仕組みが働いています。これを「スマートキャスト」と呼び、whenだけではなくifでも使用できます(リスト13)。
//ifの条件式で型チェック済
if(something is Point){
println("Point: {something.X}, {something.Y}") //キャストなしで使える
}else if(something is Color){
println("Color: [{something.R},{something.G},{something.B}]")
}
リスト13 ifでのスマートキャスト
スマートキャストを使うことで、型チェックによる分岐コードを書く際の自明なキャストが不要になりますね。これもまた「Kotlinにしてよかった」と思える瞬間です*3。
*3)ちなみにC# 7では、「パターンマッチング」と呼ばれる機能によってキャストを省略できる仕組みが導入されました。Kotlinとは違ったアプローチですが、シンプルな記述法となっています。興味のある方は以下の記事を参照してください。
C# 7の新機能詳説:第3回 型による分岐の改良
次回は、拡張関数、範囲(Range)、分解宣言と多重戻り値など
今回はクラス継承に関するもろもろの機能について解説しました。やはりベースとなっているJavaに似ている部分が多いですが、インタフェースにプロパティを持てる部分など、ちょっとした違いにより書きやすさが向上している面もあります。
また、スマートキャストは静的な型付け言語を使う際に煩わしく感じる自明なキャストの手間を省いてくれます。こちらも小粒ですが、うれしい機能ですね。
最終回となる次回は、拡張関数、範囲(Range)、分解宣言と多重戻り値など、残っている目玉機能をできる限り総ざらいする予定です。どうぞお楽しみに。