Kotlinで抽象クラスを完全理解する方法10選

Kotlin言語のロゴと抽象クラスのイラストKotlin
この記事は約22分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Kotlinを学んでいる中で、「抽象クラス」について気になったことはありませんか?

抽象クラスは、オブジェクト指向プログラミングの基本的な要素の1つであり、多くのプログラミング言語でサポートされています。

しかし、その概念や使い方、利点などをしっかりと把握していないと、効果的に活用するのが難しいものです。

この記事では、Kotlinを使用して抽象クラスをどのように定義し、どのように利用するのかを徹底的に解説します。

初心者の方でもスムーズに理解できるよう、具体的なサンプルコードとともに、抽象クラスの全貌を明らかにします。

●Kotlinとは

Kotlinは、現代のアプリケーション開発に適したプログラミング言語の1つです。

特にAndroidアプリの開発で注目を浴びていますが、サーバーサイドやWeb開発、デスクトップアプリケーションなど、多岐にわたる用途で使用されています。

○Kotlinの特徴

Kotlinは、Javaよりも簡潔で表現力豊かな文法を持っています。

そのため、少ないコード量で、より明瞭かつ効率的なプログラムを書くことが可能です。

また、Javaとの相互運用性が高いため、既存のJavaプロジェクトにKotlinを導入することも容易です。

また、null安全な言語設計や拡張関数など、多くの便利な機能を持つこともKotlinの大きな魅力となっています。

○KotlinとJavaの違い

Kotlinは、Javaをベースに作られた言語であるため、多くの部分でJavaとの類似性を持ちますが、次のような違いがあります。

  1. 文法の簡潔さ:Kotlinは、Javaよりも文法が簡潔であり、冗長なコードを書く必要が少ない。
  2. null安全:Kotlinでは、nullの扱いに特別な注意が払われており、nullポインタ例外を防ぐ仕組みが組み込まれています。
  3. 拡張関数:Kotlinでは、既存のクラスに新しい関数を追加することができる拡張関数という機能があります。
  4. スクリプトとしての実行:Kotlinは、Javaとは異なり、スクリプトとしても実行できます。
  5. コルーチン:非同期処理を簡単に扱うためのコルーチンという機能が組み込まれています。

これらの特徴により、Kotlinは多くの開発者から支持されており、今後の成長が期待されています。

●抽象クラスとは

抽象クラスとは、オブジェクト指向プログラミングにおいて特定の具体的な実装を持たないクラスを指します。

この抽象クラスは、他のクラスに継承されることを前提として設計されます。

具体的な実装がないため、このクラスから直接インスタンスを生成することはできません。

その代わり、抽象クラスを継承する子クラスで、抽象メソッドの具体的な内容を定義します。

○抽象クラスの特徴

  1. インスタンス化不可:抽象クラス自体はインスタンスを生成することができません。実体化するためには、抽象クラスを継承した子クラスを作成する必要があります。
  2. 抽象メソッドを持つ:抽象クラス内で定義されるメソッドは、具体的な実装を持たないことが多く、これを抽象メソッドといいます。子クラスでこの抽象メソッドの実装を行います。
  3. 具体的なメソッドも持つことが可能:すべてのメソッドが抽象である必要はありません。必要に応じて、具体的な実装を持つメソッドも抽象クラス内に定義できます。

○抽象クラスのメリットと用途

抽象クラスを使用するメリットは、設計の段階で共通の性質や機能をまとめることができる点にあります。

具体的には、次のようなメリットや用途が挙げられます。

  1. 再利用性の向上:共通の機能や性質を抽象クラスにまとめることで、そのクラスを継承する複数の子クラスで再利用することができます。
  2. 拡張性の確保:未来の拡張を前提とした部分を抽象メソッドとして定義し、必要に応じて子クラスで実装することで、柔軟な設計が可能になります。
  3. 標準化:抽象クラスによって、子クラスが持つべきメソッドや性質を標準化することができます。

抽象クラスのメリットを活かすことで、効率的で保守性の高いコードを設計することができます。

●Kotlinでの抽象クラスの基本的な使い方

KotlinではJavaと同様に、抽象クラスという概念が存在します。

ここでは、Kotlinでの抽象クラスの基本的な定義方法と、それを活用したコードの記述方法について学んでいきます。

○抽象クラスの定義方法

Kotlinで抽象クラスを定義する際は、abstractキーワードを用いて宣言します。

このキーワードはクラスだけでなく、抽象メソッドにも使用されます。

具体的なメソッドの実装がない場合、そのメソッドは抽象メソッドとなります。

abstract class 動物 {
    abstract fun 鳴く(): String
}

上記のコードは動物という抽象クラスを定義しており、鳴くという抽象メソッドが含まれています。

この動物クラスは具体的な動物のクラスで継承され、鳴くメソッドが実装されることを想定しています。

○サンプルコード1:基本的な抽象クラスの作成

次に、実際に抽象クラスを継承して具体的なクラスを作成してみましょう。

この例では、前述の動物クラスを継承して、犬と猫という二つのクラスを定義します。

abstract class 動物 {
    abstract fun 鳴く(): String
}

class 犬: 動物() {
    override fun 鳴く(): String {
        return "ワンワン"
    }
}

class 猫: 動物() {
    override fun 鳴く(): String {
        return "ニャー"
    }
}

fun main() {
    val ポチ = 犬()
    val たま = 猫()

    println(ポチ.鳴く()) // ワンワンと出力されます。
    println(たま.鳴く()) // ニャーと出力されます。
}

犬クラスと猫クラスは動物クラスを継承しており、それぞれの動物がどのような鳴き声をするのかを鳴くメソッドでオーバーライドして定義しています。

main関数内では、犬と猫のインスタンスを生成し、それぞれの鳴き声を表示しています。

○抽象メソッドの定義と実装

前述したとおり、抽象メソッドは具体的な実装を持たず、子クラスでオーバーライドして実装します。

この抽象メソッドの存在により、継承先のクラスに対して特定のメソッドの実装を強制することができるのが大きなメリットです。

抽象メソッドの宣言は非常に簡単で、メソッドの宣言の前にabstractキーワードをつけ、メソッドの本体を記述せずに終了します。

継承先のクラスでは、この抽象メソッドをオーバーライドし、具体的な処理を実装します。

●Kotlinでの抽象クラスの応用方法

Kotlinの抽象クラスは、単に抽象メソッドを持つだけでなく、さまざまな応用的な使い方が可能です。

このセクションでは、抽象クラスをより深く、そして実践的に活用するための方法をいくつか紹介します。

○サンプルコード2:抽象クラスを継承するクラスの作成

前回解説した基本的な抽象クラスの継承を少し発展させ、複数の抽象メソッドやプロパティを持つ抽象クラスの継承について紹介します。

abstract class 車 {
    abstract val 車種名: String
    abstract fun 走る(): String
    abstract fun 停止する(): String
}

class スポーツカー : 車() {
    override val 車種名 = "スポーツカー"
    override fun 走る() = "$車種名 が高速で走ります"
    override fun 停止する() = "$車種名 が急ブレーキで停止します"
}

class トラック : 車() {
    override val 車種名 = "トラック"
    override fun 走る() = "$車種名 がゆっくりと走ります"
    override fun 停止する() = "$車種名 がゆっくりと停止します"
}

fun main() {
    val フェラーリ = スポーツカー()
    val 大型トラック = トラック()
    println(フェラーリ.走る()) // スポーツカーが高速で走ります、と出力されます。
    println(大型トラック.走る()) // トラックがゆっくりと走ります、と出力されます。
}

こちらのコードでは、という抽象クラスに、車種名という抽象プロパティと、走る停止するという抽象メソッドが定義されています。

そして、具体的な車種であるスポーツカートラックがこの抽象クラスを継承し、それぞれの特性に応じた動作をオーバーライドして実装しています。

○サンプルコード3:インターフェースと抽象クラスを組み合わせた利用

Kotlinでは、インターフェースと抽象クラスを組み合わせて利用することができます。

これにより、より柔軟なクラス設計が可能となります。

interface 可動性 {
    fun 移動する(): String
}

abstract class 乗り物 : 可動性 {
    abstract val 名前: String
    override fun 移動する() = "$名前 が移動します"
}

class 自動車 : 乗り物() {
    override val 名前 = "自動車"
}

class 飛行機 : 乗り物() {
    override val 名前 = "飛行機"
    override fun 移動する() = "$名前 が空を飛びます"
}

fun main() {
    val トヨタ = 自動車()
    val ジャンボジェット = 飛行機()
    println(トヨタ.移動する()) // 自動車が移動します、と出力されます。
    println(ジャンボジェット.移動する()) // 飛行機が空を飛びます、と出力されます。
}

このコードの中心は、可動性というインターフェースと、それを実装した乗り物という抽象クラスです。

具体的な乗り物のクラス(自動車飛行機)は、この抽象クラスを継承しており、それぞれの特性に応じて移動の方法を定義しています。

このようにインターフェースと抽象クラスを組み合わせることで、クラスの階層を効果的に設計することができます。

○サンプルコード4:抽象クラス内でのプロパティの利用

Kotlinでは、抽象クラス内でプロパティを定義することで、そのプロパティの実装を継承先のクラスに委ねることが可能です。

これにより、クラスの設計がより柔軟かつ簡潔になります。

まず、抽象クラス内でのプロパティの基本的な使用方法を見てみましょう。

abstract class 動物 {
    abstract val 鳴き声: String
    fun 鳴く() = println(鳴き声)
}

class 犬 : 動物() {
    override val 鳴き声 = "ワンワン"
}

class 猫 : 動物() {
    override val 鳴き声 = "ニャー"
}

fun main() {
    val シロ = 犬()
    シロ.鳴く()  // ワンワン、と出力されます。

    val ミケ = 猫()
    ミケ.鳴く()  // ニャー、と出力されます。
}

このコードでは、動物という抽象クラスに鳴き声という抽象プロパティを定義しています。

そして、という具体的なクラスがこの抽象クラスを継承しており、それぞれの動物に応じて鳴き声のプロパティをオーバーライドして定義しています。鳴くメソッドは抽象クラスで共通の実装を持っており、継承先のクラスではそのまま利用することができます。

○サンプルコード5:抽象クラスとコンストラクタ

Kotlinの抽象クラスでも、コンストラクタを定義することができます。

これにより、継承先のクラスが生成される際に、特定のパラメータを受け取ることが可能となります。

abstract class 人(val 名前: String) {
    fun 自己紹介() = println("私の名前は $名前 です。")
}

class 学生(名前: String, val 学年: Int) : 人(名前) {
    fun 紹介() = println("私は $学年 年生の $名前 です。")
}

fun main() {
    val 田中 = 学生("田中", 3)
    田中.自己紹介()  // 私の名前は田中です、と出力されます。
    田中.紹介()      // 私は3年生の田中です、と出力されます。
}

上記のコードでは、という抽象クラスに名前というパラメータを持つコンストラクタが定義されています。

そして、学生というクラスがこの抽象クラスを継承する際に、名前と加えて学年というパラメータも持っています。

これにより、継承先のクラスを生成する際に、必要な情報を提供して、更に詳細な機能や動作を持たせることができます。

●抽象クラスをより実践的に活用するためのヒント

Kotlinでの抽象クラスの活用は、単に基礎知識を理解するだけでなく、さまざまな実践的なシチュエーションでその真価を発揮します。

ここでは、抽象クラスをより効果的に使用するためのヒントやテクニックを、具体的なサンプルコードとともにご紹介します。

○サンプルコード6:抽象クラスを利用したデザインパターン

デザインパターンは、特定の問題に対する典型的な解決策を模範的に示したものです。抽象クラスは、多くのデザインパターンでキーとなる役割を果たします。

テンプレートメソッドパターンをKotlinで実装した例を見てみましょう。

abstract class 料理 {
    fun 調理する() {
        材料を準備する()
        調理手順()
        盛り付ける()
    }

    abstract fun 調理手順()

    fun 材料を準備する() = println("材料を準備します。")

    fun 盛り付ける() = println("美味しそうに盛り付けます。")
}

class オムライス : 料理() {
    override fun 調理手順() {
        println("卵とごはんを使ってオムライスを作ります。")
    }
}

fun main() {
    val 料理1 = オムライス()
    料理1.調理する()
}

上記のコードでは、料理という抽象クラスがテンプレートメソッド調理するを持っています。

この中で、具体的な調理手順が異なる部分だけを抽象メソッド調理手順として定義しています。

オムライスクラスはこの抽象クラスを継承し、具体的な調理手順をオーバーライドして実装しています。

このように、テンプレートメソッドパターンを使用すると、アルゴリズムの骨組みを一つのメソッドにまとめ、その一部のステップをサブクラスでオーバーライドすることができます。

○サンプルコード7:ラムダ式と抽象クラスの組み合わせ

Kotlinのラムダ式は、関数を簡潔に表現することができる強力な機能です。

ラムダ式と抽象クラスを組み合わせることで、より柔軟な設計が可能になります。

abstract class タスク(val アクション: () -> Unit) {
    fun 実行() {
        アクション.invoke()
    }
}

class メール送信タスク : タスク({
    println("メールを送信します。")
})

fun main() {
    val タスク1 = メール送信タスク()
    タスク1.実行()  // メールを送信します、と出力されます。
}

このコードの中で、タスクという抽象クラスはラムダ式を引数として取るコンストラクタを持っています。

メール送信タスクという具体クラスは、この抽象クラスを継承し、特定のアクションをラムダ式として提供しています。

このように、ラムダ式と抽象クラスを組み合わせることで、動的に振る舞いを定義することができ、コードの再利用性が向上します。

○サンプルコード8:拡張関数と抽象クラス

Kotlinの拡張関数は、既存のクラスに新しい関数を追加することなく、そのクラスのインスタンスに対して新しい機能を提供するための機能です。

抽象クラスと組み合わせることで、さらに多彩なコーディングスタイルが可能となります。

例として、既存の抽象クラスに対する拡張関数を定義してみましょう。

abstract class 動物 {
    abstract fun 鳴く(): String
}

class 犬 : 動物() {
    override fun 鳴く(): String {
        return "ワンワン"
    }
}

fun 動物.自己紹介() {
    println("${this.javaClass.simpleName}は${this.鳴く()}と鳴きます。")
}

fun main() {
    val ワンちゃん = 犬()
    ワンちゃん.自己紹介()  // 犬はワンワンと鳴きます、と出力されます。
}

このコードでは、抽象クラス動物とその具体的なサブクラスを定義しています。

そして、抽象クラス動物に対して拡張関数自己紹介を追加しました。

この拡張関数を使って、任意の動物クラスのインスタンスがどのように鳴くのかを出力することができます。

このように拡張関数と抽象クラスを組み合わせることで、既存のクラス構造を変更することなく、新しい機能を追加することができます。

○サンプルコード9:デリゲートプロパティと抽象クラス

Kotlinのデリゲートプロパティは、プロパティのゲッターやセッターの動作を別のオブジェクトに委譲することができる強力な機能です。

これを抽象クラスと組み合わせると、より柔軟なコード設計が可能になります。

interface 名前Provider {
    val 名前: String
}

abstract class 人間(デリゲート: 名前Provider) : 名前Provider by デリゲート

class 学生(override val 名前: String) : 人間(学生デリゲート(名前))

data class 学生デリゲート(val _名前: String) : 名前Provider {
    override val 名前: String
        get() = "学生: $_名前"
}

fun main() {
    val 田中くん = 学生("田中")
    println(田中くん.名前)  // 学生: 田中、と出力されます。
}

上記のコードでは、名前を提供するインターフェース名前Providerと、そのインターフェースを実装するデリゲート学生デリゲートを定義しています。

抽象クラス人間はこのインターフェースを継承し、具体的なサブクラス学生を作成しています。

このサブクラスはデリゲートプロパティを使用して名前の取得を学生デリゲートに委譲しています。

このようにデリゲートプロパティと抽象クラスを組み合わせることで、コードの再利用性を向上させつつ、機能を効率よく拡張することができます。

○サンプルコード10:抽象クラスを使ったリスナーの実装

リスナーはイベント駆動プログラミングにおいて、特定のイベント(例:ボタンクリック、キー入力など)が発生した際に実行されるメソッドを定義するものです。

Kotlinでリスナーを実装する際、抽象クラスを活用することで、必要なメソッドのみを効率的にオーバーライドでき、コードの簡潔さと可読性が向上します。

下記のコードは、抽象クラスを使ってリスナーを実装する一例です。

abstract class クリックリスナー {
    // クリックイベントをハンドリングする抽象メソッド
    abstract fun クリックされた()

    // オプションで他のイベントも定義可能
    open fun ホバーされた() {
        println("ホバーイベントがトリガーされました。")
    }
}

class ボタン {
    var リスナー: クリックリスナー? = null

    fun クリック() {
        リスナー?.クリックされた()
    }
}

class カスタムリスナー : クリックリスナー() {
    override fun クリックされた() {
        println("カスタムクリックイベントがトリガーされました。")
    }
}

fun main() {
    val ボタン = ボタン()
    ボタン.リスナー = カスタムリスナー()
    ボタン.クリック()  // カスタムクリックイベントがトリガーされました、と出力されます。
}

このコード例では、クリックリスナーという抽象クラスを定義しています。

この抽象クラスは、クリックされたという抽象メソッドを持っており、具体的なクリックイベントのハンドリングはサブクラスで行います。

ホバーされたのようにオーバーライドがオプショナルなメソッドも定義することができます。

●抽象クラスの注意点と対処法

抽象クラスは、Kotlinプログラミングにおいて非常に強力なツールとなりますが、正しく理解して使用しないと思わぬエラーやバグの原因となります。

ここでは、抽象クラスを使用する際の注意点や対処法を解説します。

○初心者が陥りやすいミス

□インスタンスの生成

抽象クラスは、直接インスタンス化することができません。

しかし、初心者の中には、抽象クラスを通常のクラスと同じように扱おうとする方もいます。

abstract class 抽象動物 {
    abstract fun 鳴く()
}

// 以下のようにはできません!
val 動物 = 抽象動物()  // エラー発生!

上記のコードでは、抽象クラスの抽象動物を直接インスタンス化しようとしています。

これは不正確であり、コンパイルエラーとなります。

□抽象メソッドの実装忘れ

抽象クラスを継承するサブクラスは、親クラスの抽象メソッドを必ずオーバーライドして実装する必要があります。

これを忘れると、コンパイルエラーとなります。

abstract class 抽象動物 {
    abstract fun 鳴く()
}

class 犬 : 抽象動物() {
    // 鳴くメソッドの実装を忘れた場合、エラーが発生します。
}

○抽象クラスとオーバーライドの注意点

抽象クラスのメソッドをオーバーライドする際には、overrideキーワードを忘れずに使用することが重要です。

さらに、オーバーライドするメソッドは、そのアクセス修飾子が親クラスのものよりも狭い範囲には変更できません。

abstract class 抽象動物 {
    open fun 移動() {
        println("動物が移動します。")
    }

    abstract fun 鳴く()
}

class 鳥 : 抽象動物() {
    override fun 移動() {
        println("鳥が飛ぶ。")
    }

    // 必ずoverrideキーワードを使用して実装します。
    override fun 鳴く() {
        println("鳥が鳴く。")
    }
}

このコードを実行すると、鳥が飛ぶ。鳥が鳴く。という結果が得られます。

○継承制約に関する注意

抽象クラスには、openキーワードを使用して、そのクラスが継承可能であることを明示的に表す必要はありません。

しかし、抽象クラスのメソッドやプロパティをサブクラスでオーバーライドする場合、そのメソッドやプロパティにopenキーワードを付けておく必要があります。

また、finalキーワードを使用して、サブクラスでのオーバーライドを禁止することもできます。

しかし、abstractキーワードとfinalキーワードは同時に使用することができません。

なぜなら、抽象メソッドはサブクラスで必ず実装する必要があり、その制約とfinalキーワードの制約が矛盾するからです。

●抽象クラスのカスタマイズ方法

抽象クラスの強力な特性を理解し、活用することで、Kotlinプログラミングの効率と品質を向上させることができます。

ここでは、抽象クラスをさらにカスタマイズして効果的に使用する方法を解説します。

○抽象クラスの拡張

抽象クラスを利用する際、既存の抽象クラスに新しい機能を追加したい場面が出てくることもあるでしょう。

Kotlinでは、拡張関数を使用して、既存の抽象クラスに新しいメソッドを追加することができます。

例えば、動物という抽象クラスがあり、これに表示というメソッドを追加したい場合、次のように拡張関数を使用します。

abstract class 動物 {
    abstract fun 鳴く()
}

fun 動物.表示() {
    println("これは${this::class.simpleName}です。")
}

class 犬 : 動物() {
    override fun 鳴く() {
        println("ワンワン!")
    }
}

fun main() {
    val ポチ = 犬()
    ポチ.表示()  // このコードを実行すると、「これは犬です。」と表示されます。
}

このコードでは、動物クラスを拡張して表示メソッドを追加しています。

main関数内で犬クラスのインスタンスを生成し、そのインスタンスに対して表示メソッドを呼び出すと、「これは犬です。」という結果が得られます。

○独自の抽象クラスライブラリの作成

Kotlinの抽象クラスは非常に柔軟であるため、独自のライブラリやフレームワークを作成する際の基盤としても活用することができます。

特定の業務やアプリケーションに特化した独自の抽象クラスを作成することで、再利用性や継承の効率を高めることができます。

例として、ウェブアプリケーションでよく使用されるモデルの基本構造を持つ抽象クラスを作成します。

abstract class ベースモデル {
    abstract val id: Int
    abstract fun 保存(): Boolean
    abstract fun 削除(): Boolean
}

class ユーザモデル(override val id: Int, val 名前: String) : ベースモデル() {
    override fun 保存(): Boolean {
        // データベースへの保存ロジック
        println("ユーザ: $名前 を保存しました。")
        return true
    }

    override fun 削除(): Boolean {
        // データベースからの削除ロジック
        println("ユーザ: $名前 を削除しました。")
        return true
    }
}

fun main() {
    val user = ユーザモデル(1, "田中太郎")
    user.保存()  // このコードを実行すると、「ユーザ: 田中太郎 を保存しました。」と表示されます。
    user.削除()  // このコードを実行すると、「ユーザ: 田中太郎 を削除しました。」と表示されます。
}

このコードでは、データモデルの基本的な動作を定義したベースモデルという抽象クラスを作成しています。

継承を利用して具体的なモデルクラス(この場合はユーザモデル)を作成し、そのクラス内で抽象メソッドを実装しています。

これにより、独自のデータモデルを効率的に設計することができます。

まとめ

Kotlinでの抽象クラスの利用は、プログラムの再利用性や拡張性を大幅に向上させる重要な要素です。

この記事を通じて、抽象クラスの基本的な定義方法から、実践的な活用方法、カスタマイズのヒントまで、幅広く抽象クラスの知識を深めることができました。

抽象クラスは、具体的な実装を持たないメソッドやプロパティを定義するためのものであり、それを継承する具体的なクラスでその機能を実装します。

Kotlinでは、拡張関数を使って既存の抽象クラスに新しいメソッドを追加することもでき、独自のライブラリやフレームワークを作成する際の基盤としても大変便利です。

初心者から上級者まで、Kotlinプログラミングの質と効率を向上させるために、抽象クラスの活用は欠かせません。

日々の開発において、この記事の内容を参考にしながら、より効果的なコード設計を心掛けてください。