【第23章】抽象クラスとインターフェースについて詳しく解説

【第23章】抽象クラスとインターフェースについて詳しく解説 基礎勉強
スポンサーリンク
リンコ
リンコ

こんにちは!リンコですピヨ!この記事では自分の勉強の復習もかねて、Kotlinでの抽象クラスとインターフェースについて解説するピヨ!

この第23章では、プログラミング言語であるKotlinの抽象クラスとインターフェースについて、一緒に勉強していきましょう。

この記事を読めばプログラミング未経験の方も、Kotlin入門レベルの抽象クラスとインターフェースについて1つ1つ理解しながら勉強できると思うので、是非最後まで読んで頂ければと思います。

※この記事で出てくる「サンプルコード」は、記述が長く画面からはみ出ている場合がありますが、横にスライドすると表示されるのでご安心ください。

スポンサーリンク

【Kotlin】抽象クラスとは?

抽象クラスとは?

ではまず、抽象クラスとはどういうものなんでしょうか?

インスタンス化できないテンプレートのクラス

抽象クラスとは、インスタンス化できないクラスのことで、基本的にテンプレートとして使うようにするためのクラスになります。

前回までのBirdクラス、Rinkoクラス、Sparrowクラスの継承関係で例えると、RinkoクラスとSparrowクラスがBirdクラスを継承しているというものでした。

そのため2つのクラスがBirdクラスの機能をそのまま引き継ぐことができるようになっており、Birdクラスは「テンプレート」のようなものになっていました。

このようにBirdクラスは、あくまでテンプレートとして使われ一般的な「鳥」の概念を表しただけのものになります。

基本的にこのテンプレートは、インスタンス化して使用することは意図されていないので、このようなクラスに対しては「抽象クラス」として定義しておくようにします。

他のプログラマに設計者の意図として伝えることができる

例えば複数人でプログラムを組んでいる時に、テンプレートとして作ったBirdクラスを他のプログラマがインスタンス化しようとしていたらどうしますか?

そんな時にBirdクラスを抽象クラスとして定義しておくことで、設計者の意図を他のプログラマに伝えることができます。

リンコ
リンコ

抽象クラスはインスタンス化できない、テンプレートとして使うクラスピヨ!

【Kotlin】抽象クラスを記述してみよう

抽象クラスを記述してみよう

抽象クラスがテンプレートということがなんとなくわかったところで、抽象クラスの記述の仕方を勉強していきましょう。

抽象クラスの書式

抽象クラスの書式は、以下のようになります。

abstract class クラス名() {
    abstract fun 関数名()
}

クラスの先頭に「abstract」と付ける

上記の書式のように、クラスの宣言時に「abstract」と記述することで、特定のクラスを抽象クラスとして宣言することができます。

これにより先頭にabstractの記述があるクラスは、インスタンス化をすることができなくなります。「抽象的」なものだから「具体化(インスタンス化)」ができないというイメージです。

関数の先頭に「abstract」を付けると「抽象関数」として宣言できる

また書式では抽象クラスの中に、abstractの記述のある関数も記述していますが、これは「抽象関数」です。

抽象関数を定義しておくことで、継承した先のクラスではその関数を記述することが強制されるようになります。

抽象クラスを継承したクラスで、抽象関数の定義をしないとコンパイルエラーが発生するので注意しましょう。

※抽象クラス内に定義する関数は必ず抽象関数にする必要はなく、これまでと同様に抽象クラス内に「普通の関数」を定義してもOKです。

リンコ
リンコ

抽象クラスや抽象関数にしたい場合は「abstract」と記述するピヨ!

実際に抽象クラスを記述してみよう

では実際に、抽象クラスを記述してみましょう。

サンプルコード

以下のサンプルコードでは、リンコに5キロとスズメに3キロ羽ばたいてもらっています。その中で、テンプレートのBirdクラスは抽象クラスで、fly関数は抽象関数にしています。

fun main(){
  val bird1 = Rinko("黄")
  val bird2 = Sparrow("グレー")
  bird1.fly(5.0)
  bird2.fly(3.0)
}

abstract class Bird(val color: String) {
  var distance = 0.0
  abstract fun fly(d: Double)
}


class Rinko(color: String) : Bird(color) {
  override fun fly(d: Double) {
    distance = distance + d
    println("${color}のリンコが羽ばたいています!ピヨピヨ!")
    println("合計${distance}km飛んでいるピヨ!")
  }
}

class Sparrow(color: String) : Bird(color) {
  override fun fly(d: Double) {
    distance = distance + d
    println("${color}のスズメが羽ばたいています!チュンチュン!")
    println("合計${distance}km飛んでいるチュン!")
  }
}

出力

サンプルコードを実行すると、以下のように出力されます。

黄のリンコが羽ばたいています!ピヨピヨ!
合計5.0km飛んでいるピヨ!
グレーのスズメが羽ばたいています!チュンチュン!
合計3.0km飛んでいるチュン!

このように、抽象クラスと抽象関数を定義しても、問題なく動作することがわかりました。

Birdクラスの宣言時にabstractの記述で始まっているので、Birdクラスが抽象クラスとして宣言されたことを意味しています。

また、Birdクラスの中身がシンプルになっています。これはfly関数の宣言がabstractの記述で始まり、関数の中身の記述がごっそりなくなっているからです。

先程も勉強した通り、fly関数が「抽象関数」として宣言されていることを意味し、関数がどういう動作をするかはBirdクラスの中ではあえて記述しないようになっています。

ここではただ単に、fly関数が存在しているという事実だけを宣言していて、こうすることでBirdクラスを継承した先のクラスでは、fly関数を記述することが強制されることになります。

逆に、Birdクラスを継承したクラスは、コンパイルが成功した時点でfly関数が存在していることが保証されます。そのためポリモーフィズムを応用して、Birdクラスの継承先の全てのクラスのインスタンスに対して、確実にfly関数を呼ぶことができるようになります。

またabstractで宣言されているクラスや関数は、他のクラスに継承してもらうことが前提になっているため、抽象クラスや抽象関数には「open」はつけなくてOKです。

リンコ
リンコ

「abstract」や「open」は必要に応じて使いわけるピヨ!

【Kotlin】インターフェースとは?

インターフェースとは?

続いて、インターフェースについて勉強していきましょう。

インターフェースで複数のクラスに「共通項」を持たせることができる

Kotlinでは「あるクラスが複数のクラスを継承することはできない」というルールがあります。例えばBirdクラスとPersonクラス、この2つのクラスを継承した新たなクラスを定義するということはできません

あるクラスが複数の親クラスを持つという「多重継承」という概念はありますが、だいたいのオブジェクト指向言語ではそれができないようになっていて、同様にKotlinでもクラスの多重継承はできません

多重継承ができないとなると、複数の異なるクラスに「共通項」を持たせることができません。

ですが、インターフェースで複数のクラスに「共通項」を持たせることができるようになります。複数の親クラスを持つことはできませんが、インターフェースを使うことで複数のクラスに関数を共通項として実装できるので覚えておきましょう。

リンコ
リンコ

インターフェースは複数のクラスに共通のものを持たせることピヨ!

Kotlinを含めオブジェクト指向言語で多重継承がダメなのはなぜなのか?

ひとことで言うと「それを許してしまうと、いろいろややこしくなるから」です。たとえば「鳥であり、人間でもある」ものの設計図を作れといわれても困りますし、人並外れて想像力が豊かな人なら何とかなりますが、鳥人間の設計図を描くとなるといろいろ悩みどころが出てきます。

同じ理由で、KotlinではBirdクラスとPersonクラスの両方を継承したクラスを定義するということは、ややこしくなるのであえてできないように言語仕様として定められています。

よってKotlinでは「単一継承」のみが可能となります。

リンコ
リンコ

Kotlinでは多重継承ができないので注意ピヨ!

【Kotlin】インターフェースを記述してみよう

インターフェースを記述してみよう

インターフェースや多重継承についてなんとなく理解したところで、インターフェースの記述について勉強していきましょう。

インターフェースを使いたい場面

インターフェースの記述の仕方を勉強していきますが、その前にどんな時に使いたいかを見ていきましょう。

サンプルコード

まずは以下のサンプルコードを見てください。以下のサンプルコードでは、PersonクラスとBirdクラスを継承したそれぞれのクラスに「言葉を話す」という機能を追加するために、Talk関数を実装しています。

// 例1:Personクラスの定義
open class Person(val name: String, val age: Int)
// Personクラスを継承したTalkingPersonの定義
class TalkingPerson(name: String, age: Int): Person(name, age) {
  fun Talk() {
    println("人間は言葉を話します!")
  }
}


// 例2:Birdクラスの定義
open class Bird(val color: String)
// Birdクラスを継承したTalkingBirdの定義
class TalkingBird(color: String) : Bird(color) {
  fun Talk() {
    println("鳥が言葉を話すんですか!?")
  }
}

この2つのクラスは、それぞれ縁もゆかりもない完全に独立したクラスですが、たまたまTalkという同じ名前の関数を持っています。

この場合、言葉を話すという共通項を同じように扱えるような気がしますが、ここで「インターフェース」の登場です。この共通項をくくりだして「インターフェース」として事前に定義しておくことができるのです。

インターフェースの書式

インターフェースの書式は、以下のようになります。

interface インターフェース名 {
  fun 関数名()
}

「interface」と記述

このようにインターフェースの定義は「interface」の記述で始まります。あとは「{}」で挟まれた本体部分に関数を定義します。

関数の本体は記述しない

ただし、関数の本体は記述しません。ここは抽象関数の定義とよく似ていますね。

実際にインターフェースを記述してみよう

では実際に、インターフェースを使ったコードを記述していきましょう。

サンプルコード

以下のサンプルコードでは、インターフェースを使ってPersonクラスとBirdクラスで共通の関数を使うようにしています。

fun main() {
  val person = TalkingPerson("ここまる", 10)
  val bird = TalkingBird("青")
  makeItTalk(person)
  makeItTalk(bird)
}
fun makeItTalk(a: Talking) {
  a.talk()
}


// Talkingインターフェースの定義
interface Talking {
  fun talk()
}
//Personクラスの定義
open class Person (val name: String, val age: Int)
//Personクラスを継承したTalkingPersonの定義
class TalkingPerson(name: String, age: Int) : Person(name, age),Talking {
  override fun talk() {
    println("人間は言葉を話します!")
  }
}

// Birdクラスの定義
open class Bird(val color: String)
// Birdクラスを継承したTalkingBirdの定義
class TalkingBird(color: String) : Bird(color), Talking {
  override fun talk() {
    println("鳥が言葉を話すんですか!?")
  }
}

出力

サンプルコードを実行すると、以下のように出力されます。

人間は言葉を話します!
鳥が言葉を話すんですか!?

上記のように、クラスの定義は今までとほとんど同じですが、クラスの定義の最後の方にコンマ区切りでTalkingというインターフェース名が記述されていることがわかります。

これによりTalkingインターフェースが、2つのクラスにそれぞれ実装されたことになります。

また関数の定義にはoverrideをつけて、インターフェースで規定されている関数をオーバーライドしていることを明示し、talkという関数が実装しています。

makeItTalk関数が受け取っているパラメータの型に注目するとTalking型になっていますが、インターフェースはそのまま「変数の型」として扱えます。

Talking型であるということは、このパラメータとして受け取ったものの実体がTalkingインターフェースを実装していることを保証します。

つまり上記の変数aは、絶対にtalk関数を持っているので、何のためらいもなく「a.talk()」という関数の呼び出しができるということになります。

リンコ
リンコ

コードが少しずつ複雑になってきているけど、しっかり読み込んで理解するピヨ!

【Kotlin】抽象クラスとインターフェースの違い

抽象クラスとインターフェースの違い

では最後に、抽象クラスとインターフェースは似ていますが、この2つの違いに関して勉強していきましょう。

多重継承ができるかできないか

抽象クラスとインターフェースの違いは、多重継承ができるかできないかということです。

抽象クラスを定義する際、複数の親クラスを持つことはできませんが、インターフェースであればいくつでも自由に実装することができます。

抽象クラスであればプロパティを定義することができますが、インターフェースではプロパティの定義はできず、あくまで関数だけの定義となります。

本質的に同系統のものはクラスの継承で実装

本質的に同じ系統のものは、クラスの継承関係で実装していきます。例えばスズメやハトなどは本質的には鳥なので、クラスを継承するようにして実装します。

一部の機能だけ共通の場合はインターフェースで実装

本来異質なものだが、一部の特定機能だけに注目した場合に共通項が存在するものは、インターフェースでひとまとめに扱って実装していきます。

リンコ
リンコ

抽象クラスとインターフェースの違いはしっかり覚えるピヨ!

まとめ

まとめ

この記事では、プログラミング言語であるKotlinの抽象クラスとインターフェースについて勉強していきましたが、いかがでしたでしょうか?今回の記事をまとめると以下のようになります。

  • 「テンプレート」として使うクラスであれば、抽象クラス(abstract)にする
  • 「テンプレート」にもするが、そのクラス自体をインスタンス化して使用するケースもあるなら普通のクラス(open)にする
  • インターフェースを使うことで複数のクラスに「共通項」を持たせることができる
  • インターフェースではプロパティの定義はできず、あくまで関数の定義ができる
  • 本質的に同系統のものはクラスの継承で実装し、一部の機能だけ共通の場合はインターフェースで実装

今回勉強した「抽象クラス」や「インターフェース」はKotlinの入門レベルの知識になるので、何回も読み直してしっかり覚えておきましょう。

リンコ
リンコ

次回の記事では「例外とtry-catch文」について勉強していくピヨ!

プログラミング未経験の方や入門レベルの方、Kotlinについて詳しくなりたい方は、また一緒に勉強するピヨ!

コメント

タイトルとURLをコピーしました