この 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: パッケージを作成する
パッケージを使用すると、コードを整理できます。
- [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] を選択します。
- [Kind] プルダウンで、選択を [File] のままにして、ファイルに
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: 初期化ブロックを追加する
上記のコンストラクタの例では、プロパティを宣言して、式の値を割り当てているだけです。コンストラクタでより多くの初期化コードが必要な場合は、1 つ以上の init
ブロックに配置できます。このステップでは、Aquarium
クラスに init
ブロックを追加します。
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
ブロックで、2 回目は 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
ディメンションとボリュームは以前と同じですが、ボリュームは、プライマリ コンストラクタとセカンダリ コンストラクタの両方でオブジェクトが完全に初期化された後に一度だけ出力されます。
ステップ 5: プロパティ セッターを追加する
このステップでは、ボリュームの新しいプロパティ セッターを作成します。
Aquarium
クラスで、volume
をvar
に変更して、複数回設定できるようにします。volume
プロパティのセッターを追加します。ゲッターの下にset()
メソッドを追加し、提供された水の量に基づいて高さを再計算します。慣例により、セッター パラメータの名前は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 ドキュメントの可視性修飾子をご覧ください。
メンバー変数
クラス内のプロパティ(メンバー変数)は、デフォルトで 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
にして、次のステップでオーバーライドできるようにします。
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)
}
- 値
"rectangle"
の openshape
プロパティを追加します。
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: サブクラスを作成する
Aquarium
のサブクラスとしてTowerTank
を作成します。これは、長方形のタンクではなく丸い円筒形のタンクを実装します。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) {
- volume プロパティをオーバーライドして、円柱を計算します。円柱の体積の公式は、円周率 × 半径の 2 乗 × 高さです。定数
PI
をjava.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()
}
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
というインターフェースを作成して、すべての魚に共通する動作を定義します。
- 抽象クラスもインターフェースも単独でインスタンス化することはできません。つまり、これらの型のオブジェクトを直接作成することはできません。
- 抽象クラスにはコンストラクタがあります。
- インターフェースにはコンストラクタ ロジックを含めることも、状態を保存することもできません。
ステップ 1. 抽象クラスを作成する
- example.myapp の下に、新しいファイル
AquariumFish.kt
を作成します。 AquariumFish
というクラスを作成し、abstract
でマークします。String
プロパティcolor
を 1 つ追加し、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
次の図は、抽象クラス AquariumFish
をサブクラス化する Shark
クラスと Plecostomus
クラスを表しています。
ステップ 2. インターフェースを作成する
- AquariumFish.kt で、
eat()
メソッドを持つFishAction
というインターフェースを作成します。
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 つだけです。
コンポジションは、カプセル化の改善、結合(相互依存)の低減、インターフェースのクリーン化、コードの使いやすさの向上につながることがよくあります。これらの理由から、インターフェースとのコンポジションを使用する設計が推奨されます。一方、抽象クラスからの継承は、一部の問題に自然に適合する傾向があります。したがって、コンポジションを優先する必要がありますが、継承が妥当な場合は Kotlin でも継承を使用できます。
- メソッドが多く、デフォルトの実装が 1 つまたは 2 つしかない場合は、インターフェースを使用します(以下の
AquariumAction
の例を参照)。
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- クラスを完成させることができない場合は、抽象クラスを使用します。たとえば、
AquariumFish
クラスに戻ると、すべてのAquariumFish
にFishAction
を実装させ、eat
のデフォルト実装を提供できます。魚にデフォルトの色はないため、color
は抽象のままにします。
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
前のタスクでは、抽象クラス、インターフェース、コンポジションの概念を紹介しました。インターフェース委任は、インターフェースのメソッドをヘルパー(またはデリゲート)オブジェクトで実装し、そのオブジェクトをクラスで使用する高度な手法です。この手法は、一連の関連性のないクラスでインターフェースを使用する場合に便利です。必要なインターフェース機能を別のヘルパークラスに追加し、各クラスがヘルパークラスのインスタンスを使用して機能を実装します。
このタスクでは、インターフェース委任を使用してクラスに機能を追加します。
ステップ 1: 新しいインターフェースを作成する
- AquariumFish.kt で、
AquariumFish
クラスを削除します。AquariumFish
クラスから継承する代わりに、Plecostomus
とShark
は魚のアクションとその色の両方のインターフェースを実装します。 - 色を文字列として定義する新しいインターフェース
FishColor
を作成します。
interface FishColor {
val color: String
}
Plecostomus
を変更して、2 つのインターフェース(FishAction
とFishColor
)を実装します。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 で、
Plecostomus
からcolor
のオーバーライドを削除します。 Plecostomus
クラスを変更して、GoldColor
から色を取得します。これを行うには、クラス宣言にby GoldColor
を追加して、委任を作成します。これは、FishColor
を実装する代わりに、GoldColor
によって提供される実装を使用することを意味します。そのため、color
にアクセスするたびに、GoldColor
に委任されます。
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
このクラスのままでは、すべてのプレコが金色になりますが、実際にはプレコにはさまざまな色があります。この問題に対処するには、Plecostomus
のデフォルトの色として GoldColor
を使用して、色のコンストラクタ パラメータを追加します。
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
にインターフェース委任を使用できます。
- 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) {
}
- ファイル内のクラス外に、
"granite"
を使用してDecoration
のインスタンスを作成して出力するmakeDecorations()
関数を追加します。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
makeDecorations()
を呼び出すmain()
関数を追加して、プログラムを実行します。これはデータクラスであるため、意味のある出力が作成されます。
⇒ Decoration(rocks=granite)
makeDecorations()
で、両方とも「slate」であるDecoration
オブジェクトをさらに 2 つインスタンス化して出力します。
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 つ目の print ステートメントを追加します。データクラスで提供される 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
代わりに、プロパティごとに変数を作成し、データ オブジェクトを変数のグループに割り当てることができます。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 つ以上のフィールドを定義することもできます。
- Decoration.kt で、列挙型の例を試してみましょう。
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
列挙型はシングルトンに似ています。列挙型には 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 で
class
を使用してクラスを定義します。 - Kotlin では、プロパティのセッターとゲッターが自動的に作成されます。
- プライマリ コンストラクタをクラス定義で直接定義します。例:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- プライマリ コンストラクタに追加のコードが必要な場合は、1 つ以上の
init
ブロックに記述します。 - クラスは
constructor
を使用して 1 つ以上のセカンダリ コンストラクタを定義できますが、Kotlin スタイルでは代わりにファクトリ関数を使用します。
可視性修飾子とサブクラス
- Kotlin のすべてのクラスと関数はデフォルトで
public
ですが、修飾子を使用して可視性をinternal
、private
、protected
に変更できます。 - サブクラスを作成するには、親クラスを
open
とマークする必要があります。 - サブクラスでメソッドとプロパティをオーバーライドするには、親クラスでメソッドとプロパティを
open
とマークする必要があります。 - シールクラスは、定義されている同じファイル内でのみサブクラス化できます。宣言の前に
sealed
を付けることで、シールクラスを作成します。
データクラス、シングルトン、列挙型
- 宣言の前に
data
を付けて、データクラスを作成します。 - 分割代入は、
data
オブジェクトのプロパティを個別の変数に割り当てるための短縮形です。 class
ではなくobject
を使用して、シングルトン クラスを作成します。enum class
を使用して列挙型を定義します。
抽象クラス、インターフェース、委任
- 抽象クラスとインターフェースは、クラス間で共通の動作を共有する 2 つの方法です。
- 抽象クラスは、プロパティと動作を定義しますが、実装はサブクラスに任せます。
- インターフェースは動作を定義し、動作の一部またはすべてにデフォルトの実装を提供できます。
- インターフェースを使用してクラスを構成する場合、クラスの機能は、クラスに含まれるクラス インスタンスによって拡張されます。
- インターフェース委任はコンポジションを使用しますが、実装をインターフェース クラスに委任します。
- コンポジションは、インターフェース委任を使用してクラスに機能を追加する強力な方法です。一般的にはコンポジションが推奨されますが、抽象クラスからの継承がより適している問題もあります。
Kotlin ドキュメント
このコースのトピックについてさらに詳しい情報が必要な場合や、行き詰まった場合は、https://kotlinlang.org を参照することをおすすめします。
- クラスと継承
- コンストラクタ
- ファクトリー関数
- プロパティとフィールド
- 可視性修飾子
- 抽象クラス
- インターフェース
- 委任
- データクラス
- 等価性
- 分解
- オブジェクト宣言
- 列挙型クラス
- シールクラス
- Kotlin の Sealed クラスを使用したオプションのエラーの処理
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
クラス。
次のレッスンに進む:
他の Codelab へのリンクを含むコースの概要については、「プログラマー向け Kotlin ブートキャンプ: コースへようこそ」をご覧ください。