Kotlin cookbook

1.1 Executing Kotlin program without local compiler.

1.2 Installing Kotlin compiler in your local computer.

  • Itellij > New Project > Java > Kotlin/JVM > Src > New File > …

1.3 Compiling Kotlin file at Command line

1
2
3
> kotlinc-jvm XXX.kt        // creating class file for Jvm.

> kotlin HelloWorldKt // executing class file.

2.1 Using nullable type in Kotlin

Unlike Java, Kotlin basically remove the possibility of null. so you should define the type differently to use nullable type.

1
2
3
4
5
fun main() {
val jkRowling = Person("Joanne", null, "Rowling")
}

class Person(val first: String, val middle: String?, val last: String)

somtimes someone doesn’t have a middle name. so defining the middle name as nullable type is proper. you can define nullable type as adding question mark(= ?).

1
2
3
4
5
6
7
8
9
10
11
fun main() {
val jkRowling = Person("Joanne", null, "Rowling")

jkRowling.middle!!.length // you should add double exclamation mark to access nullable type.

if(jkRowling.middle != null) {
val middleNameLength = jkRowling.middle.length; // after checking whether it is null or not, then you don't need to use double exclamation mark.
}
}

class Person(val first: String, val middle: String?, val last: String)

prior to access middle name, if you checked nullable type whether it is null or not, then this type is automatically casted to non null type. so you don’t need to use double exclamation mark. but actually double exclamation mark (= !!) is code smell. This keyword would be one of the situation can occur NullPointerException. so Let’s use safe call operator (= ?.) instead of double exclamation mark.

1
2
3
4
5
6
7
8
fun main() {
val jkRowling = Person("Joanne", null, "Rowling")

val middleNameLengthNullable = jkRowling.middle?.length // This code also return nullable type. (= Int?)
val middleNameLength = jkRowling.middle?.length ?: 0 // if you use elvis operator (= ?:) then you can process when it is null
}

class Person(val first: String, val middle: String?, val last: String)

You can keep the safety as using the safe call operator and you always process visibly the null with Elvis operator.

2.3 Method overloading for Java

Java doesn’t support default value in parameters. so It’s doesn’t support also the constructor doesn’t have the value for optional parameters.

1
2
3
4
5
6
7
8
fun main() {
println(addProduct("Name", 5.0, "Desc"))
println(addProduct("Name", 5.0))
println(addProduct("Name"))
}

fun addProduct(name: String, price: Double = 0.0, desc: String? = null) =
"Adding product with $name, ${desc ?: "None"}, and " + NumberFormat.getCurrencyInstance().format(price)

but if you define @JvmOverloads annotation on the addProduct() funcion, This function will compile many constructors to support method default parameter in Java with overloading technique. the below is the result when you decompile the addProduct() function that is included @JvmOverloads annotation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@JvmOverloads
@NotNull
public static final String addProduct(@NotNull String name, double price, @Nullable String desc) {
Intrinsics.checkParameterIsNotNull(name, "name");
// ...
}

@JvmOverloads
@NotNull
public static final String addProduct(@NotNull String name, double price) {
return addProduct$default(name, price, (String)null, 4, (Object)null);
}

@JvmOverloads
@NotNull
public static final String addProduct(@NotNull String name) {
return addProduct$default(name, 0.0D, (String)null, 6, (Object)null);
}

You should remember the important one that @JvmOverloads annotation doesn’t call super keyword. so it means @JvmOverloads annotation only call all arguments constructor for super class.

2.4 Casting type explicitly.

Kotlin doesn’t support cast the data type to more wide implicitly. it means that integer data type can’t assign to long type. but the kotlin offers the useful function like toInt(), toLong() to cast between them.

1
2
3
4
fun main() {
val intVar: Int = 3
val longVar: Long = intVar.toLong()
}

2.6 Power

Many languages basically support power operator (= ^). but unlike them, the kotlin doens’t support. but you can use Math.pow(double a, double b) function if you need it. additionally you can also use Double.pow(x: Double) or Float.pow(x: Float) functions.

you should remember the important one that unlike double, float type, integer type doesn’t support pow() function. so you should make it yourself like the below if you want.

1
2
fun Int.pow(x: Int) = toDouble().pow(x).toInt()
fun Long.pow(x: Int) = toDouble().pow(x).toLong()

infix keyword can make new custom operator that operate as you want. let’s try to make power operator with infix keyword.

1
2
3
4
5
6
7
8
9
10
11
fun main() {
println(2 `**` 3)
println(2L `**` 3)
println(2F `**` 3)
println(2.0 `**` 3)
}

infix fun Int.`**`(x: Int) = toDouble().pow(x).toInt()
infix fun Long.`**`(x: Int) = toDouble().pow(x).toLong()
infix fun Float.`**`(x: Int) = pow(x)
infix fun Double.`**`(x: Int) = pow(x)

2.9 Creating Pair instance with to keyword

Pair is the data class has two elements named first, second. It is same concept with Map type in Java. the kotlin language support to keyword to create Pair instance easily.

1
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

The below code show the usage of to keyword. the results of p1, p2 is same. (p1 = p2)

1
2
3
4
fun main() {
val p1 = Pair("a", 1)
val p2 = "a" to 1
}

You can create the Pair instance easily with mapOf() and to keyword.

1
2
3
4
5
6
fun main() {
val map = mapOf("a" to 1, "b" to 2, "c" to 2)

println(map.keys)
println(map.values)
}

You can separate key and value like the below code if you want. It’s so intutive.

1
2
3
4
5
6
7
fun main() {
val pair = "a" to 1
val (x, y) = pair

println(x)
println(y)
}

3.1 Understaning the difference between const val and val

all of them support to define the immutable data. but
const keyword support to initialize the data when the source code are compiled (compile time initialization). unlike this, val keyword initialize the data when the service is launched (run time initialization).

You should know the role const and val is different. const do access control one like private, inline. but val is a keyword to define variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Task(val name: String, _priority: Int) {
companion object {
const val MIN_PRIORITY = 1
const val MAX_PRIORITY = 5
const val DEFAULT_PRIORITY = 3
}

var priority = validPriority(_priority)
set(value) {
field = validPriority(value)
}

private fun validPriority(p: Int) =
p.coerceIn(MIN_PRIORITY, MAX_PRIORITY)
}

MINPRIORITY, MAX_PRIORITY, DEFAULT_PRIORITY values will initialize when this source code is compiled because of _const.

3.2 Creating getter, setter

자바에서 전통적으로 객체 내에 존재하는 필드들을 접근할 때에는 getter, setter 함수를 만들어서 사용했다. getter, setter 를 사용하는 이유는 객체지향 프로그래밍 관점에서 encapsulation, 정보은닉 등의 이유로 사용된 것인데 코틀린에서는 Java 코드로 변환할 때 이를 자체적으로 만들어주기 때문에 따로 getter / setter 함수를 정의하지 않고 직접 필드에 접근해도 상관 없다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val setTest = SetTest()

println(setTest.priority)
setTest.priority = 2
println(setTest.priority)

}

class SetTest {
var priority = 1
}

getter / setter 에서 로그를 추가한다거나, 데이터 검증, 전처리 작업, 후처리 작업 등도 진행할 수 있다. 이런 경우를 위해서 코틀린에서는 getter, setter 함수를 커스터마이징 할 수 있는 기능을 제공한다. get(), set() 함수의 형태로 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
val setTest = SetTest()

println(setTest.priority)
setTest.priority = 3
println(setTest.priority)

}

class SetTest {
var priority = 1
get() = field + 10 // getter 함수 커스터마이징
set(value) { field = value.coerceIn(1..5) } // setter 함수 커스터마이징
}

get(), set() 함수에서 field 값을 사용하는 것을 볼 수 있는데, 이는 priority 값을 의미한다. 저 곳에 priority 값을 바로 넣어주면 재귀가 발생해서 결국 stackoverflowexception 이 발생한다. 이를 개선하기 위해서 코틀린은 accessor 영역에서 자기 값을 얻기 위해 field 라는 값을 제공한다. 이를 backing field 라고 부른다.

3.3 데이터 클래스 정의하기

자바의 enum 처럼 데이터의 목적으로 사용하고 싶은 클래스를 만들어야 할 경우 클래스를 정의할 때 data 키워드를 사용하면 된다.

1
2
3
4
5
6
7
8
9
fun main() {
val p1 = Product("baseball", 10.0);
val p2 = Product("baseball", 10.0, false);

val products = setOf(p1, p2)
println(products.size)
}

data class Product(val name: String, var price: Double, var onSale: Boolean = false)

products.size 값은 1이 나올 것이다. data 클래스의 경우 동일한 값을 가지고 있는 객체끼리는 같은 객체로 본다. 그렇기 때문에 setOf() 함수를 통해 두 객체 p1, p2 를 넣을 때 같은 객체로 보고 중복되어 하나는 제거 된다.

객체의 동일성에 대한 이야기가 나온 김에 추가적으로 copy() 함수에 대한 이야기를 해보자. 이 함수는 깊은 복사가 아닌 얕은 복사를 수행한다. 그렇기 때문에 복사본 객체와 원본 객체는 서로 다른 객체이다.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val item1 = OrderItem(Product("baseball", 10.0), 5)
val item2 = item1.copy()

println(item1 == item2)
println(item1 === item2)
println(item1.product == item2.product)
println(item1.product === item2.product)
}

data class OrderItem(val product: Product, val quantity: Int)
data class Product(val name: String, var price: Double, var onSale: Boolean = false)

복사된 item2 객체와 원본 item1 객체가 서로 다른 객체임을 item1 === item2 연산을 통해 확인할 수 있다. 하나 더 중요한 부분은 그 안에서 공유하고 있는 Product 객체는 서로 같은 객체를 바라보고 있음을 item1.product === item2.product 에서 확인이 가능하다.

3.4 Backing property technique

when you want to control reading the properties, initializing the properties in the class, you can use getter function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main() {
val customer = Customer("Fred").apply { messages }
println(customer.messages)
}

class Customer(val name: String) {
private var _messages: List<String>? = null

val messages: List<String>
get() {
if (_messages == null) {
_messages = loadMessages()
}
return _messages!!
}

private fun loadMessages(): MutableList<String> =
mutableListOf(
"Initial contact",
"Convinced them to use Kotlin",
"Sold training class. Sweet."
).also { println("Loaded messages") }
}

in apply() function, messages’s getter method is called. and this coding style is for lazy initialization. the kotlin offers lazy delegation function so we can implement this code more easily.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val customer = Customer("Fred").apply { messages }
println(customer.messages)
}

class Customer(val name: String) {
val messages: List<String> by lazy { loadMessages() }

private fun loadMessages(): MutableList<String> =
mutableListOf(
"Initial contact",
"Convinced them to use Kotlin",
"Sold training class. Sweet."
).also { println("Loaded messages") }
}

3.5 Operator Overloading

operator keyword can re-define calculation way as you want.

1
2
3
4
5
6
7
8
9
data class Point(val x: Int, val y: Int)

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

val point = Point(10, 20)

fun main() {
println(-point)
}

unaryMinus() method is all of kotlin class would have. and you can overload it with operator keyword

3.6 lateinit keyword

non null properies in some class should be initialized in constructor block. but we sometimes encouter the case the parameters don’t have enough to call constructor at that time. we can use lateinit keyword to solve this problem. and you can often see this problen at the spring dependency injection (@Autowired)

1
2
3
4
5
6
7
class OfficerControllerTests {
@Autowired
lateinit var client: WebTestClient

@Autowired
lateinit var repository: OfficeRepository
}
  • val : immutable type

  • var : mutable type

3.7 overriding equals(), hashcode()

the one of the features of data class is that you should keep the equality. (it’s little different with equivalence.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Customer(val name: String) {
val messages: List<String> by lazy { loadMessages() }

private fun loadMessages() : MutableList<String> =
mutableListOf(
"Initial contact",
"Covinced them to use Kotlin",
"Sold training class. Sweet."
).also { println("Loaded messages") }

override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherCustomer = (other as? Customer) ?: return false
return this.name == otherCustomer.name
}

override fun hashCode() = name.hashCode()
}

you can override equals(), hashCode() methods like the above.

3.8 creating singleton object

you should create singleton object like the below in Java

1
2
3
4
5
6
7
8
9
10
11
12
public class Runtime {
private static final Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}

// protecting to create instance with constructor
private Runtime() {}

// ...
}

but the kotlin language offers the easy way to create singleton object unlike java.

1
2
3
4
5
6
7
8
9
10
object MySingleton {
val myProperty = 3

fun myFunction() = "hello"
}

fun main() {
println(MySingleton.myProperty)
println(MySingleton.myFunction())
}

you just change from class keyword to object keyword. then it will be created as the singleton type object. the below java code is the result of decompiled of MySingleton kotlin object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class MySingleton {
private static final int myProperty = 3;
public static final MySingleton INSTANCE;

private MySingleton() {}

public final int getMyProperty() {
return myProperty;
}

public final void myFunction() {
return "Hello";
}

static {
MySingleton var0 = new MySingleton();
INSTANCE = var0;
myProperty = 3;
}
}

3.9 Nothing class

1
public class Nothing private constructor()

the above code means that Nothing instance cannot be existed. because as you can see the above code, the constructor of Nothing class declared as a private type. and anywhere in Nothing class don’t create new instance. so Nothing instance is not existed in real.

you can use this type for the value never is existed.

4.1 fold

1
2
3
4
inline fun <R> Iterable<T>.fold(
initial: R,
operation: (acc: R, T) -> R
): R

fold function needs two parameters (initial and operation).

  • initial : it’s initialization value.
  • operation : it’s the way how to calculate them.
1
2
fun sum(vararg nums: Int) = 
nums.fold(0) {acc, n -> acc + n }

in lambda named operation, acc means a accumulated value and n means n-th value in nums array. the below is factoral function example with fold() function.

1
2
3
4
5
fun factorialFold(n: Long): BigInteger =
when(n) {
0L, 1L -> BigInteger.ONE
else -> (2..n).fold(BigInteger.ONE) { acc, i -> acc * BigInteger.valueOf(i) }
}
1
2
fun fibonacciFold(n: Int) =
(2 until n).fold(1 to 1) { (prev, curr), _ -> curr to (prev + curr) }.second

and it is another example to use fold() function. especially in this function, n value in lambda doesn’t need so just mark it with ‘_’.

4.2 reduce

this function is fold() function without initial value.

1
2
3
inline fun <S, T : S> Iterable<T>.reduce(
operation: (acc: S, T) -> S
): S
1
2
3
4
5
6
7
8
9
10
11
12
public inline fun IntArray.reduce(
operation: (acc: Int, Int) -> Int
): Int {
if(isEmpty())
throw UnsupportedOperationException("Empty array can't be reduced.")

var accumulator = this[0]
for (index in 1..lastIndex) {
accumulator = operation(accumulator, this[index])
}
return accumulator;
}

this code is real implementation code of reduce in IntArray collection.

1
2
fun sumReduce(vararg  nums: Int) =
nums.reduce { acc, i -> acc + i }

and it is the one of the example with reduce() function. this code calculate summation of nums array.

4.3 tail recursion

factorial calculation is usually implemented by recursion like the below.

1
2
3
4
5
fun recursiveFactorial(n: Long): BigInteger =
when (n) {
0L, 1L -> BigInteger.ONE
else -> BigInteger.valueOf(n) * recursiveFactorial(n - 1)
}

but recursiveFactorial() function will create new frame in call stack memory when it’s called. so someday it would encounter StackOverflowError. i mean this way would waste your memory. if you wanna optimize memory with recursive call then use tailrec.

1
2
3
4
5
6
7
@JvmOverloads
tailrec fun factorial(n: Long, acc: BigInteger = BigInteger.ONE): BigInteger =
when (n) {
0L -> BigInteger.ONE
1L -> acc
else -> factorial(n - 1, acc * BigInteger.valueOf(n))
}

this code is work well in 0L, 1L cases. because of @JvmOverloads annotation. I mean you can skip acc value if you want because it has default value (= BigInteger.ONE).

and tailrec keyword change your code from recursive style to loop style to optimize call stack memory when it’s compiled to Java.

5.1 array

The kotlin create the array with arrayOf() function easily.

1
2
3
4
5
val strings = arrayOf("this", "is", "an", "array", "of", "strings")

val nullStringArray = arrayOfNulls<String>(5)

val squares = Array(5) {i -> (i * i).toString() }
1
2
3
fun <T> Array<out T>.withIndex()

data class IndexedValue<out T>(public val index: Int, public val value: T)

when you need to get index value in array you can use IndexedValue type with withIndex() function.

5.2 creating collection

  • immutable collection : listOf, setOf, mapOf
  • mutable collection : mutableListOf, mutableSetOf, mutableMapOf

5.4 creating map with collection

associate() function can create Map object from collection.

1
2
3
val keys = 'a'..'f'
val map = keys.associate { it to it.toString().repeat(5).capitalize() }
println(map)

5.5 returning default value when collection is empty

1
2
3
4
5
6
7
8
9
10
11
12
13
data class Product(val name: String, var price: Double, var onSale: Boolean = false)

fun onSaleProducts_ifEmptyCollection(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.ifEmpty { listOf("none") }
.joinToString(separator = ", ")

fun onSaleProducts_ifEmptyString(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.joinToString(separator = ", ")
.ifEmpty { "none" }

5.8 destructuring list

1
2
3
val list = listOf("a", "b", "c", "d", "e", "f", "g")
val (a, b, c, d, e) = list
println("$a $b $c $d $e")

you can get the fields of the object like (a, b, c, d, e).

5.9 sorting with multiple options.

1
2
3
4
5
6
7
fun <T> Iterable<T>.sortedWith(
comparator: Comparator<in T>
): List<T>

fun <T> compareBy(
varargs seletors (T) -> Comparable<*>?
): Comparator<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data class Golfer(val score: Int, val first: String, val last: String)

val golfers = listOf(
Golfer(70, "Jack", "Nicklaus"),
Golfer(68, "Tom", "Watson"),
Golfer(68, "Bubba", "Watson"),
Golfer(70, "Tiger", "Woods"),
Golfer(68, "Try", "Webb")
)

fun main() {
val sorted = golfers.sortedWith(compareBy({ it.score }, { it.last }, { it.first }))

println(sorted)
}

comparator be more complexible then the below code is better to read than the above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class Golfer(val score: Int, val first: String, val last: String)

val golfers = listOf(
Golfer(70, "Jack", "Nicklaus"),
Golfer(68, "Tom", "Watson"),
Golfer(68, "Bubba", "Watson"),
Golfer(70, "Tiger", "Woods"),
Golfer(68, "Try", "Webb")
)

fun main() {
val comparator = compareBy<Golfer>(Golfer::score)
.thenBy(Golfer::last)
.thenBy(Golfer::first)

golfers.sortedWith(comparator)
.forEach(::println)
}

5.10 defining custom iterator

It is the definition of Iterator interface in kotlin.

1
2
3
4
interface Iterator<out T> {
operator fun next(): T
operator fun hasNext(): Boolean
}

It’s default way to use Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data class Player(val name: String)

class Team(val name: String, val players: MutableList<Player> = mutableListOf()) {
fun addPlayers(vararg people: Player) =
players.addAll(people)
}

fun main() {
val team = Team("Warriors")
team.addPlayers(Player("Curry"), Player("Thompson"), Player("Durant"), Player("Green"), Player("Cousins"))

for(player in team.players) {
println(player)
}
}

you can access it easier.

1
2
3
4
5
operator fun Team.iterator() : Iterator<Player> = players.iterator()

for (player in team) {
println(player)
}

5.11 Filtering collection by type.

1
2
3
4
5
6
    val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val strings = list.filter { it is String }

for (s in strings) {
// s.length : it must not be compiled. because strings types is List<Any>.
}
1
2
3
4
5
6
7
8
9
10
11
val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")

val all = list.filterIsInstance<Any>()
val strings = list.filterIsInstance<String>()
val ints = list.filterIsInstance<Int>()
val dates = list.filterIsInstance(LocalDate::class.java)

println(all)
println(strings)
println(ints)
println(dates)

5.12 Create array with range.

1
2
3
4
5
6
7
8
9
10
operator fun <T : Comparable<T>> T.rangeTo(that: T) = ClosedRange<T> =
ComparableRange(this, that)

interface ClosedRange<T: Comparable<T>> {
val start: T
val endInclusive: T
operator fun contains(value: T): Boolean =
value >= start && value <= endInclusive
fun isEmpty(): Boolean = start > endInclusive
}

코틀린에서는 1..5 처럼 IntRange를 인스턴스화하는 2개의 점 연사자를 사용해서 범위를 생성한다.

Share