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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. If the object it’s applied to isn’t null, hashCode() returns the object’s hash function value.
  2. 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:

  1. 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 the GlobalScope. 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.
  2. 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.
  3. 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.
  4. 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:

  1. The runBlocking keyword allows main(), which is regular blocking code, to call suspending functions, which you can think of as a function that runs as a coroutine.
  2. 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.
  3. 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.
  4. 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.