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

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

はじめに

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

このコースのレッスンは、1 つのサンプルアプリを作成するのではなく、知識を深めるように設計されています。また、各レッスンは半独立しているため、よく知っているセクションは読み飛ばすことができます。これらの例を関連付けるため、多くは水族館をテーマにしています。水族館の物語の全体像を確認したい場合は、Udacity の 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. [Kind] プルダウンで、選択を [File] のままにして、ファイルに 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: 初期化ブロックを追加する

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

  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 ブロックで、2 回目は 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

ディメンションとボリュームは以前と同じですが、ボリュームは、プライマリ コンストラクタとセカンダリ コンストラクタの両方でオブジェクトが完全に初期化された後に一度だけ出力されます。

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

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

  1. Aquarium クラスで、volumevar に変更して、複数回設定できるようにします。
  2. volume プロパティのセッターを追加します。ゲッターの下に set() メソッドを追加し、提供された水の量に基づいて高さを再計算します。慣例により、セッター パラメータの名前は 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 ドキュメントの可視性修飾子をご覧ください。

メンバー変数

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

コードで読み取りと書き込みが可能で、外部コードでは読み取りのみが可能なプロパティが必要な場合は、次の例のように、プロパティとそのゲッターを public のままにして、セッターを private として宣言します。

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

このタスクでは、Kotlin でのサブクラスと継承の仕組みについて学習します。他の言語で見たものと似ていますが、違いもあります。

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

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

ステップ 1: Aquarium クラスをオープンにする

このステップでは、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. "rectangle" の open shape プロパティを追加します。
   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. Aquarium のサブクラスとして TowerTank を作成します。これは、長方形のタンクではなく丸い円筒形のタンクを実装します。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. volume プロパティをオーバーライドして、円柱を計算します。円柱の体積の公式は、円周率 × 半径の 2 乗 × 高さです。定数 PIjava.lang.Math からインポートする必要があります。
    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 というインターフェースを作成して、すべての魚に共通する動作を定義します。

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

ステップ 1. 抽象クラスを作成する

  1. example.myapp の下に、新しいファイル AquariumFish.kt を作成します。
  2. AquariumFish というクラスを作成し、abstract でマークします。
  3. String プロパティ color を 1 つ追加し、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.kt で、クラスをテストする makeFish() 関数を作成します。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

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

抽象クラス AquariumFish と 2 つのサブクラス Shark と Plecostumus を示す図。

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

  1. AquariumFish.kt で、eat() メソッドを持つ FishAction というインターフェースを作成します。
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 つだけです。

コンポジションは、カプセル化の改善、結合(相互依存)の低減、インターフェースのクリーン化、コードの使いやすさの向上につながることがよくあります。これらの理由から、インターフェースとのコンポジションを使用する設計が推奨されます。一方、抽象クラスからの継承は、一部の問題に自然に適合する傾向があります。したがって、コンポジションを優先する必要がありますが、継承が妥当な場合は Kotlin でも継承を使用できます。

  • メソッドが多く、デフォルトの実装が 1 つまたは 2 つしかない場合は、インターフェースを使用します(以下の AquariumAction の例を参照)。
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • クラスを完成させることができない場合は、抽象クラスを使用します。たとえば、AquariumFish クラスに戻ると、すべての AquariumFishFishAction を実装させ、eat のデフォルト実装を提供できます。魚にデフォルトの色はないため、color は抽象のままにします。
interface FishAction  {
    fun eat()
}

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

前のタスクでは、抽象クラス、インターフェース、コンポジションの概念を紹介しました。インターフェース委任は、インターフェースのメソッドをヘルパー(またはデリゲート)オブジェクトで実装し、そのオブジェクトをクラスで使用する高度な手法です。この手法は、一連の関連性のないクラスでインターフェースを使用する場合に便利です。必要なインターフェース機能を別のヘルパークラスに追加し、各クラスがヘルパークラスのインスタンスを使用して機能を実装します。

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

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

  1. AquariumFish.kt で、AquariumFish クラスを削除します。AquariumFish クラスから継承する代わりに、PlecostomusShark は魚のアクションとその色の両方のインターフェースを実装します。
  2. 色を文字列として定義する新しいインターフェース FishColor を作成します。
interface FishColor {
    val color: String
}
  1. Plecostomus を変更して、2 つのインターフェース(FishActionFishColor)を実装します。FishColor から color を、FishAction から eat() をオーバーライドする必要があります。
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 で、Plecostomus から color のオーバーライドを削除します。
  2. Plecostomus クラスを変更して、GoldColor から色を取得します。これを行うには、クラス宣言に by GoldColor を追加して、委任を作成します。これは、FishColor を実装する代わりに、GoldColor によって提供される実装を使用することを意味します。そのため、color にアクセスするたびに、GoldColor に委任されます。
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

このクラスのままでは、すべてのプレコが金色になりますが、実際にはプレコにはさまざまな色があります。この問題に対処するには、Plecostomus のデフォルトの色として GoldColor を使用して、色のコンストラクタ パラメータを追加します。

  1. Plecostomus クラスを変更して、コンストラクタで渡された fishColor を受け取り、デフォルトを 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. ファイル内のクラス外に、"granite" を使用して Decoration のインスタンスを作成して出力する makeDecorations() 関数を追加します。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. makeDecorations() を呼び出す main() 関数を追加して、プログラムを実行します。これはデータクラスであるため、意味のある出力が作成されます。
⇒ Decoration(rocks=granite)
  1. makeDecorations() で、両方とも「slate」である Decoration オブジェクトをさらに 2 つインスタンス化して出力します。
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 つ目の print ステートメントを追加します。データクラスで提供される 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

代わりに、プロパティごとに変数を作成し、データ オブジェクトを変数のグループに割り当てることができます。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

1 つ以上のプロパティが不要な場合は、次のコードに示すように、変数名の代わりに _ を使用してスキップできます。

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

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

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

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

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

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

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

Kotlin ドキュメント

このコースのトピックについてさらに詳しい情報が必要な場合や、行き詰まった場合は、https://kotlinlang.org を参照することをおすすめします。

Kotlin のチュートリアル

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

Udacity コース

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

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトで確認できます。

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

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

以下の質問に回答してください

問題 1

クラスには、そのクラスのオブジェクトを作成するための設計図として機能する特別なメソッドがあります。このメソッドの名前は何ですか?

▢ ビルダー

▢ インスタンシエータ

▢ コンストラクタ

▢ ブループリント

問題 2

インターフェースと抽象クラスに関する次の記述のうち、正しくないものはどれですか。

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

▢ インターフェースにコンストラクタを含めることはできません。

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

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

問題 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

▢ さまざまなタイプの Caretaker を作成できる abstract Caretaker クラス。

▢ 動物にきれいな水を与えたことに対する interface

▢ 給餌スケジュールのエントリの data クラス。

次のレッスンに進む: 5.1 拡張機能

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