この 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: パッケージを作成する
パッケージを使用すると、コードを整理できます。
- [Project] ペインの [Hello Kotlin] プロジェクトの下にある [src] フォルダを右クリックします。
- [New > Package] を選択し、
example.myapp
という名前を付けます。
ステップ 2: プロパティを使用してクラスを作成する
クラスはキーワード class
で定義されます。クラス名は慣例により大文字で始まります。
- example.myapp パッケージを右クリックします。
- [New > Kotlin File / Class] を選択します。
- [Kind] で [Class] を選択し、クラスに「
Aquarium
」という名前を付けます。IntelliJ IDEA はファイルにパッケージ名を含み、空のAquarium
クラスを作成します。 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
という新しいファイルを作成します。
- 左側の [Project] ペインで example.myapp パッケージを右クリックします。
- [New > Kotlin File / Class] を選択します。
- [種類] プルダウンで、選択内容を [ファイル] のままにして、ファイル名を
main.kt
にします。IntelliJ IDEA にはパッケージ名が含まれていますが、ファイルのクラス定義が含まれていません。 buildAquarium()
関数を定義し、Aquarium
のインスタンスを作成します。インスタンスを作成するには、クラスを関数Aquarium()
のように参照します。これにより、他の言語のnew
を使用する場合と同様に、クラスのコンストラクタが呼び出され、Aquarium
クラスのインスタンスが作成されます。main()
関数を定義して、buildAquarium()
を呼び出します。
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
ステップ 4: メソッドを追加する
Aquarium
クラスに、水族館のディメンション プロパティを出力するメソッドを追加します。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
-
main.kt
のbuildAquarium()
で、myAquarium
のprintSize()
メソッドを呼び出します。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
main()
関数の横にある緑色の三角形をクリックしてプログラムを実行します。結果を確認します。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
buildAquarium()
で、高さを 60 に設定し、変更したディメンション プロパティを出力するコードを追加します。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- プログラムを実行し、出力を確認します。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
このタスクでは、クラスのコンストラクタを作成し、プロパティの操作を続行します。
ステップ 1: コンストラクタを作成する
このステップでは、最初のタスクで作成した Aquarium
クラスにコンストラクタを追加します。上記の例では、Aquarium
のすべてのインスタンスが同じディメンションで作成されています。ディメンションを作成したら、プロパティを設定して変更できますが、最初は適切なサイズにする方が簡単です。
一部のプログラミング言語では、コンストラクタはクラスと同じ名前のメソッドをクラス内に作成することで定義されます。Kotlin では、クラス宣言自体でコンストラクタを直接定義し、クラスがメソッドであるかのようにかっこ内のパラメータを指定します。Kotlin の関数と同様に、これらのパラメータにはデフォルト値を含めることができます。
- 先ほど作成した
Aquarium
クラスで、クラス定義を変更して、length
、width
、height
のデフォルト値を持つ 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
...
}
- よりコンパクトな Kotlin の場合は、
var
またはval
を使用して、コンストラクタでプロパティを直接定義する方法もあります。Kotlin ではゲッターとセッターも自動的に作成されます。その後、クラスの本体にあるプロパティ定義を削除できます。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- このコンストラクタで
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()
}
- プログラムを実行して出力を確認します。
⇒ 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
クラスに追加します。
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")
}
}
- プログラムを実行して出力を確認します。
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 つ以上持つセカンダリ コンストラクタもあります。
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
}
- セカンダリ コンストラクタ内で、長さと幅(メイン コンストラクタで設定されているもの)を同じにして、タンクの指定されたボリュームに必要な高さを計算します。
// calculate the height needed
height = (tank / (length * width)).toInt()
buildAquarium()
関数に、新しいセカンダリ コンストラクタを使用してAquarium
を作成する呼び出しを追加します。サイズとボリュームを印刷します。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- プログラムを実行し、出力を確認します。
⇒ 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 行の関数で実行できます。
Aquarium
クラスで、volume
というInt
プロパティを定義し、次の行でボリュームを計算するget()
メソッドを定義します。
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- ボリュームを出力する
init
ブロックを削除します。 buildAquarium()
で、ボリュームを出力するコードを削除します。printSize()
メソッドに、ボリュームを出力する行を追加します。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- プログラムを実行し、出力を確認します。
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
サイズとボリュームは前と同じですが、ボリュームはプライマリ コンストラクタとセカンダリ コンストラクタの両方で完全に初期化された後に 1 回だけ出力されます。
ステップ 5: プロパティ セッターを追加する
このステップでは、ボリュームの新しいプロパティ セッターを作成します。
Aquarium
クラスで、volume
をvar
に変更して、複数回設定できるようにします。- ゲッターの下に
set()
メソッドを追加して、volume
プロパティのセッターを追加します。このメソッドは、指定された水分量に基づいて高さを再計算します。通常、setter パラメータの名前はvalue
ですが、必要に応じて変更できます。
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
-
buildAquarium()
に、水族館の音量を 70 リットルに設定するコードを追加します。新しいサイズを印刷します。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- プログラムを再度実行して、変化した高さとボリュームを確認します。
⇒ 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
現時点では、public
や private
などの可視性修飾子はコードに含まれていません。デフォルトでは、Kotlin のオブジェクトはすべて一般公開されているため、クラス、メソッド、プロパティ、メンバー変数など、すべてのものにアクセスできます。
Kotlin では、クラス、オブジェクト、インターフェース、コンストラクタ、関数、プロパティ、それらのセッターに、可視性修飾子を含めることができます。
public
はクラスの外部に表示されることを意味します。デフォルトでは、クラスの変数やメソッドを含め、すべてが一般公開されます。internal
は、そのモジュール内でのみ表示されることを意味します。モジュールは、ライブラリやアプリケーションなど、コンパイルされた Kotlin ファイルのセットです。private
は、そのクラス(関数の場合はソースファイル)でのみ表示されます。protected
はprivate
と同じですが、すべてのサブクラスにも表示されます。
詳しくは、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
にして、次のステップでオーバーライドできるようにします。
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)
}
- 開いている
shape
プロパティを値"rectangle"
で追加します。
open val shape = "rectangle"
Aquarium
の 90% のボリュームを返すゲッターで、water
プロパティを開きます。
open var water: Double = 0.0
get() = volume * 0.9
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)")
}
buildAquarium()
で、width = 25
、length = 25
、height = 40
を含むAquarium
を作成するようにコードを変更します。
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- プログラムを実行して、新しい出力を確認します。
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
ステップ 2: サブクラスを作成する
TowerTank
というAquarium
のサブクラスを作成します。このサブクラスでは、長方形のタンクではなく円形のシリンダー タンクを実装します。Aquarium
クラスと同じファイルに別のクラスを追加できるため、Aquarium
の下にTowerTank
を追加できます。TowerTank
で、コンストラクタで定義されているheight
プロパティをオーバーライドします。プロパティをオーバーライドするには、サブクラスでoverride
キーワードを使用します。
TowerTank
のコンストラクタがdiameter
を受け取るようにする。Aquarium
スーパークラスでコンストラクタを呼び出すときは、length
とwidth
の両方にdiameter
を使用します。
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- ボリューム プロパティをオーバーライドして円柱を計算します。円柱の計算式は、円周率に半径の 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()
}
TowerTank
で、water
プロパティをオーバーライドして、音量の 80% にします。
override var water = volume * 0.8
shape
をオーバーライドして"cylinder"
にします。
override val shape = "cylinder"
- 最終的な
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"
}
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()
}
- プログラムを実行し、出力を確認します。
⇒ 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 クラスを作成する
- example.myapp に、新しいファイル
AquariumFish.kt
を作成します。 AquariumFish
というクラスを作成し、abstract
マークを付けます。String
プロパティを 1 つ(color
)追加し、abstract
とマークします。
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
AquariumFish
の 2 つのサブクラス(Shark
とPlecostomus
)を作成します。color
は抽象クラスであるため、サブクラスはこれを実装する必要があります。Shark
グレーとPlecostomus
ゴールドにする。
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- main.kt で
makeFish()
クラスを作成し、クラスをテストします。Shark
とPlecostomus
をインスタンス化し、それぞれの色を出力します。 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()
}
- プログラムを実行し、出力を確認します。
⇒ Shark: gray Plecostomus: gold
次の図は、Shark
クラスと Plecostomus
クラスを表しています。これらは抽象クラス AquariumFish
をサブクラス化しています。
ステップ 2: インターフェースを作成する
- AquariumFish.kt で、
FishAction
という名前のインターフェースをeat()
メソッドを使って作成します。
interface FishAction {
fun eat()
}
- 各サブクラスに
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")
}
}
makeFish()
関数で、作成したそれぞれの魚にeat()
を呼び出して何かを食べさせます。
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- プログラムを実行し、出力を確認します。
⇒ 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
クラスに戻ると、すべての魚にデフォルトの色がないため、すべてのAquariumFish
にFishAction
を実装し、color
を抽象化したままeat
にデフォルトの実装を指定できます。
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
前のタスクでは、抽象クラス、インターフェース、構成の概念を導入しました。インターフェース委任は、ヘルパー(またはデリゲート)オブジェクトによってインターフェースのメソッドを実装し、それをクラスで使用する高度な手法です。この手法は、一連の無関係なクラスでインターフェースを使用する場合に役立ちます。必要なインターフェース機能を個別のヘルパークラスに追加し、各クラスはヘルパークラスのインスタンスを使用して機能を実装します。
このタスクでは、インターフェース委任を使用してクラスに機能を追加します。
ステップ 1: 新しいインターフェースを作成する
- AquariumFish.kt で、
AquariumFish
クラスを削除します。Plecostomus
とShark
は、AquariumFish
クラスから継承するのではなく、魚のアクションとその色の両方に対応するインターフェースを実装します。 - 色を文字列として定義する新しいインターフェース
FishColor
を作成します。
interface FishColor {
val color: String
}
Plecostomus
を変更して、FishAction
とFishColor
の 2 つのインターフェースを実装します。FishColor
のcolor
とFishAction
のeat()
をオーバーライドする必要があります。
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
AquariumFish
から継承するFishAction
とFishColor
という 2 つのインターフェースも実装するようにShark
クラスを変更します。
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- 完成したコードは次のようになります。
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 でシングルトンを実装する方法です。
- AquariumFish.kt で、
GoldColor
のオブジェクトを作成します。色をオーバーライドします。
object GoldColor : FishColor {
override val color = "gold"
}
手順 3: FishColor のインターフェースの委任を追加する
これで、インターフェースの委任を使用する準備が整いました。
- AquariumFish.kt で、
color
のオーバーライドをPlecostomus
から削除します。 GoldColor
から色を取得するようにPlecostomus
クラスを変更します。これを行うには、クラス宣言にby GoldColor
を追加し、委任を作成します。つまり、FishColor
を実装する代わりに、GoldColor
で提供される実装を使用します。したがって、color
がアクセスされるたびにGoldColor
に委任されます。
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
クラスはそのままで、すべてのペレコが金色になりますが、実際にはこれらの魚には多くの色があります。この問題に対処するには、Plecostomus
のデフォルト色として GoldColor
を指定して、色のコンストラクタ パラメータを追加します。
- コンストラクタを使用して
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
のインターフェースの委任も使用できます。
- AquariumFish.kt では、
FishAction
を実装するPrintingFishAction
クラスを作成します。このクラスは、String
とfood
を取り、魚の食べものを出力します。
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
Plecostomus
クラスのオーバーライド関数eat()
を削除します。委任関数に置き換えます。Plecostomus
の宣言で、FishAction
をPrintingFishAction
にデリゲートし、"eat algae"
を渡します。- そのデリゲートすべてで、
Plecostomus
クラスの本文にコードはありません。そのため、すべてのオーバーライドはインターフェースのデリゲートによって処理されるため、{}
は削除してください。
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
次の図では、Shark
クラスと Plecostomus
クラスを示しています。どちらも PrintingFishAction
インターフェースと FishColor
インターフェースで構成されていますが、実装も委任しています。
インターフェース委任は強力です。通常、別の言語で抽象クラスを使用する可能性がある場合は、その使用方法を検討する必要があります。コンポジションを使用することで、多くのサブクラスを必要とせずに、それぞれ別の方法で動作をプラグインできます。
データクラスは、他の一部の言語の struct
に似ています(主に一部のデータを保持するために存在します)が、データクラス オブジェクトはオブジェクトです。Kotlin データクラス オブジェクトには、印刷やコピーのユーティリティなど、いくつかのメリットがあります。このタスクでは、簡単なデータクラスを作成し、Kotlin によるデータクラスのサポートについて学習します。
ステップ 1: データクラスを作成する
- example.myapp パッケージの下に新しいパッケージ
decor
を追加して、新しいコードを保持します。[Project] ペインで example.myapp を右クリックし、[File > New > Package] を選択します。 - パッケージ内で、
Decoration
という新しいクラスを作成します。
package example.myapp.decor
class Decoration {
}
Decoration
をデータクラスにするには、クラス宣言の前にキーワードdata
を付けます。rocks
というString
プロパティを追加して、クラスにデータを提供します。
data class Decoration(val rocks: String) {
}
- このファイルのクラスの外側に
makeDecorations()
関数を追加して、Decoration
のインスタンスを作成し、"granite"
で出力します。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
main()
関数を追加してmakeDecorations()
を呼び出し、プログラムを実行します。これはデータクラスであるため、生成される有効な出力に注目してください。
⇒ Decoration(rocks=granite)
makeDecorations()
で、さらに「slate」である 2 つのDecoration
オブジェクトをインスタンス化して出力します。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
makeDecorations()
で、decoration1
とdecoration2
を比較した結果を出力する print ステートメントを追加し、decoration3
とdecoration2
を比較する 2 つ目のステートメントを追加します。データクラスが提供する equals() メソッドを使用する。
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- コードを実行します。
⇒ 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 つ以上のフィールドを定義することもできます。
- Decoration.kt で列挙型の例を試します。
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
列挙型はシングルトンと少し似ています。列挙型の各値は 1 つのみで、1 つだけになります。たとえば、Color.RED
、Color.GREEN
、Color.BLUE
をそれぞれ 1 つだけ指定できます。この例では、色のコンポーネントを表すために、RGB 値が rgb
プロパティに割り当てられます。また、ordinal
プロパティを使用して列挙の序数を取得し、name
プロパティを使用してその名前を取得することもできます。
- 列挙型の別の例をお試しください。
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 ではすべてのサブクラスが静的に認識されます。つまり、コンパイル時にはコンパイラがすべてのクラスとサブクラスを認識し、それらがすべて把握しているので、コンパイラは追加のチェックを行います。
- 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
ですが、修飾子を使用して可視性をinternal
、private
、protected
に変更できます。 - サブクラスを作成するには、親クラスを
open
とマークする必要があります。 - サブクラスでメソッドとプロパティをオーバーライドするには、親クラスのメソッドとプロパティを
open
としてマークする必要があります。 - シールクラスは、そのファイルが同じファイルでのみサブクラス化できます。宣言の前に
sealed
を付けて、シールクラスを作成します。
データクラス、シングルトン、列挙型
- 宣言の前に
data
を付けて、データクラスを作成します。 - 分解とは、
data
オブジェクトのプロパティを個別の変数に割り当てる方法です。 class
ではなくobject
を使用してシングルトン クラスを作成します。enum class
を使用して列挙型を定義します。
抽象クラス、インターフェース、委任
- 抽象クラスとインターフェースは、クラス間で共通の動作を共有するための 2 つの方法です。
- 抽象クラスは、プロパティと動作を定義しますが、実装はサブクラスに任せます。
- インターフェースは動作を定義し、動作の一部またはすべてにデフォルトの実装を提供する場合があります。
- インターフェースを使用してクラスを作成すると、クラスの機能が、そのクラスに含まれているクラス インスタンスによって拡張されます。
- インターフェース委任ではコンポジションが使用されますが、実装もインターフェース クラスに委任されます。
- コンポジションは、インターフェースの委任を使用してクラスに機能を追加する強力な方法です。一般的にはコンポジションが優先されますが、一部の問題には Abstract クラスから継承した方が適しています。
Kotlin ドキュメント
このコースに関するトピックについて詳しい情報が必要な場合や、ご不明な点がある場合は、https://kotlinlang.org をご覧ください。
- クラスと継承
- コンストラクタ
- ファクトリ関数
- プロパティとフィールド
- 可視性修飾子
- 抽象クラス
- インターフェース
- 委任
- データクラス
- 等価性
- 分解
- オブジェクト宣言
- 列挙型クラス
- シールクラス
- Kotlin シールクラスを使用したオプション エラーの処理
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
クラス。
次のレッスン「
他の Codelab へのリンクを含むコースの概要については、プログラマー向け Kotlin ブートキャンプ: コースへようこそをご覧ください。