What’s New in Kotlin 1.3
This article will take you through the advancements and changes the language has to offer in its latest version. By Joey deVilla.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Functions About Nothing
The isNullOrEmpty()
and orEmpty()
can now be used on more than just nullable strings. You can now use them to get similar results with arrays and collections.
Run the following in the Kotlin Playground:
fun main() {
val nullArray: Array<String>? = null
val emptyArray = arrayOf<String>()
val filledArray = arrayOf("Alpha", "Bravo", "Charlie")
println("nullArray.isNullOrEmpty(): ${nullArray.isNullOrEmpty()}") // true
println("emptyArray.isNullOrEmpty(): ${emptyArray.isNullOrEmpty()}") // true
println("filledArray.isNullOrEmpty(): ${filledArray.isNullOrEmpty()}") // false
println("nullArray.orEmpty(): ${(nullArray.orEmpty()).joinToString()}") // []
println("filledArray.orEmpty(): ${filledArray.orEmpty().joinToString()}") // ["Alpha", "Bravo", "Charlie"]
}
Kotlin 1.3 also introduces two new functions about nothing. The first, ifEmpty()
, lets you perform a lambda if the array, collection or string you apply it to contains no elements.
Add the following to the end of main()
and run the code again:
val emptyString = ""
val nonEmptyString = "Not empty!"
println(emptyString.ifEmpty {"emptyString is empty"}) // emptyString is empty
println(nonEmptyString.ifEmpty {"emptyString is empty"}) // Not empty!
The second, ifBlank()
, performs a lambda if the string you apply it to is nothing but whitespace or is the empty string.
Add the following to the end of main()
and run the code again:
val spaces = " "
val newlinesTabAndReturns = "\n\r\t\n\r\t"
val nonBlankString = "Not blank!"
println(spaces.ifBlank {"'spaces' is blank"}) // 'spaces' is blank
println(newlinesTabAndReturns.ifBlank {"'newlinesTabAndReturns' is blank"}) // 'newlinesTabAndReturns' is blank
println(emptyString.ifBlank {"'emptyString' is blank"}) // (empty string)
println(nonBlankString.ifBlank {"'nonBlankString' is blank"}) // Not blank!
Boolean
The Boolean
type now has a companion object, which means there’s something to attach extension functions to. To see a practical application of this new capability, run the following in the Kotlin Playground:
import kotlin.random.*
fun Boolean.coinToss(): Boolean = Random.nextBoolean()
fun Boolean.asHeadsOrTails(): String {
if (this) {
return "Heads"
} else {
return "Tails"
}
}
var penny = false
println("Coin toss: ${penny.coinToss().asHeadsOrTails()}")
HashCode
A HashCode takes an object of any size as its input and outputs a string or number that’s usually smaller than the object. A hash function’s result acts as a sort of fingerprint for its input object because if two objects are equal and fed into the same hash function, the results are also equal.
hashCode()
takes an object of any size as its input and outputs an integer. The object you provide as input to hashCode()
could be one byte or a million bytes in size, but its output is always an integer (which takes up 32 bytes).
Let’s try hashCode()
on a few strings. Run the following in the Kotlin Playground:
fun main() {
val shortString = "Hi."
val anotherShortString = "Hi."
val mediumString = "Predictions are hard to make, especially ones about the future."
val longString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi neque nunc, elementum vitae consectetur ut, eleifend vitae ante. Donec sit amet feugiat risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Maecenas luctus in quam eu tristique. Morbi tristique tortor arcu, sed fringilla orci hendrerit eget. Curabitur libero risus, hendrerit tincidunt enim quis, consectetur rhoncus metus. Etiam sed lacus mollis, pulvinar mauris nec, tincidunt purus."
println("shortString's hashcode: ${shortString.hashCode()}") // 72493
println("anotherShortString's hashcode: ${anotherShortString.hashCode()}") // 72493
println("mediumString's hashcode: ${mediumString.hashCode()}") // 642158843
println("longString's hashcode: ${longString.hashCode()}") // -420880130
}
Note that shortString
and anotherShortString
have the same hashCode()
value. That’s because they contain the same text, and are therefore equal.
In Kotlin 1.3, hashCode()
has been expanded to work for nullable types. It follows these simple rules:
- If the object it’s applied to isn’t null,
hashCode()
returns the object’s hash function value. - If the object it’s applied to is null,
hashCode()
returns 0.
Add the following to the end of main()
and run the code again:
val yetAnotherShortString = "Hi."
val nullString: String? = null
println("yetAnotherShortString's hashcode: ${yetAnotherShortString.hashCode()}") // 72493
println("nullString's hashcode: ${nullString.hashCode()}") // 0
Coroutines
We saved the biggest change for last! Coroutines have been in Kotlin since version 1.1 as an experimental feature, and Kotlin 1.3 is the first version where they’re a standard part of the language.
One way to describe coroutines is “a way to write asynchronous code using lightweight threads and a structured syntax”. A less technical, more practical way to phrase this is “an easier-to-read way to program things that happen at the same time”.
Let’s start with a simple example. Run the following in the Kotlin Playground:
import kotlinx.coroutines.*
fun main() {
// 1
GlobalScope.launch {
// 2
delay(1000L)
println("And then about a second later, this'll be printed.")
}
// 3
println("This'll be printed first.")
// 4
Thread.sleep(2000L)
}
You should see the following in the output section of the page:
This'll be printed first. And then about a second later, this'll be printed.
Here’s what’s happening in the code:
- The
launch
keyword is a coroutine builder, which starts a coroutine. That’ll run parallel to the current thread without blocking it. Every coroutine runs inside a scope, and this one runs inside theGlobalScope
. When launched within this scope, a coroutine operates at the same scope as the application, and the lifetime of the coroutine is limited only by the lifetime of the application. - This is the code of the coroutine.
delay()
stops the execution of the coroutine it’s in for a specified number of milliseconds and then resumes the execution once that time has passed, all without blocking the current thread. The coroutine waits one second, then prints a message. - This is the first line of code after the coroutine. The coroutine is running in parallel to this code, and since it’s waiting one second before printing its message, this
println()
executes first. - This line keeps the program “alive” for an additional two seconds, which gives the coroutine enough time to execute: one second for the delay, and the milliseconds it takes for the coroutine
println()
to do its thing. If you comment out or remove this, the program’s second line of output disappears.
Let’s try another example. Run the following in the Kotlin Playground:
import kotlinx.coroutines.*
fun main() = runBlocking { // 1
launch {
delay(200L)
println("After 200-millisecond delay.")
}
// 2
coroutineScope {
// 3
launch {
delay(500L)
println("After 500-millisecond delay.")
}
delay(100L)
println("After 100-millisecond delay.")
println("${perform200msTask ()}")
}
// 3
println("...and we're done!")
}
// 4
suspend fun perform200msTask(): String {
delay(200L)
return "Finished performing a 200ms task."
}
You’ll see the following in the output section of the page:
After 100-millisecond delay. After 200-millisecond delay. Finished performing a 200ms task. After 500-millisecond delay. ...and we're done!
Here’s what’s happening in the code:
- The
runBlocking
keyword allowsmain()
, which is regular blocking code, to call suspending functions, which you can think of as a function that runs as a coroutine. - The
coroutineScope
keyword defines a scope for coroutines to “live” in. It can contain many coroutines, which allows you to group tasks that happen concurrently. This block of code blocks the current thread until all the coroutines within it have completed. - Because the previous block is a
coroutineScope
, which is blocking, this line isn’t executed until the previous block has completed. This means that “…and we’re done!” is printed last. - The
suspend
keyword marks as a suspending function. When called, it gets executed in parallel.
If you want to confirm that the coroutineScope
blocks the current thread until its inner coroutines have completed, make the following change in the code from this:
// 2
coroutineScope {
To this:
// 2
launch {
Run the code again. The output will now look like this:
...and we're done! After 100-millisecond delay. After 200-millisecond delay. Finished performing a 200ms task. After 500-millisecond delay.
With that change, everything prior to the final line in main()
is executed in parallel and unblocked, and control jumps immediately to that line. The result is that “…and we’re done!” is printed first instead of last.