中級: オープンクラスと特殊なクラス
拡張関数
スコープ関数
レシーバ付きラムダ式
クラスとインターフェース
オブジェクト
オープンクラスと特殊なクラス
プロパティ
Null安全性
ライブラリとAPI
この章では、オープンクラス、それがインターフェースとどのように連携するか、そしてKotlinで利用できるその他の特殊なクラスについて学びます。
オープンクラス
インターフェースや抽象クラスを使用できない場合、クラスをオープンとして宣言することで、明示的に継承可能にできます。これを行うには、クラス宣言の前にopen
キーワードを使用します。
open class Vehicle
別のクラスを継承するクラスを作成するには、クラスヘッダーの後にコロンを追加し、継承したい親クラスのコンストラクタを呼び出します。
class Car : Vehicle
この例では、Car
クラスがVehicle
クラスを継承しています。
open class Vehicle(val make: String, val model: String)
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
fun main() {
// Creates an instance of the Car class
val car = Car("Toyota", "Corolla", 4)
// Prints the details of the car
println("Car Info: Make - ${car.make}, Model - ${car.model}, Number of doors - ${car.numberOfDoors}")
// Car Info: Make - Toyota, Model - Corolla, Number of doors - 4
}
通常のクラスインスタンスを作成する場合と同様に、クラスが親クラスを継承する場合、親クラスのヘッダーで宣言されているすべてのパラメータを初期化する必要があります。したがって、この例では、Car
クラスのcar
インスタンスは、親クラスのパラメータであるmake
とmodel
を初期化します。
継承された動作のオーバーライド
クラスを継承しつつ、その一部の動作を変更したい場合、継承された動作をオーバーライドできます。
デフォルトでは、親クラスのメンバ関数やプロパティをオーバーライドすることはできません。抽象クラスと同様に、特別なキーワードを追加する必要があります。
メンバ関数
親クラスの関数をオーバーライド可能にするには、親クラスでの宣言の前にopen
キーワードを使用します。
open fun displayInfo() {}
継承されたメンバ関数をオーバーライドするには、子クラスの関数宣言の前にoverride
キーワードを使用します。
override fun displayInfo() {}
例:
open class Vehicle(val make: String, val model: String) {
open fun displayInfo() {
println("Vehicle Info: Make - $make, Model - $model")
}
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override fun displayInfo() {
println("Car Info: Make - $make, Model - $model, Number of Doors - $numberOfDoors")
}
}
fun main() {
val car1 = Car("Toyota", "Corolla", 4)
val car2 = Car("Honda", "Civic", 2)
// Uses the overridden displayInfo() function
car1.displayInfo()
// Car Info: Make - Toyota, Model - Corolla, Number of Doors - 4
car2.displayInfo()
// Car Info: Make - Honda, Model - Civic, Number of Doors - 2
}
この例では、
Vehicle
クラスを継承するCar
クラスの2つのインスタンスcar1
とcar2
を作成します。Car
クラスのdisplayInfo()
関数をオーバーライドして、ドアの数も出力するようにします。car1
とcar2
インスタンスでオーバーライドされたdisplayInfo()
関数を呼び出します。
プロパティ
Kotlinでは、open
キーワードを使用してプロパティを継承可能にし、後でオーバーライドすることは一般的なプラクティスではありません。ほとんどの場合、プロパティがデフォルトで継承可能である抽象クラスやインターフェースを使用します。
オープンクラス内のプロパティは、その子クラスからアクセス可能です。一般的に、新しいプロパティでそれらをオーバーライドするよりも、直接アクセスする方が良いです。
たとえば、後でオーバーライドしたいtransmissionType
というプロパティがあるとします。プロパティをオーバーライドする構文は、メンバ関数をオーバーライドする場合とまったく同じです。次のようにできます。
open class Vehicle(val make: String, val model: String) {
open val transmissionType: String = "Manual"
}
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model) {
override val transmissionType: String = "Automatic"
}
しかし、これは良いプラクティスではありません。代わりに、継承可能なクラスのコンストラクタにプロパティを追加し、Car
子クラスを作成するときにその値を宣言できます。
open class Vehicle(val make: String, val model: String, val transmissionType: String = "Manual")
class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model, "Automatic")
プロパティをオーバーライドするのではなく直接アクセスすることで、よりシンプルで読みやすいコードになります。プロパティを親クラスで一度宣言し、コンストラクタを通じてその値を渡すことで、子クラスでの不要なオーバーライドの必要がなくなります。
クラスの継承とクラスの動作のオーバーライドに関する詳細は、継承を参照してください。
オープンクラスとインターフェース
クラスを継承し、かつ複数のインターフェースを実装するクラスを作成できます。この場合、コロンの後で、インターフェースを列挙する前に、まず親クラスを宣言する必要があります。
// Define interfaces
interface EcoFriendly {
val emissionLevel: String
}
interface ElectricVehicle {
val batteryCapacity: Double
}
// Parent class
open class Vehicle(val make: String, val model: String)
// Child class
open class Car(make: String, model: String, val numberOfDoors: Int) : Vehicle(make, model)
// New class that inherits from Car and implements two interfaces
class ElectricCar(
make: String,
model: String,
numberOfDoors: Int,
val capacity: Double,
val emission: String
) : Car(make, model, numberOfDoors), EcoFriendly, ElectricVehicle {
override val batteryCapacity: Double = capacity
override val emissionLevel: String = emission
}
特殊なクラス
抽象クラス、オープンクラス、データクラスに加えて、Kotlinには特定の動作を制限したり、小さなオブジェクトを作成する際のパフォーマンスへの影響を軽減したりするなど、様々な目的のために設計された特殊な種類のクラスがあります。
Sealedクラス
継承を制限したい場合があります。これはsealedクラスで実現できます。sealedクラスは抽象クラスの特殊な型です。クラスをsealedとして宣言すると、同じパッケージ内でのみその子クラスを作成できます。このスコープ外からsealedクラスを継承することはできません。
TIP
パッケージは、関連するクラスや関数を含むコードの集まりで、通常はディレクトリ内にあります。Kotlinのパッケージについて詳しくは、パッケージとインポートを参照してください。
sealedクラスを作成するには、sealed
キーワードを使用します。
sealed class Mammal
sealedクラスは、when
式と組み合わせると特に便利です。when
式を使用すると、考えられるすべての子クラスの動作を定義できます。例:
sealed class Mammal(val name: String)
class Cat(val catName: String) : Mammal(catName)
class Human(val humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) {
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}"
is Cat -> return "Hello ${mammal.name}"
}
}
fun main() {
println(greetMammal(Cat("Snowy")))
// Hello Snowy
}
この例では、
- コンストラクタに
name
パラメータを持つMammal
というsealedクラスがあります。 Cat
クラスはMammal
sealedクラスを継承し、Mammal
クラスのname
パラメータを自身のコンストラクタのcatName
パラメータとして使用します。Human
クラスはMammal
sealedクラスを継承し、Mammal
クラスのname
パラメータを自身のコンストラクタのhumanName
パラメータとして使用します。また、自身のコンストラクタにはjob
パラメータがあります。greetMammal()
関数はMammal
型の引数を受け取り、文字列を返します。greetMammal()
関数の本体内には、when
式があり、is
演算子を使用してmammal
の型をチェックし、実行するアクションを決定します。main()
関数は、Cat
クラスのインスタンスとSnowy
というname
パラメータを指定してgreetMammal()
関数を呼び出します。
NOTE
このツアーでは、Null安全性の章でis
演算子について詳しく説明します。
sealedクラスとその推奨されるユースケースに関する詳細は、Sealedクラスとインターフェースを参照してください。
Enumクラス
Enumクラスは、クラス内で有限の異なる値のセットを表現したい場合に便利です。enumクラスにはenum定数が含まれており、それ自体がenumクラスのインスタンスです。
enumクラスを作成するには、enum
キーワードを使用します。
enum class State
プロセスの異なる状態を含むenumクラスを作成したいとします。各enum定数はコンマ,
で区切る必要があります。
enum class State {
IDLE, RUNNING, FINISHED
}
State
enumクラスには、IDLE
、RUNNING
、FINISHED
というenum定数があります。enum定数にアクセスするには、クラス名の後に.
とenum定数の名前を使用します。
val state = State.RUNNING
このenumクラスをwhen
式と組み合わせて使用すると、enum定数の値に応じて実行するアクションを定義できます。
enum class State {
IDLE, RUNNING, FINISHED
}
fun main() {
val state = State.RUNNING
val message = when (state) {
State.IDLE -> "It's idle"
State.RUNNING -> "It's running"
State.FINISHED -> "It's finished"
}
println(message)
// It's running
}
Enumクラスは、通常のクラスと同様にプロパティやメンバ関数を持つことができます。
たとえば、HTMLを扱っていて、いくつかの色を含むenumクラスを作成したいとします。各色に、そのRGB値を16進数で含むrgb
というプロパティを持たせたいとします。enum定数を作成する際には、このプロパティで初期化する必要があります。
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00)
}
Kotlinは16進数を整数として保存するため、rgb
プロパティはInt
型であり、String
型ではありません。
このクラスにメンバ関数を追加するには、enum定数とセミコロン;
で区切ります。
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (this.rgb and 0xFF0000 != 0)
}
fun main() {
val red = Color.RED
// Calls containsRed() function on enum constant
println(red.containsRed())
// true
// Calls containsRed() function on enum constants via class names
println(Color.BLUE.containsRed())
// false
println(Color.YELLOW.containsRed())
// true
}
この例では、containsRed()
メンバ関数はthis
キーワードを使用してenum定数のrgb
プロパティの値にアクセスし、16進数値の最初のビットにFF
が含まれているかどうかをチェックしてブール値を返します。
詳細は、Enumクラスを参照してください。
インライン値クラス
コードで、クラスから小さなオブジェクトを作成し、短期間だけ使用したい場合があります。このアプローチはパフォーマンスに影響を与える可能性があります。インライン値クラスは、このパフォーマンスへの影響を回避する特殊な型のクラスです。ただし、それらは値のみを含むことができます。
インライン値クラスを作成するには、value
キーワードと@JvmInline
アノテーションを使用します。
@JvmInline
value class Email
TIP
@JvmInline
アノテーションは、コンパイル時にKotlinにコードを最適化するよう指示します。詳細については、アノテーションを参照してください。
インライン値クラスは、クラスヘッダーで初期化された単一のプロパティを必ず持たなければなりません。
メールアドレスを収集するクラスを作成したいとします。
// The address property is initialized in the class header.
@JvmInline
value class Email(val address: String)
fun sendEmail(email: Email) {
println("Sending email to ${email.address}")
}
fun main() {
val myEmail = Email("[email protected]")
sendEmail(myEmail)
// Sending email to [email protected]
}
この例では、
Email
は、クラスヘッダーにaddress
という1つのプロパティを持つインライン値クラスです。sendEmail()
関数はEmail
型のオブジェクトを受け入れ、文字列を標準出力に出力します。main()
関数は以下を実行します。email
というEmail
クラスのインスタンスを作成します。email
オブジェクトでsendEmail()
関数を呼び出します。
インライン値クラスを使用することで、クラスがインライン化され、オブジェクトを作成せずにコードで直接使用できます。これにより、メモリフットプリントを大幅に削減し、コードの実行時パフォーマンスを向上させることができます。
インライン値クラスに関する詳細は、インライン値クラスを参照してください。
練習
演習1
配達サービスを管理しており、荷物のステータスを追跡する方法が必要です。DeliveryStatus
というsealedクラスを作成し、Pending
、InTransit
、Delivered
、Canceled
の各ステータスを表すデータクラスを含めます。main()
関数のコードが正常に実行されるように、DeliveryStatus
クラスの宣言を完成させてください。
|---|---|
sealed class // Write your code here
fun printDeliveryStatus(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Pending -> {
println("The package is pending pickup from ${status.sender}.")
}
is DeliveryStatus.InTransit -> {
println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.")
}
is DeliveryStatus.Delivered -> {
println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.")
}
is DeliveryStatus.Canceled -> {
println("The delivery was canceled due to: ${status.reason}.")
}
}
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
|---|---|
sealed class DeliveryStatus {
data class Pending(val sender: String) : DeliveryStatus()
data class InTransit(val estimatedDeliveryDate: String) : DeliveryStatus()
data class Delivered(val deliveryDate: String, val recipient: String) : DeliveryStatus()
data class Canceled(val reason: String) : DeliveryStatus()
}
fun printDeliveryStatus(status: DeliveryStatus) {
when (status) {
is DeliveryStatus.Pending -> {
println("The package is pending pickup from ${status.sender}.")
}
is DeliveryStatus.InTransit -> {
println("The package is in transit and expected to arrive by ${status.estimatedDeliveryDate}.")
}
is DeliveryStatus.Delivered -> {
println("The package was delivered to ${status.recipient} on ${status.deliveryDate}.")
}
is DeliveryStatus.Canceled -> {
println("The delivery was canceled due to: ${status.reason}.")
}
}
}
fun main() {
val status1: DeliveryStatus = DeliveryStatus.Pending("Alice")
val status2: DeliveryStatus = DeliveryStatus.InTransit("2024-11-20")
val status3: DeliveryStatus = DeliveryStatus.Delivered("2024-11-18", "Bob")
val status4: DeliveryStatus = DeliveryStatus.Canceled("Address not found")
printDeliveryStatus(status1)
// The package is pending pickup from Alice.
printDeliveryStatus(status2)
// The package is in transit and expected to arrive by 2024-11-20.
printDeliveryStatus(status3)
// The package was delivered to Bob on 2024-11-18.
printDeliveryStatus(status4)
// The delivery was canceled due to: Address not found.
}
演習2
プログラムで、さまざまなステータスとエラーの種類を処理できるようにしたいと考えています。データクラスまたはオブジェクトで宣言された異なるステータスをキャプチャするためのsealedクラスがあります。以下のコードを、異なる問題の種類であるNETWORK
、TIMEOUT
、UNKNOWN
を表すProblem
というenumクラスを作成して完成させてください。
|---|---|
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
// Write your code here
}
data class OK(val data: List<String>) : Status()
}
fun handleStatus(status: Status) {
when (status) {
is Status.Loading -> println("Loading...")
is Status.OK -> println("Data received: ${status.data}")
is Status.Error -> when (status.problem) {
Status.Error.Problem.NETWORK -> println("Network issue")
Status.Error.Problem.TIMEOUT -> println("Request timed out")
Status.Error.Problem.UNKNOWN -> println("Unknown error occurred")
}
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}
|---|---|
sealed class Status {
data object Loading : Status()
data class Error(val problem: Problem) : Status() {
enum class Problem {
NETWORK,
TIMEOUT,
UNKNOWN
}
}
data class OK(val data: List<String>) : Status()
}
fun handleStatus(status: Status) {
when (status) {
is Status.Loading -> println("Loading...")
is Status.OK -> println("Data received: ${status.data}")
is Status.Error -> when (status.problem) {
Status.Error.Problem.NETWORK -> println("Network issue")
Status.Error.Problem.TIMEOUT -> println("Request timed out")
Status.Error.Problem.UNKNOWN -> println("Unknown error occurred")
}
}
}
fun main() {
val status1: Status = Status.Error(Status.Error.Problem.NETWORK)
val status2: Status = Status.OK(listOf("Data1", "Data2"))
handleStatus(status1)
// Network issue
handleStatus(status2)
// Data received: [Data1, Data2]
}