danilo.pianini@unibo.it
Compiled on: 2024-11-21 — printable version
JetBrains-made modern programming language
Gaining momentum since Google adopted is as official Android language
(along with Java and C++)
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 memebers 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!
?:
Yeah it’s actually named after Elvis Presley due to his 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 typeObject
Any
Nothing
Any
Object
Any
Nothing
Any
Any?
Nothing
Boolean
sExactly as Java/Scala, but with nullability:
Boolean
: true
/false
Boolean?
: true
/false
/null
&&
, !!
, and !
operators work for non-nullable Boolean
s.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
, ULong
Implicit 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
: Map
s 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 works
val 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]
Same as Java, plus aliasing.
Imports go at the top of file, no locally scoped imports as in Scala
implicit
s 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
val
class JunitTest {
@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
if
if
/else
is an expression and works just as in Scalaif
alone is not an expressionfor
for(init; condition; then) { block }
loopfor
/in
: for (element in collection) { block }
for
while
and do
/while
do
-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")
when
Kotlin 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 fun
ctions 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 class
class 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
init
Similar 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)
constructor
sMore 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
lateinit
It 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
open
Kotlin 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. open
The 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
memebers 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
object
sSame as Scala, but with explicit companion
s
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
where
In 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 ArrayList
Mutable
(List
/Set
/Map
)List
Sequence
s prevent a collection creation at each stepFlow
s represent collections that are processed in parallelflowOf
/listOf
/mapOf
/sequenceOf
/setOf
Very similar to Scala’s case class
es:
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 trait
s:
class
es, not supported for interface
swhere
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
definitionstype
typealias 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
A
should extendB
only ifA
truly ‘is-a’ aB
, if not, use composition instead, which meansA
should hold a reference ofB
and 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
}
by
Kotlin 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
A valid delegate for a val
is a class
with a method:
operator fun getValue(thisRef: T, property: KProperty<*>): R
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): R
where T and R are the same as in getValue
, and P is a supertype of R
Kotlin lambda expression’s syntax is inspired by Groovy
and is similar to Smalltalk / Ceylon / Xtend / Ruby as well
->
separates them from the bodyit
val 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 var
s as well as on val
s
// Side effecting from functional manipulation is bad though
var sum = 0
(0..100).map {
sum += it
it * 2
}
sum
sum == (0..100).sum()
Kotlin rule: return
returns from the closest named fun
ction
fun breakingFlow(): List<Int> = (0..10).toList().map {
if (it > 4) {
return (0..it).toList() // returns from breakingFlow
}
it
}
breakingFlow()
A qualified 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()
Lambda 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
object
s and companion
s 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) -> R
Can be invoked on an object, passing a lambda expression.
The method receiver is bound to the lambda parameter
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) -> R
Can 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.() -> R) -> R
Non-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) -> T
Similar 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) -> T
Similar 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:
noinline
and crossinline
inline class
esdanilo.pianini@unibo.it
Compiled on: 2024-11-21 — printable version