プログラマー向け Kotlin ブートキャンプ 4: オブジェクト指向プログラミング

この Codelab は、プログラマー向け Kotlin ブートキャンプ コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できます。理解度によっては、特定のセクションの概要を読むだけで済む場合があります。このコースは、Kotlin を学習し、オブジェクト指向言語を習得したいプログラマーを対象としています。

はじめに

この Codelab では、Kotlin プログラムを作成し、Kotlin のクラスとオブジェクトについて学習します。この内容の多くは、別のオブジェクト指向言語の知識があればよくわかりますが、Kotlin には、記述する必要があるコードの量を減らすための重要な違いがあります。また、抽象クラスとインターフェースの委任についても学習します。

このコースのレッスンは単一のサンプルアプリを構築するものではなく、知識を広げることを目的としていますが、相互に依存しない部分があるため、使い慣れたセクションを省略できます。これらをまとめる例の多くは、水族館のテーマを使用しています。また、アクアリウムのストーリー全文については、Udacity の Profiler 向け Kotlin ブートキャンプ コースで詳細をご覧ください。

前提となる知識

  • Kotlin の基本(型、演算子、ループなど)
  • Kotlin の関数構文
  • オブジェクト指向プログラミングの基本
  • IntelliJ IDEA や Android Studio などの IDE の基本

学習内容

  • Kotlin でクラスを作成してプロパティにアクセスする方法
  • Kotlin でクラス コンストラクタを作成して使用する方法
  • サブクラスの作成方法と継承の仕組み
  • 抽象クラス、インターフェース、インターフェース委任について
  • データクラスを作成して使用する方法
  • シングルトン、列挙型、シールクラスの使用方法

演習内容

  • プロパティを使用してクラスを作成する
  • クラスのコンストラクタを作成する
  • サブクラスを作成する
  • 抽象クラスとインターフェースの例を調べる
  • 簡単なデータクラスを作成する
  • シングルトン、列挙型、シールクラスについて学習する

次のプログラミング用語はすでにご存じでしょう。

  • クラスはオブジェクトのブループリントです。たとえば、Aquarium クラスは、水族館オブジェクトを作成するための設計図です。
  • オブジェクトはクラスのインスタンスです。水族館のオブジェクトは、実際の Aquarium の 1 つです。
  • プロパティは、Aquarium の長さ、幅、高さなどのクラスの特性です。
  • メソッド(メンバー関数とも呼ばれます)は、クラスの関数です。メソッドは、オブジェクトに対して「できること」です。たとえば、Aquarium オブジェクトを fillWithWater() できます。
  • インターフェースは、クラスが実装できる仕様です。たとえば、掃除機は水槽以外の物体に一般的で、通常、掃除は物体ごとに同様の方法で行われます。したがって、clean() メソッドを定義する Clean というインターフェースを使用できます。Aquarium クラスで Clean インターフェースを実装して、水槽を柔らかいスポンジできれいにすることができます。
  • パッケージを使用すると、関連するコードをグループ化して整理したり、コードのライブラリを作成したりできます。パッケージが作成されたら、パッケージの内容を別のファイルにインポートして、その中のコードとクラスを再利用できます。

このタスクでは、新しいパッケージ、およびいくつかのプロパティとメソッドを含むクラスを作成します。

ステップ 1: パッケージを作成する

パッケージを使用すると、コードを整理できます。

  1. [Project] ペインの [Hello Kotlin] プロジェクトの下にある [src] フォルダを右クリックします。
  2. [New > Package] を選択し、example.myapp という名前を付けます。

ステップ 2: プロパティを使用してクラスを作成する

クラスはキーワード class で定義されます。クラス名は慣例により大文字で始まります。

  1. example.myapp パッケージを右クリックします。
  2. [New > Kotlin File / Class] を選択します。
  3. [Kind] で [Class] を選択し、クラスに「Aquarium」という名前を付けます。IntelliJ IDEA はファイルにパッケージ名を含み、空の Aquarium クラスを作成します。
  4. Aquarium クラス内で、幅、高さ、長さの var プロパティを定義して初期化します(センチメートル単位)。プロパティをデフォルト値で初期化します。
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

Kotlin は、内部では Aquarium クラスで定義したプロパティにゲッターとセッターを自動的に作成するため、プロパティ(myAquarium.length など)に直接アクセスできます。

ステップ 3: main() 関数を作成する

main() 関数を保持する main.kt という新しいファイルを作成します。

  1. 左側の [Project] ペインで example.myapp パッケージを右クリックします。
  2. [New > Kotlin File / Class] を選択します。
  3. [種類] プルダウンで、選択内容を [ファイル] のままにして、ファイル名を main.kt にします。IntelliJ IDEA にはパッケージ名が含まれていますが、ファイルのクラス定義が含まれていません。
  4. buildAquarium() 関数を定義し、Aquarium のインスタンスを作成します。インスタンスを作成するには、クラスを関数 Aquarium() のように参照します。これにより、他の言語の new を使用する場合と同様に、クラスのコンストラクタが呼び出され、Aquarium クラスのインスタンスが作成されます。
  5. main() 関数を定義して、buildAquarium() を呼び出します。
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

ステップ 4: メソッドを追加する

  1. Aquarium クラスに、水族館のディメンション プロパティを出力するメソッドを追加します。
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. main.ktbuildAquarium() で、myAquariumprintSize() メソッドを呼び出します。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. main() 関数の横にある緑色の三角形をクリックしてプログラムを実行します。結果を確認します。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. buildAquarium() で、高さを 60 に設定し、変更したディメンション プロパティを出力するコードを追加します。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. プログラムを実行し、出力を確認します。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

このタスクでは、クラスのコンストラクタを作成し、プロパティの操作を続行します。

ステップ 1: コンストラクタを作成する

このステップでは、最初のタスクで作成した Aquarium クラスにコンストラクタを追加します。上記の例では、Aquarium のすべてのインスタンスが同じディメンションで作成されています。ディメンションを作成したら、プロパティを設定して変更できますが、最初は適切なサイズにする方が簡単です。

一部のプログラミング言語では、コンストラクタはクラスと同じ名前のメソッドをクラス内に作成することで定義されます。Kotlin では、クラス宣言自体でコンストラクタを直接定義し、クラスがメソッドであるかのようにかっこ内のパラメータを指定します。Kotlin の関数と同様に、これらのパラメータにはデフォルト値を含めることができます。

  1. 先ほど作成した Aquarium クラスで、クラス定義を変更して、lengthwidthheight のデフォルト値を持つ 3 つのコンストラクタ パラメータを追加し、それらを対応するプロパティに割り当てます。
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. よりコンパクトな Kotlin の場合は、var または val を使用して、コンストラクタでプロパティを直接定義する方法もあります。Kotlin ではゲッターとセッターも自動的に作成されます。その後、クラスの本体にあるプロパティ定義を削除できます。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. このコンストラクタで Aquarium オブジェクトを作成する場合、引数を指定せずにデフォルト値を取得するか、引数の一部のみを指定するか、またはすべて指定して完全にカスタムの Aquarium を作成できます。buildAquarium() 関数で、名前付きパラメータを使用して Aquarium オブジェクトを作成するさまざまな方法を試してください。
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. プログラムを実行して出力を確認します。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

この場合、コンストラクタを過負荷にし、それぞれのケースで異なるバージョンを記述する必要はありません(他の組み合わせにはさらにバージョンを追加する必要があります)。Kotlin は、デフォルト値と名前付きパラメータから必要なものを作成します。

ステップ 2: init ブロックを追加する

上記のコンストラクタの例では、単にプロパティを宣言し、式の値を割り当てています。コンストラクタでより多くの初期化コードが必要な場合は、コンストラクタを 1 つ以上の init ブロックに配置します。このステップでは、init ブロックを Aquarium クラスに追加します。

  1. Aquarium クラスに、オブジェクトが初期化されていることを出力する init ブロックと、ボリュームをリットルで出力する 2 つ目のブロックを追加します。
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. プログラムを実行して出力を確認します。
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

init ブロックは、クラス定義内の出現順序順に実行され、これらはすべてコンストラクタが呼び出されたときに実行されます。

ステップ 3: セカンダリ コンストラクタについて学習する

このステップでは、セカンダリ コンストラクタについて学び、クラスにコンストラクタを追加します。1 つ以上の init ブロックを持つことができるプライマリ コンストラクタに加えて、Kotlin クラスには、コンストラクタのオーバーロード(異なる引数を持つコンストラクタ)を 1 つ以上持つセカンダリ コンストラクタもあります。

  1. Aquarium クラスで、constructor キーワードを使用して、引数として複数の魚を取るセカンダリ コンストラクタを追加します。val タンク プロパティを作成して、魚の量に基づいて水族館の水量を計算します。魚 1 匹あたり 2 リットル(2,000 cm^3)の水と、水がこぼれるように少し余った部屋を想定してください。
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. セカンダリ コンストラクタ内で、長さと幅(メイン コンストラクタで設定されているもの)を同じにして、タンクの指定されたボリュームに必要な高さを計算します。
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. buildAquarium() 関数に、新しいセカンダリ コンストラクタを使用して Aquarium を作成する呼び出しを追加します。サイズとボリュームを印刷します。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. プログラムを実行し、出力を確認します。
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

ボリュームが 2 回出力されます。1 回目はセカンダリ コンストラクタの実行前にプライマリ コンストラクタの init ブロックで、もう 1 回は buildAquarium() のコードによって出力されます。

プライマリ コンストラクタに constructor キーワードを含めることもできますが、ほとんどの場合は必須ではありません。

ステップ 4: 新しいプロパティ ゲッターを追加する

このステップでは、明示的なプロパティ ゲッターを追加します。Kotlin では、プロパティの定義時にゲッターとセッターが自動的に定義されますが、プロパティの値の調整または計算が必要になる場合があります。たとえば、上記では Aquarium のボリュームを出力します。変数とそのゲッターを定義することにより、ボリュームをプロパティとして使用可能にできます。volume は計算される必要があるため、ゲッターは計算値を返す必要があります。これは 1 行の関数で実行できます。

  1. Aquarium クラスで、volume という Int プロパティを定義し、次の行でボリュームを計算する get() メソッドを定義します。
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. ボリュームを出力する init ブロックを削除します。
  2. buildAquarium() で、ボリュームを出力するコードを削除します。
  3. printSize() メソッドに、ボリュームを出力する行を追加します。
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. プログラムを実行し、出力を確認します。
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

サイズとボリュームは前と同じですが、ボリュームはプライマリ コンストラクタとセカンダリ コンストラクタの両方で完全に初期化された後に 1 回だけ出力されます。

ステップ 5: プロパティ セッターを追加する

このステップでは、ボリュームの新しいプロパティ セッターを作成します。

  1. Aquarium クラスで、volumevar に変更して、複数回設定できるようにします。
  2. ゲッターの下に set() メソッドを追加して、volume プロパティのセッターを追加します。このメソッドは、指定された水分量に基づいて高さを再計算します。通常、setter パラメータの名前は value ですが、必要に応じて変更できます。
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium() に、水族館の音量を 70 リットルに設定するコードを追加します。新しいサイズを印刷します。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. プログラムを再度実行して、変化した高さとボリュームを確認します。
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

現時点では、publicprivate などの可視性修飾子はコードに含まれていません。デフォルトでは、Kotlin のオブジェクトはすべて一般公開されているため、クラス、メソッド、プロパティ、メンバー変数など、すべてのものにアクセスできます。

Kotlin では、クラス、オブジェクト、インターフェース、コンストラクタ、関数、プロパティ、それらのセッターに、可視性修飾子を含めることができます。

  • public はクラスの外部に表示されることを意味します。デフォルトでは、クラスの変数やメソッドを含め、すべてが一般公開されます。
  • internal は、そのモジュール内でのみ表示されることを意味します。モジュールは、ライブラリやアプリケーションなど、コンパイルされた Kotlin ファイルのセットです。
  • private は、そのクラス(関数の場合はソースファイル)でのみ表示されます。
  • protectedprivate と同じですが、すべてのサブクラスにも表示されます。

詳しくは、Kotlin ドキュメントの Visibility 修飾子をご覧ください。

メンバー変数

クラス内のプロパティ、つまりメンバー変数はデフォルトで public です。var で定義すると可変であり、読み取りや書き込みが可能です。val で定義した場合、初期化後には読み取り専用になります。

コードに対して読み取り / 書き込み可能なプロパティが必要で、外部コードからは読み取りのみできるようにするには、プロパティとそのゲッターを公開のままにして、セッターを非公開として宣言します。以下をご覧ください。

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

このタスクでは、Kotlin でサブクラスと継承がどのように機能するかを学習します。他の言語と同様ですが、いくつか違いがあります。

Kotlin のデフォルトでは、クラスをサブクラス化することはできません。同様に、プロパティとメンバー変数はサブクラスでオーバーライドできません(ただし、アクセスできます)。

サブクラス化できるようにするには、クラスを open としてマークする必要があります。同様に、サブクラスでオーバーライドするには、プロパティとメンバー変数を open としてマークする必要があります。実装の詳細が誤ってクラスのインターフェースの一部として漏洩しないように、open キーワードが必要です。

ステップ 1: 水族館のクラスを開く

このステップでは、Aquarium クラスを open にして、次のステップでオーバーライドできるようにします。

  1. Aquarium クラスとそのすべてのプロパティに open キーワードを付けます。
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. 開いている shape プロパティを値 "rectangle" で追加します。
   open val shape = "rectangle"
  1. Aquarium の 90% のボリュームを返すゲッターで、water プロパティを開きます。
    open var water: Double = 0.0
        get() = volume * 0.9
  1. printSize() メソッドにシェイプを出力するコードを追加し、体積の割合として水の量を出力します。
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. buildAquarium() で、width = 25length = 25height = 40 を含む Aquarium を作成するようにコードを変更します。
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. プログラムを実行して、新しい出力を確認します。
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

ステップ 2: サブクラスを作成する

  1. TowerTank という Aquarium のサブクラスを作成します。このサブクラスでは、長方形のタンクではなく円形のシリンダー タンクを実装します。Aquarium クラスと同じファイルに別のクラスを追加できるため、Aquarium の下に TowerTank を追加できます。
  2. TowerTank で、コンストラクタで定義されている height プロパティをオーバーライドします。プロパティをオーバーライドするには、サブクラスで override キーワードを使用します。
  1. TowerTank のコンストラクタが diameter を受け取るようにする。Aquarium スーパークラスでコンストラクタを呼び出すときは、lengthwidth の両方に diameter を使用します。
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. ボリューム プロパティをオーバーライドして円柱を計算します。円柱の計算式は、円周率に半径の 2 乗と高さを掛けた値です。java.lang.Math から定数 PI をインポートする必要があります。
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. TowerTank で、water プロパティをオーバーライドして、音量の 80% にします。
override var water = volume * 0.8
  1. shape をオーバーライドして "cylinder" にします。
override val shape = "cylinder"
  1. 最終的な TowerTank クラスは次のコードのようになります。

Aquarium.kt:

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. buildAquarium() で、直径 25 cm、高さ 45 cm の TowerTank を作成します。サイズを印刷します。

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. プログラムを実行し、出力を確認します。
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

場合によっては、関連するクラス間で共有される共通の動作やプロパティを定義する必要があります。Kotlin には、インターフェースと抽象クラスの 2 つの方法があります。このタスクでは、すべての魚に共通するプロパティ用に抽象 AquariumFish クラスを作成します。すべての魚に共通する動作を定義するには、FishAction というインターフェースを作成します。

  • 抽象クラスもインターフェースも単独でインスタンス化できないため、これらの型のオブジェクトを直接作成することはできません。
  • Abstract クラスにはコンストラクタがあります。
  • インターフェースにはコンストラクタ ロジックを指定したり、状態を保存したりすることはできません。

ステップ 1: Abstract クラスを作成する

  1. example.myapp に、新しいファイル AquariumFish.kt を作成します。
  2. AquariumFish というクラスを作成し、abstract マークを付けます。
  3. String プロパティを 1 つ(color)追加し、abstract とマークします。
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. AquariumFish の 2 つのサブクラス(SharkPlecostomus)を作成します。
  2. color は抽象クラスであるため、サブクラスはこれを実装する必要があります。Shark グレーと Plecostomus ゴールドにする。
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. main.ktmakeFish() クラスを作成し、クラスをテストします。SharkPlecostomus をインスタンス化し、それぞれの色を出力します。
  2. main() で以前のテストコードを削除し、makeFish() の呼び出しを追加します。コードは以下のようになります。

main.kt:

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. プログラムを実行し、出力を確認します。
⇒ Shark: gray 
Plecostomus: gold

次の図は、Shark クラスと Plecostomus クラスを表しています。これらは抽象クラス AquariumFish をサブクラス化しています。

Abstract クラス(AhfishFish)と、2 つのサブクラス「Shark」と「Plecostumus」を表す図。

ステップ 2: インターフェースを作成する

  1. AquariumFish.kt で、FishAction という名前のインターフェースを eat() メソッドを使って作成します。
interface FishAction  {
    fun eat()
}
  1. 各サブクラスに FishAction を追加し、魚の動きを出力するように eat() を実装します。
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. makeFish() 関数で、作成したそれぞれの魚に eat() を呼び出して何かを食べさせます。
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. プログラムを実行し、出力を確認します。
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

次の図は、Shark クラスと Plecostomus クラスを表しています。どちらも、FishAction インターフェースで構成され、実装されています。

抽象クラスとインターフェースの使い分け

上記の例は単純ですが、相互に関連するクラスが多数ある場合、抽象クラスとインターフェースを使用すると、デザインをすっきりと整理して、メンテナンスが容易になります。

前述のように、抽象クラスにはコンストラクタを含めることができます。インターフェースはそうではありませんが、異なる点はよく似ています。それぞれを使用すべき状況

インターフェースを使用してクラスを作成すると、クラスの機能が、そのクラスに含まれているクラス インスタンスによって拡張されます。コンポジションは、抽象クラスから継承するよりも、コードの再利用と推論を簡略化する傾向があります。また、1 つのクラスで複数のインターフェースを使用できますが、サブクラスを作成できるのは 1 つの抽象クラスのみです。

多くの場合、コンポジションはカプセル化の向上、結合の低下(相互依存性)、インターフェースのクリーンアップ、コードの有用性の向上につながります。こうした理由から、インターフェースを使ったコンポジションの使用を推奨します。一方、抽象クラスからの継承は、問題によっては自然な選択になる傾向があります。コンポジションが望ましいですが、継承が理にかなっている場合は、それも可能です。

  • 多数のメソッドがある場合や、1 つまたは 2 つのデフォルト実装がある場合は、インターフェースを使用します。たとえば、下の AquariumAction のようになります。
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • クラスを修了できない場合はいつでも抽象クラスを使用します。たとえば、AquariumFish クラスに戻ると、すべての魚にデフォルトの色がないため、すべての AquariumFishFishAction を実装し、color を抽象化したまま eat にデフォルトの実装を指定できます。
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

前のタスクでは、抽象クラス、インターフェース、構成の概念を導入しました。インターフェース委任は、ヘルパー(またはデリゲート)オブジェクトによってインターフェースのメソッドを実装し、それをクラスで使用する高度な手法です。この手法は、一連の無関係なクラスでインターフェースを使用する場合に役立ちます。必要なインターフェース機能を個別のヘルパークラスに追加し、各クラスはヘルパークラスのインスタンスを使用して機能を実装します。

このタスクでは、インターフェース委任を使用してクラスに機能を追加します。

ステップ 1: 新しいインターフェースを作成する

  1. AquariumFish.kt で、AquariumFish クラスを削除します。PlecostomusShark は、AquariumFish クラスから継承するのではなく、魚のアクションとその色の両方に対応するインターフェースを実装します。
  2. 色を文字列として定義する新しいインターフェース FishColor を作成します。
interface FishColor {
    val color: String
}
  1. Plecostomus を変更して、FishActionFishColor の 2 つのインターフェースを実装します。FishColorcolorFishActioneat() をオーバーライドする必要があります。
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. AquariumFish から継承する FishActionFishColor という 2 つのインターフェースも実装するように Shark クラスを変更します。
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. 完成したコードは次のようになります。
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

ステップ 2: シングルトン クラスを作成する

次に、FishColor を実装するヘルパークラスを作成して、デリゲート部分の設定を実装します。FishColor を実装する GoldColor という基本クラスを作成します。目的は、色が金色であることだけです。

GoldColor のインスタンスは、すべて同じ動作をするので、複数指定しても意味がありません。Kotlin では、class ではなく object というキーワードを使用して、そのインスタンスを 1 つしか作成できないクラスを宣言できます。Kotlin によってそのインスタンスが 1 つ作成され、そのインスタンスがクラス名によって参照されます。そうすると、他のすべてのオブジェクトがこの 1 つのインスタンスを使用するだけで、このクラスの他のインスタンスを作成する方法はありません。シングルトン パターンに精通している場合は、これが Kotlin でシングルトンを実装する方法です。

  1. AquariumFish.kt で、GoldColor のオブジェクトを作成します。色をオーバーライドします。
object GoldColor : FishColor {
   override val color = "gold"
}

手順 3: FishColor のインターフェースの委任を追加する

これで、インターフェースの委任を使用する準備が整いました。

  1. AquariumFish.kt で、color のオーバーライドを Plecostomus から削除します。
  2. GoldColor から色を取得するように Plecostomus クラスを変更します。これを行うには、クラス宣言に by GoldColor を追加し、委任を作成します。つまり、FishColor を実装する代わりに、GoldColor で提供される実装を使用します。したがって、color がアクセスされるたびに GoldColor に委任されます。
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

クラスはそのままで、すべてのペレコが金色になりますが、実際にはこれらの魚には多くの色があります。この問題に対処するには、Plecostomus のデフォルト色として GoldColor を指定して、色のコンストラクタ パラメータを追加します。

  1. コンストラクタを使用して fishColor でパスを受け取るように Plecostomus クラスを変更し、デフォルトを GoldColor に設定します。委任を by GoldColor から by fishColor に変更します。
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

ステップ 4: FishAction のインターフェースの委任を追加する

同様に、FishAction のインターフェースの委任も使用できます。

  1. AquariumFish.kt では、FishAction を実装する PrintingFishAction クラスを作成します。このクラスは、Stringfood を取り、魚の食べものを出力します。
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Plecostomus クラスのオーバーライド関数 eat() を削除します。委任関数に置き換えます。
  2. Plecostomus の宣言で、FishActionPrintingFishAction にデリゲートし、"eat algae" を渡します。
  3. そのデリゲートすべてで、Plecostomus クラスの本文にコードはありません。そのため、すべてのオーバーライドはインターフェースのデリゲートによって処理されるため、{} は削除してください。
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

次の図では、Shark クラスと Plecostomus クラスを示しています。どちらも PrintingFishAction インターフェースと FishColor インターフェースで構成されていますが、実装も委任しています。

インターフェース委任は強力です。通常、別の言語で抽象クラスを使用する可能性がある場合は、その使用方法を検討する必要があります。コンポジションを使用することで、多くのサブクラスを必要とせずに、それぞれ別の方法で動作をプラグインできます。

データクラスは、他の一部の言語の struct に似ています(主に一部のデータを保持するために存在します)が、データクラス オブジェクトはオブジェクトです。Kotlin データクラス オブジェクトには、印刷やコピーのユーティリティなど、いくつかのメリットがあります。このタスクでは、簡単なデータクラスを作成し、Kotlin によるデータクラスのサポートについて学習します。

ステップ 1: データクラスを作成する

  1. example.myapp パッケージの下に新しいパッケージ decor を追加して、新しいコードを保持します。[Project] ペインで example.myapp を右クリックし、[File > New > Package] を選択します。
  2. パッケージ内で、Decoration という新しいクラスを作成します。
package example.myapp.decor

class Decoration {
}
  1. Decoration をデータクラスにするには、クラス宣言の前にキーワード data を付けます。
  2. rocks という String プロパティを追加して、クラスにデータを提供します。
data class Decoration(val rocks: String) {
}
  1. このファイルのクラスの外側に makeDecorations() 関数を追加して、Decoration のインスタンスを作成し、"granite" で出力します。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. main() 関数を追加して makeDecorations() を呼び出し、プログラムを実行します。これはデータクラスであるため、生成される有効な出力に注目してください。
⇒ Decoration(rocks=granite)
  1. makeDecorations() で、さらに「slate」である 2 つの Decoration オブジェクトをインスタンス化して出力します。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() で、decoration1decoration2 を比較した結果を出力する print ステートメントを追加し、decoration3decoration2 を比較する 2 つ目のステートメントを追加します。データクラスが提供する equals() メソッドを使用する。
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. コードを実行します。
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

ステップ 2: 分解の使用

データ オブジェクトのプロパティを取得して変数に割り当てるには、次のように 1 つずつ使用します。

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

代わりに、プロパティごとに 1 つずつ変数を作成し、データ オブジェクトを変数のグループに割り当てることができます。Kotlin では、各変数にプロパティ値を配置しています。

val (rock, wood, diver) = decoration

これは分解と呼ばれ、便利な省略形です。変数の数はプロパティの数と一致させてください。変数はクラスで宣言された順序で割り当てられます。Decoration.kt で試すことができる完全な例を次に示します。

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

不要なプロパティがある場合は、変数名の代わりに _ を使って、下記のコードのようにスキップできます。

    val (rock, _, diver) = d5

このタスクでは、Kotlin の以下の専用クラスについて学習します。

  • シングルトン クラス
  • 列挙型
  • 密閉型クラス

ステップ 1: シングルトン クラスを再現する

前述の例を GoldColor クラスで思い出してください。

object GoldColor : FishColor {
   override val color = "gold"
}

GoldColor のすべてのインスタンスが同じ処理を行うので、これをシングルトンにするために class ではなく object として宣言します。インスタンスは 1 つだけです。

ステップ 2: 列挙型を作成する

Kotlin は列挙型をサポートしているため、他の言語と同様に、何かを列挙して名前で参照できます。キーワードの先頭に enum を付けて列挙型を宣言します。基本的な列挙型宣言には名前のリストのみが必要ですが、名前ごとに 1 つ以上のフィールドを定義することもできます。

  1. Decoration.kt で列挙型の例を試します。
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

列挙型はシングルトンと少し似ています。列挙型の各値は 1 つのみで、1 つだけになります。たとえば、Color.REDColor.GREENColor.BLUE をそれぞれ 1 つだけ指定できます。この例では、色のコンポーネントを表すために、RGB 値が rgb プロパティに割り当てられます。また、ordinal プロパティを使用して列挙の序数を取得し、name プロパティを使用してその名前を取得することもできます。

  1. 列挙型の別の例をお試しください。
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

ステップ 3: シールドされたクラスを作成する

シールクラスは、サブクラス化できるクラスですが、宣言されたファイルの内部にのみ存在します。このクラスを別のサブクラスにサブクラス化しようとすると、エラーが発生します。

これらのクラスとサブクラスは同じファイル内にあるため、Kotlin ではすべてのサブクラスが静的に認識されます。つまり、コンパイル時にはコンパイラがすべてのクラスとサブクラスを認識し、それらがすべて把握しているので、コンパイラは追加のチェックを行います。

  1. AquariumFish.kt で、魚のテーマに沿って「シール」クラスを授業で試します。
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

Seal クラスを別のファイルでサブクラス化することはできません。さらに Seal の種類を追加する場合は、同じファイルに追加する必要があります。これにより、シールクラスは固定数の型を安全に表すことができます。たとえば、シールクラスはネットワーク API から成功またはエラーを返すのに適しています。

このレッスンでは多くのことを取り上げました。Kotlin の多くは他のオブジェクト指向プログラミング言語に精通している必要がありますが、Kotlin にはコードを簡潔で読みやすくするための機能が追加されています。

クラスとコンストラクタ

  • Kotlin で class を使用してクラスを定義します。
  • Kotlin では、プロパティのセッターとゲッターが自動的に作成されます。
  • プライマリ コンストラクタをクラス定義で直接定義します。次に例を示します。
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • プライマリ コンストラクタに追加のコードが必要な場合は、init ブロックでコードを記述します。
  • クラスでは constructor を使用して 1 つ以上のセカンダリ コンストラクタを定義できますが、Kotlin スタイルは代わりにファクトリ関数を使用します。

可視性修飾子とサブクラス

  • Kotlin のすべてのクラスと関数はデフォルトで public ですが、修飾子を使用して可視性を internalprivateprotected に変更できます。
  • サブクラスを作成するには、親クラスを open とマークする必要があります。
  • サブクラスでメソッドとプロパティをオーバーライドするには、親クラスのメソッドとプロパティを open としてマークする必要があります。
  • シールクラスは、そのファイルが同じファイルでのみサブクラス化できます。宣言の前に sealed を付けて、シールクラスを作成します。

データクラス、シングルトン、列挙型

  • 宣言の前に data を付けて、データクラスを作成します。
  • 分解とは、data オブジェクトのプロパティを個別の変数に割り当てる方法です。
  • class ではなく object を使用してシングルトン クラスを作成します。
  • enum class を使用して列挙型を定義します。

抽象クラス、インターフェース、委任

  • 抽象クラスとインターフェースは、クラス間で共通の動作を共有するための 2 つの方法です。
  • 抽象クラスは、プロパティと動作を定義しますが、実装はサブクラスに任せます。
  • インターフェースは動作を定義し、動作の一部またはすべてにデフォルトの実装を提供する場合があります。
  • インターフェースを使用してクラスを作成すると、クラスの機能が、そのクラスに含まれているクラス インスタンスによって拡張されます。
  • インターフェース委任ではコンポジションが使用されますが、実装もインターフェース クラスに委任されます。
  • コンポジションは、インターフェースの委任を使用してクラスに機能を追加する強力な方法です。一般的にはコンポジションが優先されますが、一部の問題には Abstract クラスから継承した方が適しています。

Kotlin ドキュメント

このコースに関するトピックについて詳しい情報が必要な場合や、ご不明な点がある場合は、https://kotlinlang.org をご覧ください。

Kotlin のチュートリアル

https://try.kotlinlang.org のウェブサイトには、Kotlin Koans(リッチなチュートリアル)やウェブベースのインタープリタ、サンプルを含むリファレンス ドキュメントが豊富に用意されています。

Udacity コース

このトピックに関する Udacity コースについては、プログラマー向け Kotlin ブートキャンプをご覧ください。

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトにあります。

このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。

  • 必要に応じて課題を割り当てます。
  • 宿題の提出方法を生徒に伝える。
  • 宿題を採点します。

教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。

この Codelab にご自分で取り組む場合は、これらの課題を使用して知識をテストしてください。

次の質問に答えてください。

問題 1

クラスには、そのクラスのオブジェクトを作成するための設計図となる特別なメソッドがあります。このメソッドは何と呼ばれますか。

▢ ビルダー

▢ インスタンス化

▢ コンストラクタ

▢ ブループリント

質問 2

インターフェースと抽象クラスの説明として正しくないものは次のうちどれですか。

▢ Abstract クラスにはコンストラクタを含めることができます。

▢ インターフェースにはコンストラクタがありません。

▢ インターフェースと抽象クラスを直接インスタンス化できる。

▢ 抽象プロパティは、抽象クラスのサブクラスによって実装する必要があります。

問題 3

Kotlin の可視性修飾子(プロパティ、メソッドなど)ではないものは、次のうちどれですか。

internal

nosubclass

protected

private

問題 4

このデータクラスを考えてみます。
data class Fish(val name: String, val species:String, val colors:String)
Fish オブジェクトの作成と破棄に有効なコードは次のうちどれですか。

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

問題 5

たとえば、たくさんの動物が飼育されている動物園で、すべてを世話をする必要があるとします。次のうち、ケアの実装に該当しないのはどれですか。

▢ さまざまな種類の動物が食べるもののための interface

▢ さまざまな種類の管理者を管理する abstract Caretaker クラスです。

interface - 動物にきれいな水を与えます。

▢ フィード スケジュール内のエントリの data クラス。

次のレッスン「5.1 拡張機能」に進みます。

他の Codelab へのリンクを含むコースの概要については、プログラマー向け Kotlin ブートキャンプ: コースへようこそをご覧ください。