Effectivekotlin PDF
Effectivekotlin PDF
Effectivekotlin PDF
Best practices
Marcin Moskala
This book is for sale at http://leanpub.com/effectivekotlin
Introduction: Be pragmatic 1
Chapter 1: Safety 17
Item 1: Limit mutability 18
Item 2: Minimize the scope of variables 38
Item 3: Eliminate platform types as soon as possible 45
Item 4: Do not expose inferred types 53
Item 5: Specify your expectations on arguments
and state 56
Item 6: Prefer standard errors to custom ones 68
Item 7: Prefer null or Failure result when the
lack of result is possible 70
Item 8: Handle nulls properly 74
Item 9: Close resources with use 87
Item 10: Write unit tests 90
Chapter 2: Readability 94
Item 11: Design for readability 96
Item 12: Operator meaning should be consistent
with its function name 104
Item 13: Avoid returning or operating on Unit? 111
Item 14: Specify the variable type when it is not clear 113
CONTENTS
Dictionary 472
Introduction: Be
pragmatic
Stories of how popular programming languages originate are
often fascinating.
The prototype of JavaScript (named Mocha back then) was
created in just 10 days. Its creators first considered using
Java, but they wanted to make a simpler language for Web
designers¹.
Scala was created at a university by a scientist, Martin Oder-
sky. He wanted to move some concepts from functional
programming into the Java Object Oriented Programming
world. It turned out that these worlds are connectable².
Java was originally designed in the early ‘90s for interactive
television and set-top boxes by a team called “The Green
Team” working at Sun. Eventually, this language was too
advanced for the digital cable television industry at the time.
It ended up revolutionising general programming instead³.
Many languages were designed for a totally different purpose
than for what they are used for today. Many originated
as experiments. This can be seen in them still today. The
differences in the Kotlin story are that:
¹Read about it here: www,en.wikipedia.org/wiki/JavaScript and
www.2ality.com/2011/03/javascript-how-it-all-began.html
²Read about it here: https://www.artima.com/scalazine/articles/origins_-
of_scala.html
³Read about it here: www.oracle.com/technetwork/java/javase/overview/javahistory-
index-198355.html
Introduction: Be pragmatic 2
• Safety
• Readability
• Powerful code reusability
• Tool friendliness
• Interoperability with other languages
• Efficiency
• Property
• Platform type
• Named arguments
• Property delegation
• DSL creation
• Inline classes and functions
• Tail recursion
Book design
Concepts in the book are grouped into three parts:
• Chapter 1: Safety
• Chapter 2: Readability
• Chapter 3: Reusability
• Chapter 4: Design abstractions
• Chapter 5: Objects creation
• Chapter 6: Class design
Part 3: Efficiency
Each chapter contains items, which are like rules. The concept
is that items are rules that in most cases need an explanation,
but once the idea is clear, it can be triggered just by this
title. For instance, the first rule, “Limit mutability”, might
be enigmatic for someone who meets it for the first time,
but is clear enough to just write in a comment on a code
review for someone who is familiar with this book. In the
end, suggestions designed this way, with their explanations,
should give a clear guideline on how to write good and
idiomatic Kotlin code.
Chapters organization
Chapters often start with the most important concept that
is used in other items. A very visible example is Chapter 2:
Readability which starts with Item 11: Design for readability,
but it is also true for:
Labels
It is impossible to write a book for everyone. This book is
written primarily for experienced Kotlin developers. Many
of them are already familiar with general best practices for
Introduction: Be pragmatic 10
Suggestions
If you have any suggestions or corrections regarding this
book, send them to [email protected]
Introduction: Be pragmatic 11
Acknowledgments
This book would not be so good without great reviewers who
highly influenced it by suggestions and comments. I would
like to thank all of them. Here is the whole list of reviewers,
starting from the most active ones.
1 var a = 10
2 val list: MutableList<Int> = mutableListOf()
1 class BankAccount {
2 var balance = 0.0
3 private set
4
5 fun deposit(depositAmount: Double) {
6 balance += depositAmount
7 }
8
9 @Throws(InsufficientFunds::class)
10 fun withdraw(withdrawAmount: Double) {
11 if (balance < withdrawAmount) {
12 throw InsufficientFunds()
13 }
14 balance -= withdrawAmount
15 }
16 }
Chapter 1: Safety 19
17
18 class InsufficientFunds : Exception()
19
20 val account = BankAccount()
21 println(account.balance) // 0.0
22 account.deposit(100.0)
23 println(account.balance) // 100.0
24 account.withdraw(50.0)
25 println(account.balance) // 50.0
1 var num = 0
2 for (i in 1..1000) {
3 thread {
4 Thread.sleep(10)
5 num += 1
6 }
7 }
8 Thread.sleep(5000)
9 print(num) // Very unlikely to be 1000
10 // every time a different number, like for instance\
11 973
⁶To test it, just place it in an entry point (main function) and run. Similarly
with other snippets not having an entry point.
Chapter 1: Safety 21
1 val a = 10
2 a = 20 // ERROR
can be smart-casted:
1. They are easier to reason about since their state stays the
same once they are created.
2. Immutability makes it easier to parallelize the program
as there are no conflicts among shared objects.
3. References to immutable objects can be cached as they
are not going to change.
4. We do not need to make defensive copies on immutable
objects. When we do copy immutable objects, we do not
need to make it a deep copy.
5. Immutable objects are the perfect material to construct
other objects. Both mutable and immutable. We can still
decide where mutability takes place, and it is easier to
operate on immutable objects.
6. We can add them to set or use them as keys in maps,
in opposition to mutable objects that shouldn’t be used
this way. This is because both those collections use
hash table under the hood in Kotlin/JVM, and when
we modify element already classified to a hash table,
its classification might not be correct anymore and we
won’t be able to find it. This problem will be described
in details in Item 41: Respect the contract of hashCode.
We have a similar issue when a collection is sorted.
Chapter 1: Safety 30
1 class User(
2 val name: String,
3 val surname: String
4 ) {
5 fun withSurname(surname: String) = User(name, su\
6 rname)
7 }
8
9 var user = User("Maja", "Markiewicz")
10 user = user.withSurname("Moskała")
11 print(user) // User(name=Maja, surname=Moskała)
1 list1.add(1)
2 list2 = list2 + 1
Both those ways are correct and they both have their pros and
cons. They both have a single mutating point, but it is located
in a different place. In the first one mutation takes place on
the concrete list implementation. We might depend on the fact
that it has proper synchronization in case of multithreading,
but such an assumption is also dangerous since it is not really
guaranteed. In the second one, we need to implement the
synchronization ourselves, but the overall security is better
because the mutating point is only a single property. Though,
in case of a lack of synchronization, remember that we can
still lose some elements:
Chapter 1: Safety 33
1 // Don’t do that
2 var list3 = mutableListOf<Int>()
10
11 //...
12 }
1 class UserHolder {
2 private val user: MutableUser()
3
4 fun get(): MutableUser {
5 return user.copy()
6 }
7
8 //...
9 }
Chapter 1: Safety 36
Summary
In this chapter we’ve learned why it is important to limit
mutability and to prefer immutable objects. We’ve seen that
Kotlin gives us a lot of tools that support limiting mutability.
We should use them to limit mutation points. Simple rules are:
1 val a = 1
2 fun fizz() {
3 val b = 2
4 print(a + b)
5 }
6 val buzz = {
7 val c = 3
8 print(a + c)
9 }
10 // Here we can see a, but not b nor c
1 // Bad
2 var user: User
3 for (i in users.indices) {
4 user = users[i]
5 print("User at $i is $user")
6 }
7
8 // Better
9 for (i in users.indices) {
10 val user = users[i]
11 print("User at $i is $user")
12 }
13
14 // Same variables scope, nicer syntax
15 for ((i, user) in users.withIndex()) {
16 print("User at $i is $user")
17 }
1 // Bad
2 val user: User
3 if (hasValue) {
4 user = getValue()
5 } else {
6 user = User()
7 }
8
9 // Better
10 val user: User = if(hasValue) {
11 getValue()
12 } else {
13 User()
Chapter 1: Safety 41
14 }
1 // Bad
2 fun updateWeather(degrees: Int) {
3 val description: String
4 val color: Int
5 if (degrees < 5) {
6 description = "cold"
7 color = Color.BLUE
8 } else if (degrees < 23) {
9 description = "mild"
10 color = Color.YELLOW
11 } else {
12 description = "hot"
13 color = Color.RED
14 }
15 // ...
16 }
17
18 // Better
19 fun updateWeather(degrees: Int) {
20 val (description, color) = when {
21 degrees < 5 -> "cold" to Color.BLUE
22 degrees < 23 -> "mild" to Color.YELLOW
23 else -> "hot" to Color.RED
24 }
25 // ...
26 }
Capturing
When I teach about Kotlin coroutines, one of my exercises is
to implement the Sieve of Eratosthenes to find prime num-
bers using a sequence builder. The algorithm is conceptually
simple:
1 print(primes.take(10).toList()) // [2, 3, 5, 6, 7, \
2 8, 9, 10, 11, 12]
Summary
For many reasons, we should prefer to define variables for
the closest possible scope. Also, we prefer val over var also
for local variables. We should always be aware of the fact that
variables are captured in lambdas. These simple rules can save
us a lot of trouble.
Chapter 1: Safety 45
1 // Java
2 public class JavaTest {
3
4 public String giveName() {
5 // ...
6 }
7 }
1 // Java
2 public class UserRepo {
3
4 public List<User> getUsers() {
5 //***
6 }
7 }
8
9 // Kotlin
10 val users: List<User> = UserRepo().users!!.filterNo\
11 tNull()
1 // Java
2 public class UserRepo {
3 public User getUser() {
4 //...
5 }
6 }
7
8 // Kotlin
9 val repo = UserRepo()
10 val user1 = repo.user // Type of user1 is Us\
11 er!
12 val user2: User = repo.user // Type of user2 is Us\
13 er
14 val user3: User? = repo.user // Type of user3 is Us\
15 er?
1 // Java
2 import org.jetbrains.annotations.NotNull;
3
4 public class UserRepo {
5 public @NotNull User getUser() {
6 //...
7 }
8 }
1 // Java
2 public class JavaClass {
3 public String getValue() {
4 return null;
5 }
6 }
7
8 // Kotlin
9 fun statedType() {
10 val value: String = JavaClass().value
11 //...
Chapter 1: Safety 50
12 println(value.length)
13 }
14
15 fun platformType() {
16 val value = JavaClass().value
17 //...
18 println(value.length)
19 }
1 // Java
2 public class JavaClass {
3 public String getValue() {
4 return null;
5 }
6 }
7
8 // Kotlin
9 fun platformType() {
10 val value = JavaClass().value
11 //...
12 println(value.length) // NPE
13 }
14
15 fun statedType() {
16 val value: String = JavaClass().value // NPE
17 //...
18 println(value.length)
19 }
1 interface UserRepo {
2 fun getUserName() = JavaClass().value
3 }
Summary
Types that come from another language and has unknown
nullability are known as platform types. Since they are dan-
gerous, we should eliminate them as soon as possible, and
do not let them propagate. It is also good to specify types
using annotations that specify nullability on exposed Java
constructors, methods and fields. It is precious information
both for Java and Kotlin developers using those elements.
Chapter 1: Safety 53
1 interface CarFactory {
2 fun produce(): Car
3 }
1 interface CarFactory {
2 fun produce() = DEFAULT_CAR
3 }
Now, all your factories can only produce Fiat126P. Not good.
If you defined this interface yourself, this problem will be
probably caught soon and easily fixed. Though, if it is a part
Chapter 1: Safety 55
Summary
The general rule is that if we are not sure about the type, we
should specify it. It is important information and we should
not hide it (Item 14: Specify the variable type when it is not
clear). Additionally for the sake of security, in an external
API, we should always specify types. We cannot let them be
changed by accident. Inferred types can be too restrictive or
can too easily change when our project evolves.
⁸Elements (classes, functions, objects) that might be used from some outer
module or some part of our code maintained by different developers. For
instance, in libraries, those are all public and protected classes, functions and
object declarations.
Chapter 1: Safety 56
1 // Part of Stack<T>
2 fun pop(num: Int = 1): List<T> {
3 require(num <= size) {
4 "Cannot remove more elements than current si\
5 ze"
6 }
7 check(isOpen) { "Cannot pop from closed stack" }
8 val collection = innerCollection ?: return empty\
9 List()
10 val ret = collection.take(num)
11 innerCollection = collection.drop(num)
12 assert(ret.size == num)
13 return ret
14 }
Chapter 1: Safety 57
Arguments
When you define a function with arguments, it is not un-
common to have some expectations on those arguments that
cannot be expressed using the type system. Just take a look at
a few examples:
14 requireNotNull(user.email)
15 require(isValidEmail(user.email))
16 //...
17 }
State
It is not uncommon that we only allow using some functions
in concrete conditions. A few common examples:
Assertions
With a correct implementation, there are things we know to
be true. For instance, when a function is asked to return 10
elements we might expect that it will return 10 elements. This
is something we expect to be true, but it doesn’t mean we
are always right. We all make mistakes. Maybe we made a
mistake in the implementation. Maybe someone changed a
function we used and our function does not work properly
anymore. Maybe our function does not work correctly any-
more because it was refactored. For all those problems the
most universal solution is that we should write unit tests that
check if our expectations match reality:
Chapter 1: Safety 62
1 class StackTest {
2
3 @Test
4 fun `Stack pops correct number of elements`() {
5 val stack = Stack(20) { it }
6 val ret = stack.pop(10)
7 assertEquals(10, ret.size)
8 }
9
10 //...
11 }
Summary
Specify your expectations to:
15 return Failure(JsonParsingException())
16 }
17 //...
18 return Success(result)
19 }
20
21 sealed class Result<out T>
22 class Success<out T>(val result: T): Result<T>()
23 class Failure(val throwable: Throwable): Result<Not\
24 hing>()
25
26 class JsonParsingException: Exception()
Throw an error
One problem with safe handling is that if printer could
sometimes be null, we will not be informed about it but
instead print won’t be called. This way we might have
hidden important information. If we are expecting printer
to never be null, we will be surprised when the print
method isn’t called. This can lead to errors that are really hard
to find. When we have a strong expectation about something
Chapter 1: Safety 78
¹⁴In the Kotlin Standard Library such function is called maxOf and it can
compare 2 or 3 values.
Chapter 1: Safety 80
1 class UserControllerTest {
2
3 private var dao: UserDao? = null
4 private var controller: UserController? = null
5
6 @BeforeEach
7 fun init() {
8 dao = mockk()
9 controller = UserController(dao!!)
10 }
11
12 @Test
13 fun test() {
14 controller!!.doSomething()
15 }
16
17 }
1 class UserControllerTest {
2
3 private var dao: UserDao? = null
4 private var controller: UserController? = null
5
6 @BeforeEach
7 fun init() {
8 dao = mockk()
9 controller = UserController(dao!!)
10 }
11
12 @Test
13 fun test() {
14 controller!!.doSomething()
15 }
16
17 }
1 class UserControllerTest {
2 private lateinit var dao: UserDao
3 private lateinit var controller: UserController
4
5 @BeforeEach
6 fun init() {
7 dao = mockk()
8 controller = UserController(dao)
9 }
10
11 @Test
12 fun test() {
13 controller.doSomething()
14 }
15 }
1 @Test
2 fun `fib works correctly for the first 5 positions`\
3 () {
4 assertEquals(1, fib(0))
5 assertEquals(1, fib(1))
6 assertEquals(2, fib(2))
7 assertEquals(3, fib(3))
8 assertEquals(5, fib(4))
9 }
Chapter 1: Safety 91
• Complex functionalities
• Parts that will most likely change over time or will be
refactored
• Business logic
• Parts of our public API
• Parts that have a tendency to break
• Production bugs that we fixed
Summary
This chapter was started with a reflection that the first prior-
ity should be for our programs to behave correctly. It can be
supported by using good practices presented in this chapter,
but above that, the best way to ensure that our application
behaves correctly is to check it by testing, especially unit
testing. This is why a responsible chapter about safety needs
at least a short section about unit testing. Just like responsible
business application requires at least some unit tests.
Chapter 2: Readability
Any fool can write code that a computer can under-
stand. Good programmers write code that humans
can understand.
–Martin Fowler, Refactoring: Improving the Design
of Existing Code, p. 15
1 life=:[:+/(3 4=/[:+/(,/,"0/~i:1)|.])*.1,:]
1 // Implementation A
2 if (person != null && person.isAdult) {
3 view.showPerson(person)
4 } else {
5 view.showError()
6 }
7
8 // Implementation B
9 person?.takeIf { it.isAdult }
10 ?.let(view::showPerson)
11 ?: view.showError()
15 view.hideProgress()
16 }
disciplines.
1 students
2 .filter { it.pointsInSemester > 15 && it.resul\
3 t >= 50 }
4 .sortedWith(compareBy({ it.surname }, { it.nam\
5 e }))
6 .joinToString(separator = "\n") {
7 "${it.name} ${it.surname}, ${it.result}"
8 }
9 .let(::print)
10
11 var obj = FileInputStream("/file.gz")
12 .let(::BufferedInputStream)
13 .let(::ZipInputStream)
14 .let(::ObjectInputStream)
15 .readObject() as SomeObject
Conventions
We’ve acknowledged that different people have different
views of what readability means. We constantly fight over
Chapter 2: Readability 102
1 10 * 6!
Unclear cases
The biggest problem is when it is unclear if some usage fulfills
conventions. For instance, what does it mean when we triple
a function? For some people, it is clear that it means making
another function that repeats this function 3 times:
Chapter 2: Readability 108
¹⁷The difference is that the first one is producing a function while the other
one calls a function. In the first case the result of multiplication is ()->Unit
and in the second case it is Unit
Chapter 2: Readability 109
1 body {
2 div {
3 +"Some text"
4 }
5 }
You can see that to add text into an element we use String.unaryPlus.
This is acceptable because it is clearly part of the Domain
Specific Language (DSL). In this specific context, it’s not a
surprise to readers that different rules apply.
Chapter 2: Readability 110
Summary
Use operator overloading conscientiously. The function name
should always be coherent with its behavior. Avoid cases
where operator meaning is unclear. Clarify it by using a
regular function with a descriptive name instead. If you wish
to have a more operator-like syntax, then use the infix
modifier or a top-level function.
Chapter 2: Readability 111
1 if(!keyIsCorrect(key)) return
2
3 verifyKey(key) ?: return
1 val num = 10
2 val name = "Marcin"
3 val ids = listOf(12, 112, 554, 997)
Many receivers
Using explicit receivers can be especially helpful when we are
in the scope of more than one receiver. We are often in such
Chapter 2: Readability 117
What is the result? Stop now and spend some time trying to
answer yourself before reading the answer.
It is probably expected that the result should be “Created
parent.child”, but the actual result is “Created parent”. Why?
To investigate, we can use explicit receiver before name:
The problem is that the type this inside apply is Node? and
so methods cannot be used directly. We would need to unpack
it first, for instance by using a safe call. If we do so, result will
be finally correct:
DSL marker
There is a context in which we often operate on very nested
scopes with different receivers, and we don’t need to use
explicit receivers at all. I am talking about Kotlin DSLs.
We don’t need to use receivers explicitly because DSLs are
designed in such a way. However, in DSLs, it is especially
dangerous to accidentally use functions from an outer scope.
Think of a simple HTML DSL we use to make an HTML table:
1 table {
2 tr {
3 td { +"Column 1" }
4 td { +"Column 2" }
5 }
6 tr {
7 td { +"Value 1" }
8 td { +"Value 2" }
9 }
10 }
1 table {
2 tr {
3 td { +"Column 1" }
4 td { +"Column 2" }
5 tr {
6 td { +"Value 1" }
7 td { +"Value 2" }
8 }
9 }
10 }
1 @DslMarker
2 annotation class HtmlDsl
3
4 fun table(f: TableDsl.() -> Unit) { /*...*/ }
5
6 @HtmlDsl
7 class TableDsl { /*...*/ }
1 table {
2 tr {
3 td { +"Column 1" }
4 td { +"Column 2" }
5 tr { // COMPILATION ERROR
6 td { +"Value 1" }
7 td { +"Value 2" }
8 }
9 }
10 }
1 table {
2 tr {
3 td { +"Column 1" }
4 td { +"Column 2" }
5 [email protected] {
6 td { +"Value 1" }
7 td { +"Value 2" }
8 }
9 }
10 }
Summary
Do not change scope receiver just because you can. It might
be confusing to have too many receivers all giving us methods
we can use. Explicit argument or reference is generally better.
When we do change receiver, using explicit receivers im-
proves readability because it clarifies where does the function
come from. We can even use a label when there are many
receivers to clarify from which one the function comes from.
If you want to force using explicit receivers from the outer
scope, you can use DslMarker meta-annotation.
Chapter 2: Readability 124
1 // Kotlin property
2 var name: String? = null
3
4 // Java field
5 String name = null;
Even though they can be used the same way, to hold data, we
need to remember that properties have many more capabili-
ties. Starting with the fact that they can always have custom
setters and getters:
You can see here that we are using the field identifier. This
is a reference to the backing field that lets us hold data in
this property. Such backing fields are generated by default
because default implementations of setter and getter are using
them. We can also implement custom accessors that do not
use them, and in such a case a property will not have a field
at all. For instance, a Kotlin property can be defined using
only a getter for a read-only property val:
Chapter 2: Readability 125
1 interface Person {
2 val name: String
3 }
1 // DON’T DO THIS!
2 val Tree<Int>.sum: Int
3 get() = when (this) {
4 is Leaf -> value
5 is Node -> left.sum + right.sum
6 }
1 // DON’T DO THIS!
2 class UserIncorrect {
3 private var name: String = ""
4
5 fun getName() = name
6
7 fun setName(name: String) {
8 this.name = name
9 }
10 }
11
12 class UserCorrect {
13 var name: String = ""
14 }
1 sleep(100)
How much will it sleep? 100 ms? Maybe 100 seconds? We can
clarify it using a named argument:
1 sleep(timeMillis = 100)
1 sleep(Millis(100))
1 sleep(100.ms)
1 sendEmail(
2 to = "[email protected]",
3 message = "Hello, ..."
4 )
1 thread {
2 // ...
3 }
1 // Java
2 observable.getUsers()
3 .subscribe((List<User> users) -> { // onNext
4 // ...
5 }, (Throwable throwable) -> { // onError
6 // ...
7 }, () -> { // onCompleted
8 // ...
9 });
1 observable.getUsers()
2 .subscribeBy(
3 onNext = { users: List<User> ->
4 // ...
5 },
6 onError = { throwable: Throwable ->
7 // ...
8 },
9 onCompleted = {
10 // ...
11 })
Summary
Named arguments are not only useful when we need to
skip some default values. They are important information for
developers reading our code, and they can improve the safety
of our code. We should consider them especially when we
have more parameters with the same type (or with functional
types) and for optional arguments. When we have multiple
parameters with functional types, they should almost always
be named. An exception is the last function argument when
it has a special meaning, like in DSL.
Chapter 2: Readability 137
1 class Person(
2 val id: Int = 0,
3 val name: String = "",
4 val surname: String = ""
5 ) : Human(id, name) {
6 // body
7 }
²²Link: https://github.com/pinterest/ktlint
²³It is possible to have parameters strongly related to each other like x and
y on the same line, though personally, I am not in favour of that.
Chapter 2: Readability 139
Notice that those two are very different from the convention
that leaves the first parameter in the same line and then
indents all others to it.
1 // Don’t do that
2 class Person(val id: Int = 0,
3 val name: String = "",
4 val surname: String = "") : Human(id, \
5 name) {
6 // body
7 }
change them both, but it’s harder and more error-prone when
we need to change only one.
This chapter is dedicated to reusability. It touches on many
subjects that developers do intuitively. We do so because we
learned them through practice. Mainly through observations
of how something we did in the past has an impact on us
now. We extracted something, and now it causes problems.
We haven’t extracted something, and now we have a problem
when we need to make some changes. Sometimes we deal
with code written by different developers years ago, and we
see how their decisions impact us now. Maybe we just looked
at another language or project and we thought “Wow, this is
short and readable because they did X”. This is the way we
typically learn, and this is one of the best ways to learn.
It has one problem though: it requires years of practice. To
speed it up and to help you systematize this knowledge, this
chapter will give you some generic rules to help you make
your code better in the long term. It is a bit more theoretical
than the rest of the book. If you’re looking for concrete rules
(as presented in the previous chapters), feel free to skip it.
Chapter 3: Reusability 144
Knowledge
Let’s define knowledge in programming broadly, as any piece
of intentional information. It can be stated by code or data. It
can also be stated by lack of code or data, which means that
we want to use the default behavior. For instance when we
inherit, and we don’t override a method, it’s like saying that
we want this method to behave the same as in the superclass.
²⁵Standing for We Enjoy Typing, Waste Everyone’s Time or Write Every-
thing Twice.
Chapter 3: Reusability 145
1 class Student {
2 // ...
3
4 fun isPassing(): Boolean =
5 calculatePointsFromPassedCourses() > 15
6
7 fun qualifiesForScholarship(): Boolean =
8 calculatePointsFromPassedCourses() > 30
9
10 private fun calculatePointsFromPassedCourses(): \
11 Int {
12 //...
13 }
14 }
Chapter 3: Reusability 151
Then, original rules change and the dean decides that less im-
portant courses should not qualify for scholarship points cal-
culation. A developer who was sent to introduce this change
checked class qualifiesForScholarship, finds out that it
calls the private method calculatePointsFromPassedCourses
and changes it to skip courses that do not qualify. Uninten-
tionally, they’ve changed the behavior of isPassing as well.
Students who were supposed to pass, got informed that they
failed the semester. You can imagine their reaction.
It is true that we could easily prevent such situation if we
would have unit tests (Item 10: Write unit tests), but let’s skip
this aspect for now.
The developer might check where else the calculatePointsFromPassedCou
function is used. Although the problem is that this developer
didn’t expect that this private function was used by another
property with a totally different responsibility. Private func-
tions are rarely used just by more than one function.
This problem, in general, is that it is easy to couple responsi-
bilities located very close (in the same class/file).
A simple solution would be to extract these responsibili-
ties into separate classes. We might have separate classes
StudentIsPassingValidator and
StudentQualifiesForScholarshipValidator. Though in
Kotlin we don’t need to use such heavy artillery (see more at
Chapter 4: Design abstractions). We can just define qualifiesForScholarsh
and
calculatePointsFromPassedCourses as extension func-
tions on Student located in separate modules: one over which
Scholarships Department is responsible, and another over
which Accreditations Department is responsible.
Chapter 3: Reusability 152
1 // accreditations module
2 fun Student.qualifiesForScholarship(): Boolean {
3 /*...*/
4 }
5
6 // scholarship module
7 fun Student.calculatePointsFromPassedCourses(): Boo\
8 lean {
9 /*...*/
10 }
Summary
Everything changes and it is our job to prepare for that: to
recognize common knowledge and extract it. If a bunch of
elements has similar parts and it is likely that we will need to
change it for all instances, extract it and save time on search-
ing through the project and update many instances. On the
other hand, protect yourself from unintentional modifications
by separating parts that are coming from different sources.
Often it’s even more important side of the problem. I see many
developers who are so terrified of the literal meaning of Don’t
Repeat Yourself, that they tend to looking suspiciously at any
2 lines of code that look similar. Both extremes are unhealthy,
and we need to always search for a balance. Sometimes, it is a
tough decision if something should be extracted or not. This
is why it is an art to designing information systems well. It
requires time and a lot of practice.
Chapter 3: Reusability 154
1 fun Iterable<Int>.product() =
2 fold(1) { acc, i -> acc * i }
You don’t need to wait for more than one use. It is a well-
known mathematical concept and its name should be clear
for developers. Maybe another developer will need to use
it later in the future and they’ll be happy to see that it is
already defined. Hopefully, he or she will find this function.
It is bad practice to have duplicate functions achieving the
Chapter 3: Reusability 158
Summary
Do not repeat common algorithms. First, it is likely that there
is a stdlib function that you can use instead. This is why it
is good to learn the standard library. If you need a known
algorithm that is not in the stdlib, or if you need a certain
algorithm often, feel free to define it in your project. A good
choice is to implement it as an extension function.
Chapter 3: Reusability 160
The lazy and observable delegates are not special from the
language’s point of view²⁸. They can be extracted thanks to a
more general property delegation mechanism that can be used
to extract many other patterns as well. Some good examples
are View and Resource Binding, Dependency Injection (for-
mally Service Location), or Data Binding. Many of these pat-
terns require annotation processing in Java, however Kotlin
allows you to replace them with easy, type-safe property
delegation.
12
13 // Data binding
14 private val port by bindConfiguration("port")
15 private val token: String by preferences.bind(TOKEN\
16 _KEY)
21 }
22 }
1 @JvmField
2 private val `token$delegate` = LoggingProperty<Stri\
3 ng?>(null)
4 var token: String?
5 get() = `token$delegate`.getValue(this, ::token)
6 set(value) {
7 `token$delegate`.setValue(this, ::token, val\
8 ue)
9 }
Summary
Property delegates have full control over properties and have
nearly all the information about their context. This feature
can be used to extract practically any property behavior. lazy
and observable are just two examples from the standard
library, but there are also other delegates like vetoable or
notNull. Also Map<String, *> can be used as a property
delegate. Property delegation is a universal way to extract
property patterns, and as practice shows, there are a variety of
such patterns. It is a powerful and generic freature that every
Kotlin developer should have in their toolbox. When we know
Chapter 3: Reusability 166
Generic constraints
One important feature of type parameters is that they can
be constrained to be a subtype of a concrete type. We set a
constraint by placing supertype after a colon. This type can
include previous type parameters:
Summary
Type parameters are an important part of Kotlin typing
system. We use them to have type-safe generic algorithms or
generic objects. Type parameters can be constrained to be a
subtype of a concrete type. When they are, we can safely use
methods offered by this type.
Chapter 3: Reusability 171
1 interface Tree
2 class Birch: Tree
3 class Spruce: Tree
4
5 class Forest<T: Tree> {
6
7 fun <T: Tree> addTree(tree: T) {
8 // ...
9 }
10 }
Chapter 3: Reusability 172
Summary
Avoid shadowing type parameters, and be careful when you
see that type parameter is shadowed. Unlike for other kinds of
parameters, it is not intuitive and might be highly confusing.
Chapter 3: Reusability 174
1 class Cup<T>
1 fun main() {
2 val anys: Cup<Any> = Cup<Int>() // Error: Type mi\
3 smatch
4 val nothings: Cup<Nothing> = Cup<Int>() // Error:\
5 Type mismatch
6 }
Function types
In function types (explained deeply in Item 35: Consider
defining a DSL for complex object creation) there are rela-
tions between function types with different expected types
or parameters or return types. To see it practically, think of
a function that expects as argument a function accepting an
Int and returning Any:
1 // Java
2 Integer[] numbers = {1, 4, 2, 1};
3 Object[] objects = numbers;
4 objects[2] = "B"; // Runtime error: ArrayStoreExcep\
5 tion
16
17 val dogHouse = Box<Dog>()
18 val box: Box<Any> = dogHouse
19 box.set("Some string") // But I have a place for a \
20 Dog
21 box.set(42) // But I have a place for a Dog
This fact does not get along with contravariance (in modi-
fier). They both can be used again to move from any box to
expect anything else:
1 interface Dog
2 interface Cutie
3 data class Puppy(val name: String): Dog, Cutie
4 data class Hound(val name: String): Dog
5 data class Cat(val name: String): Cutie
6
7 fun fillWithPuppies(list: MutableList<in Puppy>) {
8 list.add(Puppy("Jim"))
9 list.add(Puppy("Beam"))
10 }
11
12 val dogs = mutableListOf<Dog>(Hound("Pluto"))
13 fillWithPuppies(dogs)
14 println(dogs)
15 // [Hound(name=Pluto), Puppy(name=Jim), Puppy(name=\
16 Beam)]
17
18 val animals = mutableListOf<Cutie>(Cat("Felix"))
19 fillWithPuppies(animals)
20 println(animals)
21 // [Cat(name=Felix), Puppy(name=Jim), Puppy(name=Be\
22 am)]
Summary
Kotlin has powerful generics that support constraints and also
allow to have a relation between generic types with different
type arguments declared both on declaration-side as well as
on use-side. This fact gives us great support when we operate
on generic objects. We have the following type modifiers:
In Kotlin:
Full-stack development
Lots of companies are based on web development. Their
product is a website, but in most cases, those products need
a backend application (also called server-side). On websites,
JavaScript is the king. It nearly has a monopoly on this plat-
form. On the backend, a very popular option (if not the most
popular) is Java. Since these languages are very different, it
is common that backend and web development are separated.
Things can change, however. Now Kotlin is becoming a popu-
lar alternative to Java for backend development. For instance
with Spring, the most popular Java framework, Kotlin is a
³²In Kotlin, we view the JVM, Android, JavaScript, iOS, Linux, Windows,
Mac and even embedded systems like STM32 as separate platforms.
Chapter 3: Reusability 191
Mobile development
This capability is even more important in the mobile world.
We rarely build only for Android. Sometimes we can live
without a server, but we generally need to implement an iOS
application as well. Both applications are written for a differ-
ent platform using different languages and tools. In the end,
Android and iOS versions of the same application are very
similar. They are often designed differently, but they nearly
always have the same logic inside. Using Kotlin mulitplatform
capabilities, we can implement this logic only once and reuse
it between those two platforms. We can make a common
module and implement business logic there. Business logic
should be independent of frameworks and platforms anyway
(Clean Architecture). Such common logic can be written in
pure Kotlin or using other common modules, and it can then
be used on different platforms.
In Android, it can be used directly since Android is built the
same way using Gradle. The experience is similar to having
those common parts in our Android project.
For iOS, we compile these common parts to an Objective-
C framework using Kotlin/Native which is compiled using
LLVM³³ into native code³⁴. We can then use it from Swift
in Xcode or AppCode. Alternatively, we can implement our
whole application using Kotlin/Native.
³³Like Swift or Rust.
³⁴Native code is code that is written to run on a specific processor.
Languages like C, C++, Swift, Kotlin/Native are native because they are
compiled into machine code for each processor they need to run on.
Chapter 3: Reusability 193
Libraries
Defining common modules is also a powerful tool for li-
braries. In particular, those that do not depend strongly on
platform can easily be moved to a common module and
allow users to use them from all languages running on the
JVM, JavaScript or natively (so from Java, Scala, JavaScript,
CoffeeScript, TypeScript, C, Objective-C, Swift, Python, C#,
etc.).
All together
We can use all those platforms together. Using Kotlin, we can
develop for nearly all kinds of popular devices and platforms,
and reuse code between them however we want. Just a few
examples of what we can write in Kotlin:
Chapter 3: Reusability 194
Abstraction in programming
We often forget how abstract everything we do in program-
ming is. When we type a number, it is easy to forget that it is
actually represented by zeros and ones. When we type some
String, it is easy to forget that it is a complex object where
Chapter 4: Abstraction design 197
1 // Java
2 List<String> names = new ArrayList<>();
3 for (User user : users) {
4 names.add(user.getName());
5 }
Car metaphor
Many things happen when you drive a car. It requires the
coordinated work of the engine, alternator, suspension and
³⁶Structure and Interpretation of Computer Programs by Hal Abelson and
Gerald Jay Sussman with Julie Sussman
Chapter 4: Abstraction design 199
• Hide complexity
• Organize our code
• Give creators the freedom to change
Level of abstraction
As you can see, layers were built upon layers in computer
science. This is why computer scientists started distinguishing
how high-level something is. The higher the level, the further
from physics. In programming, we say that the higher level,
the further from the processor. The higher the level, the fewer
details we need to worry about. But you are trading this
simplicity with a lack of control. In C, memory management
is an important part of your job. In Java, the Garbage Collector
Chapter 4: Abstraction design 203
1 class CoffeeMachine {
2
3 fun makeCoffee() {
4 // Declarations of hundreds of variables
5 // Complex logic to coordinate everything
6 // with many low-level optimizations
7 }
8 }
1 class CoffeeMachine {
2
3 fun makeCoffee() {
4 boilWater()
5 brewCoffee()
6 pourCoffee()
7 pourMilk()
8 }
9
10 private fun boilWater() {
11 // ...
12 }
13
14 private fun brewCoffee() {
15 // ...
16 }
17
18 private fun pourCoffee() {
19 // ...
20 }
21
Chapter 4: Abstraction design 205
Now you can clearly see what the general flow of this
function is. Those private functions are just like chapters in
a book. Thanks to that, if you need to change something,
you can jump directly where it is implemented. We just ex-
tracted higher-level procedures, which greatly simplified the
comprehension of our first procedure. We made it readable,
and if someone wanted to understand it at a lower level, they
can just jump there and read it. By extracting very simple
abstractions, we improved readability.
Following this rule, all these new functions should be just as
simple. This is a general rule - functions should be small and
have a minimal number of responsibilities ³⁷. If one of them is
more complex, we should extract intermediary abstractions³⁸.
As a result, we should achieve many small and readable
functions, all localized at a single level of abstraction. At every
level of abstraction we operate on abstract terms (methods
and classes) and if you want to clarify them, you can always
jump into their definition (in IntelliJ or Android Studio,
holding the Ctrl key (Command on Mac) while you click on
the function name will take you to the implementation). This
way we lose nothing from extracting those functions, and we
gain a lot of readability.
Additional bonus is that functions extracted this way are eas-
ier to reuse and test. Say that we now need to make a separate
³⁷Clean Code by Robert Cecil Martin
³⁸Those might be functions like in this example, as well as classes or other
kinds of abstractions. Differences will be shown in the next item, Item 27:
Use abstraction to protect code against changes.
Chapter 4: Abstraction design 206
1 fun makeEspressoCoffee() {
2 boilWater()
3 brewCoffee()
4 pourCoffee()
5 }
Summary
Making separate abstraction layers is a popular concept used
in programming. It helps us organize knowledge and hide
details of a subsystem, allowing the separation of concerns
to facilitate interoperability and platform independence. We
separate abstractions in many ways, like functions, classes,
modules. We should try not to make any of those layers too
big. Smaller abstractions operating on a single layer are easier
to understand. The general notion of abstraction level is that
the closer to concrete actions, processor or input/output, the
lower level it is. In a lower abstraction layers we define a
language of terms (API) for a higher layer or layers.
Chapter 4: Abstraction design 209
Constant
Literal constant values are rarely self-explanatory and they
are especially problematic when they repeat in our code.
Moving the values into constant properties not only assigns
the value a meaningful name, but also helps us better manage
when this constant needs to be changed. Let’s see a simple
example with password validation:
1 val MAX_THREADS = 10
• Names it
• Helps us change its value in the future
Function
Imagine that you are developing an application and you
noticed that you often need to display a toast message to a
user. This is how you do it programmatically:
1 fun Context.toast(
2 message: String,
3 duration: Int = Toast.LENGTH_LONG
4 ) {
5 Toast.makeText(this, message, duration).show()
6 }
7
8 // Usage
9 context.toast(message)
10
11 // Usage in Activity or subclasses of Context
12 toast(message)
1 fun Context.snackbar(
2 message: String,
3 length: Int = Toast.LENGTH_LONG
4 ) {
5 //...
6 }
Chapter 4: Abstraction design 214
1 fun Context.snackbar(
2 message: String,
3 duration: Int = Snackbar.LENGTH_LONG
4 ) {
5 //...
6 }
1 fun Context.showMessage(
2 message: String,
3 duration: MessageLength = MessageLength.LONG
4 ) {
5 val toastDuration = when(duration) {
6 SHORT -> Length.LENGTH_SHORT
7 LONG -> Length.LENGTH_LONG
8 }
9 Toast.makeText(this, message, toastDuration).sho\
10 w()
11 }
12
13 enum class MessageLength { SHORT, LONG }
Class
Here is how we can abstract message display into a class:
The key reason why classes are more powerful than functions
is that they can hold a state and expose many functions
(class member functions are called methods). In this case, we
have a context in the class state, and it is injected via the
constructor. Using a dependency injection framework we can
delegate the class creation:
1 messageDisplay.setChristmasMode(true)
As you can see, the class gives us more freedom. But they still
have their limitations. For instance, when a class is final, we
know what exact implementation is under its type. We have
a bit more freedom with open classes because one could serve
a subclass instead. This abstraction is still strongly bound to
this class though. To get more freedom we can make it even
more abstract and hide this class behind an interface.
Interface
Reading the Kotlin standard library, you might notice that
nearly everything is represented as an interface. Just take a
look at a few examples:
Chapter 4: Abstraction design 218
1 interface MessageDisplay {
2 fun show(message: String, duration: MessageLengt\
3 h = LONG)
4 }
5
6 class ToastDisplay(val context: Context): MessageDi\
7 splay {
8
9 override fun show(message: String, duration: Mes\
10 sageLength) {
11 val toastDuration = when(duration) {
12 SHORT -> Length.SHORT
13 LONG -> Length.LONG
14 }
15 Toast.makeText(context, message, toastDurati\
16 on).show()
17 }
18 }
19
20 enum class MessageLength { SHORT, LONG }
Next ID
Let’s discuss one more example. Let’s say that we need a
unique ID in our project. A very simple way is to have a top-
level property to hold next ID, and increment it whenever we
need a new ID:
Seeing such usage spread around our code should cause some
alerts. What if we wanted to change the way IDs are created.
Let’s be honest, this way is far from perfect:
• Extracting constant
Chapter 4: Abstraction design 222
• Team size
• Team experience
• Project size
• Feature set
• Domain knowledge
Summary
Abstractions are not only to eliminate redundancy and to
organize our code. They also help us when we need to
change our code. Although using abstractions is harder. They
are something we need to learn and understand. It is also
harder to understand the consequences when we use abstract
structures. We need to understand both the importance and
risk of using abstractions, and we need to search for a balance
Chapter 4: Abstraction design 227
1 @Experimental(level = Experimental.Level.WARNING)
2 annotation class ExperimentalNewApi
3
4 @ExperimentalNewApi
5 suspend fun getUsers(): List<User> {
6 //...
7 }
for a long time. Doing that slows down adoption, but also
helps us design good API for longer.
When we need to change something that is part of a stable
API, to help users deal with this transition, we start with
annotating this element with the Deprecated annotation:
Summary
Users need to know about API stability. While a stable API is
preferred, there is nothing worse than unexpected changes in
an API that supposed to be stable. Such changes can be really
painful for users. Correct communication between module or
library creators and their users is important. We achieve that
by using version names, documentation, and annotations.
Also, each change in a stable API needs to follow a long
process of deprecation.
Chapter 4: Abstraction design 233
1 class CounterSet<T>(
2 private val innerSet: MutableSet<T> = setOf()
3 ) : MutableSet<T> by innerSet {
4
5 var elementsAdded: Int = 0
6 private set
7
8 override fun add(element: T): Boolean {
9 elementsAdded++
10 return innerSet.add(element)
11 }
12
13 override fun addAll(elements: Collection<T>): Bo\
14 olean {
15 elementsAdded += elements.size
16 return innerSet.addAll(elements)
17 }
18 }
1 class User(
2 val name: String,
3 val surname: String,
4 val age: Int
5 )
Summary
The rule of thumb is that: Elements visibility should be as
restrictive as possible. Visible elements constitute the public
API, and we prefer it as lean as possible because:
1 fun Context.showMessage(
2 message: String,
3 length: MessageLength = MessageLength.LONG
4 ) {
5 val toastLength = when(length) {
6 SHORT -> Length.SHORT
7 LONG -> Length.LONG
8 }
9 Toast.makeText(this, message, toastLength).show()
10 }
11
12 enum class MessageLength { SHORT, LONG }
1 /**
2 * Universal way for the project to display a short \
3 message to a user.
4 * @param message The text that should be shown to t\
5 he user
6 * @param length How long to display the message.
7 */
8 fun Context.showMessage(
9 message: String,
10 duration: MessageLength = MessageLength.LONG
11 ) {
12 val toastDuration = when(duration) {
13 SHORT -> Length.SHORT
14 LONG -> Length.LONG
15 }
16 Toast.makeText(this, message, toastDuration).sho\
17 w()
18 }
19
20 enum class MessageLength { SHORT, LONG }
In many cases, there are details that are not clearly inferred
by the name at all. For instance, powerset, even though it is
a well-defined mathematical concept, needs an explanation
since it is not so well known and interpretation is not clear
enough:
Chapter 4: Abstraction design 243
1 /**
2 * Powerset returns a set of all subsets of the rece\
3 iver
4 * including itself and the empty set
5 */
6 fun <T> Collection<T>.powerset(): Set<Set<T>> =
7 if (isEmpty()) setOf(emptySet())
8 else take(size - 1)
9 .powerset()
10 .let { it + it.map { it + last() } }
1 /**
2 * Powerset returns a set of all subsets of the rece\
3 iver
4 * including itself and empty set
5 */
6 fun <T> Collection<T>.powerset(): Set<Set<T>> =
7 powerset(this, setOf(setOf()))
8
9 private tailrec fun <T> powerset(
10 left: Collection<T>,
11 acc: Set<Set<T>>
12 ): Set<Set<T>> = when {
13 left.isEmpty() -> acc
14 else ->powerset(left.drop(1), acc + acc.map { it\
15 + left.first() })
16 }
Chapter 4: Abstraction design 244
Contract
Whenever we describe some behavior, users treat it as a
promise and based on that they adjust their expectations. We
call all such expected behaviors a contract of an element. Just
like in a real-life contract another side expects us to honor it,
here as well users will expect us to keep this contract once it
is stable (Item 28: Specify API stability).
At this point, defining a contract might sound scary, but
actually, it is great for both sides. When a contract is well
specified, creators do not need to worry about how the
class is used, and users do not need to worry about how
something is implemented under the hood. Users can rely
on this contract without knowing anything about the actual
implementation. For creators, the contract gives freedom to
change everything as long as the contract is satisfied. Both
users and creators depend on abstractions defined in the
contract, and so they can work independently. Everything
will work perfectly fine as long as the contract is respected.
This is a comfort and freedom for both sides.
What if we don’t set a contract? Without users knowing
what they can and cannot do, they’ll depend on imple-
mentation details instead. A creator without knowing
what users depend on would be either blocked or they
would risk breaking users implementations. As you can
see, it is important to specify a contract.
Chapter 4: Abstraction design 245
Defining a contract
How do we define a contract? There are various ways, includ-
ing:
Do we need comments?
Looking at history, it is amazing to see how opinions in the
community fluctuate. When Java was still young, there was
a very popular concept of literate programming. It suggested
explaining everything in comments⁴⁶. A decade later we can
hear a very strong critique of comments and strong voices
that we should omit comments and concentrate on writing
readable code instead (I believe that the most influential book
was the Clean Code by Robert C. Martin).
⁴⁶Read more about this concept in the book Literate Programming by
Donald Knuth.
Chapter 4: Abstraction design 246
I also agree that when we just need to organize our code, in-
stead of comments in the implementation, we should extract
a function. Take a look at the example below:
Chapter 4: Abstraction design 247
1 fun update() {
2 // Update users
3 for (user in users) {
4 user.update()
5 }
6
7 // Update books
8 for (book in books) {
9 updateBook(book)
10 }
11 }
1 fun update() {
2 updateUsers()
3 updateBooks()
4 }
5
6 private fun updateBooks() {
7 for (book in books) {
8 updateBook(book)
9 }
10 }
11
12 private fun updateUsers() {
13 for (user in users) {
Chapter 4: Abstraction design 248
14 user.update()
15 }
16 }
1 /**
2 * Returns a new read-only list of given elements.
3 * The returned list is serializable (JVM).
4 * @sample samples.collections.Collections.Lists.rea\
5 dOnlyList
6 */
7 public fun <T> listOf(vararg elements: T): List<T> \
8 =
9 if (elements.size > 0) elements.asList() else \
10 emptyList()
KDoc format
When we document functions using comments, the official
format in which we present that comment is called KDoc.
Chapter 4: Abstraction design 249
All KDoc comments start with /** and end with */, and
internally all lines generally start with *. Descriptions there
are written in KDoc markdown.
The structure of this KDoc comment is the following:
1 /**
2 * This is an example descriptions linking to [eleme\
3 nt1],
4 * [com.package.SomeClass.element2] and
5 * [this element with custom description][element3]
6 */
1 /**
2 * Immutable tree data structure.
3 *
4 * Class represents immutable tree having from 1 to \
5 infinitive number
6 * of elements. In the tree we hold elements on each\
7 node and nodes
8 * can have left and right subtrees...
9 *
10 * @param T the type of elements this tree holds.
11 * @property value the value kept in this node of th\
12 e tree.
13 * @property left the left subtree.
14 * @property right the right subtree.
15 */
16 class Tree<T>(
17 val value: T,
18 val left: Tree<T>? = null,
19 val right: Tree<T>? = null
20 ) {
21 /**
22 * Creates a new tree based on the current but w\
23 ith [element] added.
24 * @return newly created tree with additional el\
25 ement.
26 */
27 operator fun plus(element: T): Tree { ... }
28 }
1 interface Car {
2 fun setWheelPosition(angle: Float)
3 fun setBreakPedal(pressure: Double)
4 fun setGasPedal(pressure: Double)
5 }
6
7 class GasolineCar: Car {
8 // ...
9 }
10
Chapter 4: Abstraction design 253
1 interface Car {
2 /**
3 * Changes car direction.
4 *
5 * @param angle Represents position of wheels in\
6 radians
7 * relatively to car axis. 0 means driving strai\
8 ght, pi/2 means
9 * driving maximally right, -pi/2 maximally left\
10 . Value needs to
11 * be in (-pi/2, pi/2)
12 */
13 fun setWheelPosition(angle: Float)
14
15 /**
16 * Decelerates vehicle speed until 0.
17 *
Chapter 4: Abstraction design 254
Now all cars have set a standard that describes how they all
should behave.
Most classes in the stdlib and in popular libraries have well-
defined and well-described contracts and expectancies for
their children. We should define contracts for our elements as
well. Those contracts will make those interfaced truly useful.
They will give us the freedom to use classes that implement
those interfaces in the way their contract guarantees.
Chapter 4: Abstraction design 255
Leaking implementation
Implementation details always leak. In a car, different kinds
of engines behave a bit differently. We are still able to drive
the car, but we can feel a difference. It is fine as this is not
described in the contract.
In programming languages, implementation details leak as
well. For instance, calling a function using reflection works,
but it is significantly slower than a normal function call
(unless it is optimized by the compiler). We will see more
examples in the chapter about performance optimization.
Though as long as a language works as it promises, everything
is fine. We just need to remember and apply good practices.
In our abstractions, implementation will leak as well, but still,
we should protect it as much as we can. We protect it by
encapsulation, which can be described as “You can do what I
allow, and nothing more”. The more encapsulated classes and
functions are, the more freedom we have inside them because
we don’t need to think about how one might depend on our
implementation.
Summary
When we define an element, especially parts of external API,
we should define a contract. We do that through names,
documentation, comments, and types. The contract specifies
what the expectations are on those elements. It can also
describe how an element should be used.
A contract gives users confidence about how elements behave
now and will behave in the future, and it gives creators the
freedom to change what is not specified in the contract. The
Chapter 4: Abstraction design 256
1 class Employee {
2 private val id: Int = 2
3 override fun toString() = "User(id=$id)"
4
5 private fun privateFunction() {
6 println("Private function called")
7 }
8 }
9
10 fun callPrivateFunction(employee: Employee) {
11 employee::class.declaredMemberFunctions
12 .first { it.name == "privateFunction" }
13 .apply { isAccessible = true }
14 .call(employee)
15 }
16
17 fun changeEmployeeId(employee: Employee, newId: Int\
18 ) {
19 employee::class.java.getDeclaredField("id")
20 .apply { isAccessible = true }
21 .set(employee, newId)
22 }
23
Chapter 4: Abstraction design 258
24 fun main() {
25 val employee = Employee()
26 callPrivateFunction(employee)
27 // Prints: Private function called
28
29 changeEmployeeId(employee, 1)
30 print(employee) // Prints: User(id=1)
31 }
Summary
If you want your programs to be stable, respect contracts. If
you are forced to break them, document this fact well. Such
information will be very helpful to whoever will maintain
your code. Maybe that will be you, in a few years’ time.
Chapter 5: Object
creation
I was once asked, “Why do we need a whole chapter about
object creation? I was told that we just use constructors and
that’s it. Don’t we?”. As a response I showed that person a
whole book about Creational Patterns.
Although Kotlin can be written in a purely functional style, it
can also be written in object oriented programming (OOP),
much like Java. In OOP, we need to create every object
we use, or at least define how it ought to be created, and
different ways have different characteristics. It is important
to know what options do we have. This is why this chapter
shows different ways how we can define object creation, and
explains their advantages and disadvantages.
If you are familiar with the Effective Java book by Joshua
Bloch, then you may notice some similarities between this
chapter and that book. It is no coincidence. This chapter
relates to the first chapter of Effective Java. Although Kotlin
is very different from Java, and there are only morsels of
knowledge that can be used. For instance, static methods are
not allowed in Kotlin, but we have very good alternatives
like top-level functions and companion object functions. They
don’t work the same way as static functions, so it is important
to understand them. Similarly, with other items, you can
notice similarities, but the changes that Kotlin has introduced
are important. To cheer you up: these changes are mostly to
Chapter 5: Object creation 261
13 }
14 }
15
16 // Usage
17 val list = MyList.of(1, 2)
1 interface Tool {
2 companion object { /*...*/ }
3 }
1 Tool.createBigTool()
1 interface Tool {
2 companion object {}
3 }
Top-level functions
One popular way to create an object is by using top-level fac-
tory functions. Some common examples are listOf, setOf,
and mapOf. Similarly, library designers specify top-level func-
tions that are used to create objects. Top-level factory func-
tions are used widely. For example, in Android, we have the
tradition of defining a function to create an Intent to start
an Activity. In Kotlin, the getIntent() can be written as a
companion object function:
1 intentFor<MainActivity>()
Fake constructors
Constructors in Kotlin are used the same way as top-level
functions:
1 class A
2 val a = A()
functions under the hood. This is why they are often called
fake constructors.
Two main reasons why developers choose fake constructors
over the real ones are:
1 class Tree<T> {
2
3 companion object {
4 operator fun <T> invoke(size: Int, generator\
5 : (Int)->T): Tree<T>{
6 //...
7 }
8 }
9 }
10
11 // Usage
12 Tree(10) { "$it" }
Chapter 5: Object creation 276
1 Tree.invoke(10) { "$it" }
Fake constructor:
Summary
As you can see, Kotlin offers a variety of ways to specify
factory functions and they all have their own use. We should
have them in mind when we design object creation. Each of
them is reasonable for different cases. Some of them should
preferably be used with caution: Fake Constructors, Top-
Level Factory Method, and Extension Factory Function. The
most universal way to define a factory function is by using
a Companion Object. It is safe and very intuitive for most
developers since usage is very similar to Java Static Factory
Methods, and Kotlin mainly inherits its style and practices
from Java.
Chapter 5: Object creation 279
1 class QuotationPresenter(
2 private val view: QuotationView,
3 private val repo: QuotationRepository
4 ) {
5 private var nextQuoteId = -1
6
7 fun onStart() {
8 onNext()
9 }
10
11 fun onNext() {
12 nextQuoteId = (nextQuoteId + 1) % repo.quote\
13 sNumber
14 val quote = repo.getQuote(nextQuoteId)
15 view.showQuote(quote)
16 }
17 }
1 class Pizza {
2 val size: String
3 val cheese: Int
4 val olives: Int
5 val bacon: Int
6
7 constructor(size: String, cheese: Int, olives: I\
8 nt, bacon: Int) {
9 this.size = size
10 this.cheese = cheese
11 this.olives = olives
12 this.bacon = bacon
13 }
14 constructor(size: String, cheese: Int, olives: I\
15 nt):
16 this(size, cheese, olives, 0)
17 constructor(size: String, cheese: Int): this(siz\
18 e, cheese, 0)
19 constructor(size: String): this(size, 0)
20 }
1 class Pizza(
2 val size: String,
3 val cheese: Int = 0,
4 val olives: Int = 0,
5 val bacon: Int = 0
6 )
Default values are not only cleaner and shorter, but their
usage is also more powerful than the telescoping constructor.
We can specify just size and olives:
Builder pattern
Named parameters and default arguments are not allowed
in Java. This is why Java developers mainly use the builder
pattern. It allows them to:
• name parameters,
• specify parameters in any order,
• have default values.
Comparing these two simple usages, you can see the advan-
tages of named parameters over the builder:
11
12 val router = Router.Builder()
13 .addRoute(path = "/home", ::showHome)
14 .addRoute(path = "/users", ::showUsers)
15 .build()
1 fun Context.makeDefaultDialogBuilder() =
2 AlertDialog.Builder(this)
3 .setIcon(R.drawable.ic_dialog)
4 .setTitle(R.string.dialog_title)
5 .setOnCancelListener { it.cancel() }
Summary
Creating objects using a primary constructor is the most
appropriate approach for the vast majority of objects in our
projects. Telescoping constructor patterns should be treated as
obsolete in Kotlin. I recommend using default values instead,
as they are cleaner, more flexible, and more expressive. The
builder pattern is very rarely reasonable either as in simpler
cases we can just use a primary constructor with named
arguments, and when we need to create more complex object
we can use define a DSL for that.
Chapter 5: Object creation 291
1 body {
2 div {
3 a("https://kotlinlang.org") {
4 target = ATarget.blank
5 +"Main site"
6 }
7 }
8 +"Some content"
9 }
1 verticalLayout {
2 val name = editText()
3 button("Say Hello") {
4 onClick { toast("Hello, ${name.text}!") }
5 }
6 }
1 fun Routing.api() {
2 route("news") {
3 get {
4 val newsData = NewsUseCase.getAcceptedNe\
5 wsData()
6 call.respond(newsData)
7 }
8 get("propositions") {
9 requireSecret()
10 val newsData = NewsUseCase.getPropositio\
11 ns()
12 call.respond(newsData)
13 }
14 }
15 // ...
16 }
1 plugins {
2 `java-library`
3 }
4
5 dependencies {
6 api("junit:junit:4.12")
7 implementation("junit:junit:4.12")
8 testImplementation("junit:junit:4.12")
9 }
10
11 configurations {
12 implementation {
13 resolutionStrategy.failOnVersionConflict()
14 }
15 }
16
17 sourceSets {
18 main {
19 java.srcDir("src/core/java")
20 }
21 }
22
Chapter 5: Object creation 295
23 java {
24 sourceCompatibility = JavaVersion.VERSION_11
25 targetCompatibility = JavaVersion.VERSION_11
26 }
27
28 tasks {
29 test {
30 testLogging.showExceptions = true
31 }
32 }
1 myPlus.invoke(1, 2)
2 myPlus(1, 2)
3 1.myPlus(2)
1 class TdBuilder {
2 var text = ""
3
4 operator fun String.unaryPlus() {
5 text += this
6 }
7 }
1 class TableBuilder {
2 var trs = listOf<TrBuilder>()
3
4 fun td(init: TrBuilder.()->Unit) {
5 trs = trs + TrBuilder().apply(init)
6 }
7 }
8
9 class TrBuilder {
10 var tds = listOf<TdBuilder>()
11
12 fun td(init: TdBuilder.()->Unit) {
13 tds = tds + TdBuilder().apply(init)
14 }
15 }
Summary
A DSL is a special language inside of a language. It can make it
really simple to create complex object, and even whole object
hierarchies, like HTML code or complex configuration files.
On the other hand DSL implementations might be confusing
or hard for new developers. They are also hard to define. This
is why they should be only used when they offer real value.
For instance, for the creation of a really complex object, or
possibly for complex object hierarchies. This is why they are
also preferably defined in libraries rather than in projects. It
is not easy to make a good DSL, but a well defined DSL can
make our project much better.
⁵⁰Repeatable code not containing any important information for a reader
Chapter 6: Class design
Classes are the most important abstraction in the Object-
Oriented Programming (OOP) paradigm. Since OOP is the
most popular paradigm in Kotlin, classes are very important
for us as well. This chapter is about class design. Not about
system design, since it would require much more space and
there are already many great books on this topic such as Clean
Architecture by Robert C. Martin or Design Patterns by Erich
Gamma, John Vlissides, Ralph Johnson, and Richard Helm.
Instead, we will mainly talk about contracts that Kotlin classes
are expected to fulfill - how we use Kotlin structures and what
is expected from us when we use them. When and how should
we use inheritance? How do we expect data classes to be used?
When should we use function types instead of interfaces with
a single method? What are the contracts of equals, hashCode
and compareTo? When should we use extensions instead of
members? These are the kind of questions we will answer
here. They are all important because breaking them might
cause serious problems, and following them will help you
make your code safer and cleaner.
Chapter 6: Class design 305
1 class ProfileLoader {
2
3 fun load() {
4 // show progress
5 // load profile
6 // hide progress
7 }
8 }
9
10 class ImageLoader {
11
12 fun load() {
13 // show progress
14 // load image
15 // hide progress
Chapter 6: Class design 306
16 }
17 }
This approach works for such a simple case, but it has impor-
tant downsides we should be aware of:
Chapter 6: Class design 307
1 class Progress {
2 fun showProgress() { /* show progress */ }
3 fun hideProgress() { /* hide progress */ }
4 }
5
6 class ProfileLoader {
7 val progress = Progress()
8
9 fun load() {
10 progress.showProgress()
11 // load profile
12 progress.hideProgress()
13 }
Chapter 6: Class design 308
14 }
15
16 class ImageLoader {
17 val progress = Progress()
18
19 fun load() {
20 progress.showProgress()
21 // load image
22 progress.hideProgress()
23 }
24 }
1 class ImageLoader {
2 private val progress = Progress()
3 private val finishedAlert = FinishedAlert()
4
5 fun load() {
6 progress.showProgress()
7 // load image
8 progress.hideProgress()
9 finishedAlert.show()
10 }
11 }
Chapter 6: Class design 309
25 e) {
26
27 override fun innerLoad() {
28 // load image
29 }
30 }
What if then we need to create a robot dog that can bark but
can’t sniff?
Chapter 6: Class design 311
Why is that? The reason is that HashSet uses the add method
under the hood of addAll. The counter is then incremented
twice for each element added using addAll. The problem can
be naively solved by removing custom addAll function:
Chapter 6: Class design 313
1 class CounterSet<T> {
2 private val innerSet = HashSet<T>()
3 var elementsAdded: Int = 0
4 private set
5
6 fun add(element: T) {
7 elementsAdded++
8 innerSet.add(element)
9 }
10
11 fun addAll(elements: Collection<T>) {
Chapter 6: Class design 314
12 elementsAdded += elements.size
13 innerSet.addAll(elements)
14 }
15 }
16
17 val counterList = CounterSet<String>()
18 counterList.addAll(listOf("A", "B", "C"))
19 print(counterList.elementsAdded) // 3
1 class CounterSet<T>(
2 private val innerSet: MutableSet<T> = mutabl\
3 eSetOf()
4 ) : MutableSet<T> by innerSet {
5
6 var elementsAdded: Int = 0
7 private set
8
9 override fun add(element: T): Boolean {
10 elementsAdded++
11 return innerSet.add(element)
12 }
13
14 override fun addAll(elements: Collection<T>): Bo\
15 olean {
16 elementsAdded += elements.size
17 return innerSet.addAll(elements)
18 }
19 }
Restricting overriding
To prevent developers from extending classes that are not
designed for an inheritance, we can just keep them final.
Though if for a reason we need to allow inheritance, still all
methods are final by default. To let developers override them,
they must be set to open:
Use this mechanism wisely and open only those methods that
are designed for inheritance. Also remember that when you
override a method, you can make it final for all subclasses:
Chapter 6: Class design 318
This way you can limit the number of methods that can be
overridden in subclasses.
Summary
There are a few important differences between composition
and inheritance:
• toString
• equals and hashCode
• copy
• componentN
1 // After compilation
2 val id: Int = player.component1()
3 val name: String = player.component2()
4 val pts: Int = player.component3()
This approach has its pros and cons. The biggest advantage
is that we can name variables however we want. We can
also destructure everything we want as long as it provides
componentN functions. This includes List and Map.Entry:
Chapter 6: Class design 324
15
16 public override fun toString(): String =
17 "($first, $second, $third)"
18 }
If you don’t want this class in a wider scope, you can restrict
its visibility. It can even be private if you need to use it
for some local processing only in a single file or class. It is
worth using data classes instead of tuples. Classes are cheap
in Kotlin, do not be afraid to use them.
Chapter 6: Class design 330
1 interface OnClick {
2 fun clicked(view: View)
3 }
⁵¹Unless it is Java SAM and Java function: since in such cases there is
special support and we can pass a function type instead
Chapter 6: Class design 331
1 setOnClickListener { /*...*/ }
2 setOnClickListener(fun(view) { /*...*/ })
1 setOnClickListener(::println)
2 setOnClickListener(this::showUsers)
1 class CalendarView {
2 var listener: Listener? = null
3
4 interface Listener {
5 fun onDateClicked(date: Date)
6 fun onPageChanged(date: Date)
7 }
8 }
1 class CalendarView {
2 var onDateClicked: ((date: Date) -> Unit)? = null
3 var onPageChanged: ((date: Date) -> Unit)? = null
4 }
1 // Kotlin
2 class CalendarView() {
3 var onDateClicked: ((date: Date) -> Unit)? = null
4 var onPageChanged: OnDateClicked? = null
5 }
6
7 interface OnDateClicked {
8 fun onClick(date: Date)
9 }
10
11 // Java
Chapter 6: Class design 334
16 EQUAL,
17 NOT_EQUAL,
18 LIST_EMPTY,
19 LIST_NOT_EMPTY
20 }
21
22 companion object {
23 fun <T> equal(value: T) =
24 ValueMatcher<T>(value = value, matcher =\
25 Matcher.EQUAL)
26
27 fun <T> notEqual(value: T) =
28 ValueMatcher<T>(value = value, matcher =\
29 Matcher.NOT_EQUAL)
30
31 fun <T> emptyList() =
32 ValueMatcher<T>(matcher = Matcher.LIST_E\
33 MPTY)
34
35 fun <T> notEmptyList() =
36 ValueMatcher<T>(matcher = Matcher.LIST_N\
37 OT_EMPTY)
38 }
39 }
Sealed modifier
We do not necessarily need to use the sealed modifier.
We could use abstract instead, but sealed forbids any
subclasses to be defined outside of that file. Thanks to that,
if we cover all those types in when, we do not need to use an
else branch, as it’s guaranteed to be exhaustive. Using this
advantage, we can easily add new functionalities and know
that we won’t forget to cover them in these when statements.
This is a convenient way to define operations that behave
differently for different modes. For instance, we can define
match using when instead of by defining how it should behave
in all subclasses. New functions can be added this way even
as extensions.
Chapter 6: Class design 339
20 }
Summary
In Kotlin, we use type hierarchies instead of tagged classes.
We most often represent those type hierarchies as sealed
classes as they represent a sum type (a type collecting alter-
native class options). It doesn’t collide with the state pattern,
which is a popular and useful pattern in Kotlin. They actually
cooperate as when we implement state, we prefer to use sealed
hierarchies instead of tagged classes. This is especially true
when we implement complex yet separable states on a single
view.
Chapter 6: Class design 342
• equals
• hashCode
• toString
Equality
In Kotlin, there are two types of equality:
1 class DateTime(
2 /** The millis from 1970-01-01T00:00:00Z */
3 private var millis: Long = 0L,
4 private var timeZone: TimeZone? = null
5 ) {
6 private var asStringCache = ""
7 private var changed = false
8
9 override fun equals(other: Any?): Boolean =
10 other is DateTime &&
11 other.millis == millis &&
12 other.timeZone == timeZone
13
14 //...
15 }
Just notice that copy in such case will not copy those prop-
erties that are not declared in the primary constructor. Such
behavior is correct only when those additional properties are
truly redundant (the object will behave correctly when they
will be lost).
Chapter 6: Class design 347
1 class User(
2 val id: Int,
3 val name: String,
4 val surname: String
5 ) {
6 override fun equals(other: Any?): Boolean =
7 other is User && other.id == id
8
9 override fun hashCode(): Int = id
10 }
1 // DO NOT DO THIS!
2 class Time(
3 val millisArg: Long = -1,
4 val isNow: Boolean = false
5 ) {
6 val millis: Long get() =
7 if (isNow) System.currentTimeMillis() else m\
8 illisArg
9
10 override fun equals(other: Any?): Boolean =
11 other is Time && millis == other.millis
12 }
13
14 val now = Time(isNow = true)
15 now == now // Sometimes true, sometimes false
16 List(100000) { now }.all { it == now } // Most like\
17 ly false
1 class Complex(
2 val real: Double,
3 val imaginary: Double
4 ) {
5 // DO NOT DO THIS, violates symmetry
6 override fun equals(other: Any?): Boolean {
7 if (other is Double) {
8 return imaginary == 0.0 && real == other
9 }
10 return other is Complex &&
11 real == other.real &&
12 imaginary == other.imaginary
13 }
14 }
Chapter 6: Class design 351
17
18 o1.equals(o2) // false
19 o2.equals(o3) // false
20 o1 == o3 // false
21
22 o1.date.equals(o2) // true
23 o2.equals(o3.date) // true
24 o1.date == o3.date // true
1 import java.net.URL
2
3 fun main() {
4 val enWiki = URL("https://en.wikipedia.org/")
5 val wiki = URL("https://wikipedia.org/")
6 println(enWiki == wiki)
7 // true when internet turned on, false otherwise
8 }
Implementing equals
I recommend against implementing equals yourself unless
you have a good reason. Instead, use default or data class
equality. If you do need custom equality, always consider if
your implementation is reflexive, symmetric, transitive, and
consistent. Make such class final, or beware that subclasses
should not change how equality behaves. It is hard to make
custom equality and support inheritance at the same time.
Some even say it is impossible⁵³. Data classes are final.
⁵³As Effective Java by Joshua Bloch, third edition claims in Item 10:
Obey the general contract when overriding equals: “There is no way to
extend an instantiable class and add a value component while preserving the
equals contract, unless you’re willing to forgo the benefits of object-oriented
abstraction.”.
Chapter 6: Class design 358
Hash table
Let’s start with the problem hash table was invented to solve.
Let’s say that we need a collection that quickly both adds and
finds elements. An example of this type of collection is a set or
map, neither of which allow for duplicates. So whenever we
add an element, we first need to look for an equal element.
A collection based on an array or on linked elements is not
fast enough for checking if it contains an element, because to
check that we need to compare this element with all elements
on this list one after another. Imagine that you have an array
with millions of pieces of text, and now you need to check if
it contains a certain one. It will be really time-consuming to
compare your text one after another with those millions.
A popular solution to this problem is a hash table. All you
need is a function that will assign a number to each element.
Such a function is called a hash function and it must always
return the same value for equal elements. Additionally, it is
good if our hash function:
• Is fast
Chapter 6: Class design 359
1 class FullName(
2 var name: String,
3 var surname: String
4 ) {
5 override fun equals(other: Any?): Boolean =
6 other is FullName
7 && other.name == name
8 && other.surname == surname
9 }
10
11 val set = mutableSetOf<FullName>()
12 set.add(FullName("Marcin", "Moskała"))
13 print(FullName("Marcin", "Moskała") in set) // false
14 print(FullName("Marcin", "Moskała") ==
15 FullName("Marcin", "Moskała")) // true
34 println(Proper.equalsCounter) // 0
35 val terribleSet = List(10000) { Terrible("$it") }.t\
36 oSet()
37 println(Terrible.equalsCounter) // 50116683
38
39 Proper.equalsCounter = 0
40 println(Proper("9999") in properSet) // true
41 println(Proper.equalsCounter) // 1
42
43 Proper.equalsCounter = 0
44 println(Proper("A") in properSet) // false
45 println(Proper.equalsCounter) // 0
46
47 Terrible.equalsCounter = 0
48 println(Terrible("9999") in terribleSet) // true
49 println(Terrible.equalsCounter) // 4324
50
51 Terrible.equalsCounter = 0
52 println(Terrible("A") in terribleSet) // false
53 println(Terrible.equalsCounter) // 10001
Implementing hashCode
We define hashCode in Kotlin practically only when we
define custom equals. When we use the data modifier, it
generates both equals and a consistent hashCode. When
you do not have a custom equals method, do not define a
custom hashCode unless you are sure you know what you are
doing and you have a good reason. When you have a custom
equals, implement hashCode that always returns the same
value for equal elements.
If you implemented typical equals that checks equality of
significant properties, then a typical hashCode should be
Chapter 6: Class design 366
1 class DateTime(
2 private var millis: Long = 0L,
3 private var timeZone: TimeZone? = null
4 ) {
5 private var asStringCache = ""
6 private var changed = false
7
8 override fun equals(other: Any?): Boolean =
9 other is DateTime &&
10 other.millis == millis &&
11 other.timeZone == timeZone
12
13 override fun hashCode(): Int {
14 var result = millis.hashCode()
15 result = result * 31 + timeZone.hashCode()
16 return result
17 }
18 }
Do we need a compareTo?
In Kotlin we rarely implement compareTo ourselves. We get
more freedom by specifying the order on a case by case basis
than by assuming one global natural order. For instance, we
can sort a collection using sortedBy and provide a key that
is comparable. So in the example below, we sort users by their
surname:
1 // DON'T DO THIS!
2 print("Kotlin" > "Java") // true
Surely there are objects with a clear natural order. Date and
time is a perfect example. Although if you are not sure about
whether your object has a natural order, it is better to use
comparators instead. If you use a few of them often, you can
place them in the companion object of your class:
Chapter 6: Class design 371
Implementing compareTo
When we do need to implement compareTo ourselves, we
have top-level functions that can help us. If all you need
is to compare two values, you can use the compareValues
function:
1 class User(
2 val name: String,
3 val surname: String
4 ): Comparable<User> {
5 override fun compareTo(other: User): Int =
6 compareValues(surname, other.surname)
7 }
1 class User(
2 val name: String,
3 val surname: String
4 ): Comparable<User> {
5 override fun compareTo(other: User): Int =
6 compareValuesBy(this, other, { it.surnam\
7 e }, { it.name })
8 }
Once you did that, don’t forget to verify that your comparison
is antisymmetric, transitive and connex.
Chapter 6: Class design 373
Both approaches are similar in many ways. Their use and even
referencing them via reflection is very similar:
Chapter 6: Class design 374
1 open class C
2 class D: C()
3 fun C.foo() = "c"
4 fun D.foo() = "d"
5
6 fun main() {
7 val d = D()
8 print(d.foo()) // d
9 val c: C =d
10 print(c.foo()) // c
11
12 print(D().foo()) // d
13 print((D() as C).foo()) // c
14 }
15 return sum
16 }
Summary
The most important differences between members and exten-
sions are:
1 interface PhoneBook {
2 fun String.isPhoneNumber(): Boolean
3 }
4
5 class Fizz: PhoneBook {
6 override fun String.isPhoneNumber(): Boolean =
7 length == 7 && all { it.isDigit() }
8 }
1 PhoneBookIncorrect().apply { "1234567890".test() }
1 class A {
2 val a = 10
3 }
4 class B {
5 val b = 10
6
7 fun A.test() = a + b
8 }
1 class A {
2 //...
3 }
4 class B {
5 //...
6
7 fun A.update() = ... // Shell is update A or B?
8 }
1 class A
2 private val a = A()
3
4 // Benchmark result: 2.698 ns/op
5 fun accessA(blackhole: Blackhole) {
6 blackhole.consume(a)
7 }
8
9 // Benchmark result: 3.814 ns/op
10 fun createA(blackhole: Blackhole) {
11 blackhole.consume(A())
12 }
13
14 // Benchmark result: 3828.540 ns/op
15 fun createListAccessA(blackhole: Blackhole) {
16 blackhole.consume(List(1000) { a })
17 }
18
⁵⁷To measure the size of concrete fields on JVM objects, use Java Object
Layout.
Chapter 7: Make it cheap 388
Object declaration
A very simple way to reuse an object instead of creating it
every time is using object declaration (singleton). To see an
example, let’s imagine that you need to implement a linked
list. The linked list can be either empty, or it can be a node
containing an element and pointing to the rest. This is how it
can be implemented:
You can see that using this function for the first time is slower
than using the classic approach as there is additional overhead
on checking if the value is in the cache and adding it there.
Once values are added, the retrieval is nearly instantaneous.
It has a significant drawback though: we are reserving and
using more memory since the Map needs to be stored some-
where. Everything would be fine if this was cleared at some
point. But take into account that for the Garbage Collector
(GC), there is no difference between a cache and any other
static field that might be necessary in the future. It will hold
this data as long as possible, even if we never use the fib
function again. One thing that helps is using a soft reference
that can be removed by the GC when memory is needed. It
should not be confused with weak reference. In simple words,
the difference is:
Lazy initialization
Often when we need to create a heavy class, it is better to do
that lazily. For instance, imagine that class A needs instances
of B, C, and D that are heavy. If we just create them during
class creation, A creation will be very heavy, because it will
need to create B, C and D and then the rest of its body. The
heaviness of objects creation will just accumulates.
Chapter 7: Make it cheap 396
1 class A {
2 val b = B()
3 val c = D()
4 val d = D()
5
6 //...
7 }
1 class A {
2 val b by lazy { B() }
3 val c by lazy { C() }
4 val d by lazy { D() }
5
6 //...
7 }
Each object will then be initialized just before its first usage.
The cost of those objects creation will be spread instead of
accumulated.
Keep in mind that this sword is double-edged. You might
have a case where object creation can be heavy, but you need
methods to be as fast as possible. Imagine that A is a controller
in a backend application that responds to HTTP calls. It
starts quickly, but the first call requires all heavy objects
initialization. So the first call needs to wait significantly
longer for the response, and it doesn’t matter for how long
our application runs. This is not the desired behavior. This is
also something that might clutter our performance tests.
Chapter 7: Make it cheap 397
Using primitives
In JVM we have a special built-in type to represent basic
elements like number or character. They are called primitives,
and they are used by Kotlin/JVM compiler under the hood
wherever possible. Although there are some cases where a
wrapped class needs to be used instead. The two main cases
are:
1 /**
2 * Returns the largest element or `null` if there ar\
3 e no elements.
4 */
5 public fun <T : Comparable<T>> Iterable<T>.max(): T\
6 ? {
7 val iterator = iterator()
8 if (!iterator.hasNext()) return null
9 var max = iterator.next()
10 while (iterator.hasNext()) {
11 val e = iterator.next()
12 if (max < e) max = e
13 }
14 return max
15 }
Summary
In this item, you’ve seen different ways to avoid object
creation. Some of them are cheap in terms of readability: those
should be used freely. For instance, heavy object lifting out
of a loop or function is generally a good idea. It is a good
practice for performance, and also thanks to this extraction,
Chapter 7: Make it cheap 400
1 repeat(10) {
2 print(it)
3 }
There are also some costs to using this modifier. Let’s review
both all the advantages and costs of inline modifier.
1 print(Int::class.simpleName) // Int
2 print(Char::class.simpleName) // Char
3 print(String::class.simpleName) // String
1 class Worker
2 class Manager
3
4 val employees: List<Any> =
5 listOf(Worker(), Manager(), Worker())
6
7 val workers: List<Worker> =
8 employees.filterIsInstance<Worker>()
1 // Java
2 Function0<Unit> lambda = new Function0<Unit>() {
3 public Unit invoke() {
4 // code
5 }
6 };
1 // Java
2 // Additional class in separate file
3 public class Test$lambda implements Function0<Unit>\
4 {
5 public Unit invoke() {
6 // code
7 }
8 }
9
10 // Usage
11 Function0 lambda = new Test$lambda()
1 @Benchmark
2 fun nothingInline(blackhole: Blackhole) {
3 inlineRepeat(100_000_000) {
4 blackhole.consume(it)
5 }
6 }
7
8 @Benchmark
9 fun nothingNoninline(blackhole: Blackhole) {
10 noinlineRepeat(100_000_000) {
11 blackhole.consume(it)
12 }
13 }
1 var a = 2L
2 noinlineRepeat(100_000_000) {
3 a += a / 2
4 }
1 val a = Ref.LongRef()
2 a.element = 2L
3 noinlineRepeat(100_000_000) {
4 a.element += a.element / 2
5 }
1 fun repeatInline() {
2 var a = 2L
3 repeat(100_000_000) {
4 a += a / 2
5 }
6 }
7
8 fun repeatNoninline() {
9 var a = 2L
10 noinlineRepeat(100_000_000) {
11 a += a / 2
12 }
13 }
The first one on my machine takes 0.2 ns. The second one
283 000 000 ns. This comes from the accumulated effects of
the facts that the function is compiled to be an object, and
the local variable needs to be wrapped. This is a significant
difference. Since in most cases we don’t know how functions
with parameters of functional types will be used, when we
define a utility function with such parameters, for instance
for collection processing, it is good practice to make it inline.
This is why most extension functions with parameters of
functional types in the stdlib are inlined.
1 if(value != null) {
2 print(value)
3 }
4
5 for (i in 1..10) {
6 print(i)
7 }
8
9 repeatNoninline(10) {
10 print(it)
11 }
1 fun main() {
2 repeatNoinline(10) {
3 print(it)
4 return // ERROR: Not allowed
5 }
6 }
1 fun main() {
2 repeatInline(10) {
3 print(it)
4 return // OK
5 }
6 }
What are they all compiled to? First two are very readable:
34 print(3)
35 print(3)
36 print(3)
37 print(3)
38 print(3)
39 print(3)
40 print(3)
41 }
Summary
The main cases where we use inline functions are:
1 // Code
2 val name: Name = Name("Marcin")
3
4 // During compilation replaced with code similar to:
5 val name: String = "Marcin"
1 interface Timer {
2 fun callAfter(time: Int, callback: ()->Unit)
3 }
1 interface Timer {
2 fun callAfter(timeMillis: Int, callback: ()->Uni\
3 t)
4 }
1 interface User {
2 fun decideAboutTime(): Int
3 fun wakeUp()
4 }
5
6 interface Timer {
7 fun callAfter(timeMillis: Int, callback: ()->Uni\
8 t)
9 }
10
11 fun setUpUserWakeUpUser(user: User, timer: Timer) {
12 val time: Int = user.decideAboutTime()
13 timer.callAfter(time) {
14 user.wakeUp()
15 }
16 }
1 @Entity(tableName = "grades")
2 class Grades(
3 @ColumnInfo(name = "studentId")
4 val studentId: Int,
5 @ColumnInfo(name = "teacherId")
6 val teacherId: Int,
7 @ColumnInfo(name = "schoolId")
8 val schoolId: Int,
9 // ...
10 )
Now those id uses will be safe, and at the same time, the
database will be generated correctly because during compi-
lation, all those types will be replaced with Int anyway. This
Chapter 7: Make it cheap 426
1 interface TimeUnit {
2 val millis: Long
3 }
4
5 inline class Minutes(val minutes: Long): TimeUnit {
6 override val millis: Long get() = minutes * 60 *\
7 1000
8 // ...
9 }
10
11 inline class Millis(val milliseconds: Long): TimeUn\
12 it {
13 override val millis: Long get() = milliseconds
14 }
15
16 fun setUpTimer(time: TimeUnit) {
17 val millis = time.millis
18 //...
19 }
20
21 setUpTimer(Minutes(123))
22 setUpTimer(Millis(456789))
Chapter 7: Make it cheap 427
Typealias
Kotlin typealias lets us create another name for a type:
Summary
Inline classes let us wrap a type without performance over-
head. Thanks to that, we improve security by making our
typing system protect us from value misuse. If you use a
type with unclear meaning, especially a type that might have
different units of measure, consider wrapping it with inline
classes.
Chapter 7: Make it cheap 429
1 class Stack {
2 private var elements: Array<Any?> =
3 arrayOfNulls(DEFAULT_INITIAL_CAPACITY)
4 private var size = 0
5
6 fun push(e: Any) {
7 ensureCapacity()
8 elements[size++] = e
9 }
10
11 fun pop(): Any? {
12 if (size == 0) {
13 throw EmptyStackException()
14 }
15 return elements[--size]
16 }
17
18 private fun ensureCapacity() {
19 if (elements.size == size) {
20 elements = elements.copyOf(2 * size + 1)
21 }
22 }
23
24 companion object {
⁵⁸Example inspired by the book Effective Java by Joshua Bloch.
Chapter 7: Make it cheap 432
34 }
35 }
Usage:
34 }
15
16 return synchronized(lock) {
17 val _v2 = _value
18 if (_v2 !== UNINITIALIZED_VALUE) {
19 @Suppress("UNCHECKED_CAST") (_v2\
20 as T)
21 } else {
22 val typedValue = initializer!!()
23 _value = typedValue
24 initializer = null
25 typedValue
26 }
27 }
28 }
29
30 override fun isInitialized(): Boolean =
31 _value !== UNINITIALIZED_VALUE
32
33 override fun toString(): String =
34 if (isInitialized()) value.toString()
35 else "Lazy value not initialized yet."
36
37 private fun writeReplace(): Any = InitializedLaz\
38 yImpl(value)
39 }
1 // Python
2 primes = [2, 3, 5, 7, 13]
3 // Swift
4 let primes = [2, 3, 5, 7, 13]
1 fun List<Student>.studentsWithBestGrade():List<Stud\
2 ent> {
3 val bestStudent = this.maxBy { it.grade }
4 return this.filter { it.grade == bestStudent?.gr\
5 ade }
6 }
7
8 fun List<Student>.studentsWithBestGradeInefficient(\
9 ): List<Student> =
10 this.filter { it.grade == this.maxBy { it.grade\
11 }?.grade }
You can say that the only formal difference between them is
the name. Although Iterable and Sequence are associated
with totally different usages (have different contracts), so
nearly all their processing functions work in a different way.
Sequences are lazy, so intermediate functions for Sequence
processing don’t do any calculations. Instead, they return
a new Sequence that decorates the previous one with the
new operation. All these computations are evaluated during a
terminal operation like toList or count. Iterable processing,
on the other hand, returns a collection like List on every step.
Chapter 8: Efficient collection processing 446
Order is important
Because of how iterable and sequence processing are im-
plemented, the ordering of their operations is different. In
sequence processing, we take the first element and apply all
the operations, then we take the next element, and so on. We
will call it an element-by-element or lazy order. In iterable
processing, we take the first operation and we apply it to the
whole collection, then move to the next operation, etc.. We
will call it step-by-step or eager order.
Chapter 8: Efficient collection processing 448
1 sequenceOf(1,2,3)
2 .filter { print("F$it, "); it % 2 == 1 }
3 .map { print("M$it, "); it * 2 }
4 .forEach { print("E$it, ") }
5 // Prints: F1, M1, E2, F2, F3, M3, E6,
6
7 listOf(1,2,3)
8 .filter { print("F$it, "); it % 2 == 1 }
9 .map { print("M$it, "); it * 2 }
10 .forEach { print("E$it, ") }
11 // Prints: F1, F2, F3, M1, M3, E2, E6,
1 for (e in listOf(1,2,3)) {
2 print("F$e, ")
3 if(e % 2 == 1) {
4 print("M$e, ")
5 val mapped = e * 2
6 print("E$mapped, ")
7 }
8 }
9 // Prints: F1, M1, E2, F2, F3, M3, E6,
1 (1..10).asSequence()
2 .filter { print("F$it, "); it % 2 == 1 }
3 .map { print("M$it, "); it * 2 }
4 .find { it > 5 }
5 // Prints: F1, M1, F2, F3, M3,
6
7 (1..10)
8 .filter { print("F$it, "); it % 2 == 1 }
9 .map { print("M$it, "); it * 2 }
10 .find { it > 5 }
11 // Prints: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10,\
12 M1, M3, M5, M7, M9,
1 generateSequence(1) { it + 1 }
2 .map { it * 2 }
3 .take(10)
4 .forEach { print("$it, ") }
5 // Prints: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
12 }
13
14 fun threeStepListProcessing(): Double {
15 return productsList
16 .filter { it.bought }
17 .map { it.price }
18 .average()
19 }
20
21 fun threeStepSequenceProcessing(): Double {
22 return productsList.asSequence()
23 .filter { it.bought }
24 .map { it.price }
25 .average()
26 }
1 twoStepListProcessing 81 095\
2 ns
3 twoStepSequenceProcessing 55 685\
4 ns
5 twoStepListProcessingAndAcumulate 83 307\
6 ns
7 twoStepSequenceProcessingAndAcumulate 6 928\
8 ns
1 generateSequence(0) { it + 1 }.take(10).sorted().to\
2 List()
3 // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4 generateSequence(0) { it + 1 }.sorted().take(10).to\
5 List()
6 // Infinite time. Does not return.
1 productsList.asSequence()
2 .filter { it.bought }
3 .map { it.price }
4 .average()
5
6 productsList.stream()
7 .filter { it.bought }
8 .mapToDouble { it.price }
9 .average()
10 .orElse(0.0)
Summary
Collection and sequence processing are very similar and both
support nearly the same processing methods. Yet there are
important differences between the two. Sequence processing
is harder, as we generally keep elements in collections and so
changing to collections requires a transformation to sequence
and often also back to the desired collection. Sequences are
lazy, which brings some important advantages:
Summary
Most collection processing steps require iteration over the
whole collection and intermediate collection creation. This
cost can be limited by using more suitable collection process-
ing functions.
Chapter 8: Efficient collection processing 467
Summary
In a typical case, List or Set should be preferred over Array.
Though if you hold big collections of primitives, using Array
might significantly improve your performance and memory
use. This item is especially for library creators or developers
writing games or advanced graphic processing.
Chapter 8: Efficient collection processing 470
Summary
Adding to mutable collections is generally faster, but im-
mutable collections give us more control over how they are
changed. Though in local scope we generally do not need that
control, so mutable collections should be preferred. Especially
in utils, where element insertion might happen many times.
Dictionary
Some technical terms are not well-understood and they re-
quire explanation. It is especially problematic when a term
is confused with another, similar one. This is why in this
chapter, I present some important terms used in this book, in
opposition to other terms that they are often confused with.
Function vs method
In Kotlin, basic functions start with the fun keyword, and
they can be defined:
10 return triple(fourTimes())
11 }
12 }
Extension vs member
Member is an element defined in a class. In the following
example, there are 4 members declared: the name, surname,
and fullName properties, and the withSurname method.
Dictionary 475
1 class User(
2 val name: String,
3 val surname: String
4 ) {
5
6 val fullName: String
7 get() = "$name $surname"
8
9 fun withSurname(surname: String) =
10 User(this.name, surname)
11 }
Parameter vs argument
The parameter is a variable defined in a function declaration.
The argument is the actual value of this variable that gets
Dictionary 476
1 class SomeObject {
2 val text: String
3
4 constructor(text: String) {
5 this.text = text
6 print("Creating object")
7 }
8 }