Functions & OOP in Kotlin

Default and named arguments, classes, data classes, and extension functions.

Functions with default and named arguments

Kotlin
fun greet(name: String, greeting: String = "Hello"): String { // greeting has a default value
    return "$greeting, $name!"
}

fun main() {
    println(greet("Ada"))                     // Hello, Ada!
    println(greet("Ada", "Welcome"))           // Welcome, Ada!
    println(greet(name = "Ada", greeting = "Hi")) // Hi, Ada! — named arguments, order doesn't matter
    println(greet(greeting = "Hey", name = "Grace")) // Hey, Grace!
}

Default arguments dramatically cut down on the "telescoping overloads" pattern common in Java, where you'd otherwise need several overloaded versions of the same method just to support optional parameters. Named arguments make call sites self-documenting, especially useful when a function takes several parameters of the same type.

Single-expression functions can drop the braces and return entirely:

Kotlin
fun square(n: Int): Int = n * n // equivalent to { return n * n }

fun square(n: Int) = n * n // return type is inferred as Int too, when unambiguous

Classes and the primary constructor

Kotlin classes declare their main constructor directly in the class header, which tends to eliminate a lot of the boilerplate Java requires:

Kotlin
class Car(val model: String, var speed: Int = 0) { // primary constructor, right in the header
    fun accelerate(amount: Int) {
        speed += amount
    }

    fun describe(): String {
        return "$model is going $speed km/h"
    }
}

fun main() {
    val car = Car("Civic")           // speed defaults to 0
    car.accelerate(40)
    println(car.describe())           // Civic is going 40 km/h
    println(car.model)                 // Civic — 'val' in the constructor exposes it as a property directly
}

Declaring a constructor parameter as val or var (as above) automatically makes it a property of the class — no separate field declaration or manual assignment needed, similar in spirit to PHP's constructor property promotion.

An init block runs as part of construction, useful for validation logic that doesn't belong in the property declarations themselves:

Kotlin
class BankAccount(initialBalance: Double) {
    var balance: Double = initialBalance
        private set // the getter is public, but only this class can call the setter

    init {
        require(initialBalance >= 0) { "Initial balance cannot be negative" }
    }

    fun deposit(amount: Double) {
        require(amount > 0) { "Amount must be positive" }
        balance += amount
    }
}

Data classes

A data class is purpose-built for classes that primarily hold data — the compiler automatically generates equals(), hashCode(), toString(), and a copy() method, none of which you have to write (or keep in sync) by hand.

Kotlin
data class User(val name: String, val age: Int)

fun main() {
    val user1 = User("Ada", 30)
    val user2 = User("Ada", 30)

    println(user1)                  // User(name=Ada, age=30) — auto-generated toString
    println(user1 == user2)         // true — auto-generated equals() compares CONTENT, not reference

    val olderUser = user1.copy(age = 31) // copy() creates a new instance with just 'age' changed
    println(olderUser)               // User(name=Ada, age=31)
    println(user1)                    // User(name=Ada, age=30) — original is untouched
}

Compare this to an ordinary class, where == compares references by default and you'd have to hand-write equals()/hashCode()/toString() yourself (or generate them with an IDE) to get the same behavior.

Inheritance

Kotlin classes are final (cannot be subclassed) by default — you must explicitly mark a class open to allow inheritance, the opposite default from Java. This is a deliberate design choice: it forces you to decide upfront whether a class is meant to be extended, rather than allowing it by accident.

Kotlin
open class Vehicle(protected var speed: Int = 0) {
    open fun accelerate() { // must be 'open' to be overridable
        speed += 10
    }

    fun currentSpeed() = speed
}

class SportsCar : Vehicle() {
    override fun accelerate() { // 'override' is mandatory, not optional, unlike Java's @Override
        speed += 30
    }
}

fun main() {
    val car: Vehicle = SportsCar()
    car.accelerate()
    println(car.currentSpeed()) // 30 — the overridden version ran
}

Extension functions

An extension function lets you add a new function to an existing type — including types you don't own, like String or a Java library class — without inheriting from it or modifying its source:

Kotlin
fun String.isPalindrome(): Boolean {
    val cleaned = this.lowercase().replace(" ", "")
    return cleaned == cleaned.reversed()
}

fun main() {
    println("racecar".isPalindrome())       // true
    println("A man a plan a canal Panama".isPalindrome()) // true
    println("hello".isPalindrome())          // false
}

Under the hood, an extension function is just a regular static function taking the receiver as its first argument — Kotlin's compiler resolves the call syntactically at compile time, so it's not true dynamic dispatch or actual modification of the original class. This is exactly how much of Kotlin's standard library (.map, .filter, .also) is implemented on top of Java's existing collection types.

Kotlin
fun List<Int>.sumOfSquares(): Int = this.sumOf { it * it }

fun main() {
    println(listOf(1, 2, 3).sumOfSquares()) // 14 — 1 + 4 + 9
}

Common mistakes

  • Forgetting a class must be marked open before it can be subclassed, and being confused by the resulting compile error — this is intentional, not a bug.
  • Using a plain class for a simple data holder instead of data class, then hand-writing (and forgetting to keep in sync) equals()/hashCode()/toString().
  • Assuming an extension function can access a class's private members — it can't; it only has access to what the class already exposes publicly (or as internal, within the same module).

Interview questions

Q: What does a data class give you for free that a regular class doesn't? Automatically generated equals() and hashCode() (based on all properties declared in the primary constructor, comparing content rather than reference), a readable toString(), and a copy() method for creating a modified shallow copy — all without writing or maintaining any of that boilerplate by hand.

Q: What is an extension function, and what are its limits? It's a function that appears to add new behavior to an existing type (fun String.isPalindrome()) without modifying its source or subclassing it — under the hood it's just a regular function taking the receiver as an implicit first parameter, resolved statically at compile time. Because of that static resolution, it cannot access the class's private members, and it can't be truly polymorphic the way a real method override can (which extension function gets called is decided by the declared, compile-time type of the reference, not the actual runtime type).