Kotlin - ttulka/programming GitHub Wiki

Fundamentals

fun main(args: Array<String>) {
    println("Hello, World!")
}

Variables

// read-only
val a: Int = 1  // immediate assignment
val b = 2   // `Int` type is inferred
val c: Int  // Type required when no initializer is provided
c = 3       // deferred assignment

// mutable
var x = 5 // `Int` type is inferred
x += 1

Lazy property:

val p: String by lazy {
    // compute the string
}

The lazy function return an object that has a method called getValue with the proper signature, so you can use it together with the by keyword to create a delegated property. The argument of lazy is a lambda that is calls to initialize the value. The lazy function is thread-safe by default.

Swapping two variables:

var a = 1
var b = 2
a = b.also { b = a }

Destructuring:

val (name, age) = person

for ((key, value) in map) {
}

Underscore for unused variables:

val (_, status) = getResult()

Functions

fun sum(a: Int, b: Int): Int {
    return a + b
}
// or
fun sum(a: Int, b: Int) = a + b

fun printSum(a: Int, b: Int)/*: Unit*/ {
    println("sum of $a and $b is ${a + b}")
}

Default arguments

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { /*...*/ }
fun foo(bar: Int = 0, baz: Int) { /*...*/ }

foo(baz = 1) // The default value bar = 0 is used
  • Overriding methods always use the same default parameter values as the base method.

Varargs

fun printAll(str: String, vararg nums: Int) {
    for (n in nums) println(n)
}

printAll("abc", 1, 2, 3)

val arr = intArrayOf(1, 2, 3)
printAll("abc", *arr)

Infix notation

Omitting the dot and the parentheses for the call.

infix fun Int.shl(x: Int): Int { ... }

1 shl 2  // infix notation
// is the same as:
1.shl(2)

Lower precedence than the arithmetic operators, type casts, and the rangeTo operator:

  • 1 shl 2 + 3 is equivalent to 1 shl (2 + 3)

Tail recursive functions

Kotlin supports a style of functional programming known as tail recursion.

val eps = 1E-10 // "good enough", could be 10^-15

tailrec fun findFixPoint(x: Double = 1.0): Double
    = if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))

Higher-Order Functions

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun 

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // hellohellohello

Anonymous Functions

max(strings, { a, b -> a.length < b.length })
ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter(fun(item) = item > 0)

Closures

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

Inline Functions

Overhead can be eliminated by inlining the lambda expressions. Inlining means the body is copied into the bytecode instead of making a call.

  • Inlining may cause the generated code to grow.
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }

Scope Functions

To execute a block of code within the context of an object.

  • let, run, with, apply, and also.
  • let and run use it as available context object argument.
  • apply and also return the context object.
  • let, run, and with return the lambda result.
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    val firstItem = first()
    val lastItem = last()        
    println("First item: $firstItem, last item: $lastItem")
}
let
  • can be used to invoke one or more functions on results of call chains.
  • is often used for executing a code block only with non-null values.
val str: String? = "Hello"   
//processNonNullString(str)  // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
    it.length
}
with
  • recommended for calling functions on the context object without providing the lambda result.
  • or as a helper object whose properties or functions will be used for calculating a value.
  • can be read as “with this object, do the following.”
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)
run
  • useful when your lambda contains both the object initialization and the computation of the return value.
  • lets you execute a block of several statements where an expression is required.
val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}
// +1234
// -FFFF
// -a
// be
apply
  • for code blocks that don't return a value and mainly operate on the members of the receiver object.
  • common case is the object configuration.
  • can be read as “apply the following assignments to the object.”
val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
also
  • for performing some actions that take the context object as an argument.
  • for actions that need a reference rather to the object than to its properties and functions, or when you don't want to shadow this reference from an outer scope.
  • can read it as “and also do the following with the object.”
val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")
takeIf and takeUnless

These functions let you embed checks of the object state in call chains.

  • takeIf returns this object if it matches the predicate.
  • takeUnless returns the object if it doesn't match the predicate and null if it does.
val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }

Lambdas

One can move lambdas out of the parenthesis if the lambda is the last parameter:

run { println("I'm in a lambda") }
// is a shortcut for: run({ println("I'm in a lambda") })
// or { println("I'm in a lambda") }()

If a lambda has only one parameter, it can be used instead:

val lambda: (Int) -> String = { "Value: $it" }
// is a shortcut for: val lambda: (Int) -> String = { v -> "Value: $v" }
typealias IntToString = (Int) -> String

val lambda1: IntToString = { i -> "$i" }
val lambda2: IntToString = { "$it" }
val lambda3: IntToString = Int::toString  // callable reference to an existing declaration
val lambda4: IntToString = fun (i) = "$i"
interface IntToString: (Int) -> String

val lambda1: IntToString = object: IntToString { 
    override operator fun invoke(i: Int) = "$i"
}
val lambda2: (Int) -> String = object: IntToString { 
    override operator fun invoke(i: Int) = "$i"
}
class IntToString: (Int) -> String {
    override operator fun invoke(i: Int): String = "$i"
}
val lambda: (Int) -> String = IntToString()

Lambdas with receivers

fun countTo100(): String {
    val sb = StringBuilder()
    return with(sb) {
         for (i in 1..100) {
             append(i)
             append(", ")
         }
         toString()  // last expression is returned
    }
}
// or even:
fun countTo100() =
    with(StringBuilder()) {
         for (i in 1..100) {
             append(i)
             append(", ")
         }
         toString()
    }
// or:
fun countTo100() =
    StringBuilder().apply {
         for (i in 1..100) {
             append(i)
             append(", ")
         }         
    }.toString()

Local returns with labels

fun findByName(people: List<Person>, name: String) {
    people.forEach myReturnLabel@ {
        if (it.name == name) {
            println("Person $name found.")
            return@myReturnLabel  // otherwise the whole function would return
        }
    }
    println("Finding a person finished.")
}
"Some string".apply somestr@ {
    "Other string".apply {
        println(this.toUpperCase())  // OTHER STRING
        println(this@somestr.toUpperCase())  // SOME STRING
    }
}

Operator overloading

https://kotlinlang.org/docs/reference/operator-overloading.html

Expression Translated to
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
`a++ a.inc()
a-- a.dec()
a + b a.plus(b)
a - b a.minus(b)
data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
   println(-point)  // prints "Point(x=-10, y=-20)"
}

Imports

import org.example.Message // Message is now accessible without qualification
// or
import org.example.* // everything in 'org.example' becomes accessible
// or
import org.test.Message as testMessage // testMessage stands for 'org.test.Message'

Default Imports

  • kotlin.*
  • kotlin.annotation.*
  • kotlin.collections.*
  • kotlin.comparisons.*
  • kotlin.io.*
  • kotlin.ranges.*
  • kotlin.sequences.*
  • kotlin.text.*
  • java.lang.* (JVM)
  • kotlin.jvm.* (JVM)
  • kotlin.js.* (JS)

Data Types

Everything is a class (no primitive types).

var myInt = 2
var myLong = 2L  // val myLong: Long = 2

//myLong = myInt // Error
myLong = myInt.toLong()

val myByte: Byte = 2
val myShort: Short = 2

val myDouble = 2.0
val myFloat = 2.0f

//myDouble = myFloat // Error
myDouble = myFloat.toDouble()

val myChar = 'a'
val myChar2 = 97.toChar()  // 'a'

val myBoolean = true
  • Any is basis for toString(), hashCode() and equals() (==)
  • Unit is a singleton object as void for functions returning nothing.
  • Nothing is a subclass of any class. For infinite loops, etc.

Terms type and class are not equivalent. For example, a class can be used to declare a nullable type: var x: Int?, that means each Kotlin class can be used to construct at least two types.

Automatic Casts

If an immutable local variable or property is checked for a specific type, there's no need to cast it explicitly:

if (obj is String) {
    // `obj` is automatically cast to `String` in this branch
    return obj.length
}
// or even
// `obj` is automatically cast to `String` on the right-hand side of `&&`
if (obj is String && obj.length > 0) {
    return obj.length
}

Nulls

A reference must be explicitly marked as nullable when null value is possible:

fun parseInt(str: String): Int? {
    return null
}
  • == operator does null check automatically.
  • [] array operator does null check automatically.

Nullable values are automatically cast to non-nullable after null check:

val x = parseInt(arg1)
val y = parseInt(arg2)

// println(x * y) // Compilation Error
if (x != null && y != null) {
    println(x * y)  // ok here
}

If not null shorthand:

val files = File("Test").listFiles()

println(files?.size)

If not null and else shorthand:

val files = File("Test").listFiles()

println(files?.size ?: "empty")

Executing a statement if null:

val values = ...
val email = values["email"] ?: throw Exception("Email is missing!")

Execute if not null:

val value = ...

value?.let {
    // execute this block if not null
}

Map nullable value if not null:

val value = ...

val mapped = value?.let { transformValue(it) } ?: defaultValue 
// defaultValue is returned if the value or the transform result is null.

Consuming a nullable Boolean:

val b: Boolean? = ...
if (b == true) {
    ...
} else {
    // `b` is false or null
}

If we do want NullPointerException to be thrown:

val str: String? = null
str!!.toUpperCase()  // assertion !! thows when null

Strings

String templates
var a = 1
// simple name in template:
val s1 = "a is $a" 

a = 2
// arbitrary expression in template:
val s2 = "${s1.replace("is", "was")}, but now is $a"
Raw strings
val filePath = """c:\dir\file"""
val multiLine = """This is printed
                  |on multiple
                  |lines.""".trimMargin()

Arrays

val strings: Array<String> = arrayOf("a","b","c")
val longs = arrayOf<Long>(1,2,3)
longs[0]  // 1L

val integers: Array<Int>
integers = arrayOf(1,2,3)

val mixed = arrayOf(1, "Abc", true)
mixed is Array<Any>  // true
val evenNumbers = Array(16) { i -> i * 2 }
val allZeros = Array(16) { 0 }
val nulls = arrayOfNulls<Int?>(5)
val integers1 = arrayOf(1,2,3)
val integers2 = arrayOf(4,5)
val allIntegers = arrayOf(*integers1, *integers2, 6)
// [1, 2, 3, 4, 5, 6]
Primitive Arrays
  • better performance
  • compatible with Java's arrays (eg. int[])
val myIntArray = intArrayOf(1,2,3)

val myIntArray2 = arrayOf(1,2,3).toIntArray()
myIntArray2 is IntArray  // true

val myIntArray3 = myIntArray2.toTypedArray()
myIntArray3 is Array<Int>  // true
  • constructor doesn't need the second init value:
val myIntArray = IntArray(16)

Conditionals

fun maxOf(a: Int, b: Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}
// or
fun maxOf(a: Int, b: Int) = if (a > b) a else b

When expression

fun describe(obj: Any): String =
    when (obj) {
        1          -> "One"
        2, 3       -> "Two or Three"
        in 10..99  -> "Between 10 and 99"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }

Checking if a collection contains an object using in operator:

when {
    "orange" in items -> println("juicy")
    "apple" in items -> println("apple is fine too")
}

Ranges

val r1 = 1..5
val r2 = 'a'..'z'
val r3 = "ABC".."XYZ"

3 in r1     // true
'c' in r2   // true
"DFC" in r3 // true

Check if a number is within a range using in operator:

val x = 10
val y = 9
if (x in 1..y+1) {
    println("fits in range")
}

For Loop

val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
    println(item)
}
// or
val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
    println("item at $index is ${items[index]}")
}
for ((k, v) in map) {
    println("$k -> $v")
}
for (x in 1..5) {
    print(x)
}
for (x in 1..10 step 2) {
    print(x)
}
println()
for (x in 9 downTo 0 step 3) {
    print(x)
}

While Loop

val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
    println("item at $index is ${items[index]}")
    index++
}

Returns and Jumps

  • return by default returns from the nearest enclosing function or anonymous function.
  • break terminates the nearest enclosing loop.
  • continue proceeds to the next step of the nearest enclosing loop.
loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (...) break@loop
    }
}

Objects

Classes

class Invoice { /*...*/ }
// or
class Invoice

Classes can contain class members:

  • Constructors and initializer blocks
  • Functions
  • Properties
  • Nested and Inner Classes
  • Object Declarations
Constructors

A class in Kotlin can have a primary constructor and one or more secondary constructors:

  • The primary constructor cannot contain any code.
class Person constructor(val name: String) { /*...*/ }
// or
class Person constructor(name: String) { /*...*/ }
// or
class Person(name: String) { /*...*/ }
class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<>()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

Initialization code can be placed in initializer blocks:

  • Initializer blocks are executed in the same order as they appear in the class body, interleaved with the property initializers.
class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println) // 1
    
    init {
        println("First initializer block that prints ${name}") // 2
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println) // 3
    
    init {
        println("Second initializer block that prints ${name.length}") // 4
    }
}
class Constructors {
    constructor(i: Int) {
        println("Constructor")  // 2
    }
    init {
        println("Init block")   // 1
    }
}

If the constructor has annotations or visibility modifiers, the constructor keyword is required, and the modifiers go before it:

class Person public @Inject constructor(name: String) { /*...*/ }

class DontCreateMe private constructor () { /*...*/ }

Instances

Kotlin does not have a new keyword:

val invoice = Invoice()

val person = Person("Joe Smith")

Inheritance

open class Base(p: Int)

class Derived(p: Int) : Base(p)
  • All classes in Kotlin have a common superclass Any.
  • By default, Kotlin classes are final: they can’t be inherited.
  • To make a class inheritable, mark it with the open keyword:
open class Base  // open for inheritance
  • Each secondary constructor has to initialize the base type using the super keyword:
class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
Overriding methods

Kotlin requires explicit modifiers for overridable members:

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}
Calling the superclass implementation

Code in a derived class can call its superclass functions and property accessors implementations using the super keyword:

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }
    val fillColor: String get() = super.borderColor
}

Inside an inner class, accessing the superclass of the outer class is done with the super keyword qualified with the outer class name: super@Outer:

class FilledRectangle: Rectangle() {
    fun draw() { /* ... */ }
    val borderColor: String get() = "black"
    
    inner class Filler {
        fun fill() { /* ... */ }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn with color ${super@FilledRectangle.borderColor}")
        }
    }
}
Abstract classes

A class and some of its members may be declared abstract:

open class Polygon {
    open fun draw() {}
}
abstract class Rectangle : Polygon() {
    abstract override fun draw()
}

Properties and Fields

Getters and Setters
class Person(name: String) {
    var name = name
    get() { return field }
    set(value) { field = value }
}
// is equivalent to
class Person(var name: String)
var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }
val isEmpty get() = this.size == 0
var setterVisibility: String = "abc"
    private set 

var setterWithAnnotation: Any? = null
    @Inject set
Late-Initialized Properties and Variables

When you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}
Package Visibility Modifiers
  • public (default) - visible everywhere,
  • private - only be visible inside the file containing the declaration,
  • protected - not available for top-level declarations,
  • internal - visible everywhere in the same module.
// file name: example.kt
package foo

private fun foo() { ... } // visible inside example.kt

public var bar: Int = 5 // property is visible everywhere
    private set         // setter is visible only in example.kt
    
internal val baz = 6    // visible inside the same module
Classes and Interfaces Visibility Modifiers
  • public (default) - any client who sees the declaring class sees its public members,
  • private - means visible inside this class only (including all its members),
  • protected - same as private + visible in subclasses too,
  • internal - any client inside this module who sees the declaring class sees its internal members.
open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // public by default
    
    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a is not visible
    // b, c and d are visible
    // Nested and e are visible

    override val b = 5   // 'b' is protected
}

class Unrelated(o: Outer) {
    // o.a, o.b are not visible
    // o.c and o.d are visible (same module)
    // Outer.Nested is not visible, and Nested::e is not visible either 
}

Delegation

interface Base {
    fun print()
}
class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}
class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

Equality

val employee1 = Employee("Mary", 33)
val employee2 = Employee("Mary", 33)

employee1 == employee2   // true, Structural Equality
employee1 === employee2  // false, Referencial Equality

Smart Casting

val something: Any = ...
if (something is Employee) {
    var employee = something // as Employee // not necessary
    println(emloyee.name)
}

Companion Objects

class MyClass private constructor() {
    companion object Factory { /* name is optional */
        fun create(): MyClass = MyClass()
    }
}
//val instance = MyClass() // Error, private constructor
val instance = MyClass.create()
interface Factory<T> {
    fun create(): T
}
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}
val f: Factory<MyClass> = MyClass
val instance = f.create()

Interfaces

interface MyInterface {
    val prop: Int // abstract
    val propertyWithImplementation: String
        get() = "foo"

    fun bar()
    fun foo() {
      // optional body
    }
}
class Child : MyInterface {
    override val prop: Int = 29
    override fun bar() {
        // body
    }
}

Extensions

Kotlin provides the ability to extend a class with new functionality without having to inherit from the class.

  • Extensions are resolved statically
// The following adds a swap function to MutableList<Int>:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

Data Classes

Classes whose main purpose is to hold data.

  • The primary constructor needs to have at least one parameter.
  • All primary constructor parameters need to be marked as val or var.
  • Data classes cannot be abstract, open, sealed or inner.
  • Data classes may only implement interfaces.
data class Person(val name: String, val age: Int)

Following members from all properties declared in the primary constructor:

  • equals() / hashCode() pair;
  • toString() of the form "User(name=John, age=42)";
  • componentN() functions corresponding to the properties in their order of declaration;
  • copy() function
val jack = Person(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

Destructuring Declarations

val jane = User("Jane", 35) 
val (name, age) = jane
println("$name, $age years of age") // prints "Jane, 35 years of age"

Sealed Classes

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type.

  • They are, in a sense, an extension of enum classes.
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // the `else` clause is not required because we've covered all the cases
}

Marking a superclass with the sealed modifier restricts creating subclasses. All the direct subclasses must be nested in the superclass.

If you handle all subclasses of a sealed class in a when expression, you don't need to provide the default branch.

Generics

class Box<T>(t: T) {
    var value = t
}
val box: Box<Int> = Box<Int>(1)
// or
val box = Box(1)
val list: ArrayList<Int> = arrayListOf(1, 2, 3)

// List is covariant: List<out E>
val li2: List<Int> = list
val li3: List<Number> = list
val li4: List<Any> = list
val li5: List<*> = list

// ArrayList is invariant, not covariant
//val li6: ArrayList<Number> = list  // Error
fun <T> processList(list: List<T>) { } 
// equivalent to
fun <T: Any?> processList(list: List<T>) { } 

// only non-nullable values:
fun <T: Any> processList(list: List<T>) { } 

fun <T: String> upperList(list: List<T>) =
    list.map { it.toUpperCase() }

fun <T> appendList(list: List<T>)
	where T: CharSequence, T: Appendable =
    list.map { it.append(",") }

Reified parameters

  • As JVM removes generics in the runtime, one cannot check generic type T.
  • In Kotlin this is possible with inline function and reified parameters:
inline fun <reified T> getElementOfType(list: List<Any>): List<T> =
    list.filter { it is T } as List<T>

val integers: List<Int> = getElementOfType(listOf("abc", 1, true, 5)) // [1, 5]

Declaration-site variance

A generic class, eg. MutableList, is called invariant on the type parameter if, for any two different types A and B, MutableList<A> isn't a subtype of MutableList<B>. In Java, all classes are invariant.

A covariant class is a generic class (eg. Producer<T>) for which holds: Producer<A> is a subtype of Producer<B> if A is a subtype of B, subtyping is preserved. For example, Producer<Cat> is a subtype of Producer<Animal>.

interface Source<out T> {  // covariance, T can be only used as return type
    fun nextT(): T         // we can only read, not write
}
fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // This is OK, since T is an out-parameter
    // ...
}

A class that is contravariant on the type parameter is a generic class (eg. Consumer<T>) for which holds: Consumer<A> is a subtype of Consumer<B> if B is a subtype of A, subtyping is reversed. For example, Consumer<Animal> is a subtype of Consumer<Cat>.

interface Comparable<in T> {  // contravariance, T can be only used as parameter type
    operator fun compareTo(other: T): Int  // we can only write, not read
}
fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
    // Thus, we can assign x to a variable of type Comparable<Double>
    val y: Comparable<Double> = x // OK!
}

Use-site variance declarations in Kotlin correspond directly to Java bounded wildcards. MutableList<out T> means the same as MutableList<? extends T> and MutableList<in T> corresponds to MutableList<? super T>.

Enum Classes

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

Object Expressions

When we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it.

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }
    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}
fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

Singletons and Anonymous Instances

object Resource {
    val name = "Name"
    fun printName() = println("Resource $name")
}
Resource.name
Resource.printName()
interface MyInterface

fun myfun(o: MyInterface) {}

myfun(object: MyInterface {}) // anonymous instance of MyInterface
}

Type aliases

typealias FileTable<K> = MutableMap<K, MutableList<File>>
typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}

Exceptions

Only unchecked exceptions exist in Kotlin.

fun getNumber(str: String): Int {
    return try {
        Integer.parseInt(str)
    } catch (e: NumberFormatException) {
        -1
    } finally {
        println("We're done.")
    }
}

Try with resources

val stream = Files.newInputStream(Paths.get("/some/file.txt"))
stream.buffered().reader().use { reader ->
    println(reader.readText())
}

Collections

Collection and MutableCollection

List and MutableList

val bob = Person("Bob", 31)
val people = listOf<Person>(Person("Adam", 20), bob, bob)
val people2 = listOf<Person>(Person("Adam", 20), Person("Bob", 31), bob)
println(people == people2)
bob.age = 32
println(people == people2)
val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5)
numbers.removeAt(1)
numbers[0] = 0
numbers.shuffle()
println(numbers)

Set and MutableSet

val numbers = setOf(1, 2, 3, 4)
println("Number of elements: ${numbers.size}")
if (numbers.contains(1)) println("1 is in the set")

Map and MutableMap

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

println("All keys: ${numbersMap.keys}")
println("All values: ${numbersMap.values}")
if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")    
if (1 in numbersMap.values) println("The value 1 is in the map")
if (numbersMap.containsValue(1)) println("The value 1 is in the map") // same as previous

Sequences

Sequences offer the same functions as Iterable but implement another approach to multi-step collection processing.

  • Multi-step processing of sequences is executed lazily when possible.
  • Performs all the processing steps one-by-one for every single element.
    • Iterable completes each step for the whole collection and then proceeds to the next step.
  • Similar to Stream in Java.
  • Itermediate and terminal operations.
val numbersSequence = sequenceOf("four", "three", "two", "one")

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence() 

val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]

To create a finite sequence, provide a function that returns null after the last element you need:

val oddNumbersLessThan10 = generateSequence(1) { if (it < 10) it + 2 else null }
println(oddNumbersLessThan10.count())  // 6
val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]

Constructing Collections

val empty = emptyList<String>()
val doubled = List(3, { it * 2 })
val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Copy size: ${copyList.size}")   

//readOnlyCopyList.add(4)             // compilation error
println("Read-only copy size: ${readOnlyCopyList.size}")

plus and minus

val numbers = listOf(1, 2, 3)

val plusList = numbers + 4    // [1, 2, 3, 4]
val minusList1 = numbers - listOf(1, 3)  // [2]
val minusList2 = numbers - 3  // [1, 2]
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

numbersMap + Pair("four", 4)  // {one=1, two=2, three=3, four=4}
numbersMap - "one"                  // {two=2, three=3}
numbersMap - listOf("two", "four")  // {one=1, three=3}

Retrieving Single Elements

val numbers = listOf("one", "two", "three")

numbers.elementAt(2)          // three
numbers.elementAtOrNull(9)    // null
numbers.elementAtOrElse(9) {  // value for 9 undefined
    index -> "value for $index undefined"}

numbers.first()  // one
numbers.last()   // three
numbers.first { it.startsWith("t") }  // two
numbers.last { it.startsWith("t") }   // three
numbers.firstOrNull { it.length > 9 } // null

numbers.find { it.startsWith("t") }      // two
numbers.findLast { it.startsWith("t") }  // three

numbers.random() // one or two or three

numbers.contains("four")  // false
"zero" in numbers         // false

numbers.containsAll(listOf("four", "two")) // false
numbers.containsAll(listOf("one", "zero")) // false

numbers.isEmpty()    // false
numbers.isNotEmpty() // true

Checking existence

println(numbers.contains("four"))
println("zero" in numbers)

println(numbers.containsAll(listOf("four", "two")))
println(numbers.containsAll(listOf("one", "zero")))
println(numbers.isEmpty())
println(numbers.isNotEmpty())

Ordering

class Version(val version: Int) : Comparable<Version> {
    override fun compareTo(other: Version): Int = 
        when {
            other.version > this.version -> -1
            other.version < this.version -> 1
            else                         -> 0
        }
}
val v1 = Version(1)
val v2 = Version(2)

println(v1 > v2)  // false
println(v1 <= v2) // true

val list1 = listOf(v1, v2)
val list2 = listOf(v2, v1)
    
println(list1 == list2)	         // false
println(list1 == list2.sorted()) // true

Collection Operations

Mapping

val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })  // [3, 6, 9]
println(numbers.mapIndexed { idx, value -> value * idx })  // [0, 2, 6]

Zipping

Zipping transformation is building pairs from elements with the same positions in both collections.

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear")
println(colors.zip(animals))  
// [(red, fox), (brown, bear)]

Association

Association transformations allow building maps from the collection elements and certain values associated with them.

val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })
// {one=3, two=3, three=5, four=4}

Flattening

flatten:

val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5))
println(numberSets.flatten())  // [1, 2, 3, 4, 5]

flatMap:

val containers = listOf(
    StringContainer(listOf("one", "two", "three")),
    StringContainer(listOf("four", "five"))
)
println(containers.flatMap { it.values })
// [one, two, three, four, five]

Filtering

val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3) // [three, four]

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") }
println(filteredMap) // {key1=1, key11=11}
val numbers = listOf("one", "two", "three", "four")

val filteredIdx = numbers.filterIndexed { 
   index, s -> index != 0 && s.length < 5 }
// [two, four]

val filteredNot = numbers.filterNot { it.length <= 3 }
// [three, four]
val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
    println(it.length)   // length is unavailable for nullable Strings
}

Partitioning

val numbers = listOf("one", "two", "three", "four")
val (match, rest) = numbers.partition { it.length > 3 }

println(match)  // [three, four]
println(rest)   // [one, two]

Testing predicates

val numbers = listOf("one", "two", "three", "four")

numbers.any { it.endsWith("e") }  // true
numbers.none { it.endsWith("a") } // true
numbers.all { it.endsWith("e") }  // false

emptyList<Int>().all { it > 5 }   // true, vacuous truth

Grouping

val numbers = listOf("one", "two", "three", "four", "five")

numbers.groupBy { it.first().toUpperCase() }
// {O=[one], T=[two, three], F=[four, five]}

numbers.groupBy(keySelector = { it.first() }, 
                valueTransform = { it.toUpperCase() })
// {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}

Slicing

val numbers = listOf(1, 2, 3, 4, 5, 6)

numbers.slice(1..3)            // [2, 3, 4]
numbers.slice(0..4 step 2)     // [1, 3, 5]
numbers.slice(setOf(3, 5, 0))  // [4, 6, 1]

Take and drop

val numbers = listOf(1, 2, 3, 4, 5, 6)

numbers.take(3)      // [1, 2, 3]
numbers.takeLast(3)  // [4, 5, 6]
numbers.drop(1)      // [2, 3, 4, 5, 6]
numbers.dropLast(5)  // [1]

Chunked and Windowed

  • Chunked - to break a collection onto parts of a given size.
  • Windowed - to retrieve all possible ranges of the collection elements of a given size.
val numbers = (1..5).toList()

numbers.chunked(2)  // [[1, 2], [3, 4], [5]]
numbers.windowed(2) // [[1, 2], [2, 3], [3, 4], [4, 5]]

Aggregating

  • min, max, average, sum, count.
  • minBy, maxBy take a selector function.
  • minWith, maxWith take a Comparator.
  • sumBy, sumByDouble apply functions that return Int / Double.
val numbers = listOf(1, 2, 3)

println(numbers.sumBy { it * 2 })  // 12
println(numbers.sumByDouble { it.toDouble() / 2 })  // 3
  • reduce, fold (takes an initial value)
val numbers = listOf(1, 2, 3)

numbers.reduce { sum, element -> sum + element }  // 6
numbers.fold(1) { sum, element -> sum + element } // 7

Coroutines

Coroutines are light-weight threads.

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

Waiting for a job

val job = GlobalScope.launch { // launch a new coroutine
    delay(1000L)
    println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes

Cancellation

val job = launch {
    repeat(1000) { i ->
        println("job: I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion 
// or job.cancelAndJoin()
println("main: Now I can quit.")

Timeout

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

Suspending Functions

suspend fun doSomethingOne(): Int {
    delay(1000L)
    return 13
}
suspend fun doSomethingTwo(): Int {
    delay(1000L) 
    return 29
}

val time = measureTimeMillis {
    val one = doSomethingOne()
    val two = doSomethingTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")
// The answer is 42
// Completed in 2013 ms

val time = measureTimeMillis {
    val one = async { doSomethingOne() }
    val two = async { doSomethingTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
// The answer is 42
// Completed in 1017 ms

Async-style functions

Async functions are not suspending functions.

  • They can be used from anywhere.
  • Their use always implies asynchronous (here meaning concurrent) execution.
// The result type is Deferred<Int>
fun somethingOneAsync() = GlobalScope.async {
    doSomethingOne()
}
// The result type is Deferred<Int>
fun somethingTwoAsync() = GlobalScope.async {
    doSomethingTwo()
}

// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingOneAsync()
        val two = somethingTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}
// The answer is 42
// Completed in 1120 ms

Dispatchers and threads

Dispatcher determines what thread or threads the corresponding coroutine uses for its execution.

launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
// Unconfined            : I'm working in thread main @coroutine#3
// Default               : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4
// newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#5
// main runBlocking      : I'm working in thread main @coroutine#2

When launch { ... } is used without parameters, it inherits the context (and thus dispatcher) from the CoroutineScope it is being launched from.

Flows

To represent the stream of values that are being asynchronously computed.

fun foo(): Flow<Int> = flow { // flow builder
    for (i in 1..3) {
        delay(100) // pretend we are doing something useful here
        emit(i) // emit next value
    }
}
fun main() = runBlocking<Unit> {
    // Collect the flow
    foo().collect { value -> println(value) } 
}

Flows are cold streams similar to sequences — the code inside a flow builder does not run until the flow is collected.

Channels

Channels provide a way to transfer a stream of values.

val channel = Channel<Int>()
launch {
    // this might be heavy CPU-consuming computation or async logic, we'll just send five squares
    for (x in 1..3) channel.send(x * x)
}
// here we print five received integers:
repeat(3) { println(channel.receive()) }
println("Done!")
// 1
// 4
// 9
// Done!
val channel = Channel<Int>()
launch {
    for (x in 1..3) channel.send(x * x)
    channel.close() // we're done sending
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println("Done!")
// 1
// 4
// 9
// Done!

Pipelines

A pipeline is a pattern where one coroutine is producing, possibly infinite, stream of values, and another coroutine or coroutines are consuming that stream, doing some processing, and producing some other results.

fun CoroutineScope.produceNumbers() = produce<Int> {
    var x = 1
    while (true) send(x++) // infinite stream of integers starting from 1
}
fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
    for (x in numbers) send(x * x)
}
val numbers = produceNumbers() // produces integers from 1 and on
val squares = square(numbers) // squares integers
repeat(3) {
    println(squares.receive()) // print first five
}
println("Done!") // we are done
coroutineContext.cancelChildren() // cancel children coroutines
// 1
// 4
// 9
// Done!

Actors

An actor is an entity made up of a combination of a coroutine, the state that is confined and encapsulated into this coroutine, and a channel to communicate with other coroutines.

// Message types for counterActor
sealed class CounterMsg
object IncCounter : CounterMsg() // one-way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // a request with reply

// This function launches a new counter actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
    var counter = 0 // actor state
    for (msg in channel) { // iterate over incoming messages
        when (msg) {
            is IncCounter -> counter++
            is GetCounter -> msg.response.complete(counter)
        }
    }
}

fun main() = runBlocking<Unit> {
    val counter = counterActor() // create the actor
    withContext(Dispatchers.Default) {
        massiveRun {
            counter.send(IncCounter)
        }
    }
    // send a message to get a counter value from an actor
    val response = CompletableDeferred<Int>()
    counter.send(GetCounter(response))
    println("Counter = ${response.await()}")
    counter.close() // shutdown the actor
}
// Completed 100000 actions in 973 ms
// Counter = 100000

Select Expression

Select expression makes it possible to await multiple suspending functions simultaneously and select the first one that becomes available.

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result 
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}
val fizz = fizz()
val buzz = buzz()
repeat(7) {
    selectFizzBuzz(fizz, buzz)
}
coroutineContext.cancelChildren() // cancel fizz & buzz coroutines

I/O

use close the closable automatically:

Files.newBufferedReader(path).use {
    it.read()
}

Compilation and Packaging

kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar

# library (no Kotlin runtime included)
kotlinc hello.kt -d hello.jar
kotlin -classpath hello.jar HelloKt

Maven

<properties>
    <kotlin.version>1.3.72</kotlin.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
</dependencies>
<build>
    <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>${kotlin.version}</version>
            <executions>
                <execution>
                    <id>compile</id>
                    <goals><goal>compile</goal></goals>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <goals><goal>test-compile</goal></goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

References

⚠️ **GitHub.com Fallback** ⚠️