abstract(抽象的な)メンバを持つ「未完成のクラス」が抽象クラス、継承先に「制約」や「ルール」を定めるのがインターフェイスです。今回は抽象クラスとインターフェイスの定義方法と、その実装についてお話したいと思います。
抽象クラスの定義方法
まずは抽象クラスを定義していきます。
//抽象メソッドを1つだけ持つ抽象クラス
abstract class Business{
abstract fun make()
}
抽象クラスは頭に「abstract」、その後に「class クラス名」を記述します。
値や処理が未定であるなら、個別に「abstract」を追加してください。これを「抽象メンバ」と呼びます。
それではもう少し、この抽象クラスを拡張してみましょう。
//引数を取り、コンストラクタでプロパティを初期化できる
abstract class Business(val name: String){
//抽象プロパティ
abstract var stores: Int
//抽象メソッド
abstract fun make(some: String): String
abstract fun sell(item: String): String
//具体的な値や処理を持つメンバ
val employees: MutableList<String> = mutableListOf()
fun employ(employee: String){
println("${employee}を雇った")
employees += employee
}
}
抽象クラスには抽象メンバだけではなく、コンストラクタ引数や、具体的なメンバを定義することもできます。
しかし抽象クラスは継承されることが前提のクラスです。抽象クラスそのもののインスタンスを生成することはできないので注意してください。
val example = Business("SONY")
//エラー : Cannot create an instance of an abstract class
- 抽象クラスは未完成なメンバに個別に「abstract」を記述する
- 抽象クラスはコンストラクタ引数を持つことができ、値を初期化することもできる
- 抽象クラスは具体的なプロパティやメソッドを持つことができる
- 抽象クラスは直接インスタンス化することができない
抽象クラスを実装する
抽象クラスを実際に使用するためには、それを継承したサブクラスを定義します。
class Company(name: String): Business(name){
override var stores = 20
override fun make(some: String): String = "${some}を作ります"
override fun sell(item: String): String = "${item}を売ります"
}
//overrideは個別に記述する
//具体的なメンバはオーバーライド不要
抽象クラスの継承は、文法的には通常のクラスの継承とほぼ同じであり、複数の抽象クラスを継承すること(多重継承)はできません。
サブクラス側は抽象メンバを全てオーバーライドし、処理や値を実装しなければ、通常のクラスとして宣言できないことに注意しましょう。
ちなみに以下は抽象メンバを残したサブクラスですが、頭には「abstruct」が付いたままです。抽象メンバが残っている限りクラスにはなれません。
//抽象メンバが残っている段階では、そのクラスは抽象クラスのまま
abstract class Company(name: String): Business(name){
//抽象メソッドのまま継承する場合、「abstract override」とする。
abstract override var stores: Int
override fun make(some: String): String = "${some}を作ります"
override fun sell(item: String): String = "${item}を売ります"
}
ではこの新たな通常クラスをインスタンス化して使ってみましょう。
//Business型としてインスタンス化できる
val sony: Business = Company("SONY")
println(sony.name) //SONY
println(sony.sell("電化製品")) //電化製品を売ります
sony.employ("田中さん") //田中さんを雇った
println(sony.employeesList) //[田中さん]
インスタンス化も通常のサブクラスと同様、スーパークラス(抽象クラス)の型としてインスタンス化することが可能です。
- 抽象クラスをクラスで継承する場合、全ての抽象メンバを実装する必要がある
- インスタンスは通常と同様、サブクラスの型としてもスーパークラスの型としてもインスタンス化できる
- 抽象クラスを2つ以上継承することはできない
インターフェイスの定義方法
次にインターフェイスです。こちらも先ほどと同じように定義していきます。
interface Enterprise{
fun make()
}
インターフェイスは頭に「interface」を付けて定義。抽象クラスとは違い、抽象メンバに「open」や「abstract」は必要ありません。
上のように「抽象メソッドが1つしかない」インターフェイスをSAM(Single Abstruct Method)インターフェイスと呼びますが、この記事では深く扱いません。
Android開発に興味がある方はこちらの記事を読んでみてください。SAMインターフェイスやSAM変換について解説されています。
https://qiita.com/RyotaMurohoshi/items/01b370f34a4bf96f5c39
さて、このインターフェイスも少し拡張してみましょう。
//コンストラクタは持てない
interface Enterprise{
//抽象プロパティ
var stores: Int
//プロパティはバッキングフィールドを生成できない
val item: String
get() = "商品名"
//メソッドは具体的な処理を含んでも含まなくてもよい
fun make()
fun sell(item: String) = println("${item}を売却した")
}
インターフェイスにも具体的なメンバを設定できますが、抽象クラスとは違ってコンストラクタ引数やバッキングフィールドを持てないという点に注意です。
バッキングフィールドが無いということは、算出プロパティの値を参照するためのカスタムゲッターが必須となります。
https://pouhon.net/kotlin-getter-setter/3223/
- インターフェイスのメンバには「open」や「abstruct」が不要である
- インターフェイスはコンストラクタ引数、バッキングフィールドを持つことができない
- インターフェイスのプロパティが値を持つなら、それは算出プロパティである
- インターフェイスのメソッドには抽象クラスと同様、具体的な処理を含めることができる
- インターフェイスは抽象クラスと同様、インスタンス化できない
インターフェイスを実装する
それではインターフェイスを実装したクラスを定義します。
//ヘッダはクラスの継承と同じ。多重継承の場合はカンマで区切る
class Company(name: String): Enterprise{
//抽象メンバのオーバーライドには「override」が必要
override var stores = 20
//未完成のメソッドならTODO関数もアリ
override fun make() {
TODO("Not yet implemented")
}
override fun sell(item: String) = println("${item}を売ります")
}
//インターフェイス側で処理やゲッターが含まれるものは、オーバーライドは強制されない
メソッドの実装が未定であることを示すには、TODO関数を使うのも1つの手です。これによってメソッドの処理が未定でも、「形としては」クラスとして成立させることができます。
https://pouhon.net/kotlin-nothing/3145/
同じようにインスタンス化して使ってみましょう。
//インターフェイスの型としてインスタンス化
val sony: Enterprise = Company("SONY")
//インターフェイスのプロパティを参照
println(sony.item) //商品名
//インターフェイスのメソッド
sony.sell("電化製品") //電化製品を売ります
文法としては特に難しい点はありませんが、抽象クラスと大きく異なるのは、インターフェイスは多重継承が可能であるという点です。
- インターフェイス側で処理やゲッターが含まれるものは、オーバーライドは強制されない
- インターフェイスは抽象クラスと違い、多重継承が許される
似ている?いいえ、全然違います
今回は抽象クラスとインターフェイスの宣言と実装についてお話ししてきましたが、抽象クラスとインターフェイスが同じようなものだと感じた方は注意してください。これらは全く意味合いが違うものです。
インターフェイスは「継承」のためのクラスではない
抽象クラスの概念は説明不要でしょう。抽象度を高く保つことで、必要であればサブクラスをまとめて扱うことができるクラスであり、行っていることはあくまで「継承」です。
人間も犬も猫も、同じ「生命」や「哺乳類」という共通の枠にはめることができます。簡単に言えばこれが継承です。
しかしインターフェイスはどうでしょう?
本と人間と車。これらを1つの共通の枠にはめることができるでしょうか?
しかしインターフェイスの場合、これはこれで許される継承となります。なぜなら全てのクラスにsteer
メソッドが存在するからです。
Steerableインターフェイスのサブクラスに求められる条件、それは(多少乱暴に言えば)「steerというメソッドをオーバーライドして持っていること」以外にありません。
インターフェイスは継承というより「制約」や「規格」、「約束事」といったほうが、より実際の働きに近い存在と言えます。このページで抽象クラスとインターフェイスを同列のように扱ってきたのはむしろ、2つの違いをより浮き彫りにするためであり、決して2つを混同しないようにするためです。
参考文献
インターフェイスをより理解するためには、オブジェクト指向のデザインパターンにも踏み込んでいく必要があります。こちらの記事ではJavaを使用されていますが、Javaを知らない方にとってもインターフェイスを理解する助けになるでしょう。