Skip to content

プロパティ

Kotlin では、プロパティを使用することで、データへのアクセスや変更のための関数を記述することなく、データを保存および管理できます。 プロパティは、クラスインターフェースオブジェクトコンパニオンオブジェクト内で使用できるほか、これらの構造の外でトップレベルプロパティとしても使用できます。

すべてのプロパティには、名前、型、そしてゲッター(getter)と呼ばれる自動生成された get() 関数があります。ゲッターを使用することで、プロパティの値を読み取ることができます。プロパティがミュータブル(可変)である場合は、セッター(setter)と呼ばれる set() 関数も持ち、これを使用してプロパティの値を変更できます。

ゲッターとセッターは アクセサ(accessors) と呼ばれます。

プロパティの宣言

プロパティは、ミュータブル(var)または読み取り専用(val)にできます。 これらは .kt ファイル内のトップレベルプロパティとして宣言できます。トップレベルプロパティは、特定のパッケージに属するグローバル変数のようなものだと考えてください。

kotlin
// ファイル: Constants.kt
package my.app

val pi = 3.14159
var counter = 0

また、クラス、インターフェース、またはオブジェクトの内部でプロパティを宣言することもできます。

kotlin
// プロパティを持つクラス
class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
}

// プロパティを持つインターフェース
interface ContactInfo {
    val email: String
}

// プロパティを持つオブジェクト
object Company {
    var name: String = "Detective Inc."
    val country: String = "UK"
}

// インターフェースを実装するクラス
class PersonContact : ContactInfo {
    override val email: String = "[email protected]"
}

プロパティを使用するには、その名前で参照します。

kotlin
class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
}

interface ContactInfo {
    val email: String
}

object Company {
    var name: String = "Detective Inc."
    val country: String = "UK"
}

class PersonContact : ContactInfo {
    override val email: String = "[email protected]"
}

fun copyAddress(address: Address): Address {
    val result = Address()
    // result インスタンスのプロパティにアクセスする
    result.name = address.name
    result.street = address.street
    result.city = address.city
    return result
}

fun main() {
    val sherlockAddress = Address()
    val copy = copyAddress(sherlockAddress)
    // copy インスタンスのプロパティにアクセスする
    println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
    // Copied address: Holmes, Sherlock, Baker, London

    // Company オブジェクトのプロパティにアクセスする
    println("Company: ${Company.name} in ${Company.country}")
    // Company: Detective Inc. in UK
    
    val contact = PersonContact()
    // contact インスタンスのプロパティにアクセスする
    println("Email: ${contact.email}")
    // Email: [email protected]
}

Kotlin では、コードの安全性と読みやすさを保つために、宣言時にプロパティを初期化することをお勧めします。ただし、特別な場合には後で初期化することもできます。

コンパイラが初期化子(initializer)またはゲッターの戻り値の型から型を推論できる場合、プロパティ型の宣言は省略可能です。

kotlin
var initialized = 1 // 推論される型は Int
var allByDefault    // エラー: プロパティは初期化される必要があります。

カスタムゲッターとカスタムセッター

デフォルトでは、Kotlin は自動的にゲッターとセッターを生成します。バリデーション、フォーマット、または他のプロパティに基づいた計算など、追加のロジックが必要な場合は、独自のカスタムアクセサを定義できます。

カスタムゲッターは、プロパティがアクセスされるたびに実行されます。

kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = this.width * this.height
}
fun main() {
    val rectangle = Rectangle(3, 4)
    println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}

コンパイラがゲッターから型を推論できる場合は、型を省略できます。

kotlin
val area get() = this.width * this.height

カスタムセッターは、初期化時を除き、プロパティに値が代入されるたびに実行されます。 慣習としてセッターのパラメータ名は value ですが、別の名前を選択することもできます。

kotlin
class Point(var x: Int, var y: Int) {
    var coordinates: String
        get() = "$x,$y"
        set(value) {
            val parts = value.split(",")
            x = parts[0].toInt()
            y = parts[1].toInt()
        }
}

fun main() {
    val location = Point(1, 2)
    println(location.coordinates) 
    // 1,2

    location.coordinates = "10,20"
    println("${location.x}, ${location.y}") 
    // 10, 20
}

可視性の変更やアノテーションの追加

Kotlin では、デフォルトの実装を置き換えることなく、アクセサの可視性を変更したりアノテーションを追加したりできます。これらの変更のためにボディ {} を記述する必要はありません。

アクセサの可視性を変更するには、get または set キーワードの前に修飾子を使用します。

kotlin
class BankAccount(initialBalance: Int) {
    var balance: Int = initialBalance
        // クラス内からのみ残高を変更できる
        private set 

    fun deposit(amount: Int) {
        if (amount > 0) balance += amount
    }

    fun withdraw(amount: Int) {
        if (amount > 0 && amount <= balance) balance -= amount
    }
}

fun main() {
    val account = BankAccount(100)
    println("Initial balance: ${account.balance}") 
    // 100

    account.deposit(50)
    println("After deposit: ${account.balance}") 
    // 150

    account.withdraw(70)
    println("After withdrawal: ${account.balance}") 
    // 80

    // account.balance = 1000  
    // エラー: セッターが private であるため代入できません
}

アクセサにアノテーションを付けるには、get または set キーワードの前にアノテーションを使用します。

kotlin
// ゲッターに適用可能なアノテーションを定義
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject

class Service {
    var dependency: String = "Default Service"
        // ゲッターにアノテーションを付ける
        @Inject get 
}

fun main() {
    val service = Service()
    println(service.dependency)
    // Default service
    println(service::dependency.getter.annotations)
    // [@Inject()]
    println(service::dependency.setter.annotations)
    // []
}

この例では、リフレクションを使用して、ゲッターとセッターにどのアノテーションが存在するかを示しています。

バッキングフィールド

Kotlin では、アクセサはメモリ内にプロパティの値を格納するために「バッキングフィールド(backing fields)」を使用します。バッキングフィールドは、ゲッターやセッターに追加のロジックを加えたい場合や、プロパティが変更されるたびに追加のアクションをトリガーしたい場合に役立ちます。

バッキングフィールドを直接宣言することはできません。Kotlin は必要な場合にのみこれらを生成します。アクセサ内では field キーワードを使用して、バッキングフィールドを参照できます。

Kotlin は、デフォルトのゲッターまたはセッターを使用している場合、あるいは少なくとも 1 つのカスタムアクセサで field を使用している場合にのみ、バッキングフィールドを生成します。

例えば、以下の isEmpty プロパティは、field キーワードを使用しないカスタムゲッターを使用しているため、バッキングフィールドを持ちません。

kotlin
val isEmpty: Boolean
    get() = this.size == 0

この例では、セッターで field キーワードを使用しているため、score プロパティはバッキングフィールドを持ちます。

kotlin
class Scoreboard {
    var score: Int = 0
        set(value) {
            field = value
            // 値の更新時にログを追加
            println("Score updated to $field")
        }
}

fun main() {
    val board = Scoreboard()
    board.score = 10  
    // Score updated to 10
    board.score = 20  
    // Score updated to 20
}

バッキングプロパティ

バッキングフィールドを使用する以上の柔軟性が必要な場合があります。例えば、プロパティを内部的には変更可能にし、外部からは変更不可にしたい API がある場合です。このような場合、バッキングプロパティ(backing property) と呼ばれるコーディングパターンを使用できます。

以下の例では、ShoppingCart クラスにショッピングカート内のすべての項目を表す items プロパティがあります。items プロパティをクラス外部からは読み取り専用にしつつ、ユーザーが直接 items プロパティを操作できる「承認された」唯一の方法を許可したいとします。これを実現するために、_items というプライベートなバッキングプロパティを定義し、そのバッキングプロパティの値に委譲する公開プロパティ items を定義できます。

kotlin
class ShoppingCart {
    // バッキングプロパティ
    private val _items = mutableListOf<String>()

    // 公開された読み取り専用ビュー
    val items: List<String>
        get() = _items

    fun addItem(item: String) {
        _items.add(item)
    }

    fun removeItem(item: String) {
        _items.remove(item)
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.addItem("Apple")
    cart.addItem("Banana")

    println(cart.items) 
    // [Apple, Banana]
    
    cart.removeItem("Apple")
    println(cart.items) 
    // [Banana]
}

この例では、ユーザーは addItem() 関数を通じてのみカートに項目を追加できますが、items プロパティにアクセスして中身を確認することはできます。

バッキングプロパティを命名する際は、Kotlin の コーディング規約 に従い、先頭にアンダースコアを使用してください。

JVM 上では、コンパイラがデフォルトのアクセサを持つプライベートプロパティへのアクセスを最適化し、関数呼び出しのオーバーヘッドを回避します。

バッキングプロパティは、複数の公開プロパティで状態を共有したい場合にも役立ちます。例えば:

kotlin
class Temperature {
    // 摂氏での温度を格納するバッキングプロパティ
    private var _celsius: Double = 0.0

    var celsius: Double
        get() = _celsius
        set(value) { _celsius = value }

    var fahrenheit: Double
        get() = _celsius * 9 / 5 + 32
        set(value) { _celsius = (value - 32) * 5 / 9 }
}

fun main() {
    val temp = Temperature()
    temp.celsius = 25.0
    println("${temp.celsius}°C = ${temp.fahrenheit}°F") 
    // 25.0°C = 77.0°F

    temp.fahrenheit = 212.0
    println("${temp.celsius}°C = ${temp.fahrenheit}°F") 
    // 100.0°C = 212.0°F
}

この例では、_celsius バッキングプロパティが celsiusfahrenheit の両方のプロパティからアクセスされています。この構成により、2 つの公開ビューを持ちながら、信頼できる唯一の情報源(Single Source of Truth)が提供されます。

コンパイル時定数

読み取り専用プロパティの値がコンパイル時に判明している場合は、const 修飾子を使用して コンパイル時定数(compile-time constant) としてマークします。コンパイル時定数はコンパイル時にインライン化されるため、各参照はその実際の値に置き換えられます。ゲッターが呼び出されないため、より効率的にアクセスされます。

kotlin
// ファイル: AppConfig.kt
package com.example

// コンパイル時定数
const val MAX_LOGIN_ATTEMPTS = 3

コンパイル時定数は、以下の要件を満たす必要があります。

コンパイル時定数は依然としてバッキングフィールドを持つため、リフレクションを使用して対話することができます。

これらのプロパティはアノテーションでも使用できます。

kotlin
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }

遅延初期化プロパティと変数

通常、プロパティはコンストラクタで初期化する必要があります。 しかし、これが常に都合が良いわけではありません。例えば、依存関係注入(Dependency Injection)を介してプロパティを初期化したり、ユニットテストのセットアップメソッド内で初期化したりする場合などです。

このような状況を処理するには、プロパティを lateinit 修飾子でマークします。

kotlin
public class OrderServiceTest {
    lateinit var orderService: OrderService

    @SetUp fun setup() {
        orderService = OrderService()
    }

    @Test fun processesOrderSuccessfully() {
        // null や初期化のチェックをせずに直接 orderService を呼び出す
        orderService.processOrder()  
    }
}

lateinit 修飾子は、以下のように宣言された var プロパティで使用できます。

  • トップレベルプロパティ。
  • ローカル変数。
  • クラスのボディ内のプロパティ。

クラスプロパティの場合:

  • プライマリコンストラクタで宣言することはできません。
  • カスタムゲッターまたはセッターを持ってはいけません。

いずれの場合も、プロパティまたは変数は非 null 型である必要があり、プリミティブ型であってはいけません。

初期化前に lateinit プロパティにアクセスすると、Kotlin はアクセスされた未初期化のプロパティを特定する特定の例外をスローします。

kotlin
class ReportGenerator {
    lateinit var report: String

    fun printReport() {
        // 初期化前にアクセスされるため例外をスローする
        println(report)
    }
}

fun main() {
    val generator = ReportGenerator()
    generator.printReport()
    // Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}

lateinit var が既に初期化されているかどうかを確認するには、そのプロパティへの参照に対して isInitialized プロパティを使用します。

kotlin
class WeatherStation {
    lateinit var latestReading: String

    fun printReading() {
        // プロパティが初期化されているか確認
        if (this::latestReading.isInitialized) {
            println("Latest reading: $latestReading")
        } else {
            println("No reading available")
        }
    }
}

fun main() {
    val station = WeatherStation()

    station.printReading()
    // No reading available
    station.latestReading = "22°C, sunny"
    station.printReading()
    // Latest reading: 22°C, sunny
}

isInitialized を使用できるのは、そのプロパティにコード内で既にアクセス可能な場合に限られます。プロパティは同じクラス内、外部クラス内、または同じファイル内のトップレベルプロパティとして宣言されている必要があります。

プロパティのオーバーライド

プロパティのオーバーライド を参照してください。

委譲プロパティ

ロジックを再利用し、コードの重複を減らすために、プロパティの取得と設定の責任を別のオブジェクトに委譲することができます。

アクセサの動作を委譲することで、プロパティのアクセサロジックが一箇所に集約され、再利用しやすくなります。このアプローチは、以下のような動作を実装する場合に役立ちます。

  • 値を遅延(lazy)計算する。
  • 指定されたキーでマップから読み取る。
  • データベースにアクセスする。
  • プロパティがアクセスされたときにリスナーに通知する。

これらの一般的な動作は、ライブラリで自分で実装することも、外部ライブラリが提供する既存のデリゲートを使用することもできます。 詳細については、委譲プロパティを参照してください。