Kotlin Classes and Objects Explained
Kotlin's class design philosophy: primary constructor + properties in one line, init blocks replacing Java's constructor body — Charlie defines the Order class in 3 lines that are more powerful than Java's 30-line equivalent.
1. What You'll Learn
- Primary constructor + properties in one line
initblocks: validation logic- Visibility:
public/internal/protected/private - Deferred initialization:
lateinitandby lazy - Charlie in action: designing Order / OrderItem / Customer domain classes
2. An Architect's Real Story
(1) Pain Point: Java Constructor Hell
In Java, Charlie's Order class needed 3 constructors (no-arg, required-fields, all-fields), plus getters/setters/validation logic — 80+ lines for a single domain class.
(2) Kotlin's Primary Constructor Solution
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" } }
}
Primary constructor + properties + init block combined — 80 lines of Java → 3 lines of Kotlin.
3. Primary Constructor and Properties
(1) Primary Constructor in One Line
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) Constructor Parameters vs Properties
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) Primary Constructor vs Secondary Constructor
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) Constructor Comparison
| Dimension | Java | Kotlin |
|---|---|---|
| Define properties + constructor | Separate | Primary constructor in one line |
| Multiple constructors | Independent definitions | Secondary must delegate to primary |
| Param to property | Manual assignment | val/var automatic |
| Default values | Method overloading | Default parameters |
4. init Block
The init block runs immediately after the primary constructor — used for validation and initialization logic.
(1) Basic Usage
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 Block Execution Order
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. Visibility Modifiers
(1) Four Visibility Levels
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) Visibility Comparison Table
| Modifier | Inside Class | Subclass | Same Module | Global |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
internal |
✅ | ✅ | ✅ | ❌ |
protected |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
(3) Java vs Kotlin Visibility Differences
| Dimension | Java | Kotlin |
|---|---|---|
| Default visibility | package-private | public |
| Module visibility | None | internal |
| Package visibility | package-private | None (use internal instead) |
| Top-level declarations | public only | public / internal / private |
6. Deferred Initialization
(1) lateinit var
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) by 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 vs by lazy Comparison
| Dimension | lateinit |
by lazy |
|---|---|---|
| Type | var (mutable) |
val (read-only) |
| Initialization timing | Manual assignment | On first access |
| Thread safety | No | Yes (default) |
| Nullability | Non-null declared | Non-null declared |
| Access before init | Runtime exception | Won't happen |
| Use case | DI framework injection | Expensive computed properties |
7. Class Relationship Diagram
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. Complete Example: OrderProcessor Domain Model
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}")
}
Output:
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
❓ FAQ
Q Can primary and secondary constructors coexist?
A Yes, but secondary constructors must delegate to the primary using
this(...). Default parameters are preferred over secondary constructors.Q Can I have multiple init blocks?
A Yes, multiple init blocks execute in declaration order. It's recommended to merge them into one to avoid confusion.
Q Can lateinit be used for primitive types?
A No. lateinit only works with non-primitive types (object types), because primitives have default values. Use
by lazy or nullable types + defaults for Int/Double etc.Q Is by lazy initialization thread-safe?
A Yes by default (SYNCHRONIZED mode). If you're sure of single-thread access, use
LazyThreadSafetyMode.NONE for better performance.Q How does internal visibility work in Maven/Gradle multi-module projects?
A internal restricts visibility within the same Gradle module or Maven module. Different modules — even within the same project — cannot access internal members.
Q Why doesn't Kotlin have Java's package-private?
A Kotlin uses
internal (module-level visibility) instead of package-private. Package-level visibility is often misused in Java; module-level better fits modern project structures.📖 Summary
- Primary constructor +
val/vardefines properties in one line, eliminating Java's getter/setter boilerplate initblocks replace Java's constructor body for validation and initialization- Four visibility levels:
public(default) /internal/protected/private lateinitsuits DI framework injection;by lazysuits deferred computation- Property backing fields use
private var+ public read-only view pattern for defensive copying - Default parameters are preferred over secondary constructors to keep constructors clean
📝 Exercises
- Beginner (⭐): Define a
Productclass (id: String, name: String, price: Double) with an init block validatingprice >= 0. Hint:require(price >= 0) - Intermediate (⭐⭐): Design a
Serviceclass usinglateinitto injectRepositoryandby lazyto initialize a cache. Hint:lateinit+by lazy - Challenge (⭐⭐⭐): Implement a complete Order domain model with Order/OrderItem/Customer, all validation in init blocks, and defensive copying for items. Hint: see Section 8's complete example



