Defining and Calling Functions

Kotlin

2026-08-21 09:00

Where we are

From declaring to calling

Kotlin Basics taught the syntax for declaring things.

This unit is about the calling side.

And about a feature with no Java equivalent at all.

One running example

joinToString — a collection, a separator, a prefix, a postfix.

Written and rewritten as each feature arrives.

By the end: a one-line call available on every collection.

What you will be able to do

  1. Create collections and say what they are.
  2. Use named arguments.
  3. Give parameters default values.
  4. Declare top-level functions and properties.
  5. Write extension functions.
  6. Explain extension dispatch.
  7. Declare extension properties.
  8. Use vararg and the spread operator.
  9. Use infix calls and destructuring.
  10. Use the string extensions and triple-quoted strings.
  11. Extract local functions.

The question this unit answers

Unit 1 committed Kotlin to full interoperability.

This is where that commitment first costs something —

and where the payment turns out to be interesting.

What Kotlin collections really are

Creating them

val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven")

Note the to. It looks like syntax. Hold that thought.

Now ask what they are

println(set.javaClass)    // class java.util.HashSet
println(list.javaClass)   // class java.util.ArrayList
println(map.javaClass)    // class java.util.HashMap

Kotlin has no collection classes of its own.

Why

A Kotlin list passed to a Java method needs no conversion.

Because it is a Java list.

No adapter, no copy, no surprise.

But Java’s API is thin

strings.last()
setOf(1, 14, 2).max()

Neither exists on java.util.List or java.util.Set.

So how?

Kotlin cannot modify Java’s classes — they ship in the JDK.

Cannot subclass them — listOf returns the real thing.

Will not wrap them — that defeats the point.

Named arguments

The obvious version

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String
joinToString(collection, " ", " ", ".")

Three strings in a row. Which is which?

Name them

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

Checked by the compiler.

Name one, and every later one must be named too.

The limitation

Named arguments do not work on Java methods.

Java bytecode does not reliably preserve parameter names.

The IDE may show them. The compiler cannot verify them.

Default parameter values

Declare the defaults

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
): String
joinToString(list)                 // ", " and no brackets
joinToString(list, "; ")           // just the separator

Where the two features compose

Positional arguments → omit only from the end.

Named arguments → omit any subset, in any order.

joinToString(list, postfix = ";", prefix = "# ")

Java’s overload family cannot do that

You would need one overload per combination.

For Java callers who need them: @JvmOverloads generates them.

Top-level functions and properties

Where should joinToString live?

It belongs to no object.

In Java: a class that exists only as a container, modelling nothing.

Kotlin: nowhere in particular

package strings

fun joinToString(...): String { ... }

No class. No instance. No static.

What the compiler does

join.kt → a class named JoinKt, with static methods.

import strings.JoinKt;
JoinKt.joinToString(list, ", ", "", "");

Java sees exactly the utility class it expects.

@file:JvmName("StringFunctions") renames it.

Top-level properties

val UNIX_LINE_SEPARATOR = "\n"         // static field + getter
const val LINE_SEPARATOR = "\n"        // public static final

const matters at the boundary: Java sees a field, not a method.

The recurring move

A cleaner Kotlin model.

Whatever Java expects, generated underneath.

Extension functions

The central idea

package strings

fun String.lastChar(): Char = this.get(this.length - 1)
println("Kotlin".lastChar())   // n

Receiver type, receiver object

this may be omitted

fun String.lastChar(): Char = get(length - 1)

How it actually works

char c = StringUtilKt.lastChar("Java");

A static function whose first parameter is the receiver.

No modification, no patching, no proxy.

Two consequences, immediately

Java can call it. It is a static method. Extensions are interoperable by construction.

It cannot see private members. An outsider with pleasant syntax.

Which is what makes extending types you do not own safe.

Imports

import strings.lastChar
import strings.lastChar as last     // resolve a clash

A feature, not an inconvenience: nobody’s extension can silently change your code.

No overriding

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

val view: View = Button()
view.click()      // Button clicked  — member, dynamic
view.showOff()    // I'm a view!     — extension, static

Declared outside, resolved statically

And a member always wins

If a class later adds a matching method, it takes over — silently.

Not a defect. Static functions have never been polymorphic.

Good reason not to write an extension a class ought to have as a member.

Extension properties

val String.lastChar: Char
    get() = get(length - 1)

Must have a custom getter — there is nowhere to put a field.

Always computed, never held.

Now the question is answered

strings.last()
setOf(1, 14, 2).max()
list.joinToString(", ")

Every one an extension function on a Java interface.

The collection API you use every day is a library of extensions.

Varargs, infix, destructuring

vararg

fun listOf<T>(vararg values: T): List<T>
val list = listOf("args: ", *args)   // spread operator

Why the * is required

In Java, “one array” or “these elements” depends on context.

A known source of quiet bugs.

Kotlin makes the intent explicit — and lets you mix spread with fixed elements.

infix

1.to("one")     // ordinary
1 to "one"      // infix
infix fun Any.to(other: Any) = Pair(this, other)

to is not syntax

An extension function on Any, marked infix, returning a Pair.

So mapOf(1 to "one") is a function call taking varargs of Pair.

Something that looks like language support is a library.

Destructuring

And it was in unit 2 already

for ((index, element) in collection.withIndex()) { }

Itself a convention, backed by component1() and component2().

Explained properly in unit 7.

Strings and regular expressions

A genuinely bad Java API

"12.345-6.A".split(".")   // returns an empty array

split takes a regular expression. . matches anything.

The signature gives no hint. The failure is silent.

Kotlin’s fix is in the types

"12.345-6.A".split(".", "-")             // plain delimiters
"12.345-6.A".split("\\.|-".toRegex())    // a Regex, explicitly

A String argument means a literal delimiter. No accidents.

Parsing without a regex

val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")

More readable, easier to debug. A regex is not always the right tool.

With one, and no backslash soup

val regex = """(.+)/(.+)\.(.+)""".toRegex()
val (directory, filename, extension) = matchResult.destructured

Triple-quoted strings take no escape sequences.

And destructured is the feature from two sections ago, doing real work.

Multiline

val kotlinLogo = """| //
                   .| //
                   .|/ \""".trimMargin(".")

Indent the literal to match the code; the indentation does not reach the output.

Local functions

The problem

fun saveUser(user: User) {
    if (user.name.isEmpty()) throw IllegalArgumentException(
        "Can't save user ${user.id}: empty Name")
    if (user.address.isEmpty()) throw IllegalArgumentException(
        "Can't save user ${user.id}: empty Address")
}

The same shape twice. With a third field it would be three.

The unsatisfactory fix

Extract a private method.

Removes the duplication.

Adds a method to the class that nothing else should call.

Nest it instead

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) throw IllegalArgumentException(
            "Can't save user ${user.id}: empty $fieldName")   // user captured
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
}

Then tidy with an extension

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) { /* ... */ }
    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()
    // save
}

Why this closes the unit

All these features answer one question:

Where should this code live, so it is easy to call and easy to read?

Java answers “inside a class” every time. Kotlin usually does not.

Summary

The six things to carry away

  • Kotlin’s collections are Java’s collections.
  • An extension is a static function with a receiver — and everything follows from that.
  • The entire collection API is a library of extensions.
  • Named arguments plus defaults do what an overload family cannot.
  • to is a function, not syntax. Map literals are function calls.
  • The real question is where code should live, and it is not always inside a class.

Where next

Classes, Objects, and Interfaces turns to declarations.

Interfaces with defaults, open/final/abstract, constructors, data classes, delegation, and object.

Where you find out how much of a Java class the compiler will write for you.