Kotlin のクラスと継承

1. 始める前に

要件

  • Kotlin プレイグラウンドを使用した Kotlin プログラムの編集に慣れている。
  • このコースのユニット 1 で説明した Kotlin プログラミングの基本的な概念。特に、main() プログラム、引数を持ち値を返す関数、変数、データ型、オペレーション、if/else ステートメント。
  • Kotlin でクラスを定義し、そこからオブジェクト インスタンスを作成してプロパティとメソッドにアクセスできる。

学習内容

  • 継承を使用してクラスの階層を実装する Kotlin プログラムを作成する。
  • クラスを拡張し、既存の機能をオーバーライドして、新しい機能を追加する。
  • 変数に対して適切な可視性修飾子を選択する。

作成するアプリの概要

  • さまざまな種類の住居をクラス階層として実装する Kotlin プログラム。

必要なもの

2. クラス階層とは

人間にとって、特性(プロパティ)や動作が類似している項目をグループに分類し、その中になんらかの階層を作るのは自然なことです。たとえば、野菜のような範囲の広いカテゴリを作り、その中に豆類といった具体的なタイプを含めることができます。豆類は、エンドウ豆、インゲン豆、レンズ豆、ひよこ豆、大豆など、さらに具体的なタイプに分類できます。

豆類は野菜が持つすべての特性(植物、食料など)を含んでいる(継承している)ため、この関係を階層構造で表すことができます。同様に、エンドウ豆、インゲン豆、レンズ豆はすべて、豆類の特性に加えて固有の特性も持っています。

この関係をプログラミング用語で表現するとどうなるか見てみましょう。Kotlin で Vegetable をクラスにすると、LegumeVegetable クラスの子またはサブクラスとして作成できます。つまり、Vegetable クラスのすべてのプロパティとメソッドは、Legume クラスによって継承(利用)されます。

この関係は、クラス階層図で次のように表現できます。Vegetable は、Legume クラスの親またはスーパークラスとして参照できます。

87e0a5eb0f85042d.png

Legume のサブクラス(LentilChickpea など)を作成することで、クラス階層をさらに拡張できます。これにより、LegumeVegetable の子またはサブクラスになると同時に、LentilChickpea の親またはスーパークラスになります。Vegetable は、この階層のルートクラスまたはトップレベル(基本)クラスです。

638655b960530d9.png

Android クラスの継承

以前の Codelab で行ったように、Kotlin コードはクラスを使わずに記述することもできますが、Android の多くの部分(アクティビティ、ビュー、ビューグループなど)はクラスの形式で提供されます。そのため、クラス階層を理解することは Android アプリの開発に不可欠であり、それによって Android フレームワークの機能を活用できるようになります。

たとえば、Android の View クラスは、画面上で長方形の領域を表現し、描画とイベントの処理を行います。TextView クラスは View クラスのサブクラスです。つまり、TextViewView クラスのすべてのプロパティと機能を継承するとともに、ユーザーにテキストを表示するための固有のロジックも備えています。

c39a8aaa5b013de8.png

EditText クラスと Button クラスは TextView クラスの子です。これらのクラスは TextView クラスと View クラスのすべてのプロパティとメソッドを継承したうえで、固有のロジックも備えています。たとえば EditText は、画面上のテキストを編集するための独自の機能を追加で備えています。

EditText クラスは TextView クラスをサブクラス化するだけで作成できるため、View クラスと TextView クラスのすべてのロジックをコピーして EditText に貼り付ける必要はありません。TextView クラスも同様に、View クラスをサブクラス化して作成します。そのため、EditText クラスのコードでは、この UI コンポーネントに固有の、他のビューと異なる機能だけに焦点を当てることができます。

developer.android.com のウェブサイトにある Android クラスのドキュメント ページの上部で、クラス階層図を確認できます。階層の最上部に kotlin.Any と表示されているのは、Kotlin ではすべてのクラスに共通のスーパークラス「Any」があるためです。詳しくはこちらをご覧ください。

1ce2b1646b8064ab.png

ご覧のように、クラス間の継承を活用することで、コードの記述、再利用、読み取り、テストを簡単に行えるようになります。

3.基本クラスを作成する

住居のクラス階層

この Codelab では、床面積、階数、居住者といった特性(プロパティ)を持つ住居(人が住む建物)を例として使って、クラス階層の仕組みを示す Kotlin プログラムを作成します。

以下の図は、ここで作成するクラス階層を示しています。ルートには、設計図と同じように、すべての住居のプロパティと機能を指定する Dwelling があります。その下に、正方形のキャビン(SquareCabin)、円形の小屋(RoundHut)、複数の階からなる RoundHut である円塔(RoundTower)の各クラスがあります。

de1387ca7fc26c81.png

以下のクラスを実装します。

  • Dwelling: すべての住居に共通する情報を保持する、非特定の建物を表す基本クラス。
  • SquareCabin: 床が正方形の木造キャビン。
  • RoundHut: 床が円形の、わらで作られた小屋。RoundTower の親。
  • RoundTower: 円形の床と複数の階からなる、石造りの塔。

Dwelling 抽象クラスを作成する

すべてのクラスは、クラス階層の基本クラス、または他のクラスの親にすることができます。

「抽象」クラスとは、完全に実装されていないため、インスタンス化できないクラスのことです。抽象クラスは、スケッチのようなものと考えることができます。スケッチにはアイデアや計画が盛り込まれていますが、通常は何かを作成するために十分な情報は含まれていません。スケッチ(抽象クラス)を使用して、実際のオブジェクト インスタンスをビルドするための設計図(クラス)を作成します。

スーパークラスを作成することの一般的なメリットは、すべてのサブクラスに共通するプロパティと関数を含めることができる点です。プロパティの値と関数の実装が不明な場合は、クラスを抽象化します。たとえば、Vegetables にはすべての野菜に共通するプロパティが多く含まれていますが、非特定の野菜の形状や色などは不明なため、そのインスタンスを作成することはできません。そこで、Vegetable を抽象クラスにして、各野菜について具体的な詳細を決めることはサブクラスに任せます。

抽象クラスの宣言は、abstract キーワードで始めます。

Dwelling も、Vegetable と同様に抽象クラスにします。Dwelling には多くの住居に共通するプロパティと関数を含めますが、プロパティの正確な値と関数の実装の詳細は不明です。

  1. Kotlin プレイグラウンド(https://developer.android.com/training/kotlinplayground)に移動します。
  2. エディタで、main() 関数内の println("Hello, world!") を削除します。
  3. 次のコードを main() 関数の下に追加して、Dwelling という abstract クラスを作成します。
abstract class Dwelling(){
}

建築材料のプロパティを追加する

Dwelling クラスでは、住居によって多少違いはあるものの、すべての住居に共通するプロパティを定義します。すべての住居は、いくつかの建築材料で構成されています。

  1. Dwelling 内で、建築材料を表す String 型の buildingMaterial 変数を作成します。建築材料は変更しないため、val を使用して不変の変数にします。
val buildingMaterial: String
  1. プログラムを実行すると、次のエラーが出力されます。
Property must be initialized or be abstract

これは buildingMaterial プロパティに値が指定されていないことが原因ですが、非特定の建物は特定の材料で構成されているわけではないため、値を指定することはできません。そこで、エラー メッセージが示すように、buildingMaterial の宣言の先頭に abstract キーワードを付加して、ここで定義されないことを示します。

  1. 変数の定義に abstract キーワードを追加します。
abstract val buildingMaterial: String
  1. コードを実行しても何も起こりませんが、エラーなしでコンパイルできるようになりました。
  2. main() 関数で Dwelling のインスタンスを作成し、コードを実行します。
val dwelling = Dwelling()
  1. 抽象 Dwelling クラスのインスタンスを作成できないため、次のエラーが発生します。
Cannot create an instance of an abstract class
  1. 間違ったコードを削除します。

完成したコードは次のようになります。

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

収容人数のプロパティを追加する

住居のもう一つのプロパティは、収容人数(住める人の数)です。

住居の収容人数は変化しません。ただし、収容人数は Dwelling スーパークラス内で設定することはできないため、特定の住居タイプのサブクラスとして定義する必要があります。

  1. Dwelling に、capacity という abstract 整数 val を追加します。
abstract val capacity: Int

居住者数の private プロパティを追加する

すべての住居には、住んでいる residents が複数(capacity 以下の数)いるため、Dwelling スーパークラスに residents プロパティを定義して、すべてのサブクラスがこのプロパティを継承して使用できるようにします。

  1. residents は、Dwelling クラスのコンストラクタに渡すパラメータにすることができます。residents プロパティは var です。これは、インスタンスの作成後に居住者数が変わる可能性があるためです。
abstract class Dwelling(private var residents: Int) {

residents プロパティは private キーワードでマークされています。private は Kotlin の可視性修飾子で、residents プロパティがこのクラスからのみアクセスできる(内部で使用できる)ことを意味します。プログラム内の他の場所からはアクセスできません。private キーワードを使用して、プロパティやメソッドをマークできます。可視性修飾子が指定されていない場合、プロパティとメソッドはデフォルトで public となり、プログラムの他の部分からアクセスできるようになります。一般的に、居住者数は(建築材料や建物の収容人数に関する情報と比較して)個人情報として扱われるため、private を使用することは理にかなっています。

住居の capacity と現在の residents 数の両方を定義したので、hasRoom() 関数を作成し、その住居にさらに居住者が入れる余裕があるかどうかを調べることができます。この計算式はすべての住居で共通するため、Dwelling クラスで hasRoom() 関数を定義して実装できます。residents の数が capacity より少ない場合は Dwelling に余裕があるため、関数はこの比較に基づいて true または false を返す必要があります。

  1. hasRoom() 関数を Dwelling クラスに追加します。
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. このコードを実行してもエラーは出ませんが、まだ何も表示されません。

完成したコードは次のようになります。

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. サブクラスを作成する

SquareCabin サブクラスを作成する

  1. Dwelling クラスの下に、SquareCabin という名前のクラスを作成します。
class SquareCabin
  1. 次に、SquareCabinDwelling に関連していることを示します。SquareCabinDwelling の抽象部分の実装を提供するため、コードで、SquareCabinDwelling から拡張された(または、Dwelling) のサブクラスである)ことを示す必要があります。

この継承関係を指定するには、SquareCabin クラス名の後にコロン(:)を追加し、その後に親の Dwelling クラスを初期化する呼び出しを指定します。Dwelling クラス名の後には必ずかっこを追加してください。

class SquareCabin : Dwelling()
  1. スーパークラスから拡張する場合は、スーパークラスに必要なパラメータを渡す必要があります。Dwelling は入力として residents の数を必要とします。3 など、一定の居住者数を渡すこともできます。
class SquareCabin : Dwelling(3)

ただし、ここではプログラムの柔軟性を高めて、SquareCabins の居住者の可変数に対応できるようにするため、residentsSquareCabin クラス定義内のパラメータにします。親クラスの Dwelling ですでに宣言されているプロパティを再利用するため、residentsval, として宣言しないようにしてください。

class SquareCabin(residents: Int) : Dwelling(residents)
  1. コードを実行します。
  2. これにより、次のエラーが発生します。
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

抽象関数や変数を宣言すると、後でそれらに値や実装を与える約束をしたことになります。変数の場合、その抽象クラスのサブクラスは値を与える必要があります。関数の場合、サブクラスは関数本文を実装する必要があります。

Dwelling クラスで、abstract 変数 buildingMaterial を定義しました。SquareCabinDwelling のサブクラスであるため、buildingMaterial の値を指定する必要があります。override キーワードを使用して、このプロパティが親クラスで定義済みであり、このクラスでオーバーライドされることを示します。

  1. SquareCabin クラス内で、buildingMaterial プロパティを override し、値 "Wood" を割り当てます。
  2. capacity についても同様に、SquareCabin には 6 人が住めることを示します。
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

完成したコードは次のようになります。

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

コードをテストするには、プログラムで SquareCabin のインスタンスを作成します。

SquareCabin を使用する

  1. Dwelling クラスと SquareCabin クラス定義の前に空の main() 関数を挿入します。
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. main() 関数内に、squareCabin という SquareCabin のインスタンスを作成し、居住者数を 6 人とします。建築材料、収容人数、hasRoom() 関数の print ステートメントを追加します。
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

hasRoom() 関数は SquareCabin クラスではなく Dwelling クラスで定義したことに注意してください。SquareCabinDwelling クラスのサブクラスであるため、hasRoom() 関数がそのまま継承されています。コード スニペットで squareCabin.hasRoom() と表示されていることからわかるように、SquareCabin のすべてのインスタンスに対して hasRoom() 関数を呼び出すことができるようになりました。

  1. コードを実行すると、次の内容が出力されます。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

作成した squareCabin の居住者は 6 人で、capacity と同じであるため、hasRoom()false を返します。これより少ない residents 数で SquareCabin を初期化してプログラムを再度実行すると、hasRoom()true を返すようになります。

with を使用してコードを簡素化する

println() ステートメントで、squareCabin のプロパティまたは関数を参照するたびに、squareCabin. を繰り返す必要があります。print ステートメントを何度もコピーして貼り付けるのは手間がかかり、エラーの原因にもなります。

クラスの特定のインスタンスを使用していて、そのインスタンスの複数のプロパティと関数にアクセスする必要がある場合は、with ステートメントを使用します。これにより、「次のすべてのオペレーションをこのインスタンス オブジェクトで行う」ことを示すことができます。キーワード with、かっこで囲んだインスタンス名の順に記述し、その後に実行するオペレーションを中かっこで囲んで続けます。

with (instanceName) {
    // all operations to do with instanceName
}
  1. main() 関数で、with を使用するように print ステートメントを変更します。
  2. print ステートメント内の squareCabin. を削除します。
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. コードを再度実行し、エラーが出ず、同じ出力が表示されることを確認します。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

完成したコードは次のようになります。

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

RoundHut サブクラスを作成する

  1. SquareCabin と同様に、Dwelling に別のサブクラス RoundHut を追加します。
  2. buildingMaterial をオーバーライドして、値を "Straw" にします。
  3. capacity をオーバーライドして 4 に設定します。
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. main() で、RoundHut のインスタンスを作成し、居住者を 3 人とします。
val roundHut = RoundHut(3)
  1. roundHut に関する情報を出力するコードを次のように追加します。
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. コードを実行すると、プログラム全体の出力は次のようになります。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

現在のクラス階層は次のようになっています。Dwelling がルートクラスで、SquareCabinRoundHutDwelling のサブクラスです。

c19084f4a83193a0.png

RoundTower サブクラスを作成する

このクラス階層の最後のクラスは円塔です。円塔は、石造りで複数の階がある円形の小屋と考えることができるため、RoundTowerRoundHut のサブクラスにすることができます。

  1. RoundHut のサブクラスとして RoundTower クラスを作成します。RoundTower のコンストラクタに residents パラメータを追加して、そのパラメータを RoundHut スーパークラスのコンストラクタに渡します。
  2. buildingMaterial をオーバーライドして "Stone" にします。
  3. capacity4 に設定します。
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. このコードを実行すると、エラーが発生します。
This type is final, so it cannot be inherited from

このエラーは、RoundHut クラスのサブクラス化(または継承)はできないことを示しています。Kotlin では、クラスはデフォルトで final であり、サブクラス化することはできません。継承できるのは、abstract クラスか、open キーワードでマークされたクラスのみです。したがって、RoundHut クラスに open キーワードを付けて継承できるようにする必要があります。

  1. RoundHut 宣言の先頭に open キーワードを追加します。
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. main()roundTower のインスタンスを作成し、そのインスタンスに関する情報を出力します。
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

完全なコードは次のとおりです。

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. コードを実行します。エラーなしで動作し、次の出力が生成されます。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

RoundTower に複数の階を追加する

RoundHut は一般に 1 階建てですが、塔には通常、複数の階があります。

収容人数の観点から見た場合、塔の階数が多いほど、より多くの居住者を収容できます。

複数の階を持つように RoundTower を変更することで、階数に基づいて収容人数を調整できるようになります。

  1. RoundTower コンストラクタを更新して、階数の追加の整数パラメータ val floors を取るようにします。これは residents の後に配置します。floors はこの RoundTower で定義されており、RoundHut には floors がないため、これを親の RoundHut コンストラクタに渡す必要はありません。
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. コードを実行すると、main() メソッドで roundTower を作成するときにエラーが発生します。これは floors 引数に数値が指定されていないことが原因のため、不足している引数を追加します。

または、RoundTower のクラス定義で、次のように floors のデフォルト値を追加できます。これにより、floors の値がコンストラクタに渡されない場合、デフォルト値を使用してオブジェクト インスタンスを作成できるようになります。

  1. コードで、floors の宣言の後に = 2 を追加し、デフォルト値として 2 を割り当てます。
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. コードを実行します。RoundTower(4)RoundTower オブジェクト インスタンスをデフォルト値(2 階)を使用して作成するため、コンパイルが行われます。
  2. RoundTower クラスで、capacity を更新して階数と掛け合わせるようにします。
override val capacity = 4 * floors
  1. コードを実行すると、RoundTower が 2 階建ての場合の収容人数が 8 になります。

完成したコードは以下のとおりです。

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. 階層内のクラスを変更する

床面積を計算する

この演習では、抽象クラスで抽象関数を宣言し、その機能をサブクラスに実装する方法を学びます。

すべての住居には床面積がありますが、住居の形状によって計算方法が異なります。

Dwelling クラスで floorArea() を定義する

  1. まず、abstract floorArea() 関数を Dwelling クラスに追加します。Double を返します。Double は、StringInt のようなデータ型です。これは浮動小数点数(5.8793 のように、小数点の後に小数点以下が続く数)に使用されます。
abstract fun floorArea(): Double

抽象クラスで定義したすべての抽象メソッドは、そのサブクラスで実装する必要があります。そのため、コードを実行する前に、サブクラスに floorArea() を実装します。

SquareCabin の floorArea() を実装する

buildingMaterialcapacity と同様に、親クラスで定義されている abstract 関数を実装するため、override キーワードを使用する必要があります。

  1. SquareCabin クラスで、次のようにキーワード override を記述し、その後に floorArea() 関数の実際の実装を続けます。
override fun floorArea(): Double {

}
  1. 計算された床面積を返します。長方形または正方形の面積は、縦と横(1 辺と 1 辺)の長さを掛けて求めます。関数の本文は return length * length になります。
override fun floorArea(): Double {
    return length * length
}

長さはクラスの変数ではなく、インスタンスごとに異なるため、SquareCabin クラスのコンストラクタ パラメータとして追加できます。

  1. SquareCabin のクラス定義を変更して、Double 型の length パラメータを追加します。建物の長さは変化しないため、このプロパティを val として宣言します。
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

Dwelling とそのすべてのサブクラスには、コンストラクタ引数として residents が含まれています。residents は Dwelling コンストラクタの最初の引数であるため、すべてのサブクラス コンストラクタでもこれを最初の引数に指定し、すべてのクラス定義で各引数を同じ順序で指定することをおすすめします。そのため、residents パラメータの後に新しい length パラメータを挿入します。

  1. main()squareCabin インスタンスの作成を更新します。length として 50.0SquareCabin コンストラクタに渡します。
val squareCabin = SquareCabin(6, 50.0)
  1. squareCabinwith ステートメント内に、床面積用の print ステートメントを追加します。
println("Floor area: ${floorArea()}")

このままではコードを実行できません。RoundHut にも floorArea() を実装する必要があります。

RoundHut の floorArea() を実装する

同様に、RoundHut の床面積を実装します。RoundHutDwelling の直接サブクラスであるため、override キーワードを使用する必要があります。

円形の住居の床面積は、PI * 半径 * 半径で求めます。

PI は数学的な値であり、演算ライブラリで定義されています。ライブラリとは、プログラムの外部で事前定義された、プログラムが使用できる関数と値のコレクションです。ライブラリの関数や値を使用するには、そのことをコンパイラに伝える必要があります。これを行うには、関数または値をプログラムにインポートします。プログラムで PI を使用するには、kotlin.math.PI をインポートする必要があります。

  1. Kotlin 演算ライブラリから PI をインポートします。これはファイルの先頭の main() の前に記述します。
import kotlin.math.PI
  1. RoundHutfloorArea() 関数を実装します。
override fun floorArea(): Double {
    return PI * radius * radius
}

警告: kotlin.math.PI をインポートしないとエラーが発生するため、使用する前にこのライブラリをインポートしてください。または、「kotlin.math.PI * radius * radius」のように、完全修飾版の PI を記述することもできます。この場合、import ステートメントは必要ありません。

  1. RoundHut コンストラクタを更新して、radius を渡すようにします。
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. main() で、10.0radiusRoundHut コンストラクタに渡し、roundHut の初期化を更新します。
val roundHut = RoundHut(3, 10.0)
  1. roundHutwith ステートメント内に print ステートメントを追加します。
println("Floor area: ${floorArea()}")

RoundTower の floorArea() を実装する

コードはまだ正常に実行されず、次のエラーが表示されます。

Error: No value passed for parameter 'radius'

プログラムをコンパイルするにあたって、RoundTower では floorArea()RoundHut から継承されるため、これを実装する必要はありません。ただし、親の RoundHut と同じ radius 引数を持つように RoundTower クラス定義を更新する必要があります。

  1. RoundTower のコンストラクタを変更して、radius も取得するようにします。radiusresidentsfloors の間に追加します。デフォルト値を持つ変数は、末尾に配置することをおすすめします。必ず radius を親クラス コンストラクタに渡すようにしてください。
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. main()roundTower の初期化を更新します。
val roundTower = RoundTower(4, 15.5)
  1. floorArea() を呼び出す print ステートメントを追加します。
println("Floor area: ${floorArea()}")
  1. これでコードを実行できるようになりました。
  2. RoundTower の計算が正しくありません。この計算は RoundHut から継承されており、floors の数が考慮されていないためです。
  3. RoundTower で、面積と階数を掛け合わせる別の実装を指定できるように、override floorArea() を行います。抽象クラス(Dwelling)で関数を定義して、それをサブクラス(RoundHut)で実装し、サブクラス(RoundTower)のサブクラスで再度オーバーライドできます。必要な機能を継承しつつ、不要な機能はオーバーライドできるため、両方の長所を取り入れることができます。
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

コードはこのままでも実行できますが、親の RoundHut クラスにすでに存在するコードを繰り返さないようにする方法があります。親の RoundHut クラスから floorArea() 関数を呼び出すと、PI * radius * radius を返すことができます。次に、その結果に floors の数を掛けます。

  1. RoundTower で、floorArea() のスーパークラス実装を使用するように floorArea() を更新します。super キーワードを使用して、親で定義されている関数を呼び出します。
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. コードを再度実行すると、RoundTower の複数階の床面積が正しく出力されます。

完成したコードは以下のとおりです。

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

出力は次のようになります。

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

新しい居住者が部屋を持てるようにする

居住者数を 1 人ずつ増やす getRoom() 関数を使用して、新しい居住者が部屋を持てるようにします。このロジックはすべての住居に共通であるため、Dwelling に関数を実装することで、すべてのサブクラスとその子でこの関数を使用できます。

注:

  • if ステートメントを使用して、収容人数に余裕がある場合にのみ居住者を追加します。
  • 結果をメッセージとして出力します。
  • residents 変数に 1 を追加する residents = residents + 1 の短縮形として、residents++ を使用できます。
  1. getRoom() 関数を Dwelling クラスに実装します。
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. roundHutwith ステートメント ブロックに print ステートメントを追加して、getRoom()hasRoom() を一緒に使用するとどうなるかを確認します。
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

これらの print ステートメントの出力は次のようになります。

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

詳しくは、ソリューション コードをご覧ください。

カーペットのサイズを円形住居に合わせる

RoundHutRoundTower に敷くカーペットの片側の長さを確認する必要があるとします。ここでは、そのための関数を RoundHut に追加して、すべての円形住居で使用できるようにします。

2e328a198a82c793.png

  1. まず、kotlin.math ライブラリから sqrt() 関数をインポートします。
import kotlin.math.sqrt
  1. calculateMaxCarpetLength() 関数を RoundHut クラスに実装します。円に収まる正方形のカーペットの長さを計算する式は、sqrt(2) * radius です。これについては上の図で説明しています。
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Double2.0 を数学関数 sqrt(2.0) に渡します(関数の戻り値の型が Integer ではなく Double であるため)。

  1. これで、calculateMaxCarpetLength() メソッドを RoundHut インスタンスと RoundTower インスタンスで呼び出すことができるようになりました。main() 関数内の roundHutroundTower に print ステートメントを追加します。
println("Carpet Length: ${calculateMaxCarpetLength()}")

詳しくは、ソリューション コードをご覧ください。

これで、プロパティと関数を含む完全なクラス階層を作成できました。その過程で、より便利なクラスを作成するために必要なことをすべて学習しました。

6. 解答コード

この Codelab の完全なソリューション コード(コメント付き)は次のとおりです。

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. 概要

この Codelab では、以下について学びました。

  • クラス階層の作成方法。クラス階層は、子が親クラスから機能を継承するクラスのツリーです。プロパティと関数はサブクラスによって継承されます。
  • abstract クラスの作成方法。機能の一部がサブクラスによって実装されるため、abstract クラスをインスタンス化することはできません。
  • abstract クラスのサブクラスを作成する方法。
  • override キーワードを使用して、サブクラス内のプロパティと関数をオーバーライドする方法。
  • super キーワードを使用して、親クラスの関数とプロパティを参照する方法。
  • クラスを open にしてサブクラス化できるようにする方法。
  • プロパティを private にして、クラス内でのみ使用できるようにする方法。
  • with 構成を使用して、同じオブジェクト インスタンスに対して複数の呼び出しを実行する方法。
  • kotlin.math ライブラリから機能をインポートする方法。

8. 詳細