Kotlin for Java Developers

About me

  • Artem Makarov

  • Senior Software Developer @ Code Nomads

  • 10 years of software development experience

  • 3 years of Kotlin experience

  • Worked at Financial, Retail and Health industries

Artem Makarov

I am not going to talk about:

  • Performance

  • Coroutines

  • Kotlin on Android

  • Kotlin/Native

History of Kotlin

  • Development started in 2010 at JetBrains

  • Motivation:

    • Java’s lack of modern features

    • Scala poor tooling

Kotlin island

History of Kotlin: Important Milestones

  • 2012: code became open source

  • 2016: v1.0 release

  • 2017: Google announces first-class support on Android

  • 2019: Google makes Kotlin preferred language for Android app development

Kotlin vs Java: What Kotlin doesn’t have

  • checked exceptions

  • primitive types

  • ternary operator (x ? a : b)

  • public fields

  • semicolon line termination

  • new keyword

Kotlin vs Java: What Kotlin has

  • data classes*

  • string templates

  • operator overloading

  • trailing lambdas

  • named & default arguments

  • lazy initialization

  • coroutines

Variable declaration & mutability

Mutability is controlled with val/var keywords

var mutableBuffer = "Example Buffer" // mutable variable
val immutableBuffer = "Example Buffer" // immutable variable
val explicitType: String = "Foo Bar" // explicit type declaration

Functions

    fun sum(a: Int, b: Int): Int {
        return a + b
    }
    // function with expression body
    fun multiply(a: Int, b: Int) = a * b

    // function that doesn't any meaningful values
    fun logWarn(message: String): Unit { /*...*/ }

    // Unit is optional
    fun logInfo(message: String) { /*...*/ }

Nullability

Kotlin mandates explicitly defining nullable types with ? symbol at the end

    fun parseDate(input: String): LocalDate? { /*...*/ }

    val date: LocalDate? = parseDate("2022-01-02")
    // using returned nullable value
    print(date?.month) // safe call with ?.
    print(date!!.month) // unsafe call

Nullability checks: Elvis operator

    // using if-else
    val month = if (date.month != null) date.month else LocalDate.now().month
    // simplified notation with elvis operator
    val month2 = date.month ?: LocalDate.now().month

Nullability checks

Kotlin compiler keeps track of nullability checks performed

    val date2: LocalDate? = parseDate("2022-01-02")
    if (date2 != null) {
        print(date2) // smart cast to non-nullable
    } else {
        print("Unable to parse date")
    }

Classes

Classes in Kotlin can have a primary constructor and several secondary constructors

// class with implicit constructor
class Author(name: String) {/* ... */ }

// explicit constructor with visibility modifiers
class Publisher protected constructor(val books: MutableList<Book> = mutableListOf())

class Book(name: String) {
    constructor(name: String, publisher: Publisher) : this(name) {
        publisher.books.add(this)
    }
}
If the class also has a primary constructor, each secondary constructor needs to delegate to the primary constructor

Data classes

    // yay no more Lombok!
    data class Person(
        val firstName: String,
        val lastName: String,
        val active: Boolean = true
    )
    val person = Person("Jon", "Snow")
    val person2 = Person(firstName = "Jon", lastName = "Snow", active = true)

Data classes: copy

    data class Container(val element: String)

    val container1 = Container(element = "foo")
    // copy of container 1
    val container2 = container1.copy()
    // altered copy of container 1
    val container3 = container1.copy(element = "bar")

Data Classes: Destructuring

When there’s a need to return several values from a function, a data class can be used with a destructuring declaration

    data class Result(val result: Int, val status: Status)

    fun function(...): Result {
        // computations

        return Result(result, status)
    }

    // Now, to use this function:
    val (result, status) = function(...)

Standalone Objects

Objects can also be declared as singletons

object MyAwesomeObject {
    fun myAwesomeMethod() {}
    val container: Collection<String>
        get() = /* ... */
    fun staticMethod() {} // static method
}
// referring to the object methods
val temp = MyAwesomeObject.myAwesomeMethod(/* ... */)

Objects: companion

Objects can be declared inside a class using with the companion keyword

class User {
    companion object {
        private const val USER_TABLE = "users"
        fun create(): User { /* ... */ }
    }
}
companion members are members of object instance, i.e. not really static. To generate them as static, use @JvmStatic annotation

Extension functions

It is possible to write new functions for a class or an interface without modifying the class itself

    fun Car.ofMake(make: Make) =
        this.make == make // receiver object is available by `this`

Extension functions: Generics

It is possible to define generic extension functions

    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
    }

Extension functions: Fields

Kotlin also supports extension properties the same way as functions

val Book.firstAuthor: Author
    get() = authors.first()

Operator overloading

It is possible to overload predefined operators on types (e.g. +, *, -, etc.)

data class Shape(val shape: Shape) {
    operator fun plus(s2: Shape): Shape { /*...*/ }
}

Infix functions

Functions marked with infix can be called using the infix notation (without the dot and the parentheses for the call)

    infix fun Car.ofMake(make: Make): Boolean = make == this.make

    // usage
    if (car ofMake Make.VOLKSWAGEN)
        println("This is a Volkswagen")
    // same as
    if (car.ofMake(Make.VOLKSWAGEN))
        println("This is a Volkswagen")

Type aliases

Type aliases are used to provide alternative names for existing types, e.g. when the name is too long

typealias IntList = List<Int>
// shortening generic types
typealias StringTable<K> = MutableMap<K, MutableList<String>>
// aliases for function types
typealias Predicate<T> = (T) -> Boolean

Type aliases

Type alises do not introduce new types, so you can pass a variable of your aliased type whenever general function type is required (and vice versa)

typealias Predicate2<T> = (T) -> Boolean

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

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

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

Scope Functions

  • Kotlin standard library offers several functions for executing code within the context of an object.

  • Within this scope of the function the object can be accessed without using its name (i.e. via it or this)

Scope Functions: Examples

    val str: String? = "Hello"
    val uppercase = str?.let { it.toUpperCase() }
    // without let
    val str2: String? = "Hello"
    val uppercase2 = if (str2 != null) str2.toUpperCase() else null

Scope Functions: Examples

    fun findById(id: Long): Example {
        return exampleRepository.findById(id)
            .also { logger.info("retrieved record with $id") }
    }

Functional programming

In Kotlin, functions are first-class, so they can be stored in variables and data structures and passed as arguments and returned from other high-order functions

    fun <T, R> Collection<T>.fold(
        initial: R,
        combine: (acc: R, nextElement: T) -> R
    ): R {
        var accumulator: R = initial
        for (element: T in this) {
            accumulator = combine(accumulator, element)
        }
        return accumulator
    }
    // usage
    val concatenate = listOf("FOO", "BAR")
        .fold("") { acc: String, input: String -> acc + input }
    println(concatenate) // prints "FOOBAR"

Kotlin sequences

Kotlin offers more concise API for dealing with stream-like operations

Java:

    List<String> filteredStrings = List.of("cola", "coffee", "champagne").stream()
            .filter(str -> str.startsWith("c"))
            .collect(Collectors.toList());

Kotlin:

    val filteredStrings = listOf("cola", "coffee", "champagne")
        .filter { it.startsWith("c") } // automatically collected to List<T>

Kotlin sequences: Lazyness

Kotlin collection operations are not lazy, like in Java

    Integer firstOddNumber = List.of(1, 2, 3, 4, 5).stream()
            .filter(n -> n % 2 == 0)
            .findFirst() // will only cycle through the first one
            .get();
    val filteredStrings2 = listOf("cola", "coffee", "champagne")
        .filter { it.startsWith("c") }
        .first() // will cycle through all items

Kotlin sequences: Enforcing Lazyness

Lazy evaluation of sequences can be enforced via using asSequence() method

    val filteredStrings3 = listOf("cola", "coffee", "champagne").asSequence()
        .filter { it.startsWith("c") }
        .first() // will only take the first item

String templates

// inside the curly brackets there is a valid Kotlin expression
val message = "Added ${records.count()} records"
// no need for curly brackets for simple objects
val anotherMessage = "Added $recordCount records"

Advices

  • Try to think about Kotlin as a separate programming language rather than a Java library *

  • Take note of the IntelliJ Warnings and hints :-)

Q&A