Kotlin: Kotlinのクラスとオブジェクト
最終更新:2026-08-26
Kotlinでは、プライマリコンストラクタとプロパティを1行にまとめ、Javaのコンストラクタ本体をinitブロックで置き換えられます。Charlieは、Javaなら30行になるOrderクラスを、より高機能な3行のコードで定義しています。
1. 学習内容
- プライマリコンストラクタとプロパティを1行にまとめる
initブロック:検証ロジック- 表示:
public/internal/protected/private - 遅延初期化:
lateinitおよびby lazy - Charlieの実践:Order/OrderItem/Customer ドメインクラスの設計
2. ある建築家の実話
(1) 課題:Javaのコンストラクタ地獄
Javaでは、CharlieのOrderクラスには3つのコンストラクタ(引数なし、必須フィールド、全フィールド)に加え、ゲッター・セッター・検証ロジックが必要でした。たった1つのドメインクラスで80行以上ものコードが必要だったのです。
(2) Kotlinにおけるプライマリコンストラクタの解決策
KOTLIN
// Java: 80+ lines for one domain class
// Kotlin: 3 lines with validation
class Order(val id: String, var status: String, val total: BigDecimal) {
init { require(total >= BigDecimal.ZERO) { "Total must be non-negative" } }
}
プライマリコンストラクタ+プロパティ+初期化ブロックを合わせたもの — Java 80行 → Kotlin 3行。
3. プライマリコンストラクタとプロパティ
(1) 1行で記述するプライマリコンストラクタ
KOTLIN
// Primary constructor with properties in one line
class Order(val id: String, var status: String, val total: Double)
// Equivalent Java would be 20+ lines
(2) コンストラクタのパラメータとプロパティの比較
KOTLIN
// val/var in constructor = property
class Order(val id: String, // read-only property
var status: String, // mutable property
total: Double) // just constructor param, not a property
// Usage
val order = Order("ORD-001", "PENDING", 299.99)
println(order.id) // OK - val property
order.status = "PAID" // OK - var property
// order.total // ERROR - not a property
(3) プライマリコンストラクタとセカンダリコンストラクタ
KOTLIN
class Order(val id: String, var status: String, val total: Double) {
// Secondary constructor MUST delegate to primary
constructor(id: String) : this(id, "PENDING", 0.0)
// Another secondary constructor
constructor(id: String, total: Double) : this(id, "PENDING", total)
}
val order1 = Order("ORD-001") // Secondary
val order2 = Order("ORD-002", 299.99) // Secondary
val order3 = Order("ORD-003", "CONFIRMED", 1_500.00) // Primary
(4) コンストラクタの比較
| 次元 | Java | Kotlin |
|---|---|---|
| プロパティとコンストラクタの定義 | 別々 | 1行にまとめたプライマリコンストラクタ |
| 複数のコンストラクタ | 独立した定義 | 二次的なものは一次的なものに委譲しなければならない |
| パラメータからプロパティへ | 手動での割り当て | val/var 自動 |
| デフォルト値 | メソッドのオーバーロード | デフォルトパラメータ |
4. initブロック
init ブロックは、プライマリコンストラクタの直後に実行され、検証および初期化ロジックに使用されます。
(1) 基本的な使い方
KOTLIN
class Order(val id: String, var status: String, val total: Double) {
init {
require(total >= 0.0) { "Total must be non-negative, got $total" }
require(id.startsWith("ORD-")) { "Order ID must start with ORD-" }
}
// Multiple init blocks execute in order
init {
println("Order $id created with total \$$total USD")
}
}
(2) initブロックの実行順序
KOTLIN
class Example {
val a = println("1: property initialization")
init {
println("2: first init block")
}
val b = println("3: property initialization")
init {
println("4: second init block")
}
}
// Output: 1, 2, 3, 4 (declaration order)
5. 可視性修飾子
(1) 4つの可視性レベル
KOTLIN
class OrderProcessor {
// public (default): visible everywhere
fun process(order: Order) { ... }
// private: visible inside this class only
private fun validate(order: Order) { ... }
// protected: visible in this class and subclasses
protected fun calculateTax(order: Order) { ... }
// internal: visible within the same module
internal fun report() { ... }
}
(2) 可視性の比較表
| 修飾子 | クラス内部 | サブクラス | 同一モジュール | グローバル |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
internal |
✅ | ✅ | ✅ | ❌ |
protected |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
(3) JavaとKotlinの可視性の違い
| 次元 | Java | Kotlin |
|---|---|---|
| デフォルトの可視性 | パッケージプライベート | public |
| モジュールの表示設定 | なし | internal |
| パッケージの可視性 | パッケージ-private | なし(代わりに internal を使用してください) |
| 最上位の宣言 | public のみ | public / internal / private |
6. 遅延初期化
(1) lateinit 変数
KOTLIN
class OrderService {
// lateinit: promise to initialize before use
lateinit var repository: OrderRepository
fun init(repo: OrderRepository) {
repository = repo
}
fun process(order: Order) {
// Access before init throws UninitializedPropertyAccessException
repository.save(order)
}
}
(2) 投稿者:lazy
KOTLIN
class OrderProcessor {
// lazy: thread-safe, initialized on first access
val cache: OrderCache by lazy {
println("Initializing cache...")
OrderCache(maxSize = 10_000)
}
// lazy with custom lock mode
val heavyResource by lazy(LazyThreadSafetyMode.PUBLICATION) {
loadHeavyResource()
}
}
(3) lateinit と by lazy の比較
| 寸法 | lateinit |
by lazy |
|---|---|---|
| タイプ | var (変更可能) |
val (読み取り専用) |
| 初期化のタイミング | 手動による割り当て | 最初のアクセス時 |
| スレッドセーフ | いいえ | はい(デフォルト) |
| null許容性 | null不可と宣言 | null不可と宣言 |
| 初期化前のアクセス | 実行時例外 | 発生しない |
| ユースケース | DIフレームワークによる注入 | 計算コストの高い計算プロパティ |
7. クラス関係図
classDiagram
class Order {
+val id: String
+var status: String
+val total: Double
+val items: List~OrderItem~
+fun addItem(item: OrderItem)
}
class OrderItem {
+val sku: String
+val quantity: Int
+val unitPrice: Double
+fun subtotal: Double
}
class Customer {
+val id: String
+val name: String
+val email: String?
+val address: Address?
}
class Address {
+val street: String
+val city: String
+val country: String
}
Order --> OrderItem : contains
Order --> Customer : belongs to
Customer --> Address : has
8. 完全な例:OrderProcessor ドメインモデル
▶ サンプル:OrderProcessorドメインモデル
KOTLIN
// ============================================
// OrderProcessor - Domain Model
// Feature: Order, OrderItem, Customer with init validation
// ============================================
import java.math.BigDecimal
import java.math.RoundingMode
class Address(val street: String, val city: String, val country: String) {
override fun toString(): String = "$street, $city, $country"
}
class Customer(val id: String, val name: String, val email: String?) {
var address: Address? = null
fun getDisplayEmail(): String = email ?: "no-email"
override fun toString(): String = "Customer($id, $name, ${getDisplayEmail()})"
}
class OrderItem(val sku: String, val quantity: Int, val unitPrice: BigDecimal) {
init {
require(quantity > 0) { "Quantity must be positive, got $quantity" }
require(unitPrice >= BigDecimal.ZERO) { "Price must be non-negative" }
}
val subtotal: BigDecimal
get() = unitPrice.multiply(BigDecimal(quantity)).setScale(2, RoundingMode.HALF_UP)
}
class Order(
val id: String,
var status: String,
private val _items: MutableList<OrderItem> = mutableListOf()
) {
init {
require(id.startsWith("ORD-")) { "Order ID must start with ORD-" }
}
val items: List<OrderItem> get() = _items.toList()
val total: BigDecimal
get() = _items.fold(BigDecimal.ZERO) { acc, item -> acc.add(item.subtotal) }
fun addItem(item: OrderItem) {
_items.add(item)
}
val itemCount: Int get() = _items.size
// Lazy computed tax
val tax by lazy {
total.multiply(BigDecimal("0.08")).setScale(2, RoundingMode.HALF_UP)
}
override fun toString(): String = "Order($id, $status, ${itemCount} items, \$$total USD)"
}
fun main() {
val customer = Customer("CUST-001", "Alice", "alice@example.com").also {
it.address = Address("123 Main St", "New York", "US")
}
println(customer)
println("Address: ${customer.address}")
val order = Order("ORD-001", "PENDING")
order.addItem(OrderItem("SKU-WIDGET", 3, BigDecimal("9.99")))
order.addItem(OrderItem("SKU-GADGET", 1, BigDecimal("149.99")))
order.addItem(OrderItem("SKU-DOOHICKEY", 5, BigDecimal("4.50")))
println(order)
println("Subtotal: \$$${order.total} USD")
println("Tax: \$$${order.tax} USD")
println("Grand Total: \$$${order.total.add(order.tax)} USD")
order.status = "CONFIRMED"
println("Status updated: ${order.status}")
}
出力:
TEXT
📖 参照専用
Customer(CUST-001, Alice, alice@example.com)
Address: 123 Main St, New York, US
Order(ORD-001, PENDING, 3 items, $199.42 USD)
Subtotal: $$199.42 USD
Tax: $$15.95 USD
Grand Total: $$215.37 USD
Status updated: CONFIRMED
❓ よくある質問
Q プライマリコンストラクタとセカンダリコンストラクタは共存できますか?
A はい。ただし、セカンダリコンストラクタは
this(...) を使用してプライマリコンストラクタに委譲する必要があります。セカンダリコンストラクタよりもデフォルトパラメータの使用が推奨されます。Q initブロックを複数設定できますか?
A はい、複数のinitブロックは宣言順に実行されます。混乱を避けるため、1つにまとめることをお勧めします。
Q lateinit はプリミティブ型でも使用できますか?
A いいえ。プリミティブ型にはデフォルト値があるため、lateinit は非プリミティブ型(オブジェクト型)でのみ機能します。Int や Double などについては、
by lazy またはnull許容型とデフォルト値を使用してください。Q 遅延初期化はスレッドセーフですか?
A デフォルトではスレッドセーフです(SYNCHRONIZEDモード)。シングルスレッドでのアクセスが確実な場合は、パフォーマンス向上のために
LazyThreadSafetyMode.NONEを使用してください。Q Maven/Gradleのマルチモジュールプロジェクトにおいて、内部可視性はどのように機能しますか?
A 「internal」は、同じGradleモジュールまたはMavenモジュール内でのみ可視性を制限します。異なるモジュールは、たとえ同じプロジェクト内にあっても、内部メンバーにアクセスすることはできません。
Q なぜKotlinにはJavaの「パッケージプライベート」がないのですか?
A Kotlinでは、パッケージプライベートの代わりに
internal(モジュールレベルの可視性)を採用しています。Javaではパッケージレベルの可視性が誤用されることが多いため、モジュールレベルの方が現代的なプロジェクト構造に適しているからです。📖 まとめ
- プライマリコンストラクタ +
val/varを使用すると、1行でプロパティを定義でき、Javaのゲッター/セッターの定型コードが不要になります initは、検証と初期化のために Java のコンストラクタ本体を置き換える- 4つの可視性レベル:
public(デフォルト)/internal/protected/private lateinitは DI フレームワークへの注入に適しています;by lazyは遅延計算に適しています- プロパティのバッキングフィールドでは、防御的コピーを行うために
private varおよびパブリックな読み取り専用ビューパターンを使用しています - コンストラクタをすっきりとした状態に保つため、セカンダリコンストラクタよりもデフォルトパラメータを優先する
📝 練習問題
- 初心者 (⭐):
Productクラス (id: String, name: String, price: Double) を定義し、price >= 0を検証する初期化ブロックを設定してください。ヒント:require(price >= 0) - 中級 (⭐⭐):
lateinitを使用してRepositoryを注入し、by lazyを使用してキャッシュを初期化するServiceクラスを設計してください。 ヒント:lateinit+by lazy - 課題 (⭐⭐⭐): Order/OrderItem/Customer を含む完全な Order ドメインモデルを実装し、すべてのバリデーションを init ブロック内で行い、アイテムに対してディフェンシブコピーを適用してください。ヒント:第 8 節の完全な例を参照してください。