中級: クラスとインターフェース
拡張関数
スコープ関数
レシーバー付きラムダ式
クラスとインターフェース
オブジェクト
オープンクラスと特殊なクラス
プロパティ
Null安全性
ライブラリとAPI
初級ツアーでは、コード内で共有できる特徴のコレクションをデータを保存し、維持するために、クラスとデータクラスを使用する方法を学びました。最終的には、プロジェクト内でコードを効率的に共有するために階層を作成したくなるでしょう。この章では、Kotlinがコード共有のために提供するオプションと、それらがコードをより安全で保守しやすくする方法について説明します。
クラスの継承
前の章では、元のソースコードを変更せずにクラスを拡張するために拡張関数を使用する方法について説明しました。しかし、クラス間でコードを共有することが役立つような複雑なものを開発している場合はどうでしょうか?そのような場合、クラス継承を使用できます。
デフォルトでは、Kotlinのクラスは継承できません。Kotlinはこのように設計されており、意図しない継承を防ぎ、クラスを保守しやすくします。
Kotlinのクラスは単一継承のみをサポートしており、これは一度に1つのクラスからしか継承できないことを意味します。このクラスは親と呼ばれます。
クラスの親は別のクラス (祖先クラス) から継承し、階層を形成します。Kotlinのクラス階層の最上位には、共通の親クラスである Any
があります。すべてのクラスは最終的に Any
クラスから継承されます。
Any
クラスは toString()
関数をメンバー関数として自動的に提供します。したがって、この継承された関数を任意のクラスで使用できます。例えば、
class Car(val make: String, val model: String, val numberOfDoors: Int)
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
// Uses the .toString() function via string templates to print class properties
println("Car1: make=${car1.make}, model=${car1.model}, numberOfDoors=${car1.numberOfDoors}")
// Car1: make=Toyota, model=Corolla, numberOfDoors=4
}
継承を使用してクラス間でコードを共有したい場合は、まず抽象クラスの使用を検討してください。
抽象クラス
抽象クラスはデフォルトで継承できます。抽象クラスの目的は、他のクラスが継承または実装するメンバーを提供することです。結果として、それらはコンストラクタを持ちますが、そこからインスタンスを作成することはできません。子クラス内で、override
キーワードを使用して親のプロパティと関数の振る舞いを定義します。このようにして、子クラスが親クラスのメンバーを「オーバーライドする」と言うことができます。
TIP
継承された関数またはプロパティの振る舞いを定義する場合、それを実装と呼びます。
抽象クラスには、実装ありの関数とプロパティ、および実装なしの関数とプロパティ (抽象関数と抽象プロパティとして知られる) の両方を含めることができます。
抽象クラスを作成するには、abstract
キーワードを使用します。
abstract class Animal
実装なしの関数またはプロパティを宣言するには、同様に abstract
キーワードを使用します。
abstract fun makeSound()
abstract val sound: String
例えば、異なる製品カテゴリを定義するために子クラスを作成できる Product
という名前の抽象クラスを作成したいとします。
abstract class Product(val name: String, var price: Double) {
// Abstract property for the product category
abstract val category: String
// A function that can be shared by all products
fun productInfo(): String {
return "Product: $name, Category: $category, Price: $price"
}
}
抽象クラスでは、
- コンストラクタには、製品の
name
とprice
の2つのパラメータがあります。 - 製品カテゴリを文字列として含む抽象プロパティがあります。
- 製品に関する情報を出力する関数があります。
電子機器用の子クラスを作成しましょう。子クラスで category
プロパティの実装を定義する前に、override
キーワードを使用する必要があります。
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
Electronic
クラスは、
Product
抽象クラスを継承します。- コンストラクタに追加のパラメータ
warranty
があります。これは電子機器に固有のものです。 category
プロパティをオーバーライドして文字列"Electronic"
を含むようにします。
これで、これらのクラスを次のように使用できます。
abstract class Product(val name: String, var price: Double) {
// Abstract property for the product category
abstract val category: String
// A function that can be shared by all products
fun productInfo(): String {
return "Product: $name, Category: $category, Price: $price"
}
}
class Electronic(name: String, price: Double, val warranty: Int) : Product(name, price) {
override val category = "Electronic"
}
fun main() {
// Creates an instance of the Electronic class
val laptop = Electronic(name = "Laptop", price = 1000.0, warranty = 2)
println(laptop.productInfo())
// Product: Laptop, Category: Electronic, Price: 1000.0
}
抽象クラスはこのようにコードを共有するのに非常に役立ちますが、Kotlinのクラスは単一継承のみをサポートしているため、制限があります。複数のソースから継承する必要がある場合は、インターフェースの使用を検討してください。
インターフェース
インターフェースはクラスに似ていますが、いくつかの違いがあります。
- インターフェースのインスタンスを作成することはできません。それらにはコンストラクタやヘッダーがありません。
- それらの関数とプロパティは、デフォルトで暗黙的に継承可能です。Kotlinでは、これらを「オープン (open)」と呼びます。
- 実装を与えない場合、関数を
abstract
とマークする必要はありません。
抽象クラスと同様に、インターフェースを使用して、クラスが後で継承および実装できる関数とプロパティのセットを定義します。このアプローチは、特定の実装詳細ではなく、インターフェースによって記述される抽象化に焦点を当てるのに役立ちます。インターフェースを使用すると、コードは次のようになります。
- さまざまな部分を分離し、独立して進化させることができるため、よりモジュール化されます。
- 関連する関数を一貫したセットにグループ化することで、理解しやすくなります。
- テストのために実装をモックと素早く入れ替えることができるため、テストしやすくなります。
インターフェースを宣言するには、interface
キーワードを使用します。
interface PaymentMethod
インターフェースの実装
インターフェースは多重継承をサポートしているため、クラスは一度に複数のインターフェースを実装できます。まず、クラスが1つのインターフェースを実装するシナリオを考えてみましょう。
インターフェースを実装するクラスを作成するには、クラスヘッダーの後にコロンを追加し、その後に実装したいインターフェース名を追加します。インターフェースにはコンストラクタがないため、インターフェース名の後に括弧 ()
を使用しません。
class CreditCardPayment : PaymentMethod
例えば:
interface PaymentMethod {
// Functions are inheritable by default
fun initiatePayment(amount: Double): String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
}
この例では、
PaymentMethod
は、実装を持たないinitiatePayment()
関数を持つインターフェースです。CreditCardPayment
は、PaymentMethod
インターフェースを実装するクラスです。CreditCardPayment
クラスは、継承されたinitiatePayment()
関数をオーバーライドします。paymentMethod
はCreditCardPayment
クラスのインスタンスです。- オーバーライドされた
initiatePayment()
関数が、paymentMethod
インスタンスでパラメータ100.0
とともに呼び出されます。
複数のインターフェースを実装するクラスを作成するには、クラスヘッダーの後にコロンを追加し、その後に実装したいインターフェース名をコンマで区切って追加します。
class CreditCardPayment : PaymentMethod, PaymentType
例えば:
interface PaymentMethod {
fun initiatePayment(amount: Double): String
}
interface PaymentType {
val paymentType: String
}
class CreditCardPayment(val cardNumber: String, val cardHolderName: String, val expiryDate: String) : PaymentMethod,
PaymentType {
override fun initiatePayment(amount: Double): String {
// Simulate processing payment with credit card
return "Payment of $amount initiated using Credit Card ending in ${cardNumber.takeLast(4)}."
}
override val paymentType: String = "Credit Card"
}
fun main() {
val paymentMethod = CreditCardPayment("1234 5678 9012 3456", "John Doe", "12/25")
println(paymentMethod.initiatePayment(100.0))
// Payment of $100.0 initiated using Credit Card ending in 3456.
println("Payment is by ${paymentMethod.paymentType}")
// Payment is by Credit Card
}
この例では、
PaymentMethod
は、実装を持たないinitiatePayment()
関数を持つインターフェースです。PaymentType
は、初期化されていないpaymentType
プロパティを持つインターフェースです。CreditCardPayment
は、PaymentMethod
インターフェースとPaymentType
インターフェースを実装するクラスです。CreditCardPayment
クラスは、継承されたinitiatePayment()
関数とpaymentType
プロパティをオーバーライドします。paymentMethod
はCreditCardPayment
クラスのインスタンスです。- オーバーライドされた
initiatePayment()
関数が、paymentMethod
インスタンスでパラメータ100.0
とともに呼び出されます。 - オーバーライドされた
paymentType
プロパティが、paymentMethod
インスタンスでアクセスされます。
インターフェースとインターフェース継承の詳細については、インターフェースを参照してください。
デリゲーション
インターフェースは便利ですが、インターフェースに多くの関数が含まれている場合、子クラスは多くのボイラープレートコードを記述することになる可能性があります。親の振る舞いのごく一部だけをオーバーライドしたい場合でも、多くの繰り返しが必要になります。
TIP
ボイラープレートコードとは、ソフトウェアプロジェクトの複数の部分で、ほとんどまたはまったく変更せずに再利用されるコードの塊のことです。
例えば、多数の関数と color
という名前のプロパティを1つ含む Drawable
という名前のインターフェースがあるとします。
interface Drawable {
fun draw()
fun resize()
val color: String?
}
Drawable
インターフェースを実装し、そのすべてのメンバーに実装を提供する Circle
という名前のクラスを作成します。
class Circle : Drawable {
override fun draw() {
TODO("An example implementation")
}
override fun resize() {
TODO("An example implementation")
}
override val color = null
}
color
プロパティの値以外は同じ振る舞いをする Circle
クラスの子クラスを作成したい場合でも、Circle
クラスの各メンバー関数の実装を追加する必要があります。
class RedCircle(val circle: Circle) : Circle {
// Start of boilerplate code
override fun draw() {
circle.draw()
}
override fun resize() {
circle.resize()
}
// End of boilerplate code
override val color = "red"
}
Drawable
インターフェースに多数のメンバー関数がある場合、RedCircle
クラスのボイラープレートコードの量が非常に大きくなる可能性があることがわかります。しかし、代替手段があります。
Kotlinでは、デリゲーションを使用して、インターフェースの実装をクラスのインスタンスに委譲できます。例えば、Circle
クラスのインスタンスを作成し、Circle
クラスのメンバー関数の実装をこのインスタンスに委譲できます。これを行うには、by
キーワードを使用します。例えば、
class RedCircle(param: Circle) : Drawable by param
ここで、param
は、メンバー関数の実装が委譲される Circle
クラスのインスタンスの名前です。
これで、RedCircle
クラスにメンバー関数の実装を追加する必要がなくなりました。コンパイラが Circle
クラスから自動的にこれを行います。これにより、多くのボイラープレートコードを書く手間が省けます。その代わりに、子クラスで変更したい振る舞いに対してのみコードを追加します。
例えば、color
プロパティの値を変更したい場合:
class RedCircle(param : Circle) : Drawable by param {
// No boilerplate code!
override val color = "red"
}
必要であれば、RedCircle
クラスで継承されたメンバー関数の振る舞いをオーバーライドすることもできますが、すべての継承されたメンバー関数に対して新しいコード行を追加する必要はなくなります。
詳細については、デリゲーションを参照してください。
練習問題
演習1
スマートホームシステムを開発していると想像してください。スマートホームには通常、いくつかの基本的な機能と固有の振る舞いを両方持つ異なる種類のデバイスがあります。以下のコードサンプルで、子クラス SmartLight
が正常にコンパイルされるように、SmartDevice
という名前の abstract
クラスを完成させてください。
次に、SmartDevice
クラスを継承し、どのサーモスタットが加熱中またはオフになったかを説明する出力ステートメントを返す turnOn()
および turnOff()
関数を実装する SmartThermostat
という名前の別の子クラスを作成します。最後に、温度の測定値を入力として受け取り、$name thermostat set to $temperature°C.
と出力する adjustTemperature()
という名前の別の関数を追加します。
ヒント
SmartDevice
クラスに、turnOn()
と turnOff()
関数を追加し、後で SmartThermostat
クラスでそれらの振る舞いをオーバーライドできるようにします。
|--|--|
abstract class // Write your code here
class SmartLight(name: String) : SmartDevice(name) {
override fun turnOn() {
println("$name is now ON.")
}
override fun turnOff() {
println("$name is now OFF.")
}
fun adjustBrightness(level: Int) {
println("Adjusting $name brightness to $level%.")
}
}
class SmartThermostat // Write your code here
fun main() {
val livingRoomLight = SmartLight("Living Room Light")
val bedroomThermostat = SmartThermostat("Bedroom Thermostat")
livingRoomLight.turnOn()
// Living Room Light is now ON.
livingRoomLight.adjustBrightness(10)
// Adjusting Living Room Light brightness to 10%.
livingRoomLight.turnOff()
// Living Room Light is now OFF.
bedroomThermostat.turnOn()
// Bedroom Thermostat thermostat is now heating.
bedroomThermostat.adjustTemperature(5)
// Bedroom Thermostat thermostat set to 5°C.
bedroomThermostat.turnOff()
// Bedroom Thermostat thermostat is now off.
}
|---|---|
abstract class SmartDevice(val name: String) {
abstract fun turnOn()
abstract fun turnOff()
}
class SmartLight(name: String) : SmartDevice(name) {
override fun turnOn() {
println("$name is now ON.")
}
override fun turnOff() {
println("$name is now OFF.")
}
fun adjustBrightness(level: Int) {
println("Adjusting $name brightness to $level%.")
}
}
class SmartThermostat(name: String) : SmartDevice(name) {
override fun turnOn() {
println("$name thermostat is now heating.")
}
override fun turnOff() {
println("$name thermostat is now off.")
}
fun adjustTemperature(temperature: Int) {
println("$name thermostat set to $temperature°C.")
}
}
fun main() {
val livingRoomLight = SmartLight("Living Room Light")
val bedroomThermostat = SmartThermostat("Bedroom Thermostat")
livingRoomLight.turnOn()
// Living Room Light is now ON.
livingRoomLight.adjustBrightness(10)
// Adjusting Living Room Light brightness to 10%.
livingRoomLight.turnOff()
// Living Room Light is now OFF.
bedroomThermostat.turnOn()
// Bedroom Thermostat thermostat is now heating.
bedroomThermostat.adjustTemperature(5)
// Bedroom Thermostat thermostat set to 5°C.
bedroomThermostat.turnOff()
// Bedroom Thermostat thermostat is now off.
}
演習2
Audio
、Video
、Podcast
などの特定のメディアクラスを実装するために使用できる Media
という名前のインターフェースを作成します。インターフェースには以下を含める必要があります。
- メディアのタイトルを表す
title
という名前のプロパティ。 - メディアを再生する
play()
という名前の関数。
次に、Media
インターフェースを実装する Audio
という名前のクラスを作成します。Audio
クラスは、コンストラクタで title
プロパティを使用し、さらに String
型の composer
という名前の追加プロパティを持つ必要があります。クラス内で、play()
関数を実装して次の内容を出力するようにします: "Playing audio: $title, composed by $composer"
。
ヒント
クラスヘッダーで override
キーワードを使用すると、コンストラクタでインターフェースのプロパティを実装できます。
|---|---|
interface // Write your code here
class // Write your code here
fun main() {
val audio = Audio("Symphony No. 5", "Beethoven")
audio.play()
// Playing audio: Symphony No. 5, composed by Beethoven
}
|---|---|
interface Media {
val title: String
fun play()
}
class Audio(override val title: String, val composer: String) : Media {
override fun play() {
println("Playing audio: $title, composed by $composer")
}
}
fun main() {
val audio = Audio("Symphony No. 5", "Beethoven")
audio.play()
// Playing audio: Symphony No. 5, composed by Beethoven
}
演習3
eコマースアプリケーション用の決済処理システムを構築しています。各決済方法は、支払いを承認し、取引を処理できる必要があります。一部の支払いでは払い戻しを処理できる必要もあります。
Refundable
インターフェースに、払い戻しを処理するためのrefund()
という名前の関数を追加します。PaymentMethod
抽象クラスに以下を追加します。amount
を受け取り、その量を含むメッセージを出力するauthorize()
という名前の関数を追加します。- 同様に
amount
を受け取るprocessPayment()
という名前の抽象関数を追加します。
Refundable
インターフェースとPaymentMethod
抽象クラスを実装するCreditCard
という名前のクラスを作成します。このクラスで、refund()
関数とprocessPayment()
関数の実装を追加し、次のステートメントを出力するようにします。"Refunding $amount to the credit card."
"Processing credit card payment of $amount."
|---|---|
interface Refundable {
// Write your code here
}
abstract class PaymentMethod(val name: String) {
// Write your code here
}
class CreditCard // Write your code here
fun main() {
val visa = CreditCard("Visa")
visa.authorize(100.0)
// Authorizing payment of $100.0.
visa.processPayment(100.0)
// Processing credit card payment of $100.0.
visa.refund(50.0)
// Refunding $50.0 to the credit card.
}
|---|---|
interface Refundable {
fun refund(amount: Double)
}
abstract class PaymentMethod(val name: String) {
fun authorize(amount: Double) {
println("Authorizing payment of $amount.")
}
abstract fun processPayment(amount: Double)
}
class CreditCard(name: String) : PaymentMethod(name), Refundable {
override fun processPayment(amount: Double) {
println("Processing credit card payment of $amount.")
}
override fun refund(amount: Double) {
println("Refunding $amount to the credit card.")
}
}
fun main() {
val visa = CreditCard("Visa")
visa.authorize(100.0)
// Authorizing payment of $100.0.
visa.processPayment(100.0)
// Processing credit card payment of $100.0.
visa.refund(50.0)
// Refunding $50.0 to the credit card.
}
演習4
基本的な機能を持つシンプルなメッセージングアプリがありますが、コードを大幅に重複させることなく スマート メッセージの機能を追加したいと考えています。
以下のコードで、BasicMessenger
クラスを継承しつつ、その実装を BasicMessenger
クラスのインスタンスに委譲する SmartMessenger
という名前のクラスを定義してください。
SmartMessenger
クラスで、sendMessage()
関数をオーバーライドしてスマートメッセージを送信するようにします。この関数は message
を入力として受け取り、"Sending a smart message: $message"
という出力ステートメントを返す必要があります。さらに、BasicMessenger
クラスの sendMessage()
関数を呼び出し、メッセージに [smart]
をプレフィックスとして付加してください。
NOTE
SmartMessenger
クラスで receiveMessage()
関数を書き直す必要はありません。
|--|--|
interface Messenger {
fun sendMessage(message: String)
fun receiveMessage(): String
}
class BasicMessenger : Messenger {
override fun sendMessage(message: String) {
println("Sending message: $message")
}
override fun receiveMessage(): String {
return "You've got a new message!"
}
}
class SmartMessenger // Write your code here
fun main() {
val basicMessenger = BasicMessenger()
val smartMessenger = SmartMessenger(basicMessenger)
basicMessenger.sendMessage("Hello!")
// Sending message: Hello!
println(smartMessenger.receiveMessage())
// You've got a new message!
smartMessenger.sendMessage("Hello from SmartMessenger!")
// Sending a smart message: Hello from SmartMessenger!
// Sending message: [smart] Hello from SmartMessenger!
}
|---|---|
interface Messenger {
fun sendMessage(message: String)
fun receiveMessage(): String
}
class BasicMessenger : Messenger {
override fun sendMessage(message: String) {
println("Sending message: $message")
}
override fun receiveMessage(): String {
return "You've got a new message!"
}
}
class SmartMessenger(val basicMessenger: BasicMessenger) : Messenger by basicMessenger {
override fun sendMessage(message: String) {
println("Sending a smart message: $message")
basicMessenger.sendMessage("[smart] $message")
}
}
fun main() {
val basicMessenger = BasicMessenger()
val smartMessenger = SmartMessenger(basicMessenger)
basicMessenger.sendMessage("Hello!")
// Sending message: Hello!
println(smartMessenger.receiveMessage())
// You've got a new message!
smartMessenger.sendMessage("Hello from SmartMessenger!")
// Sending a smart message: Hello from SmartMessenger!
// Sending message: [smart] Hello from SmartMessenger!
}