danilo.pianini@unibo.itCompiled on: 2025-11-21 — printable version
JetBrains-made modern programming language
Gaining momentum since Google adopted is as official Android language
Clearly inspired by a mixture of Java, C#, Scala, and Groovy
In this course – we’ll need it for Gradle and internal domain specific languages
Scala is a scalable language
Kotlin is somewhat a better java
Similar to Scala. The keyword def is replaced by fun
val x = 10 // constant
var y = 20 // variable, can be reassigned
fun foo() = 20 // function definition, single expression
fun bar(): Int { // same as above with multiple expression
return 20 // requires a return in this form...
}
fun baz() { } // Unless it returns Unit
Much like Scala:
fun foo(a: Int = 0, b: String = "foo"): Int = TODO()
// TODO() is a builtin function throwing a `NotImplementedError`
foo(1, "bar") // OK, positional
foo(a = 1, b = "bar") // OK, named
foo(1, b = "bar") // OK, hybrid
foo(a = 1, "bar") // error: no value passed for parameter 'b'
foo() // OK, both defaults
foo(1) // OK, same as foo(1, "foo")
foo("bar") // error: type mismatch: inferred type is String but Int was expected
foo(b = "bar") // OK, same as foo(0, "bar")
Similar to Scala 3 (unsupported in Scala 2)
fun foo() {
...
}
def foo() {
...
}
When targeting the JVM, Kotlin simply generates a FileNameKt class behind the scenes where the function is stored.
The behaviour can be controlled via annotations.
Naming a function main makes it a valid entry point:
fun main() = println("Hello World") // Valid entry point
fun main(arguments: Array<String>) = println("Hello World") // Valid entry point
fun main(arguments: Array<String>) {
println("Hello World") // Return type is Unit, no need to return
}
Every Kotlin type exists in two forms: normal, and nullable (likely inspired by Ceylon).
Nullable types are suffixed by a ? and require special handling
null can’t be assigned to non nullable types!
Option typesvar foo = "bar" // Okay, type is String
var baz: String? = foo // Okay, normal types can be assigned to nullables
foo = baz // error: type mismatch: inferred type is String? but String was expected
foo = null // error: null can not be a value of a non-null type String
Nullable types members can’t be accessed by ..
var baz: String? = "foo"
baz.length // error: only safe (?.) or non-null asserted (!!.) calls are allowed...
// on a nullable receiver of type String?
?.Performs runtime access to a member of a nullable object if it’s not null, otherwise returns null
Option’s map (but no monad involved)var baz: String? = "foo"
baz?.length // returns 3, return type is "Int?", in fact...
val bar: Int = baz?.length // type mismatch: inferred type is Int? but Int was expected
baz = null
baz?.length // returns null, return type is still "Int?"
!!Also known as: I want my code to break badly at runtime
null at runtimevar baz: String? = "foo"
baz!! // Returns 'foo', type String (non nullable)
baz!!.length // returns 3, return type is Int
baz = null
baz!! // throws a KotlinNullPointerException, like the good ol'times!
?:Named after Elvis Presley’s haircut 🕺
null, otherwise the right onevar baz: String? = "foo"
baz ?: "bar" // Returns "foo", type String
baz?.length ?: 0 // returns 3, return type is Int
baz = null
baz ?: "bar" // Returns "bar", type String
baz?.length ?: 0 // returns 0, return type is Int
Kotlin targets the JVM, JavaScript, and native code
None of them has nullable types!
Nullability is unknown for types coming from the target platform, how to deal with them?
Kotlin considers all foreign values whose nullability is unknown as platform types
! (e.g., java.util.Date!)@NotNull (or similar common alternatives) it will be interpreted as a non-nullable typeObjectAnyNothingAnyObjectAnyNothingAnyAny?NothingBooleansExactly as Java/Scala, but with nullability:
Boolean: true/falseBoolean?: true/false/null&&, !!, and ! operators work for non-nullable Booleans.Likewise Scala, boxing under the JVM is dealt with by the compiler
Boolean? are always boxed (to be able to account for null)
Same as Scala, +nullability, +unsigned experimental types:
Byte, Short, Int, Long, Float, Double
UByte, UShort, UInt, ULongImplicit type conversion to “bigger” types is source of nasty errors when automatic boxing is involved.
Consider the following Scala code:
Double.NaN == Double.NaN
false, OK, as every sane language
Double.NaN equals Double.NaN
true! Boxing + Singleton make equality inconsistent!
val a: Int = 1
val b: Long = a
a == b // true
a equals b // false
This causes a chain of issues, as == and equals do a different job, as do ## and hashCode: Maps can become very surprising!
Kotlin numeric types are converted manually to prevent these issues:
val i: Int = 1
val l: Long = 1
val l: Long = i // error: type mismatch: inferred type is Int but Long was expected
val l: Long = i.toLong() // OK
i + l // OK, operators are overloaded
l + i // OK, operators are overloaded
1234567 // Literal Int
1_234_567 // Literal Int, underscored syntax (preferable)
123L // Literal Long
1.0 // Literal Double
123e4 // Literal Double in scientific notation
1d // Nope :)
1f // Literal Float
1u // Literal UInt
0123 // error: unsupported [literal prefixes and suffixes] (no octal)
0xCAFE // Hex literal Int
0xCAFEBABE // Hex literal Long (automatic, as it does not fit an Int)
0x0000000 // Hex literal Int, even it'd fit a Byte
0b1111111_11111111_11111111_11111111 // Binary Int (Integer.MAX_INT)
0b11111111_11111111_11111111_11111111 // Binary Long
0b11111111_11111111_11111111_11111111u // Binary UInt!
0xFFFF_FFFF_FFFFu // ULong
Spiced up version of Java strings, Groovy-style templating:
$ begins a template expression${}val batman = "Batman"
// Groovy templating and Java-style concatenation both work
"${Double.NaN}".repeat(10) + " $batman!" // NaNNaNNaNNaNNaNNaNNaNNaNNaNaN Batman!
"Batman is $batman.length characters long" // Batman is Batman.length characters long
"Batman is ${batman.length} characters long" // Batman is 6 characters long
Triple-double-quoted strings are considered raw strings
\ is a normal character$-templating still worksval dante = """
Tanto gentile e tanto onesta pare
la donna mia quand'ella altrui saluta,
ch'ogne lingua devèn, tremando, muta
e li occhi non l'ardiscon di guardare.
""".trimIndent() // Indentation can be trimmed
val finalWordsEndingInA = """\W*(\w*a)\W*$""".toRegex(RegexOption.MULTILINE) // '$' escaped
finalWordsEndingInA.findAll(dante).map { it.groups[1]?.value }.toList() // [saluta, muta]
${'$'})
val jsonSchema : String
get() = """
{
"${'$'}schema": "https://json-schema.org/draft/2020-12/schema",
"${'$'}id": "https://example.com/product.schema.json",
"${'$'}dynamicAnchor": "meta",
"title": "${findTitle()}",
"type": "object"
}
"""
val jsonSchema : String
get() = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "$${findTitle()}",
"type": "object"
}
"""
Same as Java, plus aliasing.
Imports go at the top of file, no locally scoped imports as in Scala
implicits in Kotlin, the import statement does not modify contextpackage it.unibo.spe.experiments
import it.unibo.spe.ddd.Entity // Available as Entity locally
import org.company.someproduct.Entity as SomeProductEntity // name aliasing
Functions can have a parameter marked as vararg, accepting multiple entries
Array<out T>fun printall(vararg strings: String) {
strings.forEach { println(it) } // We'll discuss this syntax later...
}
printall("Lorem", "ipsum", "dolor", "sit", "amet")
Kotlin is less permissive than Scala:
def ##°@??%&@^^() = 1 // Super ok for Scala: def $hash$hash$u00B0$at$qmark$qmark$percent$amp$at$up$up(): Int
fun `##°@??%&@^^`() = 1 // OK
`##°@??%&@^^`() // 1. Must be invoked with backticks!
val `val` = "Hey look I can name things with keywords!"
val `names can also contain spaces` = 1
valclass WebAgentTest {
@Test
fun `404 errors should cause a wait and retry`() = TODO() // Nice and very clear name
}
Functions can contain other functions (as in Scala)
fun factorial(n: UInt): ULong {
// tailrec forces optimization of tail recursion (and blocks compilation if recursion is non-tail)
tailrec fun factorialWithAccumulator(current: UInt, accumulator: ULong): ULong = when {
current >= n -> accumulator * current
else -> factorialWithAccumulator(current + 1u, accumulator * current)
}
return factorialWithAccumulator(1u, 1u)
}
Warning: local functions often hinder clarity
ifif/else is an expression and works just as in Scalaif alone is not an expressionforfor(init; condition; then) { block } loopfor/in: for (element in collection) { block }forwhile and do/whiledo-blockimport kotlin.random.Random
val lucky = 6
var attempts = 0
do {
val draw = Random.nextInt(lucky + 1)
attempts++
} while (draw != lucky) // draw is visible here
println("Launched $attempts dice before a lucky shot")
whenKotlin does not support pattern matching as Scala does (unfortunately)
The when block is somewhat a mild surrogate, more similar to a switch on steroids
The base version (without subject) is a more elegant “if/else if/else” chain
fun countBatmans(subject: String) = when {
subject.length < "batman".length -> 0
subject.length < 2 * "batman".length && subject.contains("batman") -> 1
else -> ".*?(batman)".toRegex().findAll(subject).count().toInt()
}
when is an expression in any casewhen (subject)Checks if the value of subjects is the same of the expression on the right
fun baseForSingleDigitOrNull(digit: UInt) = when(digit) {
0u, 1u -> "binary"
2u -> "ternary"
in 0u..7u -> "octal" // This is a range!
in 0u..15u -> "hexadecimal"
in 0u..36u -> "base36"
else -> null
}
when with subject can be used to elegantly check for subtypesfun splitAnything(input: Any) = when(input) {
is Int -> input / 2 // No need to cast! The compiler infers type automatically (smart cast)
is String -> input.substring(input.length / 2)
is Double -> input / 2
else -> TODO()
}
Jumping is awful, imperative, and you should not use it
…but someone might and you must be able to understand it…
break and continue work as in Javareturn does not, as we will see when discussing higher order functions…label@ 1 is a valid expressionbreak, continue, and return can be qualified with a labelouterloop@ for (i in 1..100) {
for (j in 1..100) {
if (i * j == i + j) {
println("$i * $j equals $i + $j")
break@outerloop // Qualified break
}
}
}
class introduces a class definitionnew
new is not a Kotlin keyword at allclass Foo
Foo() // a new Foo is created, no new keyword
Kotlin classes have two types of members: methods and properties
| Language / Member Type | Fields | Methods | Properties |
|---|---|---|---|
| Java | Yes | Yes | No |
| Scala | Yes | Yes | No |
| Kotlin | No (Hidden) | Yes | Yes |
| C# | Yes | Yes | Yes |
In Scala, at the caller site, methods and fields are hard to distinguish due to the Uniform Access Principle.
infix) are invoked with mandatory parenthesesProperties and fields are conceptually different
It’s considered a good practice in languages without properties (Java in particular) to hide (incapsulate) fields (Object’s actual state)
and provide access only via get/set methods: the actual state representation may change with no change to the API.
In Kotlin, fields are entirely hidden, and cannot be exposed in any way, enforcing the aforementioned convention at the language level.
class Foo {
val bar = 1
var baz: String? = null
val bazLength: Int // Property with no "backing field"
get() = baz?.length ?: 0 // As its value will be computed every time
var stringRepresentation: String = "" // Backing fields is generated
get() = baz ?: field
set(value) {
field = "custom: $value" // Access to backing field via `field` keyword
}
}
val foo = Foo()
foo.bar = 3 // error: val cannot be reassigned
foo.stringRepresentation // empty string
foo.stringRepresentation = "zed" // 'custom: zed'
The keyword field allows access to a backing field of a property
in case it is present
The Kotlin compiler, in fact, generates backing fields only when needed
class Student {
var id: String? = null // Backing field generated
val identifierOnce: String = "Student[${id ?: "unknown"}]" // Backing field generated
val identifierUpdated: String get() = "Student[${id ?: "unknown"}]" // No backing field
}
When designing with Kotlin, you must consider methods and properties, and forget about fields.
Methods are defined as functions within the scope of a class
this)class MutableComplex {
var real: Double = 0.0
var imaginary: Double = 0.0
fun plus(other: MutableComplex): MutableComplex = MutableComplex().also {
it.real = real + other.real
it.imaginary = imaginary + other.imaginary
}
}
val foo = MutableComplex()
foo.real = 1.0
foo.imaginary = 2.0
val bar = MutableComplex()
bar.real = 4.1
bar.imaginary = 0.1
val baz = foo.plus(bar)
"${baz.real}+${baz.imaginary}i" // 5.1+2.1i
interface cannot be a subclass of a Kotlin classclass A
trait B extends A // All fine in Scala
open class A
interface B : A // error: an interface cannot inherit from a class
So, no mixins
Much like Java. Subtyping keyword is :, overrides must be marked with override:
interface Shape {
val area: Double
val perimeter: Double
}
interface Shrinkable {
fun shrink(): Unit
}
class MutableCircle : Shape, Shrinkable {
var radius = 1.0
override val area get() = Math.PI * radius * radius
// What if we remove "get()"?
override val perimeter get() = 2 * Math.PI * radius
override fun shrink() {
radius /= 2
}
}
A call to super can be qualified to disambiguate between conflincting interface declarations:
interface A {
fun foo() = "foo"
}
interface B {
fun foo() = "bar"
}
class C : A, B {
override fun foo() = super<A>.foo() + super<B>.foo()
}
C().foo() // foobar
initSimilar to Scala, but code in the class body is not part of a constructor
Primary constructor code (if any) must be in an init block
class Foo(
val bar: String, // This is a val property of the class
var baz: Int, // This is a var property of the class
greeting: String = "Hello from constructor" // non-property constructor parameter. Default values allowed
) {
init {
println(greeting)
}
}
Foo("bar", 0)
constructorsMore constructors can be added to a class, but they:
Call to another constructor is performed using :
class Foo(val bar: String) {
constructor(longBar: Long) : this("number ${longBar.toString()}")
constructor(intBar: Int) : this(intBar.toLong())
}
Foo(1).bar // number 1
The primary constructor can be written in a longer form with the constructor keyword as well
class Foo constructor(val bar: String) // OK
lateinitIt is possible that some var property needs to get initialized after the object construction:
class Son(val: Father)
class Father(var son: Son) // Impossible to build either
Solution 1: allow nullability (BAD)
class Son(val father: Father)
class Father(var son: Son? = null)
val father = Father()
val son = Son(father)
father.son = son
father.son.father // error, needs ?.
Solution 2: take responsibility from the compiler (less bad)
class Son(val father: Father)
class Father { lateinit var son: Son } // lateinit: I will initialize it later, stay cool
val father = Father()
father.son // UninitializedPropertyAccessException: lateinit property son has not been initialized
val son = Son(father)
father.son = son
father.son.father // OK!
Design and document for inheritance or else prohibit it J. Bloch, Effective Java, Item 17
openKotlin enforces EJ-17 by design: all classes are final if the keyword open is not specified
class A
class B : A() // error: this type is final, so it cannot be inherited from
open class A
class B : A() // OK
As in Scala, the constructor of the superclass must be called at extension site
Differently than Scala, such invocation always requires parentheses
abstract vs. openThe same effect of open can be achieved with abstract:
abstract class A
class B : A() // Perfectly fine
With abstract, however, the superclass cannot be created
(and it should have actual abstract members anyway)
open class Open
abstract class Abstract
class FromOpen : Open()
class FromAbstract : Abstract()
FromAbstract() // OK
FromOpen() // OK
Open() // OK
Abstract() // error: cannot create an instance of an abstract class
objectsSame as Scala, but with explicit companions
In Scala
class A
object A // Same file and same name identify a companion
In Kotlin
class A {
companion object // Companions are inner to classes
}
A // refers to A.Companion
object A // This is an independent object
A // refers to the previously defined object
Simpler than Scala, more coherent than Java
public – default visibility, visible everywhere (API)internal – visible to everything in this “module”
protected – visible to subclasses (but not to other members of the package)private – visible inside this class and its membersclass Visibility internal constructor( // constructor is required to apply visibility restrictionss
private val id: Int // Same as Scala
) {
protected var state = 0
private set // visibility restriction for properties in get/set methods
}
Same as Java, but for equality:
== calls equals==) is Kotlin’s ===Kotlin does not suffer of Scala’s equality issues (no automatic conversion of types)
val a: Int = 1
val b: Long = a
a == b // true
a equals b // false O_O
Kotlin:
val a: Int = 1
val b: Long = a // error: type mismatch: inferred type is Int but Long was expected
val b: Long = a.toLong()
a == b // error: operator '==' cannot be applied to 'Int' and 'Long'
a.toLong() == b // true
a == b.toInt() // true
infix callsKotlin is less permissive than Scala:
1 equals 1 // infix invocation of 1.equals(1)
1 equals 1 // error: 'infix' modifier is required on 'equals' in 'kotlin.Int'
infix keyword for a method to be usable as infixinfix functions have lower precedence than operatorsclass Infix {
infix fun with(s: String) = "in... $s ...fix!"
}
Infix() with "Foo" // in... Foo ...fix!
Infix() with "Foo" + "Bar" // in... FooBar ...fix
Infix() with "Foo" + "Bar" + Infix() with "Baz" // error: unresolved reference: with
In Scala, operator names are valid method names, and infix calls are automatic:
executer(:/(host, port) / target << reqBody >- { fromRespStr }) // Using Databinder Dispatch
val graph = Graph((jfc ~+#> fra)(Any()), (fra ~+#> dme)(Any()) // Using ScalaGraph
Operators are succinct, but cryptic, and their meaning changes with context
This has been a source of cricism, Kotlin does not allow to define custom operators
>, /, :, etc.)class A { infix fun `~+#-`(other: A) = "I'm an arcane operator" }
A() `~+#-` A() // I'm an arcane operator
Kotlin allows for a limited set of operators to be defined/overloaded
operator keywordclass Complex(val real: Double, val imaginary: Double) {
operator fun plus(other: Complex) = Complex(real + other.real, imaginary + other.imaginary)
operator fun plus(other: Double) = plus(Complex(other, 0.0))
override fun toString() = real.toString() + when {
imaginary == 0.0 -> ""
imaginary > 0.0 -> "+${imaginary}i"
else -> "${imaginary}i"
}
}
Complex(1.0, 1.0) + 3.4 // 4.4+1.0i
| Expression | Method Name | Translation |
|---|---|---|
+x |
unaryPlus |
x.unaryPlus() |
-x |
unaryMinus |
x.unaryMinus() |
++x |
inc |
x.inc().also { x = it } |
x++ |
inc |
x.also { x = it.inc() } |
--x |
dec |
x.dec().also { x = it } |
x-- |
dec |
x.also { x = it.dec() } |
!x |
not |
x.not() |
x() |
invoke |
x.invoke() |
Function invocation is an operator and can be overloaded!
This will turn useful in future…
| Expression | Method Name | Translation |
|---|---|---|
x + y |
plus |
x.plus(y) |
x - y |
minus |
x.minus(y) |
x * y |
times |
x.times(y) |
x / y |
div |
x.div(y) |
x % y |
rem |
x.rem(y) |
| Expression | Method Name | Translation |
|---|---|---|
x += y |
plusAssign |
x.plusAssign(y) |
x -= y |
minusAssign |
x.minusAssign(y) |
x *= y |
timesAssign |
x.timesAssign(y) |
x /= y |
divAssign |
x.divAssign(y) |
x %= y |
remAssign |
x.remAssign(y) |
op is defined, the compiler infers the assign version as:
a op= b a = a op b| Expression | Method Name | Translation |
|---|---|---|
x == y |
equals |
x?.equals(y) ?: (y === null) |
x != y |
equals |
!(x?.equals(y) ?: (y === null)) |
x > y |
compareTo |
x.compareTo(y) > 0 |
x < y |
compareTo |
x.compareTo(y) < 0 |
x >= y |
compareTo |
x.compareTo(y) >= 0 |
x <= y |
compareTo |
x.compareTo(y) <= 0 |
| Expression | Method Name | Translation |
|---|---|---|
x..y |
rangeTo |
x.rangeTo(y) |
x in y |
contains |
y.contains(x) |
x !in y |
contains |
!y.contains(x) |
x[y] |
get |
x.get(y) |
x(y) |
invoke |
x.invoke(y) |
| Expression | Method Name | Translation |
|---|---|---|
x[y, z] |
get |
x.get(y, z) |
x[y] = z |
set |
x.set(y) = z |
x(y, z) |
invoke |
x.invoke(y, z) |
| Expression | Method Name | Translation |
|---|---|---|
x[y, ..., z] |
get |
x.get(y, ..., z) |
x[y, ..., z] = a |
set |
x.set(y, ..., z) = a |
x(y, ..., z) |
invoke |
x.invoke(y, ..., z) |
Kotlin’s type system supports generics
trait Functor[F[_]] // Scala 2: there is no Kotlin equivalent
type MapFunctor = Functor[({ type T[A] = Map[Int, A] })#T]
type MapFunctor = [A] =>> Map[Int, A] // Scala 3: there is no Kotlin equivalent
Syntax similar to Java generics
class Foo<A, B : CharSequence>
fun <T : Comparable<T>> maxOf3(first: T, second: T, third: T): T = when {
first >= second && first >= third -> first
second >= third -> second
else -> third
}
:fun <T> className(receiver: T) = receiver::class.simpleName
// error: expression in a class literal has a nullable type 'T', use !! to make the type non-nullable
whereIn case multiple bounds are present, the definition can become cumbersome
Kotlin provides a where keyword to specify type bounds separately from the rest of the signature
// From an actual Alchemist interface
interface NavigationStrategy<T, P, A, L, R, N, E>
where P : Position<P>, P : Vector<P>,
A : GeometricTransformation<P>,
L : ConvexGeometricShape<P, A>,
N : ConvexGeometricShape<P, A> {
// Interface content, if any
}
// Function syntax
fun <T, P, A, L, R, N, E> navigationStrategy()
where P : Position<P>, P : Vector<P>,
A : GeometricTransformation<P>,
L : ConvexGeometricShape<P, A>,
N : ConvexGeometricShape<P, A> = TODO()
Kotlin supports (co/contro)variance using:
<out T> to mark covariance (similar to Java’s <? extends T>)<in T> to mark controvariance (similar to Java’s <? super T>)<*> to mark that only the bound is known for the type (similar to Java’s <?>)Type variant in Kotlin is expressed at declaration site!
interface ProduceAndConsume<in X, out Y> {
fun consume(x: X): Any = TODO() // OK
fun consume2(y: Y): Any = TODO() // Y is declared as 'out' but occurs in 'in' position
fun produce(): Y = TODO() // OK
fun produce2(): X = TODO() // X is declared as 'in' but occurs in 'out' position
}
Generics at runtime can be dealt with two strategies:
Delicate balance between executable size, performance, and usability
Kotlin uses erasure, but allows to control inlining via the inline keyword.
In inlined functions, types can be locally monomorphized!
Local monomorphization is expressed with the reified keyword.
inline fun <reified T> checkIsType(a: Any): Boolean = a is T // instance check on a generic!
checkIsType<Long>(1) // false
checkIsType<Long>(1L) // true
Note on Java interoperability:
inline functions get inlined if the caller is Kotlin-compiled code,
they don’t if they are called by other bytecode-targeting compilers (javac, scalac…)reified types requires inlining to perform the local monorphization:
the function code is copied on call site, and the compiler must know how to do itSimilar to Scala, but based (for the JVM target) on the Java implementation
toJava()/toScala() equivalentList, Set, Map are unmodifiable but not guaranteed immutable
List may be backed by an ArrayListMutable(List/Set/Map)List
Sequences prevent a collection creation at each stepFlows represent collections that are processed in parallelflowOf/listOf/mapOf/sequenceOf/setOfVery similar to Scala’s case classes:
case classes to inherit from case classes)equals, hashCode, toString for freecopy function, to be used to generate new immutable objectscomponent1, component2, …, componentN functions, called in case of destructuringPair and Triple provided by the standard library
(Tuple4, Tuple5, and so on are not in standard library as opposed as Scala)
If a class has operator functions named called componentX with X an integer from 1,
they can be “destructured”.
This feature is way less powerful than Scala’s pattern matching.
// to is an inline function that creates a Pair, similar to Scala's ->
val ferrari2021 = "Ferrari" to Pair("Sainz", "Leclerc")
val (team, lineup) = ferrari2021
team // "Ferrari"
lineup // Sainz to Leclerc
val (driver1, driver2) = lineup
driver1 // Sainz
driver2 // Leclerc
class A {
operator fun component1() = 1
operator fun component2() = 2
operator fun component3() = 3
}
val (a, b, c) = A()
"$a$b$c"
Similar to Scala’s sealed traits:
classes, not supported for interfaceswhere clausessealed interface Booze {
object Rum : Booze
object Whisky : Booze
object Vodka : Booze
}
fun goGetMeSome(beverage: Booze) = when (beverage) {
is Booze.Rum -> "Diplomatico"
is Booze.Whisky -> "Caol Ila"
is Booze.Vodka -> "Zubrowka"
}
goGetMeSome(Booze.Rum)
static inner classinner modifier must be explicitclass Outer {
private val readMeIfYouCan = 1
class Nested { init { println(readMeIfYouCan) } } // error: unresolved reference: readMeIfYouCan
}
class Outer { class Nested() }
Outer.Nested() // OK
class Outer {
private val readMeIfYouCan = 1
inner class Inner {
init { println(readMeIfYouCan) } // ok
}
}
Outer.Inner() // error: constructor of inner class Inner can be called only with receiver of containing class
Outer().Inner() // OK
Same as Java, with Kotlin syntax
object expressions replace anonymous classes
interface Test {
fun first(): Unit
fun second(): Unit
}
object : Test {
override fun first() { }
override fun second() { }
}
type definitionstypetypealias Drivers = Pair<String, String>
typealias Lineup = Pair<String, Drivers>
typealias F1Season = Map<String, Drivers>
val `f1 2020`: F1Season = mapOf(
Team("Ferrari", Drivers("Vettel", "Leclerc")),
Team("RedBull", Drivers("Versbatten", "Albon")),
Team("Merdeces", Drivers("Hamilton", "Bottas")),
)
`f1 2020` // Map<String, Pair<String, Pair<String, String>>>
Favour composition over inheritance
Ashould extendBonly ifAtruly ‘is-a’ aB, if not, use composition instead, which meansAshould hold a reference ofBand expose a simpler API. J. Bloch, Effective Java, Item 16
Delegation is one of the mechanisms to implement composition,
see the delegation pattern
Delegation is often verbose and very mechanic in implementation
data class Student(val name: String, val surname: String, val id: String)
class Exam : MutableCollection<Student> {
private val representation = mutableListOf<Student>()
override fun add(e E) = representation.add(e)
override fun addAll(e E) = representation.addAll(e)
override fun clear() = representation.clear()
... // BOOOOOOORING
}
byKotlin supports delegation at the language level
data class Student(val name: String, val surname: String, val id: String)
class Exam : MutableCollection<Student> by mutableListOf<Student>() {
fun register(name: String, surname: String, id: String) = add(Student(name, surname, id))
override fun toString() = toList().toString() // No access to the delegate! `toString` unavailable!
}
val exam = Exam()
exam.register("Luca", "Ghiotto", "00000025")
exam // [Student(name=Luca, surname=Ghiotto, id=00000025)]
exam.clear()
exam // []
Properties and variables can be delegated as well
some delegates are built-in, e.g. lazy
val someLazyString by lazy {
println("I'm initializing myself")
"I'm intialized"
}
println("Doing stuff")
println(someLazyString) // "I'm initializing myself" gets printed here
Class properties can be stored in an appropriate Map
Useful when dealing with dynamic languages or untyped serialization (e.g. JSON or YAML)
val fromJson = mapOf("name" to "John Smith", "birthYear" to 2020)
class Person(val jsonRepresentation: Map<String, Any>) {
val name by jsonRepresentation
val birthYear: Int by jsonRepresentation
override fun toString() = "$name born in $birthYear"
}
Person(fromJson)
In case of mutable properties, a MutableMap is required as delegate
val janesJson: MutableMap<String, Any> = mutableMapOf("name" to "Jane Smith", "birthYear" to 1999)
class MutablePerson(val jsonRepresentation: MutableMap<String, Any>) {
var name by jsonRepresentation
var birthYear: Int by jsonRepresentation
override fun toString() = "$name born in $birthYear"
}
val jane = MutablePerson(janesJson)
jane.toString()
jane.name = "Janet Smitherson"
jane.toString()
janesJson // Does it change? {name=Janet Smitherson, birthYear=1999} -- YES! Bidirectional
They represent kinds of properties whose get/set behavior is defined once and for all, e.g.:
Any class with a method:
operator fun getValue(thisRef: T, property: KProperty<*>): R
is a valid delegate for a val is a class
where T is the “owner” type, and R is the type of the property
A valid delegate for a var must also have a setValue method:
operator fun setValue(thisRef: T, property: KProperty<*>, value: P): Unit
where T and R are the same as in getValue, and P is a supertype of R
Note: that this is a form of structural typing, exceptional with the Kotlin type system (typically nominal)
object OwnName {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = property.name
}
val myName by OwnName // "myName"
// can be class if you need to provide parameters
class RepeatName(val times: Int) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = property.name.repeat(times)
}
val tac by RepeatName(3) // "tactactac"
Kotlin lambda expression’s syntax is inspired by Groovy
and is similar to Smalltalk / Ceylon / Xtend / Ruby as well
-> separates them from the bodyitval myLambda = {
println("Hey I'm computing")
}
fun whatsMyReturnType() = {
"A string"
}
myLambda.invoke() // Java-style invocation
myLambda() // Decent-style invocation (invoke is an operator!)
myLambda()() // Guess error: expression 'myLambda()' of type 'Unit' cannot be invoked as a function.
whatsMyReturnType() // Guess Subtle, but the compiler raises warnings
whatsMyReturnType()() // Guess A string
Just as Scala, Kotlin supports function type literals
No need for verbose interfaces such as Function<T, R>, BiConsumer<T, R>, etc.
Function type literals have parameter types in parentheses, a ->, and the return type
() -> Any – 0-ary function returning Any(String) -> Any – Unary function taking a String and returning Any(String, Int) -> Unit – Binary function taking a String and an Int and returning Unit(String, Int?) -> Any? – Binary function taking a String and a nullable Int? returning a nullable Any?Function type literals allow for writing cleaner higher-order functions
fun <T, I, R> compose(f: (I) -> R, g: (T) -> I): (T) -> R = { f(g(it)) }
compose({v: Int -> v * v}, {v: Double -> v.toInt()})(3.9) // 9
Functions can be referred by using ::
the left operand is the receiver (if present)
the right operand is the function name
fun <T, I, R> compose(f: (I) -> R, g: (T) -> I): (T) -> R = { f(g(it)) }
fun square(v: Int) = v * v
fun floor(v: Double) = v.toInt()
compose(::square, ::floor)(3.9)
A simple special rule that enables very elegant syntactic forms:
if a lambda expression is the last parameter in a function call
then it can be placed outside of the parentheses
If used correctly, feels like adding custom blocks to a language
// Java's thread + trailing lambda + SAM conversion
fun delayed(delay: Long = 1000L, operation: () -> Unit) = Thread {
Thread.sleep(delay)
operation()
}.start()
println("Start")
// Now we have a delayed block!
delayed {
println("I was waiting")
}
delayed(300) { println("I wait less") }
println("Finished")
Closures are supported
They are allowed on vars as well as on vals
// Side effecting from functional manipulation is bad though
var sum = 0
(0..100).map {
sum += it
it * 2
}
sum
sum == (0..100).sum()
return from lambdasreturn in lambda expressions is forbiddeninline, since its code and the code of its lambdas are copied at call site,
return is allowed and returns from the enclosing functionfun breakingFlow(): List<Int> = (0..10).toList().map { // map is declared inline in the stdlib
if (it > 4) {
return (0..it).toList() // returns from breakingFlow
}
it
}
breakingFlow()
return can be used to return from lambdas:fun breakingFlow(): List<Int> = (0..10).toList().map {
if (it > 4) {
return@map it * 10 // returns from the lambda
}
it
}
breakingFlow()
crossinlineThe return from inline functions can be disabled by marking the lambda parameter with the crossinline keyword
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable { override fun run() = body() } // body is called from another context and cannot return from f
}
noinlineCross-inlining is insufficient when the lambda must be passed around or stored for later use
noinlinecrossinline or noinline are requiredLambda parameters can be destructured
mapOf(46 to "Rossi", 4 to "Dovizioso").map { (number, rider) ->
// destructured Pair
"$rider has number $number"
}
Kotlin allows to extend any type capabilities from anywhere
via extension functions
fun String.containsBatman(): Boolean = ".*b.*a.*t.*m.*a.*n.*".toRegex().matches(this)
"battere le mani".containsBatman() // true
Inside extension functions, the receiver of the method is overridden
Any type, including nullables, can be extended
objects and companions can be extended as well
IMPORTANT: calls to extension methods are resolved statically.
Namely, the receiver type is determined at compile time.
IMPORTANT/2: Extensions cannot shadow members, members always take priority
Same as functions, but for properties
val String.containsBatman get(): Boolean = ".*b.*a.*t.*m.*a.*n.*".toRegex().matches(this)
"battere le mani".containsBatman // true
Note:
get and set accessors.Extensions functions are… functions, like any other
as such, their type can be legally expressed by:
.// Extension function taking an extension function as parameter
fun <T> MutableList<T>.configure(configuration: MutableList<T>.() -> Unit): MutableList<T> {
configuration()
return this
}
// We are creating a configuration block!
mutableListOf<String>().configure {
add("Pippo")
add("Pluto")
add("Paperino")
}
…sounds easy to write DSLs…
When extensions are defined as members, there are multiple implicit recevers:
object or instance of the class in which the extension is declaredExtension receivers have priority, dispatch receivers access requires the qualified this syntax
object Batman { // the Batman object is the dispatch receiver
val name = "Batman"
val String.Companion.intro get() = generateSequence { Double.NaN } // String.Companion is extension receiver
.take(10)
.joinToString(separator = "")
fun String.withBatman() = "$this ${ this@Batman.name }!" // Qualified this access to the dispatch receiver
}
Extension members are visible only when the dispatch receiver is the type where the extensions were defined
This enables a powerful form of scope control
object Batman { // Batman is the dispatch receiver
val name = "Batman"
val String.Companion.intro get() = generateSequence { Double.NaN } // String is extension receiver
.take(10)
.joinToString(separator = "")
fun String.withBatman() = "$this ${ this@Batman.name }!" // Qualified this access to the dispatch receiver
}
// Extension members are actual members! They require a receiver!
String.intro.withBatman() // error: unresolved reference: intro
fun <T, R> insideTheScopeOf(receiver: T, method: T.() -> R): R = receiver.method()
insideTheScopeOf(Batman) { // inside this function, Batman is the dispatch receiver!
String.intro.withBatman() // OK!
}
Kotlin provides a number of built-in functions that run a lambda expression in a custom scope:
insideTheScopeOf in the previous slide)it parameterlet : T.((T) -> R) -> RCan be invoked on an object, passing a lambda expression.
The lambda parameter is bound to the let receiver
the return type is the result of the function
1.let { "${it + 1}1" } // 21: String
1.let { one -> "${one + 1}1" } // Same as above: it's a normal lambda
run : T.(T.() -> R) -> RCan be invoked on an object, passing a lambda expression.
The method receiver is bound to the implicit receiver this
the return type is the result of the function
1.run { "${this + 1}1" } // 21: String
with : (T, T.() -> R) -> RNon-extension version of run,
the context object is passed as first parameter
The method receiver is bound to the implicit receiver this
the return type is the result of the function
with(1) { "${this + 1}1" } // 21: String
apply : T.(T.() -> Unit) -> TSimilar to run,
but returns the context object
Used to cause side effects from a specific context,
and returning the original object
1.apply { println("${this + 1}1") } // Prints 21, returns 1
mutableListOf<Int>().apply {
addAll((1..10).toList())
} // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
also : T.((T) -> Unit) -> TSimilar to apply, but does not change the context,
the context object is bound to the first lambda parameter
Used to cause side effects and returning the original object
1.also { println("${it + 1}1") } // Prints 21, returns 1
A lot of language details have been left out of this guide, non complete list:
value classesdanilo.pianini@unibo.itCompiled on: 2025-11-21 — printable version