Classes and Constructors
Class Basics
A class in Kotlin is a blueprint for creating objects. Kotlin classes are final by default (cannot be extended) unless marked open.
class Person(val name: String, var age: Int)
val person = Person("Alice", 30)
println(person.name) // Alice
println(person.age) // 30
person.age = 31 // Allowed: age is var
// person.name = "Bob" // Compile error: name is val
The primary constructor is declared in the class header. Properties declared with val are read-only; var are mutable.
Primary Constructor with Init Block
The init block runs as part of the primary constructor. Use it for validation or computed properties:
class User(val name: String, val email: String) {
init {
require(name.isNotBlank()) { "Name must not be blank" }
require(email.contains("@")) { "Invalid email" }
}
}
Secondary Constructors
Secondary constructors are declared inside the class body. Each must delegate to the primary constructor or another secondary constructor:
class Logger(val tag: String) {
constructor() : this("DEFAULT")
constructor(prefix: String, tag: String) : this("$prefix:$tag")
}
val log1 = Logger() // tag = DEFAULT
val log2 = Logger("APP") // tag = APP
val log3 = Logger("APP", "DB") // tag = APP:DB
Property Declarations
Properties declared in the class body (not in the constructor) need explicit initialization or a custom getter:
class Rectangle(val width: Double, val height: Double) {
val area: Double
get() = width * height
var label: String = "untitled"
set(value) {
field = value.uppercase()
}
}
val rect = Rectangle(5.0, 3.0)
println(rect.area) // 15.0
rect.label = "box"
println(rect.label) // BOX
The field keyword is the backing field. Use it in custom accessors to avoid infinite recursion.
Inheritance, Interfaces, and Objects
Open Classes and Inheritance
By default, Kotlin classes are final. Mark a class open to allow inheritance:
open class Animal(val name: String) {
open fun speak(): String = "..."
}
class Dog(name: String) : Animal(name) {
override fun speak(): String = "Woof!"
}
class Cat(name: String) : Animal(name) {
override fun speak(): String = "Meow!"
}
val dog = Dog("Rex")
println("${dog.name} says ${dog.speak()}") // Rex says Woof!
The override keyword is required when overriding a method. Methods are not open by default.
Abstract Classes
Abstract classes cannot be instantiated directly. They combine abstract (unimplemented) and concrete methods:
abstract class Shape {
abstract fun area(): Double
fun describe(): String = "Area: ${area()}"
}
class Circle(val radius: Double) : Shape() {
override fun area(): Double = Math.PI * radius * radius
}
class Rectangle(val w: Double, val h: Double) : Shape() {
override fun area(): Double = w * h
}
Interfaces
Interfaces define contracts without implementation state. A class can implement multiple interfaces:
interface Drawable {
fun draw()
}
interface Resizable {
fun resize(factor: Double)
}
class Canvas(val width: Double, val height: Double) : Drawable, Resizable {
override fun draw() = println("Drawing ${width}x$height canvas")
override fun resize(factor: Double) {
// Modify dimensions
}
}
Interfaces can have default implementations (unlike Java 7) and can declare properties (abstract or with accessors).
Object Declarations and Companions
An object declaration creates a singleton:
object DatabaseConfig {
val host = "localhost"
val port = 5432
}
println(DatabaseConfig.host) // Access directly, no instantiation
A companion object provides static-like members to a class:
class User private constructor(val name: String) {
companion object {
fun create(name: String): User {
return User(name)
}
}
}
val user = User.create("Alice") // Factory method via companion
Data Classes
Data classes automatically generate equals(), hashCode(), toString(), copy(), and destructuring:
data class Point(val x: Int, val y: Int)
val p1 = Point(1, 2)
val p2 = Point(1, 2)
println(p1 == p2) // true (structural equality)
println(p1) // Point(x=1, y=2)
val p3 = p1.copy(y = 5) // Point(x=1, y=5)
val (x, y) = p1 // Destructuring: x=1, y=2
Sealed Classes
Sealed classes restrict inheritance to a fixed set of subclasses. They are essential for modeling state machines and algebraic data types:
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result): String {
return when (result) {
is Result.Success -> "Data: ${result.data}"
is Result.Error -> "Error: ${result.message}"
Result.Loading -> "Loading..."
// No else branch needed: compiler knows all cases
}
}
The compiler exhausts all cases in when expressions, so you get a compile error if a new subclass is added and the when is not updated.
Practice Problems
Create a BankAccount data class with owner (String), balance (Double, private var), and methods deposit(amount) and withdraw(amount). Withdraw should print 'Insufficient funds' if the amount exceeds balance.
Solution
data class BankAccount(val owner: String) {
private var balance: Double = 0.0
fun deposit(amount: Double) {
require(amount > 0) { "Amount must be positive" }
balance += amount
println("Deposited $amount. Balance: $balance")
}
fun withdraw(amount: Double) {
require(amount > 0) { "Amount must be positive" }
if (amount > balance) {
println("Insufficient funds")
} else {
balance -= amount
println("Withdrew $amount. Balance: $balance")
}
}
}
fun main() {
val account = BankAccount("Alice")
account.deposit(1000.0) // Deposited 1000.0. Balance: 1000.0
account.withdraw(500.0) // Withdrew 500.0. Balance: 500.0
account.withdraw(600.0) // Insufficient funds
} Create a sealed class Token with subclasses Number(value: Double), Operator(symbol: String), and Parenthesis(open: Boolean). Write a function describe(token: Token) that uses when to return a human-readable description of each token.
Solution
sealed class Token {
data class Number(val value: Double) : Token()
data class Operator(val symbol: String) : Token()
data class Parenthesis(val open: Boolean) : Token()
}
fun describe(token: Token): String {
return when (token) {
is Token.Number -> "Number: ${token.value}"
is Token.Operator -> "Operator: ${token.symbol}"
is Token.Parenthesis -> if (token.open) "Open paren" else "Close paren"
}
}
fun main() {
println(describe(Token.Number(42.0))) // Number: 42.0
println(describe(Token.Operator("+"))) // Operator: +
println(describe(Token.Parenthesis(true))) // Open paren
} Define an interface PaymentProcessor with methods charge(amount: Double) and refund(amount: Double). Implement it in two classes: CreditCardProcessor and PayPalProcessor, each printing the action taken.
Solution
interface PaymentProcessor {
fun charge(amount: Double)
fun refund(amount: Double)
}
class CreditCardProcessor(val lastFour: String) : PaymentProcessor {
override fun charge(amount: Double) {
println("Charging $amount to card ending in $lastFour")
}
override fun refund(amount: Double) {
println("Refunding $amount to card ending in $lastFour")
}
}
class PayPalProcessor(val email: String) : PaymentProcessor {
override fun charge(amount: Double) {
println("Charging $amount to PayPal account $email")
}
override fun refund(amount: Double) {
println("Refunding $amount to PayPal account $email")
}
}
fun main() {
val cc = CreditCardProcessor("1234")
cc.charge(99.99) // Charging 99.99 to card ending in 1234
cc.refund(50.0) // Refunding 50.0 to card ending in 1234
val pp = PayPalProcessor("user@example.com")
pp.charge(49.99) // Charging 49.99 to PayPal account user@example.com
} Quiz
1. Why are Kotlin classes final by default?
2. What does a data class automatically generate?
3. What is the advantage of sealed classes in when expressions?
4. What is the difference between a companion object and a Java static method?
Flashcards
Question
What is a data class in Kotlin?
Click to reveal answer
Answer
A class marked with data that auto-generates equals(), hashCode(), toString(), copy(), and componentN() functions based on constructor properties. Used for holding data.
Question
What is a sealed class?
Click to reveal answer
Answer
A class that restricts inheritance to a fixed set of subclasses (all in the same file). Enables exhaustive when expressions for pattern matching.
Question
What is the difference between open class and abstract class?
Click to reveal answer
Answer
An open class can be extended but all methods have implementations. An abstract class can also be extended, but can have abstract (unimplemented) methods and cannot be instantiated directly.
Question
What does the init block do?
Click to reveal answer
Answer
Runs as part of the primary constructor. Used for validation, computed property initialization, or setup logic that cannot be expressed in property initializers.
Revision Notes
Key Takeaways
- 1. Classes are final by default. Mark them open when inheritance is intentional.
- 2. Data classes eliminate boilerplate for data holders.
- 3. Sealed classes enable exhaustive when expressions for state modeling.
- 4. Companion objects replace Java static methods with real object instances.
- 5. Interfaces define contracts; abstract classes provide partial implementations.
Interview Tips
- • Explain why Kotlin makes classes final by default (composition over inheritance).
- • Know the difference between data class and regular class.
- • Be ready to discuss when to use sealed classes vs enums.
- • Understand companion objects as factory pattern enablers.
Cheat Sheet
OOP in Kotlin Cheat Sheet
Classes:
- Final by default; use
opento allow inheritance - Primary constructor in class header
initblock for validation/setup- Secondary constructors delegate to primary
Data Classes:
- Auto-generates equals, hashCode, toString, copy, destructuring
- Use for data holders
Sealed Classes:
- Fixed set of subclasses (same file)
- Exhaustive when expressions
Interfaces:
- Can have default implementations
- Multiple interface implementation
- No state (only abstract properties)
Objects:
object= singletoncompanion object= static-like members
Visibility:
- public (default), private, protected, internal