プロパティ
Kotlin では、プロパティを使用することで、データへのアクセスや変更のための関数を記述することなく、データを保存および管理できます。 プロパティは、クラス、インターフェース、オブジェクト、コンパニオンオブジェクト内で使用できるほか、これらの構造の外でトップレベルプロパティとしても使用できます。
すべてのプロパティには、名前、型、そしてゲッター(getter)と呼ばれる自動生成された get() 関数があります。ゲッターを使用することで、プロパティの値を読み取ることができます。プロパティがミュータブル(可変)である場合は、セッター(setter)と呼ばれる set() 関数も持ち、これを使用してプロパティの値を変更できます。
ゲッターとセッターは アクセサ(accessors) と呼ばれます。
プロパティの宣言
プロパティは、ミュータブル(var)または読み取り専用(val)にできます。 これらは .kt ファイル内のトップレベルプロパティとして宣言できます。トップレベルプロパティは、特定のパッケージに属するグローバル変数のようなものだと考えてください。
// ファイル: Constants.kt
package my.app
val pi = 3.14159
var counter = 0また、クラス、インターフェース、またはオブジェクトの内部でプロパティを宣言することもできます。
// プロパティを持つクラス
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]"
}プロパティを使用するには、その名前で参照します。
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)またはゲッターの戻り値の型から型を推論できる場合、プロパティ型の宣言は省略可能です。
var initialized = 1 // 推論される型は Int
var allByDefault // エラー: プロパティは初期化される必要があります。カスタムゲッターとカスタムセッター
デフォルトでは、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}")
}コンパイラがゲッターから型を推論できる場合は、型を省略できます。
val area get() = this.width * this.heightカスタムセッターは、初期化時を除き、プロパティに値が代入されるたびに実行されます。 慣習としてセッターのパラメータ名は value ですが、別の名前を選択することもできます。
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 キーワードの前に修飾子を使用します。
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 キーワードの前にアノテーションを使用します。
// ゲッターに適用可能なアノテーションを定義
@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 キーワードを使用しないカスタムゲッターを使用しているため、バッキングフィールドを持ちません。
val isEmpty: Boolean
get() = this.size == 0この例では、セッターで field キーワードを使用しているため、score プロパティはバッキングフィールドを持ちます。
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 を定義できます。
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 上では、コンパイラがデフォルトのアクセサを持つプライベートプロパティへのアクセスを最適化し、関数呼び出しのオーバーヘッドを回避します。
バッキングプロパティは、複数の公開プロパティで状態を共有したい場合にも役立ちます。例えば:
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 バッキングプロパティが celsius と fahrenheit の両方のプロパティからアクセスされています。この構成により、2 つの公開ビューを持ちながら、信頼できる唯一の情報源(Single Source of Truth)が提供されます。
コンパイル時定数
読み取り専用プロパティの値がコンパイル時に判明している場合は、const 修飾子を使用して コンパイル時定数(compile-time constant) としてマークします。コンパイル時定数はコンパイル時にインライン化されるため、各参照はその実際の値に置き換えられます。ゲッターが呼び出されないため、より効率的にアクセスされます。
// ファイル: AppConfig.kt
package com.example
// コンパイル時定数
const val MAX_LOGIN_ATTEMPTS = 3コンパイル時定数は、以下の要件を満たす必要があります。
- トップレベルプロパティであるか、
object宣言 または コンパニオンオブジェクト のメンバーであること。 String型または プリミティブ型 の値で初期化されていること。- カスタムゲッターを持たないこと。
コンパイル時定数は依然としてバッキングフィールドを持つため、リフレクションを使用して対話することができます。
これらのプロパティはアノテーションでも使用できます。
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }遅延初期化プロパティと変数
通常、プロパティはコンストラクタで初期化する必要があります。 しかし、これが常に都合が良いわけではありません。例えば、依存関係注入(Dependency Injection)を介してプロパティを初期化したり、ユニットテストのセットアップメソッド内で初期化したりする場合などです。
このような状況を処理するには、プロパティを lateinit 修飾子でマークします。
public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// null や初期化のチェックをせずに直接 orderService を呼び出す
orderService.processOrder()
}
}lateinit 修飾子は、以下のように宣言された var プロパティで使用できます。
- トップレベルプロパティ。
- ローカル変数。
- クラスのボディ内のプロパティ。
クラスプロパティの場合:
- プライマリコンストラクタで宣言することはできません。
- カスタムゲッターまたはセッターを持ってはいけません。
いずれの場合も、プロパティまたは変数は非 null 型である必要があり、プリミティブ型であってはいけません。
初期化前に lateinit プロパティにアクセスすると、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 プロパティを使用します。
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)計算する。
- 指定されたキーでマップから読み取る。
- データベースにアクセスする。
- プロパティがアクセスされたときにリスナーに通知する。
これらの一般的な動作は、ライブラリで自分で実装することも、外部ライブラリが提供する既存のデリゲートを使用することもできます。 詳細については、委譲プロパティを参照してください。
