1. 始める前に
要件
- Kotlin プレイグラウンドを使用した Kotlin プログラムの編集に慣れている。
- このコースのユニット 1 で説明した Kotlin プログラミングの基本的な概念。特に、
main()
プログラム、引数を持ち値を返す関数、変数、データ型、オペレーション、if/else
ステートメント。 - Kotlin でクラスを定義し、そこからオブジェクト インスタンスを作成してプロパティとメソッドにアクセスできる。
学習内容
- 継承を使用してクラスの階層を実装する Kotlin プログラムを作成する。
- クラスを拡張し、既存の機能をオーバーライドして、新しい機能を追加する。
- 変数に対して適切な可視性修飾子を選択する。
作成するアプリの概要
- さまざまな種類の住居をクラス階層として実装する Kotlin プログラム。
必要なもの
- Kotlin プレイグラウンドにアクセスできる、インターネットに接続されたパソコン
2. クラス階層とは
人間にとって、特性(プロパティ)や動作が類似している項目をグループに分類し、その中になんらかの階層を作るのは自然なことです。たとえば、野菜のような範囲の広いカテゴリを作り、その中に豆類といった具体的なタイプを含めることができます。豆類は、エンドウ豆、インゲン豆、レンズ豆、ひよこ豆、大豆など、さらに具体的なタイプに分類できます。
豆類は野菜が持つすべての特性(植物、食料など)を含んでいる(継承している)ため、この関係を階層構造で表すことができます。同様に、エンドウ豆、インゲン豆、レンズ豆はすべて、豆類の特性に加えて固有の特性も持っています。
この関係をプログラミング用語で表現するとどうなるか見てみましょう。Kotlin で Vegetable
をクラスにすると、Legume
を Vegetable
クラスの子またはサブクラスとして作成できます。つまり、Vegetable
クラスのすべてのプロパティとメソッドは、Legume
クラスによって継承(利用)されます。
この関係は、クラス階層図で次のように表現できます。Vegetable
は、Legume
クラスの親またはスーパークラスとして参照できます。
Legume
のサブクラス(Lentil
や Chickpea
など)を作成することで、クラス階層をさらに拡張できます。これにより、Legume
は Vegetable
の子またはサブクラスになると同時に、Lentil
と Chickpea
の親またはスーパークラスになります。Vegetable
は、この階層のルートクラスまたはトップレベル(基本)クラスです。
Android クラスの継承
以前の Codelab で行ったように、Kotlin コードはクラスを使わずに記述することもできますが、Android の多くの部分(アクティビティ、ビュー、ビューグループなど)はクラスの形式で提供されます。そのため、クラス階層を理解することは Android アプリの開発に不可欠であり、それによって Android フレームワークの機能を活用できるようになります。
たとえば、Android の View
クラスは、画面上で長方形の領域を表現し、描画とイベントの処理を行います。TextView
クラスは View
クラスのサブクラスです。つまり、TextView
は View
クラスのすべてのプロパティと機能を継承するとともに、ユーザーにテキストを表示するための固有のロジックも備えています。
EditText
クラスと Button
クラスは TextView
クラスの子です。これらのクラスは TextView
クラスと View
クラスのすべてのプロパティとメソッドを継承したうえで、固有のロジックも備えています。たとえば EditText
は、画面上のテキストを編集するための独自の機能を追加で備えています。
EditText
クラスは TextView
クラスをサブクラス化するだけで作成できるため、View
クラスと TextView
クラスのすべてのロジックをコピーして EditText
に貼り付ける必要はありません。TextView クラスも同様に、View
クラスをサブクラス化して作成します。そのため、EditText
クラスのコードでは、この UI コンポーネントに固有の、他のビューと異なる機能だけに焦点を当てることができます。
developer.android.com のウェブサイトにある Android クラスのドキュメント ページの上部で、クラス階層図を確認できます。階層の最上部に kotlin.Any
と表示されているのは、Kotlin ではすべてのクラスに共通のスーパークラス「Any」があるためです。詳しくはこちらをご覧ください。
ご覧のように、クラス間の継承を活用することで、コードの記述、再利用、読み取り、テストを簡単に行えるようになります。
3.基本クラスを作成する
住居のクラス階層
この Codelab では、床面積、階数、居住者といった特性(プロパティ)を持つ住居(人が住む建物)を例として使って、クラス階層の仕組みを示す Kotlin プログラムを作成します。
以下の図は、ここで作成するクラス階層を示しています。ルートには、設計図と同じように、すべての住居のプロパティと機能を指定する Dwelling
があります。その下に、正方形のキャビン(SquareCabin
)、円形の小屋(RoundHut
)、複数の階からなる RoundHut
である円塔(RoundTower
)の各クラスがあります。
以下のクラスを実装します。
Dwelling
: すべての住居に共通する情報を保持する、非特定の建物を表す基本クラス。SquareCabin
: 床が正方形の木造キャビン。RoundHut
: 床が円形の、わらで作られた小屋。RoundTower
の親。RoundTower
: 円形の床と複数の階からなる、石造りの塔。
Dwelling 抽象クラスを作成する
すべてのクラスは、クラス階層の基本クラス、または他のクラスの親にすることができます。
「抽象」クラスとは、完全に実装されていないため、インスタンス化できないクラスのことです。抽象クラスは、スケッチのようなものと考えることができます。スケッチにはアイデアや計画が盛り込まれていますが、通常は何かを作成するために十分な情報は含まれていません。スケッチ(抽象クラス)を使用して、実際のオブジェクト インスタンスをビルドするための設計図(クラス)を作成します。
スーパークラスを作成することの一般的なメリットは、すべてのサブクラスに共通するプロパティと関数を含めることができる点です。プロパティの値と関数の実装が不明な場合は、クラスを抽象化します。たとえば、Vegetables
にはすべての野菜に共通するプロパティが多く含まれていますが、非特定の野菜の形状や色などは不明なため、そのインスタンスを作成することはできません。そこで、Vegetable
を抽象クラスにして、各野菜について具体的な詳細を決めることはサブクラスに任せます。
抽象クラスの宣言は、abstract
キーワードで始めます。
Dwelling
も、Vegetable
と同様に抽象クラスにします。Dwelling には多くの住居に共通するプロパティと関数を含めますが、プロパティの正確な値と関数の実装の詳細は不明です。
- Kotlin プレイグラウンド(https://developer.android.com/training/kotlinplayground)に移動します。
- エディタで、
main()
関数内のprintln("Hello, world!")
を削除します。 - 次のコードを
main()
関数の下に追加して、Dwelling
というabstract
クラスを作成します。
abstract class Dwelling(){
}
建築材料のプロパティを追加する
Dwelling
クラスでは、住居によって多少違いはあるものの、すべての住居に共通するプロパティを定義します。すべての住居は、いくつかの建築材料で構成されています。
Dwelling
内で、建築材料を表すString
型のbuildingMaterial
変数を作成します。建築材料は変更しないため、val
を使用して不変の変数にします。
val buildingMaterial: String
- プログラムを実行すると、次のエラーが出力されます。
Property must be initialized or be abstract
これは buildingMaterial
プロパティに値が指定されていないことが原因ですが、非特定の建物は特定の材料で構成されているわけではないため、値を指定することはできません。そこで、エラー メッセージが示すように、buildingMaterial
の宣言の先頭に abstract
キーワードを付加して、ここで定義されないことを示します。
- 変数の定義に
abstract
キーワードを追加します。
abstract val buildingMaterial: String
- コードを実行しても何も起こりませんが、エラーなしでコンパイルできるようになりました。
main()
関数でDwelling
のインスタンスを作成し、コードを実行します。
val dwelling = Dwelling()
- 抽象
Dwelling
クラスのインスタンスを作成できないため、次のエラーが発生します。
Cannot create an instance of an abstract class
- 間違ったコードを削除します。
完成したコードは次のようになります。
abstract class Dwelling(){
abstract val buildingMaterial: String
}
収容人数のプロパティを追加する
住居のもう一つのプロパティは、収容人数(住める人の数)です。
住居の収容人数は変化しません。ただし、収容人数は Dwelling
スーパークラス内で設定することはできないため、特定の住居タイプのサブクラスとして定義する必要があります。
Dwelling
に、capacity
というabstract
整数val
を追加します。
abstract val capacity: Int
居住者数の private プロパティを追加する
すべての住居には、住んでいる residents
が複数(capacity
以下の数)いるため、Dwelling
スーパークラスに residents
プロパティを定義して、すべてのサブクラスがこのプロパティを継承して使用できるようにします。
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
を返す必要があります。
hasRoom()
関数をDwelling
クラスに追加します。
fun hasRoom(): Boolean {
return residents < capacity
}
- このコードを実行してもエラーは出ませんが、まだ何も表示されません。
完成したコードは次のようになります。
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
4. サブクラスを作成する
SquareCabin サブクラスを作成する
Dwelling
クラスの下に、SquareCabin
という名前のクラスを作成します。
class SquareCabin
- 次に、
SquareCabin
がDwelling
に関連していることを示します。SquareCabin
はDwelling
の抽象部分の実装を提供するため、コードで、SquareCabin
がDwelling
から拡張された(または、Dwelling)
のサブクラスである)ことを示す必要があります。
この継承関係を指定するには、SquareCabin
クラス名の後にコロン(:
)を追加し、その後に親の Dwelling
クラスを初期化する呼び出しを指定します。Dwelling
クラス名の後には必ずかっこを追加してください。
class SquareCabin : Dwelling()
- スーパークラスから拡張する場合は、スーパークラスに必要なパラメータを渡す必要があります。
Dwelling
は入力としてresidents
の数を必要とします。3
など、一定の居住者数を渡すこともできます。
class SquareCabin : Dwelling(3)
ただし、ここではプログラムの柔軟性を高めて、SquareCabins
の居住者の可変数に対応できるようにするため、residents
を SquareCabin
クラス定義内のパラメータにします。親クラスの Dwelling
ですでに宣言されているプロパティを再利用するため、residents
を val,
として宣言しないようにしてください。
class SquareCabin(residents: Int) : Dwelling(residents)
- コードを実行します。
- これにより、次のエラーが発生します。
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling
抽象関数や変数を宣言すると、後でそれらに値や実装を与える約束をしたことになります。変数の場合、その抽象クラスのサブクラスは値を与える必要があります。関数の場合、サブクラスは関数本文を実装する必要があります。
Dwelling
クラスで、abstract
変数 buildingMaterial
を定義しました。SquareCabin
は Dwelling
のサブクラスであるため、buildingMaterial
の値を指定する必要があります。override
キーワードを使用して、このプロパティが親クラスで定義済みであり、このクラスでオーバーライドされることを示します。
SquareCabin
クラス内で、buildingMaterial
プロパティをoverride
し、値"Wood"
を割り当てます。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 を使用する
Dwelling
クラスとSquareCabin
クラス定義の前に空のmain()
関数を挿入します。
fun main() {
}
abstract class Dwelling(private var residents: Int) {
...
}
class SquareCabin(residents: Int) : Dwelling(residents) {
...
}
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
クラスで定義したことに注意してください。SquareCabin
は Dwelling
クラスのサブクラスであるため、hasRoom()
関数がそのまま継承されています。コード スニペットで squareCabin.hasRoom()
と表示されていることからわかるように、SquareCabin
のすべてのインスタンスに対して hasRoom()
関数を呼び出すことができるようになりました。
- コードを実行すると、次の内容が出力されます。
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
}
main()
関数で、with
を使用するように print ステートメントを変更します。- print ステートメント内の
squareCabin.
を削除します。
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
- コードを再度実行し、エラーが出ず、同じ出力が表示されることを確認します。
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 サブクラスを作成する
SquareCabin
と同様に、Dwelling
に別のサブクラスRoundHut
を追加します。buildingMaterial
をオーバーライドして、値を"Straw"
にします。capacity
をオーバーライドして 4 に設定します。
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
main()
で、RoundHut
のインスタンスを作成し、居住者を 3 人とします。
val roundHut = RoundHut(3)
roundHut
に関する情報を出力するコードを次のように追加します。
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- コードを実行すると、プログラム全体の出力は次のようになります。
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true
現在のクラス階層は次のようになっています。Dwelling
がルートクラスで、SquareCabin
、RoundHut
は Dwelling
のサブクラスです。
RoundTower サブクラスを作成する
このクラス階層の最後のクラスは円塔です。円塔は、石造りで複数の階がある円形の小屋と考えることができるため、RoundTower
を RoundHut
のサブクラスにすることができます。
RoundHut
のサブクラスとしてRoundTower
クラスを作成します。RoundTower
のコンストラクタにresidents
パラメータを追加して、そのパラメータをRoundHut
スーパークラスのコンストラクタに渡します。buildingMaterial
をオーバーライドして"Stone"
にします。capacity
を4
に設定します。
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- このコードを実行すると、エラーが発生します。
This type is final, so it cannot be inherited from
このエラーは、RoundHut
クラスのサブクラス化(または継承)はできないことを示しています。Kotlin では、クラスはデフォルトで final であり、サブクラス化することはできません。継承できるのは、abstract
クラスか、open
キーワードでマークされたクラスのみです。したがって、RoundHut
クラスに open
キーワードを付けて継承できるようにする必要があります。
RoundHut
宣言の先頭にopen
キーワードを追加します。
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
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
}
- コードを実行します。エラーなしで動作し、次の出力が生成されます。
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
を変更することで、階数に基づいて収容人数を調整できるようになります。
RoundTower
コンストラクタを更新して、階数の追加の整数パラメータval floors
を取るようにします。これはresidents
の後に配置します。floors
はこのRoundTower
で定義されており、RoundHut
にはfloors
がないため、これを親のRoundHut
コンストラクタに渡す必要はありません。
class RoundTower(
residents: Int,
val floors: Int) : RoundHut(residents) {
...
}
- コードを実行すると、
main()
メソッドでroundTower
を作成するときにエラーが発生します。これはfloors
引数に数値が指定されていないことが原因のため、不足している引数を追加します。
または、RoundTower
のクラス定義で、次のように floors
のデフォルト値を追加できます。これにより、floors
の値がコンストラクタに渡されない場合、デフォルト値を使用してオブジェクト インスタンスを作成できるようになります。
- コードで、
floors
の宣言の後に= 2
を追加し、デフォルト値として 2 を割り当てます。
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
...
}
- コードを実行します。
RoundTower(4)
はRoundTower
オブジェクト インスタンスをデフォルト値(2 階)を使用して作成するため、コンパイルが行われます。 RoundTower
クラスで、capacity
を更新して階数と掛け合わせるようにします。
override val capacity = 4 * floors
- コードを実行すると、
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() を定義する
- まず、
abstract
floorArea()
関数をDwelling
クラスに追加します。Double
を返します。Double は、String
やInt
のようなデータ型です。これは浮動小数点数(5.8793 のように、小数点の後に小数点以下が続く数)に使用されます。
abstract fun floorArea(): Double
抽象クラスで定義したすべての抽象メソッドは、そのサブクラスで実装する必要があります。そのため、コードを実行する前に、サブクラスに floorArea()
を実装します。
SquareCabin の floorArea() を実装する
buildingMaterial
や capacity
と同様に、親クラスで定義されている abstract
関数を実装するため、override
キーワードを使用する必要があります。
SquareCabin
クラスで、次のようにキーワードoverride
を記述し、その後にfloorArea()
関数の実際の実装を続けます。
override fun floorArea(): Double {
}
- 計算された床面積を返します。長方形または正方形の面積は、縦と横(1 辺と 1 辺)の長さを掛けて求めます。関数の本文は
return length * length
になります。
override fun floorArea(): Double {
return length * length
}
長さはクラスの変数ではなく、インスタンスごとに異なるため、SquareCabin
クラスのコンストラクタ パラメータとして追加できます。
SquareCabin
のクラス定義を変更して、Double
型のlength
パラメータを追加します。建物の長さは変化しないため、このプロパティをval
として宣言します。
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
Dwelling
とそのすべてのサブクラスには、コンストラクタ引数として residents
が含まれています。residents は Dwelling
コンストラクタの最初の引数であるため、すべてのサブクラス コンストラクタでもこれを最初の引数に指定し、すべてのクラス定義で各引数を同じ順序で指定することをおすすめします。そのため、residents
パラメータの後に新しい length
パラメータを挿入します。
main()
でsquareCabin
インスタンスの作成を更新します。length
として50.0
をSquareCabin
コンストラクタに渡します。
val squareCabin = SquareCabin(6, 50.0)
squareCabin
のwith
ステートメント内に、床面積用の print ステートメントを追加します。
println("Floor area: ${floorArea()}")
このままではコードを実行できません。RoundHut
にも floorArea()
を実装する必要があります。
RoundHut の floorArea() を実装する
同様に、RoundHut
の床面積を実装します。RoundHut
も Dwelling
の直接サブクラスであるため、override
キーワードを使用する必要があります。
円形の住居の床面積は、PI * 半径 * 半径で求めます。
PI
は数学的な値であり、演算ライブラリで定義されています。ライブラリとは、プログラムの外部で事前定義された、プログラムが使用できる関数と値のコレクションです。ライブラリの関数や値を使用するには、そのことをコンパイラに伝える必要があります。これを行うには、関数または値をプログラムにインポートします。プログラムで PI
を使用するには、kotlin.math.PI
をインポートする必要があります。
- Kotlin 演算ライブラリから
PI
をインポートします。これはファイルの先頭のmain()
の前に記述します。
import kotlin.math.PI
RoundHut
のfloorArea()
関数を実装します。
override fun floorArea(): Double {
return PI * radius * radius
}
警告: kotlin.math.PI をインポートしないとエラーが発生するため、使用する前にこのライブラリをインポートしてください。または、「kotlin.math.PI * radius * radius」のように、完全修飾版の PI を記述することもできます。この場合、import ステートメントは必要ありません。
RoundHut
コンストラクタを更新して、radius
を渡すようにします。
open class RoundHut(
residents: Int,
val radius: Double) : Dwelling(residents) {
main()
で、10.0
のradius
をRoundHut
コンストラクタに渡し、roundHut
の初期化を更新します。
val roundHut = RoundHut(3, 10.0)
roundHut
のwith
ステートメント内に print ステートメントを追加します。
println("Floor area: ${floorArea()}")
RoundTower の floorArea() を実装する
コードはまだ正常に実行されず、次のエラーが表示されます。
Error: No value passed for parameter 'radius'
プログラムをコンパイルするにあたって、RoundTower
では floorArea()
が RoundHut
から継承されるため、これを実装する必要はありません。ただし、親の RoundHut
と同じ radius
引数を持つように RoundTower
クラス定義を更新する必要があります。
- RoundTower のコンストラクタを変更して、
radius
も取得するようにします。radius
をresidents
とfloors
の間に追加します。デフォルト値を持つ変数は、末尾に配置することをおすすめします。必ずradius
を親クラス コンストラクタに渡すようにしてください。
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
main()
でroundTower
の初期化を更新します。
val roundTower = RoundTower(4, 15.5)
floorArea()
を呼び出す print ステートメントを追加します。
println("Floor area: ${floorArea()}")
- これでコードを実行できるようになりました。
RoundTower
の計算が正しくありません。この計算はRoundHut
から継承されており、floors
の数が考慮されていないためです。RoundTower
で、面積と階数を掛け合わせる別の実装を指定できるように、override floorArea()
を行います。抽象クラス(Dwelling
)で関数を定義して、それをサブクラス(RoundHut
)で実装し、サブクラス(RoundTower
)のサブクラスで再度オーバーライドできます。必要な機能を継承しつつ、不要な機能はオーバーライドできるため、両方の長所を取り入れることができます。
override fun floorArea(): Double {
return PI * radius * radius * floors
}
コードはこのままでも実行できますが、親の RoundHut
クラスにすでに存在するコードを繰り返さないようにする方法があります。親の RoundHut
クラスから floorArea()
関数を呼び出すと、PI * radius * radius
を返すことができます。次に、その結果に floors
の数を掛けます。
RoundTower
で、floorArea()
のスーパークラス実装を使用するようにfloorArea()
を更新します。super
キーワードを使用して、親で定義されている関数を呼び出します。
override fun floorArea(): Double {
return super.floorArea() * floors
}
- コードを再度実行すると、
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++
を使用できます。
getRoom()
関数をDwelling
クラスに実装します。
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
roundHut
のwith
ステートメント ブロックに 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.
詳しくは、ソリューション コードをご覧ください。
カーペットのサイズを円形住居に合わせる
RoundHut
や RoundTower
に敷くカーペットの片側の長さを確認する必要があるとします。ここでは、そのための関数を RoundHut
に追加して、すべての円形住居で使用できるようにします。
- まず、
kotlin.math
ライブラリからsqrt()
関数をインポートします。
import kotlin.math.sqrt
calculateMaxCarpetLength()
関数をRoundHut
クラスに実装します。円に収まる正方形のカーペットの長さを計算する式は、sqrt(2) * radius
です。これについては上の図で説明しています。
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
Double
値 2.0
を数学関数 sqrt(2.0)
に渡します(関数の戻り値の型が Integer
ではなく Double
であるため)。
- これで、
calculateMaxCarpetLength()
メソッドをRoundHut
インスタンスとRoundTower
インスタンスで呼び出すことができるようになりました。main()
関数内のroundHut
とroundTower
に 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
ライブラリから機能をインポートする方法。