Skip to content

中級: オープンクラスと特殊なクラス

この章では、オープンクラス、それがインターフェースとどのように連携するか、そしてKotlinで利用できるその他の特殊なクラスについて学びます。

オープンクラス

インターフェースや抽象クラスを使用できない場合、クラスをオープンとして宣言することで、明示的に継承可能にできます。これを行うには、クラス宣言の前にopenキーワードを使用します。

kotlin
open class Vehicle

別のクラスを継承するクラスを作成するには、クラスヘッダーの後にコロンを追加し、継承したい親クラスのコンストラクタを呼び出します。

kotlin
class Car : Vehicle

この例では、CarクラスがVehicleクラスを継承しています。

kotlin
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インスタンスは、親クラスのパラメータであるmakemodelを初期化します。

継承された動作のオーバーライド

クラスを継承しつつ、その一部の動作を変更したい場合、継承された動作をオーバーライドできます。

デフォルトでは、親クラスのメンバ関数やプロパティをオーバーライドすることはできません。抽象クラスと同様に、特別なキーワードを追加する必要があります。

メンバ関数

親クラスの関数をオーバーライド可能にするには、親クラスでの宣言の前にopenキーワードを使用します。

kotlin
open fun displayInfo() {}

継承されたメンバ関数をオーバーライドするには、子クラスの関数宣言の前にoverrideキーワードを使用します。

kotlin
override fun displayInfo() {}

例:

kotlin
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つのインスタンスcar1car2を作成します。
  • CarクラスのdisplayInfo()関数をオーバーライドして、ドアの数も出力するようにします。
  • car1car2インスタンスでオーバーライドされたdisplayInfo()関数を呼び出します。

プロパティ

Kotlinでは、openキーワードを使用してプロパティを継承可能にし、後でオーバーライドすることは一般的なプラクティスではありません。ほとんどの場合、プロパティがデフォルトで継承可能である抽象クラスやインターフェースを使用します。

オープンクラス内のプロパティは、その子クラスからアクセス可能です。一般的に、新しいプロパティでそれらをオーバーライドするよりも、直接アクセスする方が良いです。

たとえば、後でオーバーライドしたいtransmissionTypeというプロパティがあるとします。プロパティをオーバーライドする構文は、メンバ関数をオーバーライドする場合とまったく同じです。次のようにできます。

kotlin
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子クラスを作成するときにその値を宣言できます。

kotlin
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")

プロパティをオーバーライドするのではなく直接アクセスすることで、よりシンプルで読みやすいコードになります。プロパティを親クラスで一度宣言し、コンストラクタを通じてその値を渡すことで、子クラスでの不要なオーバーライドの必要がなくなります。

クラスの継承とクラスの動作のオーバーライドに関する詳細は、継承を参照してください。

オープンクラスとインターフェース

クラスを継承し、かつ複数のインターフェースを実装するクラスを作成できます。この場合、コロンの後で、インターフェースを列挙する前に、まず親クラスを宣言する必要があります。

kotlin
// 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キーワードを使用します。

kotlin
sealed class Mammal

sealedクラスは、when式と組み合わせると特に便利です。when式を使用すると、考えられるすべての子クラスの動作を定義できます。例:

kotlin
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キーワードを使用します。

kotlin

enum class State

プロセスの異なる状態を含むenumクラスを作成したいとします。各enum定数はコンマ,で区切る必要があります。

kotlin

enum class State {

    IDLE, RUNNING, FINISHED

}

State enumクラスには、IDLERUNNINGFINISHEDというenum定数があります。enum定数にアクセスするには、クラス名の後に.とenum定数の名前を使用します。

kotlin

val state = State.RUNNING

このenumクラスをwhen式と組み合わせて使用​​すると、enum定数の値に応じて実行するアクションを定義できます。

kotlin

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定数を作成する際には、このプロパティで初期化する必要があります。

kotlin

enum class Color(val rgb: Int) {

    RED(0xFF0000),

    GREEN(0x00FF00),

    BLUE(0x0000FF),

    YELLOW(0xFFFF00)

}

Kotlinは16進数を整数として保存するため、rgbプロパティはInt型であり、String型ではありません。

このクラスにメンバ関数を追加するには、enum定数とセミコロン;で区切ります。

kotlin
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アノテーションを使用します。

kotlin
@JvmInline
value class Email

TIP

@JvmInlineアノテーションは、コンパイル時にKotlinにコードを最適化するよう指示します。詳細については、アノテーションを参照してください。

インライン値クラスは、クラスヘッダーで初期化された単一のプロパティを必ず持たなければなりません。

メールアドレスを収集するクラスを作成したいとします。

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クラスを作成し、PendingInTransitDeliveredCanceledの各ステータスを表すデータクラスを含めます。main()関数のコードが正常に実行されるように、DeliveryStatusクラスの宣言を完成させてください。

|---|---|

kotlin
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.
}

|---|---|

kotlin
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クラスがあります。以下のコードを、異なる問題の種類であるNETWORKTIMEOUTUNKNOWNを表すProblemというenumクラスを作成して完成させてください。

|---|---|

kotlin
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]
}

|---|---|

kotlin
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]
}

次のステップ

中級: プロパティ