frectures / kotlin

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Kotlin

Testimonials

  • Meta has adopted Kotlin as its new primary language for Android development
  • Meta is migrating its Android codebase from Java to Kotlin
  • over 10 million lines of Kotlin code
  • Kotlin is now the recommended programming language
    • not only for Android programming
    • but also for server-side JVM usage at Google
  • over 8 million lines of Kotlin code

Kotlin in a nutshell

  • “Java after spring cleaning”
    • “Java without the boilerplate”
  • Killer features:
    • Null safety
    • Extension functions
    • Properties
  • Compilation targets:

Birth

                                                                                    1.1    1.3       1.5    1.8
Java                            Scala                         Kotlin            1.0 |  1.2 |      1.4| 1.6  | 1.9
|                               |                             |                 |   |  |   |      |  | | 1.7| |
+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---+---|---+---+---+---
96             00   |              05                  10         |        15          |       20
                |   IntelliJ IDEA                                 |          |         |
                |                                                 Karel      skorbut   Karel
                JetBrains                                        (Scala)     (Kotlin) (Kotlin)

Hello World

fun main(args: Array<String>): Unit {
    println("Hello Kotlin!");
}
  • Dedicated fun keyword for functions
  • Top-level functions (main, println)
  • Classic name: type declaration syntax
  • No special syntax for arrays
    • args array can be omitted
  • Unit is Kotlin-speak for void
    • Return type Unit can be omitted
  • Semicolons can be omitted
fun main() {
    println("Hello Kotlin!")
}

Exercise

  1. Download IntelliJ IDEA Community Edition 👇 Scroll Down 👇
  2. Import the Hello World project
  3. If the Project View is hidden, press Alt+1
  4. Open src/main/kotlin/Kotlin.kt
  5. Start the main function via the green triangle beside it
  6. Modify the program to print something else

Basic types

What problem does Nothing solve?

public static String interviewQuestion(List<String> strings) {
    // return "";
    // return null;
    // throw new NotImplementedYet();

    TODO(); // Missing return statement
}

public static void TODO() {
    throw new NotImplementedYet();
}
  • void means “returns normally, without a result”
    • same as Unit in Kotlin
  • Nothing means “always throws an exception”:
fun TODO(): Nothing {
    throw NotImplementedError()
}

fun interviewQuestion(strings: List<String>): String {
    TODO() // okay
}

Boxing

  • Int is usually int, unless boxing necessitates Integer:
fun sumNumbers(numbers: List<Int>): Int {
                        // Integer  int
    var result = 0
             // int
    for (x in numbers) {
     // int
        result += x
    }
    return result
}

fun main() {
    println(sumNumbers(listOf(2, 3, 5, 7)))
    // Summing numbers is already implemented in the standard library though:
    println(listOf(2, 3, 5, 7).sum())
}

Type inference

  • var infers the type of the variable from its initializer:
var a: Int = 1

var b      = 2
// Int <<<< Int
  • Type inference is not dynamic typing:
var c
// This variable must either have a type annotation or be initialized


var theAnswerToLife = 42

    theAnswerToLife = "fish"
//    Required: Int   Found: String

Control structure expressions

if expression

fun digitOrNumber(x: Int): String {
    if (x in 0..9) {
        return "digit"
    } else {
        return "number"
    }
}


fun digitOrNumber(x: Int): String {
    return if (x in 0..9) "digit" else "number"
}


fun digitOrNumber(x: Int): String = if (x in 0..9) "digit" else "number"


fun digitOrNumber(x: Int) = if (x in 0..9) "digit" else "number"

when expression

fun averageMonthLength(month: Int): Double = when (month) {
    2                     -> 28.2425
    4, 6, 9, 11           -> 30.0
    1, 3, 5, 7, 8, 10, 12 -> 31.0
    else                  -> throw IllegalArgumentException("illegal month $month")
}

fun count(x: Any): Int = when (x) {
    is Array<*>      -> x.size
    is String        -> x.length
    is Collection<*> -> x.size
    else             -> 0
}

fun signum(x: Int): Int = when {
    x<0  -> -1
    x>0  -> +1
    else ->  0
}

Exercise

  1. Implement a function gcd which computes the greatest common divisor of two integers

Default and Named arguments

public class Joiner {
    public static String join(Iterable<String> strings, String delimiter, String prefix, String suffix) {
        // ...
    }

    public static void main(String[] args) {
        var fruits = List.of("apple", "banana", "cherry", "date", "elderberry");

        String a = join(fruits, ", ", "[", "]");
        String b = join(fruits, ", ");
        String c = join(fruits);
    }

    public static String join(Iterable<String> strings, String delimiter) {
        return join(strings, delimiter, "", "");
    }

    public static String join(Iterable<String> strings) {
        return join(strings, "");
    }
}
fun join(strings: Iterable<String>, delimiter: String = "", prefix: String = "", suffix: String = ""): String {
    // ...
}

fun main() {
    val fruits = listOf("apple", "banana", "cherry", "date", "elderberry")

    val a: String = join(fruits, ", ", "[", "]")
    val b: String = join(fruits, ", ")
    val c: String = join(fruits)

    val d: String = join(fruits, delimiter = ", ", prefix = "[", suffix = "]")
    val e: String = join(fruits, prefix = "[", delimiter = ", ", suffix = "]")
    val f: String = join(fruits, prefix = "\t")
}
  • Default arguments are evaluated inside the called function
    • arbitrary expressions
    • not hardwired into call sites
  • Unnamed arguments are forbidden after the first named argument

Any is the new Object

  • Every class without an explicit parent inherits from Any:
    • class Child
    • class Child : Any()
package kotlin

public open class Any {

    public open fun equals(other: Any): Boolean

    public open fun hashCode(): Int

    public open fun toString(): String
}
  • open classes are inheritable
  • open functions are overridable
class CountingList<E> extends ArrayList<E> {
    private int elementsAdded;
    
    @Override
    public boolean add(E e) {
        elementsAdded += 1;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        elementsAdded += c.size();
        // Does super.addAll call this.add internally?
        return super.addAll(c);
    }
}
  • Designing a class for inheritance is hard work
  • You must document all its self-use patterns, and once you've documented them, you must commit to them for the life of the class
  • If you fail to do this, subclasses may become dependent on implementation details of the superclass and may break if the implementation of the superclass changes
  • Unless you know there is a real need for subclasses, you are probably better off prohibiting inheritance by declaring your class final
  • Kotlin has classes and their members final by default
    • which makes it inconvenient to use frameworks and libraries such as Spring AOP that require classes to be open
  • The all-open compiler plugin adapts Kotlin to the requirements of those frameworks
    • and makes classes annotated with a specific annotation and their members open without the explicit open keyword
    • e.g. @Transactional classes

Can you spot the bug?

public static void login() { 
    Scanner scanner = new Scanner(System.in);
    String password;
    do {
        System.out.print("Password? ");
        password = scanner.nextLine();
    } while (password != "java");
    System.out.println("Welcome to the system!");
}

Sane equality

fun login() {
    val scanner = Scanner(System.`in`)
    do {
        print("Password? ")
        val password = scanner.nextLine()
    } while (password != "kotlin")
    println("Welcome to the system!")
}
  • Kotlin has no new keyword for constructor calls
    • Constructor calls look like function calls
    • Human disambiguation by first letter case
  • Clashing keywords like in must be escaped with backticks
    • Backticks also enable readable test names like square root should fail for negative inputs
  • do scope extends to while condition
    • i.e. password still accessible for comparison
  • == and != on references are null-safe equals comparisons
  • === and !== compare the references themselves
println("hello".uppercase() ==  "HELLO")  // true
println("hello".uppercase() === "HELLO") // false

Data classes

record Name(String forename, String surename) {
    Name {
        if (forename.isBlank()) throw new IllegalArgumentException();
        if (surename.isBlank()) throw new IllegalArgumentException();
    }

    // auto-generated: equals, hashCode, toString
}
data class Name(val forename: String, val surename: String) {
    init {
        require(forename.isNotBlank())
        require(surename.isNotBlank())
    }

    // auto-generated: equals, hashCode, toString, copy, component1, component2
}

Exercise

  1. Implement a data class Address with fields of your choice
  2. Populate a list of addresses
  3. Print the addresses to the console

Null safety

fun mustPassString(s: String) {
    // ...
}

fun canPassString(s: String?) {
    // ...
}

fun main() {
    mustPassString("hello")
    canPassString("world")

    mustPassString(null) // Null can not be a value of a non-null type String
    canPassString(null) // okay
}

NullPointerException is very rare in Kotlin

fun mustPassString(s: String) {
    val length: Int = s.length
}                   // ^ okay

fun canPassString(s: String?) {
    val length: Int = s.length
                    // ^ Only safe (?.) or non-null asserted (!!.) calls
                    //   are allowed on nullable receiver of type String?

    if (s != null) {
        val length: Int = s.length
    }                  // ^ Smart cast to kotlin.String

    val lengthOrNull: Int? = s?.length
    val lengthOrZero: Int  = s?.length ?: 0
}

In vivo

val shortestString = strings.minByOrNull(String::length) ?: ""
         // @return user's input, or null meaning the user canceled the input
val input = JOptionPane.showInputDialog( /* ... */ ) ?: return
// ...
    // colors: Map<FlexerState, Int>
return colors[endState] ?: 0x000000
fun largestDownload(): File {
    return File(System.getProperty("user.home") + File.separator + "Downloads")
        .listFiles()                // null if Downloads folder does not exist
        ?.maxByOrNull(File::length) // null if Downloads folder is empty
        ?: throw IOException("no Downloads")
}

Java Interoperability

  • String! denotes unknown nullability for Kotlin→Java interop:

  • public functions check null parameters for Java→Kotlin interop:
Kotlin Java
private fun privateFunction(s: String) {
    // ...
}

fun publicFunction(s: String) {

    // ...
}
private static void privateFunction(String s) {
    // ...
}

public static void publicFunction(String s) {
    Intrinsics.checkNotNullParameter(s);
    // ...
}

Exercise

  1. Add a nullable field remark to the Address class
  2. Print all addresses with remarks before all addresses without remarks

copy data objects

val jamesGosling = Name("James", "Gosling")

val jamesBond = jamesGosling.copy(surename = "Bond")
  • The copy method has default arguments for all fields:
fun copy(forename: String = this.forename, surename: String = this.surename) = Name(forename, surename)

Exercise

  1. Implement a withoutRemark(): Address method, using the copy method

Extension functions

fun String.isAscii(): Boolean {
    for (ch in this) {
        if (ch > '\u007f') return false
    }
    return true
}

fun main() {
    println("Kaesebroetchen/Muesli".isAscii()) // true
    println("Käsebrötchen/Müsli".isAscii())   // false
}
  • Extension functions are compiled to static helper methods with an additional $receiver parameter
  • Inside an extension function, only the public interface of the $receiver is available

Exercise

  1. Implement an extension function String.isPalindrome()
  2. Implement an extension function such that listOf(2, 3, 5, 7).product() returns 210

Properties

(Data classes)

data class Name(val forename: String, val surename: String)

Enumerations

enum class Month(val averageDays: Double) {
    JAN(31.0),
    FEB(28.2425),
    MAR(31.0),
    APR(30.0),
    MAY(31.0),
    JUN(30.0),
    JUL(31.0),
    AUG(31.0),
    SEP(30.0),
    OCT(31.0),
    NOV(30.0),
    DEC(31.0);
}

Data Transfer Objects

class PersonDto(var forename: String, var surename: String)

Dependency Injection

class MyService(
    private val someOtherService: SomeOtherService,
    private val yetAnotherService: YetAnotherService,
) {
    // ...
}

In vivo

// The first instruction starts at address 256.
// This makes it easier to distinguish addresses
// from truth values and loop counters on the stack.
const val ENTRY_POINT = 256

class VirtualMachine(
    private val program: List<Instruction>,
    // ...
) {

    var pc: Int = ENTRY_POINT
        private set(value) {
            assert(value in ENTRY_POINT..program.lastIndex) { "invalid pc $value" }
            field = value
        }

    val currentInstruction: Instruction
        get() = program[pc]

    // ...
}

Function types and Lambdas

// _Strings.kt
public inline fun CharSequence.all(predicate: (Char) -> Boolean): Boolean {
    //                               Function1<Char, Boolean>
    for (element in this) {
         if (!predicate(element)) return false
    // predicate.invoke(element)
    }
    return true
}


fun String.isAscii1(): Boolean {
    return this.all({ ch -> ch <= '\u007f' })
}

fun String.isAscii2(): Boolean {
    return this.all { ch -> ch <= '\u007f' }
}

fun String.isAscii3(): Boolean {
    return this.all {       it <= '\u007f' }
}
  • If the only argument is a lambda, the parentheses can be omitted

Custom control structures

  • The last lambda argument can be moved out of the argument list

repeat

fun main() {
    repeat(10) {
        println(it)
    }
}

// Standard.kt
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    for (index in 0 until times) {
        action(index)
    }
}

synchronized

fun addBook(book: Book) {
    synchronized(books) {
        books.add(book)
    }
}

// Synchronized.kt
public inline fun <R> synchronized(lock: Any, block: () -> R): R {
    monitorEnter(lock)
    try {
        return block()
    } finally {
        monitorExit(lock)
    }
}

close after use

val inputStream = largestDownload().inputStream().buffered()
// ... arbitrary code, might throw exception before close ...
inputStream.close()
val inputStream = largestDownload().inputStream().buffered()
inputStream.use {
    // ...
}
// ... arbitrary code, might use inputStream after close ...
largestDownload().inputStream().buffered().use { inputStream ->
    // ...
}

Scope functions in vivo

let

private fun Expression.evaluate(): Value {
    if (value != null) {
        // error: 'value' is a mutable property
        // that could have been changed by this time
        return value
    }

    value?.let { return it }

    // ...
}
fun lookup(name: Token): Symbol? {
    val text = name.text
    for (i in current downTo 0) {
        scopes[i].get(text)?.let { symbol -> return symbol }
    }
    return null
}

apply

private val randomize = JButton("🎲").apply {
    isEnabled = false
}

also

val temp = result.evaluate()
memory.popStackFrameUnlessEntryPoint()
return temp
return result.evaluate().also { memory.popStackFrameUnlessEntryPoint() }

with

var previousValue = controlPanel.slider.value

controlPanel.pause.addActionListener {
    val slider = controlPanel.slider
    if (slider.value != slider.minimum) {
        if (slider.value != slider.maximum) {
            previousValue = slider.value
        }
        slider.value = slider.minimum
    } else {
        slider.value = previousValue
    }
}
var previousValue = controlPanel.slider.value

controlPanel.pause.addActionListener {
    with(controlPanel.slider) {
        if (value != minimum) {
            if (value != maximum) {
                previousValue = value
            }
            value = minimum
        } else {
            value = previousValue
        }
    }
}

run

-   with(controlPanel.slider)    {
+        controlPanel.slider.run {

takeIf

virtualMachine = VirtualMachine(
    instructions, initialWorld,
    onCall = editor::push.takeIf { compiledFromSource },
    onReturn = editor::pop.takeIf { compiledFromSource },
)

Eager vs. Lazy

Eager list

package people

data class Person(val name: String, val birthday: LocalDate)

val people = listOf(
    Person("Miles", LocalDate.of(1979, 3, 28)),
    Person("Toby", LocalDate.of(1986, 4, 26)),
    Person("Tina", LocalDate.of(2011, 3, 11)),
)

val fortyYearsAgo = LocalDate.now().minusYears(40)
val firstSinceThen = people
    .map { person -> println(person); person.birthday }
    .filter { day -> println(day); day.isAfter(fortyYearsAgo) }
    .first()
Person(name=Miles, birthday=1979-03-28)
Person(name=Toby, birthday=1986-04-26)
Person(name=Tina, birthday=2011-03-11)
1979-03-28
1986-04-26
2011-03-11

Lazy sequence

val firstSinceThen = people
    .asSequence()
    .map { person -> println(person); person.birthday }
    .filter { day -> println(day); day.isAfter(fortyYearsAgo) }
    .first()
Person(name=Miles, birthday=1979-03-28)
1979-03-28
Person(name=Toby, birthday=1986-04-26)
1986-04-26

Collections

Why does this not compile?

public static List<Fruit> fruitBowl() {
    if (Math.random() < 0.5) {
        return new LinkedList<Apple>();
    } else {
        return new ArrayList<Banana>();
    }
}


interface Fruit {
}

class Apple implements Fruit {
}

class Banana implements Fruit {
}

For type safety!

  • A subtype has all the operations of its supertype(s)
  • List<Fruit> has an operation void add(Fruit)
  • List<Apple> has no such operation
  • Hence, List<Apple> is not a List<Fruit>
  • Unfortunately, Apple[] is a Fruit[] in Java:
Apple[] apples = new Apple[3];
apples[0] = new Apple();

Fruit[] fruits = apples;
fruits[1] = new Apple();

fruits[2] = new Banana(); // java.lang.ArrayStoreException: Banana

Java Generics have use-site variance only

public static List<? extends Fruit> fruitBowl() {
    if (Math.random() < 0.5) {
        return new LinkedList<Apple>();
    } else {
        return new ArrayList<Banana>();
    }
}

Kotlin Generics also have declaration-site variance

fun fruitBowl(): List<Fruit> {
    if (Math.random() < 0.5) {
        return LinkedList<Apple>()
    } else {
        return ArrayList<Banana>()
    }
}
  • Kotlin Lists are read-only, hence this is type-safe:
/**
 * Methods in this interface support only read-only access to the list;
 * read/write access is supported through the [MutableList] interface.
 *
 * The list is    covariant    in its element type.
 */
public interface List<out E> : Collection<E> {
    // size
    // isEmpty
    // contains
    // get
    // indexOf
    // ...
}

/**
 * A generic ordered collection of elements
 * that supports adding and removing elements.
 *
 * The mutable list is invariant in its element type.
 */
public interface MutableList<E> : List<E>, MutableCollection<E> {
    // add
    // set
    // clear
    // remove
    // removeAt
    // ...
}

  • Similar relations exist for Set/MutableSet and Map/MutableMap

Iteration

for each element:

val fruits = listOf("apple", "banana", "cherry")

for (fruit in fruits) {
    println(fruit)
}

fruits.forEach { fruit ->
    println(fruit)
}

fruits.forEach {
    println(it)
}

fruits.forEach(::println)

for each index & element:

for (index in 0 until fruits.size) {
    val fruit = fruits[index]
    println("$index: $fruit")
}

for (index in 0 .. fruits.lastIndex) {
    val fruit = fruits[index]
    println("$index: $fruit")
}

for (index in fruits.indices) {
    val fruit = fruits[index]
    println("$index: $fruit")
}

for ((index, fruit) in fruits.withIndex()) {
    println("$index: $fruit")
}

fruits.forEachIndexed { index, fruit ->
    println("$index: $fruit")
}

for each key & value:

val fruits = mapOf("apple" to "🍎", "banana" to "🍌", "cherry" to "🍒")

for (entry in fruits) {
    println("${entry.key}: ${entry.value}")
}

fruits.forEach { entry ->
    println("${entry.key}: ${entry.value}")
}

for ((key, value) in fruits) {
    println("$key: $value")
}

fruits.forEach { (key, value) ->
    println("$key: $value")
}

Katas/Dojos

Regular expressions

data class Address(val street: String, val streetNumber: String) {
    companion object {
        val ADDRESS: Regex = """(\D+?)\s*(\d+.*)""".toRegex()

        @JvmStatic
        fun parse(line: String): Address {
            val (street, streetNumber) = ADDRESS.matchEntire(line)?.destructured ?: error(line)
            return Address(street, streetNumber)
        }
    }
}

fun main() {
    val text = """
        Musterstr.123
        Muster-Gasse 4e
        Unter der Ulme 6g
    """.trimIndent()
    val addresses = text.lines().map(Address::parse)
    println(addresses)
}
  • Companion objects replace static members
    • adopted from Scala
    • @JvmStatic provides static bridge for Java interop
  • Triple quotes introduce raw strings
    • \ instead of \\
    • actual line break instead of \n

About


Languages

Language:Kotlin 87.9%Language:Java 12.1%