Kotlin - Classes & Objects

Kotlin Classes & Objects

Classes are the blueprint for creating objects in Kotlin. Learn how to declare classes, create objects, define properties and methods, and understand Kotlin's approach to object-oriented programming.

Basic Class Declaration

Simple Class

class Person {
    // Class body (can be empty)
}

// Creating objects (instances)
val person1 = Person()
val person2 = Person()

Class with Properties

class Person {
    var name: String = ""
    var age: Int = 0
    var email: String? = null
}

// Using the class
val person = Person()
person.name = "Alice"
person.age = 30
person.email = "[email protected]"

println("${person.name} is ${person.age} years old")

Primary Constructor

class Person(firstName: String, age: Int) {
    val name: String = firstName
    val age: Int = age
    
    init {
        println("Person created: $name, age $age")
    }
}

// Creating objects with constructor parameters
val person = Person("Bob", 25)
Key Concept: Kotlin classes are public and final by default. Properties declared in the primary constructor become class properties automatically.

Properties

Property Declaration

class Person {
    // Mutable property
    var name: String = "Unknown"
    
    // Read-only property
    val id: Int = generateId()
    
    // Nullable property
    var email: String? = null
    
    // Property with custom getter
    val displayName: String
        get() = if (name.isNotBlank()) name else "Anonymous"
    
    // Property with custom getter and setter
    var fullName: String = ""
        get() = field.uppercase()
        set(value) {
            field = value.trim()
        }
}

fun generateId(): Int = (1..1000).random()

Primary Constructor Properties

// Concise way - properties declared in constructor
class Person(
    val name: String,           // Read-only property
    var age: Int,              // Mutable property
    val email: String? = null  // Optional property with default
) {
    // Additional properties
    var isActive: Boolean = true
    
    init {
        require(age >= 0) { "Age cannot be negative" }
        require(name.isNotBlank()) { "Name cannot be blank" }
    }
}

// Usage
val person = Person("Charlie", 28, "[email protected]")
println(person.name)  // Charlie
person.age = 29       // Can modify mutable properties
// person.name = "Chuck"  // ❌ Compilation error - val is read-only

Methods (Member Functions)

Basic Methods

class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
    
    fun subtract(a: Int, b: Int): Int = a - b  // Single expression
    
    fun multiply(a: Int, b: Int) = a * b       // Inferred return type
    
    fun greet() {
        println("Hello from Calculator!")
    }
}

val calc = Calculator()
println(calc.add(5, 3))      // 8
println(calc.subtract(10, 4)) // 6
calc.greet()                 // Hello from Calculator!

Methods with Class Properties

class BankAccount(val accountNumber: String, initialBalance: Double = 0.0) {
    private var balance: Double = initialBalance
    
    fun deposit(amount: Double) {
        require(amount > 0) { "Deposit amount must be positive" }
        balance += amount
        println("Deposited $$amount. New balance: $$balance")
    }
    
    fun withdraw(amount: Double): Boolean {
        return if (amount > 0 && amount <= balance) {
            balance -= amount
            println("Withdrew $$amount. New balance: $$balance")
            true
        } else {
            println("Insufficient funds or invalid amount")
            false
        }
    }
    
    fun getBalance(): Double = balance
    
    fun getAccountInfo(): String {
        return "Account: $accountNumber, Balance: $$balance"
    }
}

val account = BankAccount("12345", 1000.0)
account.deposit(500.0)      // Deposited $500.0. New balance: $1500.0
account.withdraw(200.0)     // Withdrew $200.0. New balance: $1300.0
println(account.getAccountInfo()) // Account: 12345, Balance: $1300.0

Visibility Modifiers

Access Control

class Employee(
    val id: Int,                    // public (default)
    private val salary: Double,     // private to this class
    protected val department: String, // accessible in subclasses
    internal val company: String    // accessible within same module
) {
    // Private property
    private var performanceRating: Int = 0
    
    // Public method
    fun getEmployeeInfo(): String {
        return "Employee #$id in $department"
    }
    
    // Private method
    private fun calculateBonus(): Double {
        return salary * 0.1 * performanceRating
    }
    
    // Protected method
    protected fun updateRating(rating: Int) {
        performanceRating = rating
    }
    
    // Internal method
    internal fun getCompanyInfo(): String {
        return "Works at $company"
    }
}

val employee = Employee(1, 75000.0, "Engineering", "TechCorp")
println(employee.id)                    // ✅ Public
println(employee.getEmployeeInfo())     // ✅ Public method
// println(employee.salary)             // ❌ Private
// employee.updateRating(5)             // ❌ Protected

Object Instances and Initialization

Object Creation and Usage

class Book(
    val title: String,
    val author: String,
    var pages: Int
) {
    var isRead: Boolean = false
    var currentPage: Int = 0
    
    fun startReading() {
        if (!isRead) {
            currentPage = 1
            println("Started reading '$title'")
        }
    }
    
    fun finishReading() {
        if (!isRead) {
            isRead = true
            currentPage = pages
            println("Finished reading '$title'")
        }
    }
    
    fun getProgress(): String {
        return if (isRead) {
            "Completed"
        } else if (currentPage > 0) {
            "Page $currentPage of $pages (${(currentPage * 100) / pages}%)"
        } else {
            "Not started"
        }
    }
}

// Creating and using objects
val book1 = Book("1984", "George Orwell", 328)
val book2 = Book("Kotlin in Action", "Dmitry Jemerov", 360)

book1.startReading()
println(book1.getProgress())  // Page 1 of 328 (0%)

book2.finishReading()
println(book2.getProgress())  // Completed

Multiple Constructors

class Rectangle {
    val width: Double
    val height: Double
    
    // Primary constructor
    constructor(width: Double, height: Double) {
        this.width = width
        this.height = height
    }
    
    // Secondary constructor for square
    constructor(side: Double) : this(side, side)
    
    // Secondary constructor with default values
    constructor() : this(1.0, 1.0)
    
    fun area(): Double = width * height
    fun perimeter(): Double = 2 * (width + height)
    
    fun isSquare(): Boolean = width == height
}

val rectangle = Rectangle(5.0, 3.0)
val square = Rectangle(4.0)         // Uses secondary constructor
val unit = Rectangle()              // Uses default constructor

println("Rectangle area: ${rectangle.area()}")     // 15.0
println("Square area: ${square.area()}")           // 16.0
println("Is square? ${square.isSquare()}")         // true

Data Classes

Automatic Methods Generation

// Data class automatically generates equals(), hashCode(), toString(), copy()
data class User(
    val id: Int,
    val username: String,
    val email: String
)

val user1 = User(1, "alice", "[email protected]")
val user2 = User(1, "alice", "[email protected]")
val user3 = User(2, "bob", "[email protected]")

// toString() automatically generated
println(user1)  // User(id=1, username=alice, [email protected])

// equals() automatically generated
println(user1 == user2)  // true (same values)
println(user1 == user3)  // false (different values)

// copy() method for creating modified copies
val updatedUser = user1.copy(email = "[email protected]")
println(updatedUser)  // User(id=1, username=alice, [email protected])

// Destructuring
val (id, username, email) = user1
println("User: $username ($id) - $email")

Nested and Inner Classes

Nested Classes

class OuterClass {
    private val outerProperty = "I'm outer"
    
    class NestedClass {
        fun doSomething() {
            println("Doing something in nested class")
            // Cannot access outerProperty directly
        }
    }
    
    inner class InnerClass {
        fun doSomething() {
            println("Doing something in inner class")
            println("Accessing: $outerProperty")  // Can access outer properties
        }
    }
}

// Nested class usage
val nested = OuterClass.NestedClass()
nested.doSomething()

// Inner class usage
val outer = OuterClass()
val inner = outer.InnerClass()
inner.doSomething()

Object Declarations and Expressions

Singleton Objects

object DatabaseManager {
    private var connectionCount = 0
    
    fun connect(): String {
        connectionCount++
        return "Connection #$connectionCount established"
    }
    
    fun getStats(): String {
        return "Total connections: $connectionCount"
    }
}

// Usage - object is a singleton
println(DatabaseManager.connect())  // Connection #1 established
println(DatabaseManager.connect())  // Connection #2 established
println(DatabaseManager.getStats()) // Total connections: 2

Companion Objects

class MathUtils {
    companion object {
        const val PI = 3.14159
        
        fun max(a: Int, b: Int): Int {
            return if (a > b) a else b
        }
        
        fun factorial(n: Int): Long {
            return if (n <= 1) 1 else n * factorial(n - 1)
        }
    }
    
    // Instance methods
    fun instanceMethod() {
        println("This is an instance method")
    }
}

// Using companion object (like static methods in Java)
println(MathUtils.PI)               // 3.14159
println(MathUtils.max(5, 3))        // 5
println(MathUtils.factorial(5))     // 120

// Still need instance for regular methods
val mathUtils = MathUtils()
mathUtils.instanceMethod()

Real-World Examples

E-commerce Product System

enum class ProductCategory {
    ELECTRONICS, CLOTHING, BOOKS, HOME, SPORTS
}

data class Product(
    val id: String,
    val name: String,
    val price: Double,
    val category: ProductCategory,
    var stockQuantity: Int
) {
    fun isInStock(): Boolean = stockQuantity > 0
    
    fun reduceStock(quantity: Int): Boolean {
        return if (quantity <= stockQuantity) {
            stockQuantity -= quantity
            true
        } else {
            false
        }
    }
}

class ShoppingCart {
    private val items = mutableMapOf()
    
    fun addItem(product: Product, quantity: Int = 1) {
        if (product.reduceStock(quantity)) {
            items[product.id] = items.getOrDefault(product.id, 0) + quantity
            println("Added $quantity x ${product.name} to cart")
        } else {
            println("Insufficient stock for ${product.name}")
        }
    }
    
    fun getItemCount(): Int = items.values.sum()
    
    fun clear() {
        items.clear()
        println("Cart cleared")
    }
}

// Usage
val laptop = Product("LAPTOP001", "Gaming Laptop", 1299.99, ProductCategory.ELECTRONICS, 5)
val shirt = Product("SHIRT001", "Cotton T-Shirt", 29.99, ProductCategory.CLOTHING, 20)

val cart = ShoppingCart()
cart.addItem(laptop, 1)
cart.addItem(shirt, 2)
println("Items in cart: ${cart.getItemCount()}")

Bank Account System

abstract class Account(
    val accountNumber: String,
    val holderName: String,
    protected var balance: Double
) {
    abstract fun calculateInterest(): Double
    
    fun deposit(amount: Double) {
        require(amount > 0) { "Amount must be positive" }
        balance += amount
        println("Deposited $$amount to $accountNumber")
    }
    
    open fun withdraw(amount: Double): Boolean {
        return if (amount > 0 && amount <= balance) {
            balance -= amount
            println("Withdrew $$amount from $accountNumber")
            true
        } else {
            println("Insufficient funds")
            false
        }
    }
    
    fun getBalance(): Double = balance
}

class SavingsAccount(
    accountNumber: String,
    holderName: String,
    balance: Double,
    private val interestRate: Double = 0.02
) : Account(accountNumber, holderName, balance) {
    
    override fun calculateInterest(): Double {
        return balance * interestRate
    }
}

class CheckingAccount(
    accountNumber: String,
    holderName: String,
    balance: Double,
    private val overdraftLimit: Double = 500.0
) : Account(accountNumber, holderName, balance) {
    
    override fun calculateInterest(): Double = 0.0  // No interest
    
    override fun withdraw(amount: Double): Boolean {
        return if (amount > 0 && amount <= (balance + overdraftLimit)) {
            balance -= amount
            println("Withdrew $$amount from $accountNumber")
            if (balance < 0) {
                println("Account is overdrawn by $$${-balance}")
            }
            true
        } else {
            println("Exceeds overdraft limit")
            false
        }
    }
}

// Usage
val savings = SavingsAccount("SAV001", "Alice Johnson", 1000.0)
val checking = CheckingAccount("CHK001", "Bob Smith", 500.0)

savings.deposit(200.0)
println("Savings interest: $${savings.calculateInterest()}")

checking.withdraw(800.0)  // Uses overdraft
println("Checking balance: $${checking.getBalance()}")

Best Practices

✅ Good Practices

  • Use data classes for simple data containers
  • Prefer val over var for immutable properties
  • Use meaningful names for classes and properties
  • Keep constructors simple and use init blocks for validation
  • Use private visibility by default, expose only what's necessary
  • Prefer composition over inheritance

❌ Avoid

  • Creating classes that do too many things
  • Making everything public
  • Using mutable properties when immutable would work
  • Complex logic in constructors
  • Deep inheritance hierarchies
Architecture Note: Kotlin's class system promotes immutability and encapsulation while reducing boilerplate. Data classes are perfect for DTOs and value objects, while regular classes handle behavior-rich entities.

Practice Exercises

  1. Create a Student class with properties and methods for grade management
  2. Build a simple library system with Book and Library classes
  3. Design a vehicle hierarchy with different types of vehicles
  4. Create a calculator class with various mathematical operations
  5. Implement a simple game character system with different character types

Quick Quiz

  1. What's the difference between val and var for class properties?
  2. What methods does a data class automatically generate?
  3. How do you make a class property private?
  4. What's the difference between nested and inner classes?
Show answers
  1. val creates read-only properties, var creates mutable properties
  2. equals(), hashCode(), toString(), copy(), and componentN() functions for destructuring
  3. Use the private modifier: private val/var propertyName
  4. Nested classes don't have access to outer class members, inner classes do