1. 始める前に
この Codelab では、Kotlin でクラスとオブジェクトを使用する方法について説明します。
クラスは、オブジェクトを作成する際の土台にする設計図を提供します。オブジェクトは、そのオブジェクト用のデータで構成されるクラスのインスタンスです。オブジェクトとクラス インスタンスは同じ意味です。
家を建てることを考えてみましょう。クラスは建築家が描いた設計プランに似ています。これは設計図とも呼ばれます。設計図は家ではありません。家を建てる方法を示した説明書です。家は実在物であり、設計図に従って作成されたオブジェクトであると言えます。
家の設計図に複数の部屋があり、各部屋に独自の設計と用途があるように、各クラスには独自の設計と用途があります。クラスの設計方法を理解するには、データ、ロジック、動作をオブジェクトで包み込む方法を指南する、オブジェクト指向プログラミング(OOP)というフレームワークについて理解する必要があります。
OOP を使用すると、実世界の複雑な問題を小さなオブジェクトにまとめて単純化することができます。OOP には 4 つの基本コンセプトがあります。それぞれのコンセプトの詳細については、この Codelab で後ほど説明します。
- カプセル化。関連するプロパティと、それらのプロパティにアクションを実行するメソッドをクラスで包みます。スマートフォンについて考えてみましょう。カメラ、ディスプレイ、メモリカードなどのハードウェア部品やソフトウェア部品がカプセル化されています。部品が内部でどのように接続されているかを気にする必要はありません。
- 抽象化。カプセル化の延長で、内部の実装ロジックを可能な限り隠すという考え方です。たとえば、スマートフォンで写真を撮るには、カメラアプリを開き、撮影するシーンにスマートフォンを向けて、ボタンをクリックする必要があります。カメラアプリの作成方法や、スマートフォンのカメラ ハードウェアの仕組みを知る必要はありません。つまり、カメラアプリの内部の仕組みやモバイルカメラが写真を撮影する方法が抽象化され、重要なタスクを実行できるようになっています。
- 継承。親子関係を作ることで、他のクラスの特性と動作のうえにクラスを作成できるようにします。たとえば、各メーカーは Android OS を搭載したさまざまなモバイル デバイスを製造していますが、デバイスごとに UI は異なります。つまり、メーカーは Android OS の機能を継承し、そのうえにカスタマイズを行っていると言えます。
- ポリモーフィズム。この単語は、ギリシャ語の「ポリ」(多数)と「モーフィズム」(形態)を合わせたものです。ポリモーフィズムとは、異なるオブジェクトを単一かつ共通の方法で使用することを指します。たとえば、Bluetooth スピーカーをスマートフォンに接続する場合、スマートフォンが知る必要があるのは Bluetooth で音声を再生できるデバイスがあることだけです。さまざまな Bluetooth スピーカーを使用できますが、スマートフォンが各スピーカーの使い方を個別に知っている必要はありません。
最後に、プロパティ委譲についても学習します。これを使用すると、プロパティ値を操作する再利用可能なコードを簡潔な構文で記述できます。この Codelab では、スマートホーム アプリのクラス構造を構築することを題材にして、こうしたコンセプトについて学習します。
前提条件
- Kotlin のプレイグラウンドでコードを開き、編集し、実行できる。
- Kotlin プログラミングの基礎知識(変数と関数を含む)、および
println()
関数とmain()
関数の知識
学習内容
- OOP の概要
- クラスとは何か
- コンストラクタ、関数、プロパティを備えたクラスを定義する方法
- オブジェクトをインスタンス化する方法
- 継承とは何か
- IS-A 関係と HAS-A 関係の違い
- プロパティと関数をオーバーライドする方法
- 可視性修飾子とは
- 委譲とは何か、
by
委譲の使用方法
作成するコードの概要
- スマートホームのクラス構造
- スマートテレビやスマートライトなどのスマート デバイスを表すクラス
必要なもの
- ウェブブラウザがインストールされた、インターネットに接続できるパソコン
2. クラスを定義する
クラスを定義するときには、そのクラスのすべてのオブジェクトに必要なプロパティとメソッドを指定します。
クラス定義は class
キーワードで始まり、その後に名前、中括弧の組が続きます。構文の左中括弧の前の部分は、クラスヘッダーとも呼ばれます。中括弧の内側では、クラスのプロパティと関数を指定できます。プロパティと関数については後ほど説明します。クラス定義の構文を次の図に示します。
クラスには、以下のような命名規則が推奨されています。
- クラス名は任意に選択できますが、Kotlin のキーワードは使用しません(例:
fun
キーワード)。 - クラス名は PascalCase(各単語が大文字で始まり、単語間にスペースがない)で記述します。たとえば、SmartDevice では、各単語の先頭を大文字にし、単語間にはスペースを入れません。
クラスには、主に 3 つの構成要素があります。
- プロパティ。クラスのオブジェクトの属性を指定する変数。
- メソッド。クラスの動作とアクションを含んでいる関数。
- コンストラクタ。クラスを定義しているプログラムの中でそのクラスのインスタンスを作成する特別なメンバー関数。
クラスを扱う課題は今回が初めてではありません。これまでの Codelab で、Int
、Float
、String
、Double
などのデータ型について学習しました。Kotlin では、これらのデータ型がクラスとして定義されています。次のコード スニペットに示すように変数を定義すると、1
の値でインスタンス化された Int
クラスのオブジェクトが作成されます。
val number: Int = 1
SmartDevice
クラスを定義しましょう。
- Kotlin のプレイグラウンドで、内容を空の
main()
関数に置き換えます。
fun main() {
}
main()
関数の前の行で、本体に//
empty
body
というコメントが入ったSmartDevice
クラスを定義します。
class SmartDevice {
// empty body
}
fun main() {
}
3. クラスのインスタンスを作成する
すでに学習したとおり、クラスはオブジェクトの設計図です。Kotlin ランタイムは、クラス(設計図)を使用して、その型のオブジェクトを作成します。この SmartDevice
クラスを、スマート デバイスの設計図として使用できます。プログラムで実際のスマート デバイスを使うには、SmartDevice
のオブジェクト インスタンスを作成する必要があります。インスタンス化構文は、次の図に示すように、クラス名で始まり、その後に括弧の組が続きます。
オブジェクトを使用するには、変数の定義と同様に、オブジェクトを作成して変数に代入します。不変変数は val
キーワードを使用して作成し、可変変数は var
キーワードを使用して作成します。val
キーワードまたは var
キーワードの後に、変数名、=
代入演算子、クラス オブジェクトのインスタンス化と続きます。この構文を図にすると次のようになります。
SmartDevice
クラスをオブジェクトとしてインスタンス化しましょう。
main()
関数で、val
キーワードを使用してsmartTvDevice
という名前の変数を作成し、SmartDevice
クラスのインスタンスとして初期化します。
fun main() {
val smartTvDevice = SmartDevice()
}
4. クラスメソッドを定義する
ユニット 1 では、以下のことを学びました。
- 関数の定義には、
fun
キーワードの後に、括弧の組と中括弧の組を続けたものを使用します。中括弧の中には、タスクを実行するために必要な手順であるコードが含まれています。 - 関数を呼び出すと、その関数に含まれるコードが実行されます。
クラスが実行できるアクションは、クラスの関数として定義されます。たとえば、スマート デバイス、スマートテレビ、スマートライトを所有していて、スマートフォンでオンとオフを切り替えられるとします。スマート デバイスは、プログラミングでは SmartDevice
クラスに置き換えられ、オンとオフを切り替えるアクションは、オン / オフ動作を実現する turnOn()
関数と turnOff()
関数で表されます。
クラスで関数を定義する構文は、前に学習したものと同じです。唯一の違いは、関数がクラス本体にあることです。クラス本体で定義された関数は、メンバー関数またはメソッドと呼ばれ、クラスの動作を表します。この Codelab の残りの部分では、クラスの本体にある関数をメソッドと呼びます。
SmartDevice
クラスで turnOn()
メソッドと turnOff()
メソッドを定義しましょう。
SmartDevice
クラスの本体で、本体が空のturnOn()
メソッドを定義します。
class SmartDevice {
fun turnOn() {
}
}
turnOn()
メソッドの本体にprintln()
文を追加し、"Smart
device
is
turned
on."
という文字列を渡します。
class SmartDevice {
fun turnOn() {
println("Smart device is turned on.")
}
}
turnOn()
メソッドの後に、"Smart
device
is
turned
off."
という文字列を出力するturnOff()
メソッドを追加します。
class SmartDevice {
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
オブジェクトのメソッドを呼び出す
ここまで、スマート デバイスの設計図となるクラスを定義し、そのクラスのインスタンスを作成して変数に代入しました。次に、SmartDevice
クラスのメソッドを使用して、デバイスの電源をオンにしてからオフにします。
クラス内でのメソッドの呼び出しは、前の Codelab の main()
関数から他の関数を呼び出す方法に似ています。たとえば、turnOn()
メソッドから turnOff()
メソッドを呼び出す必要がある場合は、次のコード スニペットのように記述します。
class SmartDevice {
fun turnOn() {
// A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
turnOff()
...
}
...
}
クラスの外部でクラスメソッドを呼び出すには、クラス オブジェクトの後に、.
演算子、関数名、括弧の組と続けたものを使用します。必要に応じて、メソッドに必要な引数を括弧内に入れます。この構文を図にすると次のようになります。
このオブジェクトの turnOn()
メソッドと turnOff()
メソッドを呼び出しましょう。
main()
関数のsmartTvDevice
変数の後の行で、turnOn()
メソッドを呼び出します。
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
}
turnOn()
メソッドの後の行で、turnOff()
メソッドを呼び出します。
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
smartTvDevice.turnOff()
}
- コードを実行します。
次のような出力が表示されます。
Smart device is turned on. Smart device is turned off.
5. クラスのプロパティを定義する
ユニット 1 では、変数(1 つのデータを納めるコンテナ)について学びました。val
キーワードを使用して読み取り専用変数を作成する方法と、var
キーワードを使用して可変変数を作成する方法を学びました。
メソッドはクラスが実行できるアクションを定義しますが、プロパティはクラスの特性またはデータ属性を定義します。たとえば、スマート デバイスには次のような特性があります。
- 名前。デバイスの名前。
- カテゴリ。スマート デバイスのタイプ(エンターテイメント、設備、調理など)。
- デバイスのステータス。デバイスがオンかオフか、オンラインかオフラインか。デバイスは、インターネットに接続されているときにオンラインとみなされ、そうでないときにはオフラインとみなされます。
プロパティは基本的に、関数本体ではなくクラス本体で定義される変数だと言えます。定義する構文は変数と同じだということです。不変プロパティは val
キーワードで定義し、可変プロパティは var
キーワードで定義します。
上で述べた特性を SmartDevice
クラスのプロパティとして実装しましょう。
turnOn()
メソッドの前の行で、name
プロパティを定義して"Android
TV"
という文字列を代入します。
class SmartDevice {
val name = "Android TV"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
name
プロパティの後の行で、category
プロパティを定義して"Entertainment"
という文字列を代入し、deviceStatus
プロパティを定義して"online"
という文字列を代入します。
class SmartDevice {
val name = "Android TV"
val category = "Entertainment"
var deviceStatus = "online"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
smartTvDevice
変数の後の行で、println()
関数を呼び出して、"Device
name
is:
${smartTvDevice.name}"
という文字列を渡します。
fun main() {
val smartTvDevice = SmartDevice()
println("Device name is: ${smartTvDevice.name}")
smartTvDevice.turnOn()
smartTvDevice.turnOff()
}
- コードを実行します。
次のような出力が表示されます。
Device name is: Android TV Smart device is turned on. Smart device is turned off.
プロパティのゲッター関数とセッター関数
プロパティでは、変数よりも多くの処理を行えます。たとえば、スマートテレビを表すクラス構造を作成するとします。一般的な操作の一つに、音量の上げ下げがあります。このアクションをプログラミングで表現するために、speakerVolume
という名前のプロパティを作成します。このプロパティには、テレビのスピーカーに設定されている現在の音量レベルが保持されていますが、音量の値には設定可能な範囲があります。設定できる音量の最小値は 0 で、最大値は 100 です。speakerVolume
プロパティが 100 を超えたり、0 を下回ったりしないように、セッター関数を作成します。プロパティの値を更新するときに、値が 0 から 100 の範囲内にあるかどうかを確認する必要があります。別の例として、名前を常に大文字で記述する必要がある場合を考えます。ゲッター関数を実装すれば、そこで name
プロパティを大文字に変換できます。
これらのプロパティを実装する方法を詳しく確認する前に、宣言するための完全な構文を理解しておく必要があります。可変プロパティを定義する完全な構文は、変数定義から始まり、その後に省略可能な get()
関数と set()
関数が続きます。この構文を図にすると次のようになります。
プロパティにゲッター関数とセッター関数を定義しない場合は、Kotlin コンパイラが内部的に作成します。たとえば、var
キーワードを使用して speakerVolume
プロパティを定義し、2
という値を代入すると、コンパイラは次のコード スニペットのようにゲッター関数とセッター関数を自動生成します。
var speakerVolume = 2
get() = field
set(value) {
field = value
}
これらの行は、コンパイラがバックグラウンドで追加するものであり、コードには現れません。
不変プロパティの完全な構文は、次の 2 つの点が異なります。
val
キーワードで始まります。val
型の変数は読み取り専用であるため、set()
関数がありません。
Kotlin プロパティは、メモリに値を保持するためにバッキング フィールドを使用します。バッキング フィールドは、基本的には、プロパティで内部的に定義されているクラス変数です。バッキング フィールドのスコープはプロパティです。つまり、get()
プロパティ関数や set()
プロパティ関数からのみアクセスできます。
get()
関数内でのプロパティ値の読み取りと、set()
関数内での値の更新には、プロパティのバッキング フィールドを使用する必要があります。これは Kotlin コンパイラにより自動生成され、field
識別子で参照されます。
たとえば、set()
関数内でプロパティの値を更新する場合は、次のコード スニペットのように、value
パラメータとして参照される set()
関数のパラメータを使用し、それを field
変数に代入します。
var speakerVolume = 2
set(value) {
field = value
}
たとえば、speakerVolume
プロパティに割り当てる値を 0 から 100 の範囲にするには、次のコード スニペットに示すようなセッター関数を実装します。
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
set()
関数は、in
キーワードと値の範囲を使用して、Int
値が 0 から 100 の範囲内にあるかどうかをチェックします。値が範囲内の場合、field
の値が更新されます。それ以外の場合、プロパティの値は変更されません。
このプロパティは、この Codelab の「クラス間の関係を実装する」のクラスに含まれているため、ここでセッター関数をコードに追加する必要はありません。
6. コンストラクタを定義する
コンストラクタの主な目的は、クラスのオブジェクトを作成する方法を定めることです。別の言い方をすると、コンストラクタがオブジェクトを初期化することで、そのオブジェクトが使用可能になるということです。この操作は、オブジェクトをインスタンス化するときに行いました。クラスのオブジェクトがインスタンス化されるときに、コンストラクタ内のコードが実行されます。コンストラクタは、パラメータの有無にかかわらず定義できます。
デフォルト コンストラクタ
デフォルト コンストラクタは、パラメータのないコンストラクタです。デフォルト コンストラクタは、次のコード スニペットに示すように定義します。
class SmartDevice constructor() {
...
}
Kotlin では簡潔な表現を目指しており、コンストラクタにアノテーションや可視性修飾子がない場合は、後で学習するように constructor
キーワードを省くことができます。次のコード スニペットに示すように、コンストラクタにパラメータがない場合は、括弧も省くことができます。
class SmartDevice {
...
}
Kotlin コンパイラはデフォルト コンストラクタを自動生成します。自動生成されるデフォルト コンストラクタは、コンパイラがバックグラウンドで追加するため、コードには現れません。
パラメータ付きコンストラクタを定義する
SmartDevice
クラス内では、name
プロパティと category
プロパティを変更できません。そのため、SmartDevice
クラスのすべてのインスタンスで、必ず name
プロパティと category
プロパティを初期化する必要があります。現在の実装では、name
プロパティと category
プロパティの値がハードコードされています。これは、すべてのスマート デバイスが "Android
TV"
という文字列の名前を持ち、"Entertainment"
という文字列のカテゴリに分類されることを意味します。
不変性を維持したまま、値のハードコードを避けるために、パラメータ付きコンストラクタを使用して初期化します。
SmartDevice
クラスで、デフォルト値を指定せずにname
プロパティとcategory
プロパティをコンストラクタに移動します。
class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
fun turnOn() {
println("Smart device is turned on.")
}
fun turnOff() {
println("Smart device is turned off.")
}
}
これで、プロパティを設定するためのパラメータをコンストラクタに渡せるようになったので、このクラスのオブジェクトをインスタンス化する方法も変わります。オブジェクトをインスタンス化するための完全な構文を次の図に示します。
コードで表すと次のようになります。
SmartDevice("Android TV", "Entertainment")
このコンストラクタの引数はどちらも文字列です。どの値がどのパラメータのものなのか判別しにくくなっています。これを解決するには、関数に引数を渡した方法と同様に、次のコード スニペットに示すように名前付き引数を備えたコンストラクタを作成します。
SmartDevice(name = "Android TV", category = "Entertainment")
Kotlin のコンストラクタには、主に 2 つのタイプがあります。
- プライマリ コンストラクタ。クラスには、プライマリ コンストラクタを 1 つだけ定義でき、これはクラスヘッダーの中で定義します。プライマリ コンストラクタは、デフォルト コンストラクタかパラメータ付きコンストラクタのいずれかです。プライマリ コンストラクタに本体はありません。つまり、コードがありません。
- セカンダリ コンストラクタ。クラスには、複数のセカンダリ コンストラクタを定義できます。セカンダリ コンストラクタは、パラメータの有無にかかわらず定義できます。セカンダリ コンストラクタは、クラスを初期化することができ、本体を持たせてそこに初期化ロジックを入れることができます。クラスにプライマリ コンストラクタがある場合、各セカンダリ コンストラクタでプライマリ コンストラクタを初期化する必要があります。
プライマリ コンストラクタを使用すると、クラスヘッダー内でプロパティを初期化できます。コンストラクタに渡される引数は、プロパティに代入されます。プライマリ コンストラクタを定義する構文は、クラス名の後に constructor
キーワード、括弧のペアと続きます。括弧内にはプライマリ コンストラクタのパラメータが入ります。パラメータが複数ある場合は、パラメータ定義をカンマで区切ります。プライマリ コンストラクタを定義する完全な構文を次の図に示します。
セカンダリ コンストラクタは、クラスの本体の中にあり、その構文は以下の 3 つの部分で構成されます。
- セカンダリ コンストラクタの宣言。セカンダリ コンストラクタの定義は、
constructor
キーワードで始まり、その後に括弧のペアが続きます。セカンダリ コンストラクタに必要なパラメータがあれば、それを括弧内に入れます。 - プライマリ コンストラクタの初期化。初期化は、コロンで始まり、その後に
this
キーワード、括弧のペアと続きます。プライマリ コンストラクタに必要なパラメータがある場合は、それを括弧内に入れます。 - セカンダリ コンストラクタの本体。セカンダリ コンストラクタの本体は、プライマリ コンストラクタの初期化の後ろに、中括弧で囲んで記述します。
この構文を図にすると次のようになります。
例として、スマート デバイス プロバイダが開発した API を統合する場合を考えます。この API は、初期デバイス ステータスを示す Int
タイプのステータス コードを返します。また、この API は、デバイスがオフラインの場合は 0
の値を返し、オンラインの場合は 1
の値を返します。それ以外の整数値の場合、ステータスは不明とみなされます。次のコード スニペットに示すように、SmartDevice
クラスにセカンダリ コンストラクタを作成すると、この statusCode
パラメータを文字列表現に変換できます。
class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
constructor(name: String, category: String, statusCode: Int) : this(name, category) {
deviceStatus = when (statusCode) {
0 -> "offline"
1 -> "online"
else -> "unknown"
}
}
...
}
7. クラス間で関係を作る
継承を使用すると、別のクラスの特性と動作に基づいてクラスを作成できます。これは、再利用可能なコードを記述し、クラス間で関係を作るための有効で強力なメカニズムです。
たとえば、スマートテレビ、スマートライト、スマート スイッチなど、多くのスマート デバイスが販売されています。プログラミングでスマート デバイスを表すとき、名前、カテゴリ、ステータスなどの共通の特性を各デバイスが共有します。また、オンとオフを切り替えられるといった共通の動作もあります。
ただし、スマート デバイスによってオンとオフの切り替え方は異なります。たとえば、テレビの電源をオンにするには、ディスプレイをオンにしてから、最後に設定されていた音量とチャンネルを設定し直す必要があります。一方、ライトをオンにする場合に必要なのは、明るさの増減だけです。
また、各スマート デバイスには、その他にも実行できる機能やアクションがあります。たとえば、テレビの場合は、音量の調節やチャンネルの変更ができます。ライトの場合は、明るさや色を調整できます。
つまり、すべてのスマート デバイスが異なる機能を持っている一方で、共通の特性もあるということです。こういった共通の特性は、各スマート デバイス クラスにコピーすることもできますが、継承してコードを再利用可能にすることもできます。
継承するには、SmartDevice
の親クラスを作成し、上記の共通のプロパティと動作を定義する必要があります。さらに、親クラスのプロパティを継承する SmartTvDevice
クラスや SmartLightDevice
クラスなどの子クラスを作成します。
これをプログラミング用語では、SmartTvDevice
クラスと SmartLightDevice
クラスが SmartDevice
親クラスを「拡張」していると表現します。親クラスはスーパークラスとも呼ばれ、子クラスはサブクラスとも呼ばれます。これらの関係を次の図に示します。
ただし Kotlin では、すべてのクラスがデフォルトで final です。これはつまり、そのようなクラスは拡張できないということなので、クラス間の関係を定義する必要があります。
SmartDevice
スーパークラスとそのサブクラスの関係を定義しましょう。
SmartDevice
スーパークラスで、class
キーワードの前にopen
キーワードを追加して拡張可能にします。
open class SmartDevice(val name: String, val category: String) {
...
}
open
キーワードは、このクラスが拡張可能であることをコンパイラに知らせるもので、これによって他のクラスがこのクラスを拡張できるようになります。
サブクラスを作成する構文は、すでに行ったように、クラスヘッダーの作成から始まります。コンストラクタの右括弧の後に、スペース、コロン、もう一つのスペース、スーパークラス名、括弧のペアと続けます。括弧内には、必要に応じて、スーパークラスのコンストラクタで必要なパラメータを入れます。この構文を図にすると次のようになります。
SmartDevice
スーパークラスを拡張するSmartTvDevice
サブクラスを作成します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
}
SmartTvDevice
の constructor
定義では、プロパティが可変か不変かを指定しません。つまり、deviceName
パラメータと deviceCategory
パラメータは、クラス プロパティではなく、単なる constructor
パラメータです。このクラスでは使用できず、スーパークラスのコンストラクタに渡すだけです。
SmartTvDevice
サブクラスの本体で、ゲッター関数とセッター関数について学習したときに作成したspeakerVolume
プロパティを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
}
0..200
の範囲を設定するセッター関数を備え、1
の値が代入されるchannelNumber
プロパティを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
}
- ボリュームを上げて、
"Speaker
volume
increased
to
$speakerVolume."
という文字列を出力するincreaseSpeakerVolume()
メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
}
- チャンネル番号を増やし、
"Channel
number
increased
to
$channelNumber."
という文字列を出力するnextChannel()
メソッドを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
}
SmartTvDevice
サブクラスの後に、SmartDevice
スーパークラスを拡張するSmartLightDevice
サブクラスを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
}
SmartLightDevice
サブクラスの本体で、0..100
の範囲を指定するセッター関数を備え、0
の値が代入されるbrightnessLevel
プロパティを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
}
- ライトの明るさを上げ、
"Brightness
increased
to
$brightnessLevel."
という文字列を出力するincreaseBrightness()
メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
}
クラス間の関係
継承を使用する場合、2 つのクラス間の関係は「IS-A 関係」と呼ばれます。あるクラスのオブジェクトは、そのクラスを継承しているクラスのインスタンスでもあります。「HAS-A 関係」の場合、あるクラスのオブジェクトが別のクラスのインスタンスとなることなく、そのクラスのインスタンスを所有できます。次の図は、この関係をおおまかに示したものです。
IS-A 関係
SmartDevice
スーパークラスと SmartTvDevice
サブクラスの間に IS-A 関係があるとは、SmartDevice
スーパークラスで可能なすべての操作を、SmartTvDevice
サブクラスで行えるということです。この関係は一方向の関係です。つまり、すべてのスマートテレビがスマート デバイスであると言えますが、すべてのスマート デバイスがスマートテレビであるとは言えません。IS-A 関係をコードで表すと次のコード スニペットのようになります。
// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}
コードの再利用を可能にするためだけに継承を使用しないでください。その前に、2 つのクラスが互いに関連しているかどうかを確認してください。関係がある場合は、IS-A 関係の条件を満たしているかどうかを確認してください。そのサブクラスはスーパークラスだと言えるのかどうかを自問してください。たとえば、「Android はオペレーティング システムである」と言うことができます。
HAS-A 関係
HAS-A 関係は、2 つのクラスの関係を特定するもう一つの方法です。たとえば、通常、スマートテレビは家で使用します。このとき、スマートテレビと家の間には、なんらかの関係があります。家にスマート デバイスがある、つまり家がスマート デバイスを「持っている」ということです。2 つのクラス間の HAS-A 関係は、コンポジションとも呼ばれます。
ここまでにスマート デバイスをいくつか作成しています。次は、スマート デバイスが入っている SmartHome
クラスを作成します。SmartHome
クラスを使ってスマート デバイスを操作できます。
HAS-A 関係を使用して SmartHome
クラスを定義しましょう。
SmartLightDevice
クラスとmain()
関数の間で、SmartHome
クラスを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
...
}
class SmartHome {
}
fun main() {
...
}
SmartHome
クラスのコンストラクタで、val
キーワードを使用してSmartTvDevice
型のsmartTvDevice
プロパティを作成します。
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {
}
SmartHome
クラスの本体で、smartTvDevice
プロパティのturnOn()
メソッドを呼び出すturnOnTv()
メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
}
turnOnTv()
メソッドの後の行で、smartTvDevice
プロパティのturnOff()
メソッドを呼び出すturnOffTv()
メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
fun turnOffTv() {
smartTvDevice.turnOff()
}
}
turnOffTv()
メソッドの後の行で、smartTvDevice
プロパティのincreaseSpeakerVolume()
メソッドを呼び出すincreaseTvVolume()
メソッドを定義し、smartTvDevice
プロパティのnextChannel()
メソッドを呼び出すchangeTvChannelToNext()
メソッドを定義します。
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
fun turnOffTv() {
smartTvDevice.turnOff()
}
fun increaseTvVolume() {
smartTvDevice.increaseSpeakerVolume()
}
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
}
SmartHome
クラスのコンストラクタで、smartTvDevice
プロパティ パラメータを独立した行に移動し、その後にカンマを追加します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
) {
...
}
smartTvDevice
プロパティの後ろの行で、val
キーワードを使用してSmartLightDevice
タイプのsmartLightDevice
プロパティを定義します。
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
}
SmartHome
の本体で、smartLightDevice
オブジェクトのturnOn()
メソッドを呼び出すturnOnLight()
メソッドと、smartLightDevice
オブジェクトのturnOff()
メソッドを呼び出すturnOffLight()
メソッドを定義します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
fun turnOnLight() {
smartLightDevice.turnOn()
}
fun turnOffLight() {
smartLightDevice.turnOff()
}
}
turnOffLight()
メソッドの後ろの行で、smartLightDevice
プロパティのincreaseBrightness()
メソッドを呼び出すincreaseLightBrightness()
メソッドを定義します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
fun turnOnLight() {
smartLightDevice.turnOn()
}
fun turnOffLight() {
smartLightDevice.turnOff()
}
fun increaseLightBrightness() {
smartLightDevice.increaseBrightness()
}
}
increaseLightBrightness()
メソッドの後ろの行で、turnOffTv()
メソッドとturnOffLight()
メソッドを呼び出すturnOffAllDevices()
メソッドを定義します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
fun turnOffAllDevices() {
turnOffTv()
turnOffLight()
}
}
スーパークラスのメソッドをサブクラスでオーバーライドする
前述のように、オンとオフを切り替える機能はすべてのスマート デバイスでサポートされていますが、その中の実行方法は異なります。このデバイス固有の動作を指定するには、スーパークラスで定義されている turnOn()
メソッドと turnOff()
メソッドをオーバーライドする必要があります。オーバーライドするとは、操作を横取りすること、典型的には手動で制御することを意味します。メソッドをオーバーライドすると、サブクラスのメソッドがスーパークラスで定義されたメソッドの実行をさえぎって、独自の実行を行います。
SmartDevice
クラスの turnOn()
メソッドと turnOff()
メソッドをオーバーライドしましょう。
SmartDevice
スーパークラスの本体で、各メソッドのfun
キーワードの前にopen
キーワードを追加します。
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open fun turnOn() {
// function body
}
open fun turnOff() {
// function body
}
}
SmartLightDevice
クラスの本体で、本体が空のturnOn()
メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
fun turnOn() {
}
}
turnOn()
メソッドの本体で、deviceStatus
プロパティを文字列「on
」に設定し、2
の値にbrightnessLevel
プロパティを設定し、println()
文を追加して、"$name
turned
on.
The
brightness
level
is
$brightnessLevel."
という文字列を渡します。
fun turnOn() {
deviceStatus = "on"
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
SmartLightDevice
クラスの本体で、本体が空のturnOff()
メソッドを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
fun turnOn() {
deviceStatus = "on"
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
fun turnOff() {
}
}
turnOff()
メソッドの本体で、deviceStatus
プロパティを文字列「off
」に設定し、0
の値にbrightnessLevel
プロパティを設定し、println()
文を追加して、"Smart
Light
turned
off"
という文字列を渡します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
fun turnOn() {
deviceStatus = "on"
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
fun turnOff() {
deviceStatus = "off"
brightnessLevel = 0
println("Smart Light turned off")
}
}
SmartLightDevice
サブクラスで、turnOn()
メソッドとturnOff()
メソッドのfun
キーワードの前にoverride
キーワードを追加します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
override fun turnOn() {
deviceStatus = "on"
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
override fun turnOff() {
deviceStatus = "off"
brightnessLevel = 0
println("Smart Light turned off")
}
}
override
キーワードは、サブクラスで定義されたメソッドに入っているコードを実行するように Kotlin ランタイムに指示するものです。
SmartTvDevice
クラスの本体で、本体が空のturnOn()
メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
fun turnOn() {
}
}
turnOn()
メソッドの本体で、deviceStatus
プロパティを文字列「on
」に設定し、println()
文を追加して、"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber."
という文字列を渡します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {
...
fun turnOn() {
deviceStatus = "on"
println(
"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
"set to $channelNumber."
)
}
}
SmartTvDevice
クラスの本体で、turnOn()
メソッドの後に、空の本体を持つturnOff()
メソッドを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {
...
fun turnOn() {
...
}
fun turnOff() {
}
}
turnOff()
メソッドの本体で、deviceStatus
プロパティを文字列「off
」に設定し、println()
文を追加して、"$name
turned
off"
という文字列を渡します。
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {
...
fun turnOn() {
...
}
fun turnOff() {
deviceStatus = "off"
println("$name turned off")
}
}
SmartTvDevice
クラスで、turnOn()
メソッドとturnOff()
メソッドのfun
キーワードの前にoverride
キーワードを追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
override fun turnOn() {
deviceStatus = "on"
println(
"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
"set to $channelNumber."
)
}
override fun turnOff() {
deviceStatus = "off"
println("$name turned off")
}
}
main()
関数で、var
キーワードを使用してSmartDevice
型のsmartDevice
変数を定義します。そこで、"Android
TV"
と"Entertainment"
という引数を渡してSmartTvDevice
オブジェクトをインスタンス化します。
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
smartDevice
変数の後ろの行で、smartDevice
オブジェクトのturnOn()
メソッドを呼び出します。
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
smartDevice.turnOn()
}
- コードを実行します。
次のような出力が表示されます。
Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
turnOn()
メソッドの呼び出しの後ろの行で、"Google
Light"
と"Utility"
という引数を渡してSmartLightDevice
クラスをインスタンス化し、smartDevice
変数に再代入してから、smartDevice
オブジェクト参照のturnOn()
メソッドを呼び出します。
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
smartDevice.turnOn()
smartDevice = SmartLightDevice("Google Light", "Utility")
smartDevice.turnOn()
}
- コードを実行します。
次のような出力が表示されます。
Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1. Google Light turned on. The brightness level is 2.
これはポリモーフィズムの例になっています。このコードでは、SmartDevice
型の変数の turnOn()
メソッドを呼び出しますが、変数の実際の値に応じて turnOn()
メソッドの異なる実装を実行することができます。
super
キーワードを使用してサブクラスでスーパークラスのコードを再利用する
turnOn()
メソッドと turnOff()
メソッドをよく見ると、SmartTvDevice
サブクラスと SmartLightDevice
サブクラスでメソッドが呼び出されるときの deviceStatus
変数を更新する仕組みが似ていることに気が付くでしょう。つまり、コードが重複しているのです。SmartDevice
クラスでステータスを更新するときのコードは再利用できます。
サブクラスからスーパークラスのオーバーライドされたメソッドを呼び出すには、super
キーワードを使用する必要があります。スーパークラスのメソッドを呼び出す方法は、クラスの外部からメソッドを呼び出す場合と同様です。オブジェクトとメソッドの間で .
演算子を使用する代わりに、super
キーワードを使用する必要があります。これは、サブクラスではなくスーパークラスのメソッドを呼び出すように Kotlin コンパイラに指示するものです。
スーパークラスのメソッドを呼び出す構文は、super
キーワードの後に .
演算子、関数名、括弧のペアと続きます。必要に応じて、括弧内に引数を入れます。この構文を図にすると次のようになります。
SmartDevice
スーパークラスのコードを再利用しましょう。
turnOn()
メソッドとturnOff()
メソッドからprintln()
文を削除し、SmartTvDevice
サブクラスとSmartLightDevice
サブクラスにある重複するコードをSmartDevice
スーパークラスに移動します。
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open fun turnOn() {
deviceStatus = "on"
}
open fun turnOff() {
deviceStatus = "off"
}
}
super
キーワードを使用して、SmartTvDevice
サブクラスとSmartLightDevice
サブクラス内でSmartDevice
クラスのメソッドを呼び出します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
override fun turnOn() {
super.turnOn()
println(
"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
"set to $channelNumber."
)
}
override fun turnOff() {
super.turnOff()
println("$name turned off")
}
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
override fun turnOn() {
super.turnOn()
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
override fun turnOff() {
super.turnOff()
brightnessLevel = 0
println("Smart Light turned off")
}
}
サブクラスでスーパークラスのプロパティをオーバーライドする
メソッドと同様に、同じ手順でプロパティもオーバーライドできます。
deviceType
プロパティをオーバーライドしましょう。
SmartDevice
スーパークラスのdeviceStatus
プロパティの後ろの行で、open
キーワードとval
キーワードを使用してdeviceType
プロパティを定義し、"unknown"
という文字列を設定します。
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open val deviceType = "unknown"
...
}
SmartTvDevice
クラスで、override
キーワードとval
キーワードを使用して、deviceType
プロパティを定義し、"Smart
TV"
という文字列を設定します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart TV"
...
}
SmartLightDevice
クラスで、override
キーワードとval
キーワードを使用して、deviceType
プロパティを定義し、"Smart
Light"
という文字列を設定します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart Light"
...
}
8. 可視性修飾子
可視性修飾子は、カプセル化を実現するうえで重要な役割を果たします。
- クラスでは、クラスの外部から不正にアクセスできないようにプロパティやメソッドを隠すことができます。
- パッケージでは、パッケージの外部から不正にアクセスできないようにクラスやインターフェースを隠すことができます。
Kotlin には、以下の 4 つの可視性修飾子が用意されています。
public
: デフォルトの可視性修飾子。宣言をどこからでもアクセスできるようにします。クラス外で使用するプロパティとメソッドは public とマークします。private
: 宣言を同じクラスまたは同じソースファイルでアクセスできるようにします。
多くの場合、プロパティやメソッドの中には、クラス内でのみ使用し、他のクラスでは使用する必要のないものがあります。こういったプロパティやメソッドを private
可視性修飾子でマークすると、別のクラスが誤ってアクセスすることがなくなります。
protected
: 宣言をサブクラスでアクセスできるようにします。定義したクラスとそのサブクラスで使用するプロパティとメソッドは、protected
可視性修飾子でマークします。internal
: 宣言を同じモジュール内でアクセスできるようにします。internal 修飾子は private と似ていますが、internal プロパティと internal メソッドには、同じモジュール内であればクラスの外部からでもアクセスできます。
クラスを定義すると公開となり、それをインポートするパッケージからアクセスできるようになります。つまり、可視性修飾子を指定しない限り、デフォルトで公開になります。同様に、クラス内でプロパティやメソッドを定義または宣言すると、デフォルトではクラスの外部からクラス オブジェクトを使ってアクセスできます。コードに適切な可視性を定義するために、特に他のクラスがアクセスする必要のないプロパティやメソッドを隠す際に不可欠なものです。
例として、運転手が自動車を操作する仕組みについて考えてみましょう。自動車を構成する部品の詳細や自動車内部の仕組みは、デフォルトで隠されています。自動車は、できるだけ直感的に操作できるように作られています。自動車の操作が航空機のように複雑になることが望ましくないように、他の開発者や将来の自分がクラスのプロパティやメソッドの用途がわからなくなることも望ましいことではありません。
可視性修飾子を使用すると、コードの適切な部分をプロジェクト内の他のクラスから見えるようにして、実装が意図せず使用されないようにできるため、コードが理解しやすくなり、バグが発生しにくくなります。
可視性修飾子は、次の図のように、クラス、メソッド、プロパティを宣言するとき、その宣言の構文の前に置く必要があります。
プロパティに可視性修飾子を指定する
プロパティに可視性修飾子を指定する構文は、private
、protected
、または internal
の修飾子で始まり、その後にプロパティを定義する構文が続きます。この構文を図にすると次のようになります。
たとえば、次のコード スニペットで、deviceStatus
プロパティを非公開にする方法を示しています。
open class SmartDevice(val name: String, val category: String) {
...
private var deviceStatus = "online"
...
}
セッター関数に可視性修飾子を設定することもできます。修飾子は set
キーワードの前に置きます。この構文を図にすると次のようになります。
SmartDevice
クラスの場合、deviceStatus
プロパティの値は、クラス オブジェクトを使ってクラスの外部で読み取ることができる必要があります。しかし、値の更新や書き込みができるのは、そのクラスとその子だけにする必要があります。この要件を実現するには、deviceStatus
プロパティの set()
関数に protected
修飾子を使用する必要があります。
deviceStatus
プロパティの set()
関数に protected
修飾子を使用しましょう。
SmartDevice
スーパークラスのdeviceStatus
プロパティで、protected
修飾子をset()
関数に追加します。
open class SmartDevice(val name: String, val category: String) {
...
var deviceStatus = "online"
protected set(value) {
field = value
}
...
}
set()
関数ではアクションやチェックを実行していません。単に value
パラメータを field
変数に代入しているだけです。すでに学習したとおり、これはプロパティ セッターのデフォルト実装に似ています。この場合、set()
関数の括弧と本体を省略できます。
open class SmartDevice(val name: String, val category: String) {
...
var deviceStatus = "online"
protected set
...
}
SmartHome
クラスで、private のセッター関数を使用して0
の値に設定されたdeviceTurnOnCount
プロパティを定義します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
var deviceTurnOnCount = 0
private set
...
}
turnOnTv()
メソッドとturnOnLight()
メソッドに、deviceTurnOnCount
プロパティとそれに続けて++
算術演算子を追加します。さらに、turnOffTv()
メソッドとturnOffLight()
メソッドにdeviceTurnOnCount
プロパティとそれに続けて--
算術演算子を追加します。
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
var deviceTurnOnCount = 0
private set
fun turnOnTv() {
deviceTurnOnCount++
smartTvDevice.turnOn()
}
fun turnOffTv() {
deviceTurnOnCount--
smartTvDevice.turnOff()
}
...
fun turnOnLight() {
deviceTurnOnCount++
smartLightDevice.turnOn()
}
fun turnOffLight() {
deviceTurnOnCount--
smartLightDevice.turnOff()
}
...
}
メソッドの可視性修飾子
メソッドに可視性修飾子を指定する構文は、private
、protected
、または internal
の修飾子で始まり、その後にメソッドを定義する構文が続きます。この構文を図にすると次のようになります。
例として、SmartTvDevice
クラスで、nextChannel()
メソッドに protected
修飾子を指定する方法を、次のコード スニペットに示します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
...
protected fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
...
}
コンストラクタの可視性修飾子
コンストラクタの可視性修飾子を指定する構文は、いくつかの点を除き、プライマリ コンストラクタの定義に似ています。
- 修飾子は、クラス名の後、
constructor
キーワードの前に指定します。 - プライマリ コンストラクタに修飾子を指定する必要がある場合は、パラメータがない場合でも
constructor
キーワードと括弧を残す必要があります。
この構文を図にすると次のようになります。
例として、protected
修飾子を SmartDevice
コンストラクタに追加する方法を、次のコード スニペットに示します。
open class SmartDevice protected constructor (val name: String, val category: String) {
...
}
クラスの可視性修飾子
クラスに可視性修飾子を指定する構文は、private
、protected
、または internal
の修飾子で始まり、その後にクラスを定義する構文が続きます。この構文を図にすると次のようになります。
例として、SmartDevice
クラスに internal
修飾子を指定する方法を、次のコード スニペットに示します。
internal open class SmartDevice(val name: String, val category: String) {
...
}
理想的には、プロパティとメソッドに指定する可視性を厳格なものにするよう努力すべきです。そのために、可能な限り private
修飾子を使って宣言してください。private にできない場合は、protected
修飾子を使用してください。protected にできない場合は、internal
修飾子を使用してください。internal にできない場合は、public
修飾子を使用してください。
適切な可視性修飾子を指定する
次の表を参考にすれば、クラスやコンストラクタのプロパティやメソッドにアクセスできる場所に応じて、適切な可視性修飾子を決めることができます。
修飾子 | 同じクラスでアクセス可能 | サブクラスでアクセス可能 | 同じモジュールでアクセス可能 | モジュール外からアクセス可能 |
| ✔ | 𝗫 | 𝗫 | 𝗫 |
| ✔ | ✔ | 𝗫 | 𝗫 |
| ✔ | ✔ | ✔ | 𝗫 |
| ✔ | ✔ | ✔ | ✔ |
SmartTvDevice
サブクラスでは、speakerVolume
プロパティと channelNumber
プロパティをクラスの外部から制御可能にすべきではありません。これらのプロパティは、increaseSpeakerVolume()
メソッドと nextChannel()
メソッドのみで制御すべきです。
同様に、SmartLightDevice
サブクラスでは、brightnessLevel
プロパティは increaseLightBrightness()
メソッドのみで制御すべきです。
SmartTvDevice
サブクラスと SmartLightDevice
サブクラスに適切な可視性修飾子を追加しましょう。
SmartTvDevice
クラスで、private
可視性修飾子をspeakerVolume
プロパティとchannelNumber
プロパティに追加します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
private var speakerVolume = 2
set(value) {
if (value in 0..100) {
field = value
}
}
private var channelNumber = 1
set(value) {
if (value in 0..200) {
field = value
}
}
...
}
SmartLightDevice
クラスで、brightnessLevel
プロパティにprivate
修飾子を追加します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
...
private var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
...
}
9. プロパティ委譲を定義する
前のセクションで説明したように、Kotlin のプロパティはバッキング フィールドを使用して、その値をメモリに保持します。これは、field
識別子を使用して参照します。
ここまでのコードを見ると、SmartTvDevice
クラスと SmartLightDevice
クラスの speakerVolume
プロパティ、channelNumber
プロパティ、brightnessLevel
プロパティで値が範囲内にあるかどうかをチェックするコードが重複しています。委譲を使用すれば、セッター関数で範囲チェックのコードを再利用できます。値の管理にフィールド、ゲッター関数、セッター関数を使用するのではなく、委譲で値を管理します。
プロパティ委譲を作成するための構文は、変数の宣言で始まり、by
キーワード、プロパティのゲッター関数とセッター関数を処理する委譲オブジェクトと続きます。この構文を図にすると次のようになります。
実装を委譲できるクラスを実装する前に、インターフェースについて理解しておく必要があります。インターフェースとは、それを実装するクラスが守る必要のある決まり事のことです。インターフェースでは、アクションを「どう行うか」ではなく「何を行うか」に焦点を当てます。つまり、インターフェースを使えば抽象化することができるということです。
たとえば、家を建てる前には、建築家に、寝室、子供部屋、リビングルーム、キッチン、複数のバスルームが必要だと伝えます。つまり、施主は「何が欲しいか」を指定し、建築家はそれを「どうやって実現するか」を設計します。インターフェースを作成する構文は、次の図のようになります。
クラスを拡張して、その機能をオーバーライドする方法については、すでに学習しました。インターフェースでは、クラスがインターフェースを実装します。そのクラスでは、インターフェースで宣言されたメソッドとプロパティの実装の詳細を提供します。委譲の作成方法は、ReadWriteProperty
インターフェースの場合と同様です。インターフェースの詳細については、次のユニットで説明します。
var
型の委譲クラスを作成するには、ReadWriteProperty
インターフェースを実装する必要があります。同様に、val
型の場合は ReadOnlyProperty
インターフェースを実装する必要があります。
var
型の委譲を作成しましょう。
main()
関数の前に、ReadWriteProperty<Any?,
Int>
インターフェースを実装するRangeRegulator
クラスを作成します。
class RangeRegulator() : ReadWriteProperty<Any?, Int> {
}
fun main() {
...
}
山括弧とその内側は気にしないでください。これらは一般的な型を表しており、次のユニットで学習します。
RangeRegulator
クラスのプライマリ コンストラクタに、initialValue
パラメータ、private のminValue
プロパティ、private のmaxValue
プロパティ(どれもInt
型)を追加します。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
}
RangeRegulator
クラスの本体で、getValue()
メソッドとsetValue()
メソッドをオーバーライドします。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
}
}
これらのメソッドは、プロパティのゲッター関数とセッター関数の役目を果たします。
SmartDevice
クラスの前の行で、ReadWriteProperty
インターフェースとKProperty
インターフェースをインポートします。
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
open class SmartDevice(val name: String, val category: String) {
...
}
...
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
}
}
...
RangeRegulator
クラスのgetValue()
メソッドの前の行で、fieldData
プロパティを定義し、initialValue
パラメータで初期化します。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
var fieldData = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
}
}
このプロパティが変数のバッキング フィールドとなります。
getValue()
メソッドの本体で、fieldData
プロパティを返します。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
var fieldData = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return fieldData
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
}
}
setValue()
メソッドの本体で、value
パラメータがminValue..maxValue
の範囲にあるかどうかをチェックしてから、fieldData
プロパティに代入します。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
var fieldData = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return fieldData
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
if (value in minValue..maxValue) {
fieldData = value
}
}
}
SmartTvDevice
クラスで、委譲クラスを使用してspeakerVolume
プロパティとchannelNumber
プロパティを定義します。
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart TV"
private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)
private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)
...
}
SmartLightDevice
クラスで、委譲クラスを使用してbrightnessLevel
プロパティを定義します。
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart Light"
private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)
...
}
10. 解答をテストする
解答コードは、次のコード スニペットのとおりです。
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
protected set
open val deviceType = "unknown"
open fun turnOn() {
deviceStatus = "on"
}
open fun turnOff() {
deviceStatus = "off"
}
}
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart TV"
private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)
private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)
fun increaseSpeakerVolume() {
speakerVolume++
println("Speaker volume increased to $speakerVolume.")
}
fun nextChannel() {
channelNumber++
println("Channel number increased to $channelNumber.")
}
override fun turnOn() {
super.turnOn()
println(
"$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
"set to $channelNumber."
)
}
override fun turnOff() {
super.turnOff()
println("$name turned off")
}
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart Light"
private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)
fun increaseBrightness() {
brightnessLevel++
println("Brightness increased to $brightnessLevel.")
}
override fun turnOn() {
super.turnOn()
brightnessLevel = 2
println("$name turned on. The brightness level is $brightnessLevel.")
}
override fun turnOff() {
super.turnOff()
brightnessLevel = 0
println("Smart Light turned off")
}
}
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
var deviceTurnOnCount = 0
private set
fun turnOnTv() {
deviceTurnOnCount++
smartTvDevice.turnOn()
}
fun turnOffTv() {
deviceTurnOnCount--
smartTvDevice.turnOff()
}
fun increaseTvVolume() {
smartTvDevice.increaseSpeakerVolume()
}
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
fun turnOnLight() {
deviceTurnOnCount++
smartLightDevice.turnOn()
}
fun turnOffLight() {
deviceTurnOnCount--
smartLightDevice.turnOff()
}
fun increaseLightBrightness() {
smartLightDevice.increaseBrightness()
}
fun turnOffAllDevices() {
turnOffTv()
turnOffLight()
}
}
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
var fieldData = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return fieldData
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
if (value in minValue..maxValue) {
fieldData = value
}
}
}
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
smartDevice.turnOn()
smartDevice = SmartLightDevice("Google Light", "Utility")
smartDevice.turnOn()
}
次のような出力が表示されます。
Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1. Google Light turned on. The brightness level is 2.
11. 課題に挑戦しましょう
SmartDevice
クラスで、"Device
name:
$name,
category:
$category,
type:
$deviceType"
という文字列を出力するprintDeviceInfo()
メソッドを定義してください。SmartTvDevice
クラスで、音量を下げるdecreaseVolume()
メソッドと、前のチャンネルに切り替えるpreviousChannel()
メソッドを定義してください。SmartLightDevice
クラスで、明るさを下げるdecreaseBrightness()
メソッドを定義してください。SmartHome
クラスで、すべてのアクションを、各デバイスのdeviceStatus
プロパティが"on"
文字列に設定されている場合にのみ実行できるようにしてください。また、deviceTurnOnCount
プロパティが正しく更新されるようにしてください。
実装が完了したら、次のことを行ってください。
SmartHome
クラスで、decreaseTvVolume()
、changeTvChannelToPrevious()
、printSmartTvInfo()
、printSmartLightInfo()
、decreaseLightBrightness()
の各メソッドを定義してください。SmartHome
クラスで、SmartTvDevice
クラスとSmartLightDevice
クラスの適当なメソッドを呼び出してください。main()
関数で、追加したメソッドを呼び出してテストしてください。
12. おわりに
これで、クラスを定義する方法と、オブジェクトをインスタンス化する方法の学習が終わりました。また、クラス間で関係を結ぶ方法と、プロパティ委譲を作成する方法についても学習しました。
まとめ
- OOP には、カプセル化、抽象化、継承、ポリモーフィズムという 4 つの主要な原理があります。
- クラスは
class
キーワードで定義され、プロパティとメソッドを含みます。 - プロパティは変数に似ていますが、プロパティにはカスタムのゲッターとセッターを設けることができます。
- コンストラクタでは、クラスのオブジェクトをインスタンス化する方法を指定します。
- プライマリ コンストラクタを定義する際には、
constructor
キーワードを省略できます。 - 継承により、コードの再利用が簡単になります。
- IS-A 関係は継承を表します。
- HAS-A 関係はコンポジションを表します。
- 可視性修飾子は、カプセル化を実現するうえで重要な役割を果たします。
- Kotlin には、
public
、private
、protected
、internal
の 4 つの可視性修飾子があります。 - プロパティ委譲を使用すると、ゲッターとセッターのコードを複数のクラスで再利用できます。