Classes and Objects

When to Use

  • When you need to represent real-world entities in your code
  • When you want to organize related data and functions together
  • When you need to create multiple objects with similar properties
  • When you want to reuse code across your program
Component What It Does Example
Class Declaration Defines a new type of object class Dog { }
Properties Stores data in the class var name: String = "Unknown"
Methods Defines actions the class can perform fun bark() { println("Woof!") }
Object Creation Creates an instance of the class val myDog = Dog()

Practical Examples

// ===========================================
// BASIC CLASS WITH PROPERTIES AND METHODS
// ===========================================

// Example 1: Simple Dog class
// This demonstrates the basic structure of a class
class Dog {
    // Properties - data that describes the dog
    var name: String = "Unknown"
    var age: Int = 0
    var breed: String = "Mixed"
    var isHungry: Boolean = true
    
    // Methods - actions the dog can perform
    fun bark() {
        println("$name says: Woof! Woof!")
    }
    
    fun eat() {
        if (isHungry) {
            println("$name is eating...")
            isHungry = false
            println("$name is no longer hungry!")
        } else {
            println("$name is not hungry right now.")
        }
    }
    
    fun sleep() {
        println("$name is sleeping... Zzz...")
        isHungry = true  // Dog gets hungry while sleeping
    }
    
    fun celebrateBirthday() {
        age++
        println("Happy Birthday, $name! You are now $age years old!")
    }
    
    fun displayInfo() {
        println("""
        Dog Information:
          Name: $name
          Age: $age
          Breed: $breed
          Hungry: $isHungry
        """.trimIndent())
    }
}

// Example 2: Bank Account class
// This shows a more practical business application
class BankAccount {
    // Properties
    var accountNumber: String = ""
    var accountHolder: String = ""
    var balance: Double = 0.0
    var isActive: Boolean = true
    
    // Methods
    fun deposit(amount: Double) {
        if (amount > 0 && isActive) {
            balance += amount
            println("Deposited $${String.format("%.2f", amount)} to account $accountNumber")
            println("New balance: $${String.format("%.2f", balance)}")
        } else {
            println("Invalid deposit amount or account inactive")
        }
    }
    
    fun withdraw(amount: Double): Boolean {
        if (amount > 0 && amount <= balance && isActive) {
            balance -= amount
            println("Withdrew $${String.format("%.2f", amount)} from account $accountNumber")
            println("New balance: $${String.format("%.2f", balance)}")
            return true
        } else {
            println("Insufficient funds, invalid amount, or account inactive")
            return false
        }
    }
    
    fun checkBalance() {
        println("Account $accountNumber balance: $${String.format("%.2f", balance)}")
    }
    
    fun deactivate() {
        isActive = false
        println("Account $accountNumber has been deactivated")
    }
}

// ===========================================
// CREATING AND USING OBJECTS
// ===========================================

println("=== CREATING AND USING DOG OBJECTS ===")

// Create a new Dog object (instance of the Dog class)
val myDog = Dog()

// Set the dog's properties
myDog.name = "Buddy"
myDog.age = 3
myDog.breed = "Golden Retriever"

// Use the dog's methods
myDog.displayInfo()
myDog.bark()
myDog.eat()
myDog.sleep()
myDog.celebrateBirthday()
myDog.displayInfo()

println("\n=== CREATING MULTIPLE DOG OBJECTS ===")

// Create another dog object
val anotherDog = Dog()
anotherDog.name = "Max"
anotherDog.age = 5
anotherDog.breed = "Labrador"

// Each dog object has its own state
println("First dog: ${myDog.name}, Age: ${myDog.age}")
println("Second dog: ${anotherDog.name}, Age: ${anotherDog.age}")

// Dogs can have different states
myDog.eat()      // Buddy eats
anotherDog.eat() // Max eats
myDog.eat()      // Buddy is not hungry anymore

println("\n=== CREATING AND USING BANK ACCOUNTS ===")

// Create a bank account
val account1 = BankAccount()
account1.accountNumber = "ACC001"
account1.accountHolder = "John Doe"
account1.balance = 1000.0

// Use the bank account
account1.checkBalance()
account1.deposit(500.0)
account1.withdraw(200.0)
account1.checkBalance()

// Create another account
val account2 = BankAccount()
account2.accountNumber = "ACC002"
account2.accountHolder = "Jane Smith"
account2.balance = 2500.0

// Each account has its own state
println("\nAccount Summary:")
println("Account 1: $${String.format("%.2f", account1.balance)}")
println("Account 2: $${String.format("%.2f", account2.balance)}")

Constructors

When to Use

  • When you need to initialize objects with specific values
  • When you want to ensure objects are created with valid data
  • When you need different ways to create objects
Type What It Does When to Use It
Primary Constructor Main way to initialize objects For basic initialization
Secondary Constructor Alternative way to initialize objects When you need multiple initialization options
Initializer Block Runs code during initialization When you need to perform setup tasks

Practical Examples

// ===========================================
// PRIMARY CONSTRUCTOR EXAMPLES
// ===========================================

// Example 1: Class with primary constructor
// This is the most common way to create classes in Kotlin
class Person(val name: String, var age: Int) {
    // The constructor parameters automatically create properties
    // 'val name' creates an immutable property
    // 'var age' creates a mutable property
    
    // You can add additional properties
    var email: String = ""
    var isActive: Boolean = true
    
    // Initializer block - runs when object is created
    init {
        println("Creating person: $name, age $age")
        
        // You can add validation logic
        if (age < 0) {
            throw IllegalArgumentException("Age cannot be negative")
        }
        
        if (name.isEmpty()) {
            throw IllegalArgumentException("Name cannot be empty")
        }
    }
    
    // Methods
    fun introduce() {
        println("Hi, I'm $name and I'm $age years old.")
    }
    
    fun haveBirthday() {
        age++
        println("Happy Birthday, $name! You are now $age years old!")
    }
    
    fun updateEmail(newEmail: String) {
        email = newEmail
        println("$name's email updated to: $email")
    }
}

// Example 2: Class with default parameters
// This makes the class more flexible
class Car(
    val brand: String,
    val model: String,
    var year: Int = 2024,
    var color: String = "White",
    var mileage: Double = 0.0
) {
    init {
        println("Creating car: $year $brand $model in $color")
    }
    
    fun start() {
        println("Starting $brand $model...")
    }
    
    fun drive(distance: Double) {
        mileage += distance
        println("Drove $distance miles. Total mileage: $mileage")
    }
    
    fun displayInfo() {
        println("""
        Car Information:
          Brand: $brand
          Model: $model
          Year: $year
          Color: $color
          Mileage: $mileage miles
        """.trimIndent())
    }
}

// ===========================================
// SECONDARY CONSTRUCTOR EXAMPLES
// ===========================================

// Example 3: Class with secondary constructor
// This provides alternative ways to create objects
class BankAccount(val accountNumber: String, var balance: Double) {
    var accountHolder: String = "Unknown"
    var accountType: String = "Checking"
    var isActive: Boolean = true
    
    // Primary constructor
    init {
        println("Account $accountNumber created with balance $${String.format("%.2f", balance)}")
    }
    
    // Secondary constructor - calls primary constructor with 'this'
    constructor(number: String, balance: Double, holder: String) : this(number, balance) {
        accountHolder = holder
        println("Account created for $holder")
    }
    
    // Another secondary constructor
    constructor(number: String, balance: Double, holder: String, type: String) : this(number, balance, holder) {
        accountType = type
        println("Account type set to: $type")
    }
    
    // Methods
    fun displayAccountInfo() {
        println("""
        Account Information:
          Number: $accountNumber
          Holder: $accountHolder
          Type: $accountType
          Balance: $${String.format("%.2f", balance)}
          Active: $isActive
        """.trimIndent())
    }
    
    fun deposit(amount: Double) {
        if (amount > 0 && isActive) {
            balance += amount
            println("Deposited $${String.format("%.2f", amount)}")
        }
    }
    
    fun withdraw(amount: Double): Boolean {
        if (amount > 0 && amount <= balance && isActive) {
            balance -= amount
            println("Withdrew $${String.format("%.2f", amount)}")
            return true
        }
        return false
    }
}

// ===========================================
// USING CONSTRUCTORS
// ===========================================

println("=== USING PRIMARY CONSTRUCTORS ===")

// Create Person objects using primary constructor
val person1 = Person("Alice", 25)
val person2 = Person("Bob", 30)

person1.introduce()
person2.introduce()

person1.haveBirthday()
person1.updateEmail("alice@example.com")

println("\n=== USING CAR CONSTRUCTORS WITH DEFAULTS ===")

// Create cars with different numbers of parameters
val car1 = Car("Toyota", "Camry")  // Uses defaults for year, color, mileage
val car2 = Car("Honda", "Civic", 2023, "Blue")  // Override some defaults
val car3 = Car("Ford", "Mustang", 2022, "Red", 15000.0)  // Override all defaults

car1.displayInfo()
car2.displayInfo()
car3.displayInfo()

car1.start()
car1.drive(50.0)
car1.displayInfo()

println("\n=== USING SECONDARY CONSTRUCTORS ===")

// Create bank accounts using different constructors
val account1 = BankAccount("ACC001", 1000.0)  // Primary constructor
val account2 = BankAccount("ACC002", 2000.0, "John Doe")  // Secondary constructor
val account3 = BankAccount("ACC003", 3000.0, "Jane Smith", "Savings")  // Another secondary constructor

account1.displayAccountInfo()
account2.displayAccountInfo()
account3.displayAccountInfo()

// Test account operations
account1.deposit(500.0)
account2.withdraw(300.0)
account3.deposit(1000.0)

println("\nAfter transactions:")
account1.displayAccountInfo()
account2.displayAccountInfo()
account3.displayAccountInfo()

Properties and Accessors

When to Use

  • Use properties to store data in your classes
  • Use custom accessors when you need to control how properties are accessed
  • Use private properties when you want to restrict access
Feature What It Does When to Use It
Basic Property Stores a value For simple data storage
Custom Getter Computes a value when accessed When the value needs to be calculated
Custom Setter Validates or transforms values When you need to control how values are set
Private Property Restricts access to the property When you want to protect data

Practical Examples

// ===========================================
// BASIC PROPERTIES AND ACCESSORS
// ===========================================

// Example 1: Class with basic properties
// This shows the standard way to define properties
class Student(
    val studentId: String,  // Immutable property (read-only)
    var name: String,       // Mutable property (read-write)
    var age: Int            // Mutable property (read-write)
) {
    // Additional properties
    var email: String = ""
    var major: String = "Undeclared"
    var gpa: Double = 0.0
    
    // Method to display student information
    fun displayInfo() {
        println("""
        Student Information:
          ID: $studentId
          Name: $name
          Age: $age
          Email: $email
          Major: $major
          GPA: $gpa
        """.trimIndent())
    }
}

// Example 2: Class with custom getters and setters
// This demonstrates how to control property access
class BankAccount(val accountNumber: Int, private var accountBalance: Double) {
    // Public property with custom getter
    val balance: Double
        get() {
            println("Balance accessed for account $accountNumber")
            return accountBalance
        }
    
    // Public property with custom getter and setter
    var balanceLessFees: Double
        get() = accountBalance - 25.0  // $25 monthly fee
        set(value) {
            // Validate the new value
            if (value >= 0) {
                accountBalance = value + 25.0  // Add fee back to get actual balance
                println("Balance updated to $${String.format("%.2f", value)} (plus fees)")
            } else {
                println("Error: Balance cannot be negative")
            }
        }
    
    // Private property for internal use
    private val monthlyFee: Double = 25.0
    
    // Method to get account type
    val accountType: String
        get() = when {
            accountBalance >= 10000 -> "Premium"
            accountBalance >= 5000 -> "Standard"
            else -> "Basic"
        }
    
    // Method to display account information
    fun displayAccountInfo() {
        println("""
        Account Information:
          Number: $accountNumber
          Balance: $${String.format("%.2f", balance)}
          Balance after fees: $${String.format("%.2f", balanceLessFees)}
          Account Type: $accountType
          Monthly Fee: $${String.format("%.2f", monthlyFee)}
        """.trimIndent())
    }
    
    // Method to deposit money
    fun deposit(amount: Double) {
        if (amount > 0) {
            accountBalance += amount
            println("Deposited $${String.format("%.2f", amount)}")
            println("New balance: $${String.format("%.2f", balance)}")
        } else {
            println("Invalid deposit amount")
        }
    }
    
    // Method to withdraw money
    fun withdraw(amount: Double): Boolean {
        if (amount > 0 && amount <= balance) {
            accountBalance -= amount
            println("Withdrew $${String.format("%.2f", amount)}")
            println("New balance: $${String.format("%.2f", balance)}")
            return true
        } else {
            println("Invalid withdrawal amount or insufficient funds")
            return false
        }
    }
}

// Example 3: Class with computed properties
// This shows properties that calculate values on demand
class Rectangle(val width: Double, val height: Double) {
    // Computed property for area
    val area: Double
        get() = width * height
    
    // Computed property for perimeter
    val perimeter: Double
        get() = 2 * (width + height)
    
    // Computed property for diagonal
    val diagonal: Double
        get() = kotlin.math.sqrt(width * width + height * height)
    
    // Computed property for shape description
    val shapeDescription: String
        get() = when {
            width == height -> "Square"
            width > height -> "Landscape Rectangle"
            else -> "Portrait Rectangle"
        }
    
    // Method to display rectangle information
    fun displayInfo() {
        println("""
        Rectangle Information:
          Width: $width
          Height: $height
          Area: $area
          Perimeter: $perimeter
          Diagonal: ${String.format("%.2f", diagonal)}
          Shape: $shapeDescription
        """.trimIndent())
    }
}

// Example 4: Class with validation in setters
// This demonstrates how to validate data when setting properties
class Person(var name: String, var age: Int) {
    // Custom setter with validation for name
    var fullName: String = name
        set(value) {
            if (value.isNotBlank() && value.length >= 2) {
                field = value.trim()  // 'field' refers to the backing field
                println("Name updated to: '$field'")
            } else {
                println("Error: Name must be at least 2 characters long")
            }
        }
    
    // Custom setter with validation for age
    var personAge: Int = age
        set(value) {
            if (value >= 0 && value <= 150) {
                field = value
                println("Age updated to: $field")
            } else {
                println("Error: Age must be between 0 and 150")
            }
        }
    
    // Computed property for age group
    val ageGroup: String
        get() = when {
            personAge < 13 -> "Child"
            personAge < 20 -> "Teenager"
            personAge < 65 -> "Adult"
            else -> "Senior"
        }
    
    // Computed property for voting eligibility
    val canVote: Boolean
        get() = personAge >= 18
    
    // Method to display person information
    fun displayInfo() {
        println("""
        Person Information:
          Name: '$fullName'
          Age: $personAge
          Age Group: $ageGroup
          Can Vote: $canVote
        """.trimIndent())
    }
}

// ===========================================
// USING PROPERTIES AND ACCESSORS
// ===========================================

println("=== USING BASIC PROPERTIES ===")

// Create and use Student objects
val student1 = Student("S001", "Alice Johnson", 20)
student1.email = "alice@university.edu"
student1.major = "Computer Science"
student1.gpa = 3.8

student1.displayInfo()

println("\n=== USING CUSTOM ACCESSORS ===")

// Create and use BankAccount objects
val account1 = BankAccount(1001, 5000.0)
account1.displayAccountInfo()

// Test custom getters and setters
println("\nAccessing balance: ${account1.balance}")
account1.balanceLessFees = 2000.0  // This will update the actual balance
account1.displayAccountInfo()

// Test deposit and withdrawal
account1.deposit(1000.0)
account1.withdraw(500.0)
account1.displayAccountInfo()

println("\n=== USING COMPUTED PROPERTIES ===")

// Create and use Rectangle objects
val rect1 = Rectangle(5.0, 3.0)
val rect2 = Rectangle(4.0, 4.0)

rect1.displayInfo()
println()
rect2.displayInfo()

println("\n=== USING VALIDATION IN SETTERS ===")

// Create and use Person objects
val person1 = Person("John", 25)
person1.displayInfo()

// Test validation in setters
person1.fullName = "John Doe"      // Valid name
person1.fullName = "A"             // Invalid name (too short)
person1.fullName = "   "           // Invalid name (blank)

person1.personAge = 30             // Valid age
person1.personAge = -5             // Invalid age (negative)
person1.personAge = 200            // Invalid age (too high)

person1.displayInfo()

Companion Objects

When to Use

  • When you need to create static members in your class
  • When you want to create factory methods
  • When you need to track class-level information
Feature What It Does When to Use It
Companion Object Holds static members For class-level functionality
Factory Method Creates objects in a controlled way When you need special object creation
Static Property Shares data across all instances When you need to track class-level data

Practical Examples

// ===========================================
// COMPANION OBJECT EXAMPLES
// ===========================================

// Example 1: Basic companion object
// This demonstrates the fundamental concept
class Car {
    var brand: String = "Unknown"
    var model: String = "Unknown"
    var year: Int = 2024
    
    companion object {
        // Static property - shared across all Car instances
        var totalCars: Int = 0
        
        // Static method - can be called without creating an instance
        fun getTotalCars(): Int {
            return totalCars
        }
        
        // Factory method - creates cars in a controlled way
        fun createCar(brand: String, model: String, year: Int): Car {
            val car = Car()
            car.brand = brand
            car.model = model
            car.year = year
            totalCars++  // Increment the counter
            return car
        }
        
        // Static method to reset counter
        fun resetCounter() {
            totalCars = 0
            println("Car counter reset to 0")
        }
    }
    
    // Instance method
    fun displayInfo() {
        println("$year $brand $model")
    }
}

// Example 2: Companion object with constants and utilities
// This shows more practical usage patterns
class MathUtils {
    companion object {
        // Mathematical constants
        const val PI = 3.14159265359
        const val E = 2.71828182846
        const val GOLDEN_RATIO = 1.61803398875
        
        // Utility functions
        fun factorial(n: Int): Long {
            return if (n <= 1) 1 else n * factorial(n - 1)
        }
        
        fun isPrime(n: Int): Boolean {
            if (n < 2) return false
            for (i in 2..kotlin.math.sqrt(n.toDouble()).toInt()) {
                if (n % i == 0) return false
            }
            return true
        }
        
        fun fibonacci(n: Int): Long {
            return if (n <= 1) n.toLong() else fibonacci(n - 1) + fibonacci(n - 2)
        }
        
        fun roundToDecimal(value: Double, decimals: Int): Double {
            val factor = kotlin.math.pow(10.0, decimals.toDouble())
            return kotlin.math.round(value * factor) / factor
        }
    }
}

// Example 3: Companion object for configuration and settings
// This demonstrates managing application-wide settings
class AppConfig {
    companion object {
        // Application settings
        var debugMode: Boolean = false
        var maxUsers: Int = 100
        var sessionTimeout: Int = 3600  // seconds
        var databaseUrl: String = "localhost:5432"
        
        // Configuration methods
        fun enableDebugMode() {
            debugMode = true
            println("Debug mode enabled")
        }
        
        fun disableDebugMode() {
            debugMode = false
            println("Debug mode disabled")
        }
        
        fun updateMaxUsers(newMax: Int) {
            if (newMax > 0) {
                maxUsers = newMax
                println("Maximum users updated to: $maxUsers")
            } else {
                println("Error: Maximum users must be positive")
            }
        }
        
        fun displayConfig() {
            println("""
            Application Configuration:
              Debug Mode: $debugMode
              Max Users: $maxUsers
              Session Timeout: ${sessionTimeout} seconds
              Database URL: $databaseUrl
            """.trimIndent())
        }
    }
}

// Example 4: Companion object for validation and formatting
// This shows utility functions that don't need instance data
class StringUtils {
    companion object {
        // Validation functions
        fun isValidEmail(email: String): Boolean {
            return email.contains("@") && 
                   email.contains(".") && 
                   email.indexOf("@") < email.lastIndexOf(".")
        }
        
        fun isValidPhoneNumber(phone: String): Boolean {
            val digits = phone.filter { it.isDigit() }
            return digits.length in 10..11
        }
        
        fun isValidPassword(password: String): Boolean {
            return password.length >= 8 && 
                   password.any { it.isUpperCase() } &&
                   password.any { it.isDigit() }
        }
        
        // Formatting functions
        fun formatPhoneNumber(phone: String): String {
            val digits = phone.filter { it.isDigit() }
            return when {
                digits.length == 10 -> "(${digits.substring(0, 3)}) ${digits.substring(3, 6)}-${digits.substring(6)}"
                digits.length == 11 && digits.startsWith("1") -> "1 (${digits.substring(1, 4)}) ${digits.substring(4, 7)}-${digits.substring(7)}"
                else -> phone
            }
        }
        
        fun formatCurrency(amount: Double): String {
            return "$${String.format("%.2f", amount)}"
        }
        
        fun capitalizeWords(text: String): String {
            return text.split(" ").joinToString(" ") { word ->
                word.lowercase().replaceFirstChar { it.uppercase() }
            }
        }
    }
}

// ===========================================
// USING COMPANION OBJECTS
// ===========================================

println("=== USING CAR COMPANION OBJECT ===")

// Use companion object methods without creating instances
println("Total cars before creation: ${Car.getTotalCars()}")

// Create cars using the factory method
val car1 = Car.createCar("Toyota", "Camry", 2024)
val car2 = Car.createCar("Honda", "Civic", 2023)
val car3 = Car.createCar("Ford", "Mustang", 2022)

// Display car information
car1.displayInfo()
car2.displayInfo()
car3.displayInfo()

// Check total cars
println("Total cars after creation: ${Car.getTotalCars()}")

// Reset counter
Car.resetCounter()
println("Total cars after reset: ${Car.getTotalCars()}")

println("\n=== USING MATH UTILS COMPANION OBJECT ===")

// Use mathematical constants and utilities
println("PI: ${MathUtils.PI}")
println("E: ${MathUtils.E}")
println("Golden Ratio: ${MathUtils.GOLDEN_RATIO}")

println("Factorial of 5: ${MathUtils.factorial(5)}")
println("Is 17 prime? ${MathUtils.isPrime(17)}")
println("Fibonacci number 10: ${MathUtils.fibonacci(10)}")
println("3.14159 rounded to 2 decimals: ${MathUtils.roundToDecimal(3.14159, 2)}")

println("\n=== USING APP CONFIG COMPANION OBJECT ===")

// Display initial configuration
AppConfig.displayConfig()

// Modify configuration
AppConfig.enableDebugMode()
AppConfig.updateMaxUsers(200)
AppConfig.sessionTimeout = 7200

// Display updated configuration
AppConfig.displayConfig()

println("\n=== USING STRING UTILS COMPANION OBJECT ===")

// Test validation functions
val testEmails = listOf("user@example.com", "invalid-email", "test@", "@domain.com")
val testPhones = listOf("555-123-4567", "1234567890", "1-800-555-0123", "invalid")
val testPasswords = listOf("StrongPass123", "weak", "12345678", "NoNumbers")

println("Email Validation:")
for (email in testEmails) {
    println("  '$email': ${StringUtils.isValidEmail(email)}")
}

println("\nPhone Validation:")
for (phone in testPhones) {
    println("  '$phone': ${StringUtils.isValidPhoneNumber(phone)}")
}

println("\nPassword Validation:")
for (password in testPasswords) {
    println("  '$password': ${StringUtils.isValidPassword(password)}")
}

// Test formatting functions
println("\nFormatting Examples:")
println("Phone: ${StringUtils.formatPhoneNumber("5551234567")}")
println("Currency: ${StringUtils.formatCurrency(1234.56)}")
println("Capitalized: ${StringUtils.capitalizeWords("hello world kotlin programming")}")