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
- Create a Student class with properties and methods for grade management
- Build a simple library system with Book and Library classes
- Design a vehicle hierarchy with different types of vehicles
- Create a calculator class with various mathematical operations
- Implement a simple game character system with different character types
Quick Quiz
- What's the difference between val and var for class properties?
- What methods does a data class automatically generate?
- How do you make a class property private?
- What's the difference between nested and inner classes?
Show answers
- val creates read-only properties, var creates mutable properties
- equals(), hashCode(), toString(), copy(), and componentN() functions for destructuring
- Use the private modifier: private val/var propertyName
- Nested classes don't have access to outer class members, inner classes do