Kotlin provides a modern, statically-typed, and expressive alternative to Java, offering null safety, coroutines for asynchronous programming, and a succinct, intuitive syntax.
This presentation will give an introduction to Kotlin, looking at various language features, how those features are utilized by the Kotlin Standard Library, and how they are implemented in performance-conscious ways.
2. Agenda (non-linear)
• Basic language concepts
• Examples of the Kotlin Standard Library
• Coroutines
• Language performance characteristics
• Goal of this talk: to introduce Kotlin, not to teach it
3. What is Kotlin?
• Introduced in 2011 by JetBrains (the Intellij folks)
• Goal was to offer a “better Java”, not unlike Scala, but without Scala’s
slow compilation times
• Compiles to JVM bytecode, as well as JavaScript and native binaries
for various operating systems, including Android and iOS
• Kotlin can call Java libraries, and Java can call Kotlin libraries (with
limitations)
• https://kotlinfoundation.org/ - Backed by JetBrains, Google, Gradle
and more.
4. Why Kotlin?
• Whereas Scala adds “everything”, Kotlin adds “just enough”
• Well thought out “syntactic sugar”makes for a much more expressive
language, without becoming inaccessible
• While still evolving, backwards compatibility is prioritized
• Excellent Intellij support, but also a good Eclipse plugin
• Intellij can convert your Java code to Kotlin for you!
• The language of choice for Android development, and the the default
DSL for new Gradle builds
5. HelloWorld.kt
fun main() {
println("Hello, World")
}
public final class HelloWorldKt {
public static final void main() {
System.out.println("Hello, World");
}
public static void main(String[] var0) {
main();
}
}
Semicolon
inference
6. HelloWorld2.kt
class HelloWorld2 {
companion object {
@JvmStatic
fun main(args:
Array<String>) {
println("Hello, World")
}
}
}
How Kotlin
does static
public final class HelloWorld2 {
public static final Companion Companion =
new Companion();
public static final void main(String[] args) {
Companion.main(args);
}
public static final class Companion {
public final void main(String[] args) {
System.out.println("Hello, World");
}
private Companion() {
// object in Kotlin is a singleton
}
}
}
7. Functions
fun double(n: Int): Int {
return n * 2
}
Type follows name
Single expression function
Type inference
Equivalent to : Unit
fun quadruple(n: Int) = n * 4
fun say(text: String): Unit {
println(text)
}
fun shout(text: String) {
println(text.toUpperCase())
}
fun triple(n: Int): Int = n * 3
8. Default and named parameters
fun log(message: String, level: String = "INFO", logger: String = "DEFAULT") =
println("$logger [$level] $message")
fun main() {
log("all is well")
log("uh oh", "WARN")
log(logger = "THERMAL", level = "FATAL", message = "Fire!")
}
• Convenient way to do method overloading
• Named arguments in constructors largely eliminate the need for the Builder
Pattern
DEFAULT [INFO] all is well
DEFAULT [WARN] uh oh
THERMAL [FATAL] Fire!
9. class Person(val name: String, var age: Int, heightInInches: Int) {
private val heightInCm: Int = (heightInInches * 2.54).toInt()
val heightInMeters: Double
get() = heightInCm / 100.0
fun failHeightInMeters(): Double = heightInInches * 0.0254
// ❌Unresolved reference: heightInInches
}
fun main() {
val founder = Person("Jeff", 58, 67)
val name = founder.name
// time marches on
founder.age = 59
}
Classes
public static final void main() {
Person founder = new Person(”Jeff", 58, 67);
String name = founder.getName(); // not field reference!
// time marches on
founder.setAge(55); // not field reference!
}
Final
property
Mutable
property
No “new”
Just a constructor
parameter, not a property
10. Data Classes
data class RgbColor(val red: Int, val blue: Int, val green: Int)
Class body optional
fun main() {
val yellow = RgbColor(255, 255, 0)
println(yellow) // prints "RgbColor(red=255, blue=255, green=0)"
if (yellow == RgbColor(255, 255, 0)) { // auto-generated equals
println("matches")
}
val magenta = yellow.copy(blue = 0, green = 255)
val (r, g, b) = yellow // destructuring
}
.equals(…)
11. Null Safety – Optional as a language feature
class Contact(val name: String, val phone: String?)
fun produce() {
val noPhone = Contact("Bob", null) // OK
val noName = Contact(null, "867-5309") // ❌Null can not be a value of a non-null type String
}
fun consume(contact: Contact) {
val nameLength: String = contact.name.length // OK
val phoneLength = contact.phone.length
// ❌ Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
val nullablePhoneLength = contact.phone?.length // type: Int?
val jenny = Contact("Jenny", "867-5309")
val phone: String = jenny.phone!! // could throw NPE
}
Nullable
Non-nullable
12. Branching
fun salutations(arriving: Boolean) {
if(arriving) {
println("hello")
}
else {
println("goodbye")
}
}
fun salutations2(arriving: Boolean): String = if(arriving) "hello" else "goodbye" //ternary
fun size(item: Any): Int = when(item) {
"meaning of life" -> 42
is String -> item.length
is Collection<*> -> item.size
is Number -> item.toInt()
else -> throw IllegalArgumentException("cannot determine size of $item")
}
item is “smart cast”
String template
13. Looping
fun repeatGreet(times: Int) {
for (i in 1..times) {
println("hello $i")
}
}
fun repeatGreetExpanded(times: Int) {
val range = IntRange(1, times)
for (i in range) {
println("hello $i")
}
}
public void repeatGreet(int times) {
for (int i = 1; i <= times; i++) {
System.out.println("hello");
}
}
public void repeatGreetExpanded(int times) {
IntRange range = new IntRange(1, times);
int var1 = range.getLast();
for (int i = 1; i <= var1; i++) {
System.out.println("hello");
}
}
14. Extension methods
fun Int.double() = this * 2
fun Int?.add(summand: Int) = (this ?: 0) + summand
fun main() {
println(5.double()) // 10
println(5.add(3)) // 8
println(null.add(3)) // 3
}
• Syntactic Sugar for static methods whose first argument is the
receiver type
• Extension methods must be imported to be used
• Many StdLib extension methods are auto-imported.
Can apply to null
15. Functional Parameters / Lambdas
functional
signature
fun transform(n: Int, transformer: (Int) -> Int) {
println("$n maps to ${transformer(n)}")
}
fun main() {
transform(3, {i -> i * 2})
transform(4) {i -> i * 3}
transform(5) { it * 4 }
fun square(n: Int) = n * n
transform(6, ::square)
}
function
invocation
3 maps to 6
4 maps to 12
5 maps to 20
6 maps to 36
last param lambda syntax
“it” for single parameter
method reference
16. Inline Functions
inline fun <T> maxBy(a: T, b: T, ranker: (T) -> Int) =
if (ranker(a) > ranker(b)) a else b
• If possible, invocations will be replaced with function body
• Especially useful for megamorphic call sites in loops.
• Used extensively in the Kotlin Collections API and other Standard Library
functions (as appropriate)
• Allows for clean syntax without performance penalties
• ⚠️ Changes require recompilation of client code
arrayOf("a", "b", "c", "d").forEach(::println) // traditional for loop
17. Value Classes – type safety, for free
public final class CustomerId {
private final long id;
private CustomerId(long id) { this.id = id; }
public final long getId() { return this.id; }
public static long constructor_impl (long id) {
return id; }
}
public static final void delete_cos_Kmc(long
custId) {
System.out.println("Hasta la vista, " + custId);
}
public static final void main() {
long customerId =
CustomerId.constructor_impl(123L);
delete_cos_Kmc(customerId);
@JvmInline
value class CustomerId(val id:
Long)
fun delete(custId: CustomerId) {
println("Hasta la vista,
${custId.id}")
}
fun main() {
val customerId =
CustomerId(123)
delete(customerId);
}
18. Infix Functions
public infix fun Int.downTo(to: Int): IntProgression {
return IntProgression.fromClosedRange(this, to, -1)
}
for (i in 5 downTo 1) { println(i) }
Invokes IntProgression.iterator()
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
mapOf(1 to "one", 2 to "two")
19. Scope Functions
• Extension methods applied to all types
• Act like language features, but are just library functions
• Provide for useful idioms
20. Scope Functions - let
public inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
• Particularly useful for working only on non-null objects
someMap.get(key)?.let { println("$key maps to $it") }
21. Scope Functions - apply
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
• Useful for configuring objects without creating a temp var
class Order { // as if it came from a Java library
var productName: String? = null
var quantity: Int? = null
}
fun makeOrder() = Order().apply { // "this" refers to Order
this.productName = "BeDazzler"
quantity = 2 // this. not actually needed
}
Function type with receiver
22. Scope Functions - with
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
• Useful for referencing an object repeatedly
fun describe(order: Order) = with(order) {
"order for $quantity of $productName"
}
23. Scope Functions - use
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
try {
return block(this)
} finally {
this?.close()
}
}
• Like Java’s try-with-resources
FileOutputStream("foo").use { it.write("Hello".toByteArray()) }
24. Kotlin Collections API
• Built on top of Java’s Collections API
• Collections have mutable and immutable variants, but only at API
level (“immutable” collections can be cast to mutable ones and
mutated)
• Lots of transformation APIs, implemented as extension methods
• Sequences are similar to java Streams, but are reusable.
• Familiar methods: map, flatMap, filter, groupBy, etc.
• Also: associate, takeWhile, filterNotNull, etc.
• Factory methods: listOf, mutableListOf, mapOf, mutableMapOf, setOf,
etc.
25. Kotlin Collections API
• Built on top of Java’s Collections API
• Lots of additional methods added as extension methods
• “Immutable” versions of collections
• Tranformation methods available without using Streams
• Sequences for stream-like laziness
26. Kotlin Collections Basics
val names: List<String> = listOf("Gosling", "Block", "Goetz")
println(names[2]) // operator overload for names.get(2)
val capitols: Map<String, String> = mapOf(
"Utah" to "Salt Lake City",
"Idaho" to "Boise")
println(capitols["Utah"]) // capitols.get("Utah")
val nums: List<Int> = (1 .. 4).toList()
val evens: List<Int> = nums.filter { it % 2 == 0}
val evensAndOdds: Pair<List<Int>, List<Int>> = nums.partition { it %
2 == 0 }
val mod3: Map<Int, List<Int>> = nums.groupBy { it % 3 }
val squares: Map<Int, Int> = nums.associate { it to it * it }
val cubes: Map<Int, Int> = nums.associateWith { it * it * it }
Goetz
Salt Lake City
[1, 2, 3, 4]
[2, 4]
([2, 4], [1, 3])
{1=[1, 4], 2=[2], 0=[3]}
{1=1, 2=4, 3=9, 4=16}
{1=1, 2=8, 3=27, 4=64}
27. Kotlin Sequences – like Java Streams
fun negate(n:Int): Int {
println("negating $n")
return -n;
}
fun main() {
val seq = (1..1000).asSequence()
seq.map(::negate).take(2).forEach(::println)
println("Unlike Streams, Sequences are
reusable")
seq.map(::negate).take(2).forEach(::println)
}
negating 1
-1
negating 2
-2
Unlike Streams, Sequences are reusable
negating 1
-1
negating 2
-2
28. Kotlin Collections: Mutablility
val numbers: List<Int> = (1..10).toList()
numbers.add(11) // compiler error: no such method
val mutableNumbers: MutableList<Int> =
numbers.toMutableList()
mutableNumbers.add(11)
ListProcessor.process(numbers)
println(numbers)
println(numbers.javaClass.name)
public class ListProcessor { // java class
public static void process(List<?> list) {
list.remove(0);
}
}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
[2, 3, 4, 5, 6, 7, 8, 9, 10]
java.util.ArrayList
29. Coroutines
• Similar to project Loom, but without JVM support
• Special compiler logic to generate reentrant methods
• Support for deferred evaluation
• Support for reactive-type idioms
• Support for actors
30. Concurrency Example – single thread
fun main() = runBlocking { // run a top-level coroutine
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second
log("World!")
}
log("Hello")
}
main [320] - Hello
main [1335] - World!
Helper method –
Prints thread name,
elapsed time and message
31. Concurrency Example – multiple threads
fun main() = runBlocking(Dispatchers.IO) {
for (i in 1..2) {
launch {
log("$i before yield")
yield()
log("$i after yield")
}
}
log("Hello")
}
DefaultDispatcher-worker-2 [309] - 2 before yield
DefaultDispatcher-worker-3 [310] - 1 before yield
DefaultDispatcher-worker-1 [310] - Hello
DefaultDispatcher-worker-3 [314] - 2 after yield
DefaultDispatcher-worker-2 [314] - 1 after yield
Uses elastic thread pool
Different thread
after delay
32. Under the hood
• Coroutine method is compiled to a class
• Class has a label attribute that determines where to pick up
execution
• Local variables are stored in class fields.
• Essentially, coroutine compilation shifts the burden of the stack onto
the heap.
33. Under the hood
{
delay(1000L)
log("World!")
}
public final class BasicCoroutine extends SuspendLambda {
private int label = 0;
@Nullable
public final Object invokeSuspend() {
switch (this.label) {
case 0:
DelayKt.delay(1000L, this);
this.label = 1;
return Intrinsics.COROUTINE_SUSPENDED;
case 1:
LoggingKt.log("World!");
return Unit.INSTANCE;
}
}
}
34. Suspendable functions
fun main() = runBlocking {
launch {
delay(1000L)
log("World!")
}
log("Hello")
}
fun main() = runBlocking {
launch {
world()
}
log("Hello")
}
private suspend fun world() {
delay(1000L)
log("World!")
}
Cannot be called without
suspend modifier
35. Deferred results with async {…}
private suspend fun fetchQuote(provider: String): Int {
delay(1000) // simulate network delay
log("quote fetched for $provider")
return provider.length
}
fun main() {
runBlocking {
val quote1: Deferred<Int> = async { fetchQuote("StateFarm") }
val quote2: Deferred<Int> = async { fetchQuote("AllState") }
log("awaiting quotes")
val lowestQuote = Math.min(quote1.await(), quote2.await())
log("min quote: $lowestQuote")
}
} main [385] - awaiting quotes
main [1393] - quote fetched for StateFarm
main [1396] - quote fetched for AllState
main [1397] - min quote: 8
36. Channels
• A Channel<T> supports
• send(value: T)
• receive(): T
• close()
• isClosedForReceive(): Boolean
• A channel has a capacity:
• > 0 – can hold this many elements, then sending blocked
• 0 – a rendezvous channel
• CONFLATED – receive() just returns the most recent sent value; older
values dropped
37. Channels – example
fun main() {
val channel = Channel<Int>(CONFLATED)
runBlocking {
launch {
for (i in 1..6) {
delay(400)
channel.send(i)
}
channel.close()
}
while (!channel.isClosedForReceive) {
log("received ${channel.receive()}")
delay(1000)
}
}
}
main [718] - received 1
main [1719] - received 3
main [2721] - received 6
38. Actors
• Actor – a coroutine with state
• Other coroutines communicate with it by sending messages to a
dedicate channel
• An actor is guaranteed to process messages serially, useful for
managing mutable state
39. Actor Example: Counter
sealed interface CounterMessage
object Increment: CounterMessage
class GetCount(val response: CompletableDeferred<Int>): CounterMessage
fun CoroutineScope.counterActor() = actor<CounterMessage> {
var counter: Int = 0
for (message in channel) {
when(message) {
is Increment -> counter++
is GetCount -> message.response.complete(counter)
}
}
}
Method on CoroutineScope
41. Adopting Kotlin
• https://kotlinlang.org/ is an excellent source.
• Get buy-in from team members first! Brown-bags can help
• Try starting with unit tests
• Use IntelliJ to auto-convert some code; some hand-tuning may be
required.
• Lombok can work with Kotlin, but really isn’t needed.
• Not all classes need to be converted from Java.
• But you may find yourself temped after a bit!
42. Questions
interface Question {}
interface Audience {
fun questions(): Sequence<Question>
}
fun answerQuestions(audience: Audience) {
for(question in audience.questions()) {
answer(question)
}
}
fun answer(question: Question) {
println("Let me get back to you")
}