Kotlin Bootcamp for Programmers 5.1: Extensions

1. Welcome

This codelab is part of the Kotlin Bootcamp for Programmers course. You'll get the most value out of this course if you work through the codelabs in sequence. Depending on your knowledge, you may be able to skim some sections. This course is geared towards programmers who know an object-oriented language, and want to learn Kotlin.

sEioGm-YJlcEfGjX0S6M-MQDi23k2ZjQCNPkuImT4e5BIqCJ7XCoLqvDJlUK4cB9XfffJQOcpcW_I8J1LRpYN6qk_b7NMWSQi_0yAWk6Gm5e9C-vvNo5v8geG9iINqKPc_byPxgqMA

Introduction

In this codelab you are introduced to a number of different useful features in Kotlin, including pairs, collections, and extension functions.

Rather than build a single sample app, the lessons in this course are designed to build your knowledge, but be semi-independent of each other so you can skim sections you're familiar with. To tie them together, many of the examples use an aquarium theme. And if you want to see the full aquarium story, check out the Kotlin Bootcamp for Programmers Udacity course.

BLgezynJ_92kR7lbZPbmkh7cDUCFMm3Ugo_JUOdDd5IpMdkk8nu3nbMiSkQWK5dx4-NX4qlbUXwU9l_Pj_7QqoRSUX2YbiddIUO9I100elofv-IY6xAHo7RL9CCXnjEwBKyLknPHzw

What you should already know

  • The syntax of Kotlin functions, classes, and methods
  • How to work with Kotlin's REPL (Read-Eval-Print Loop) in IntelliJ IDEA
  • How to create a new class in the IntelliJ IDEA and run a program

What you'll learn

  • How to work with pairs and triples
  • More about collections
  • Defining and using constants
  • Writing extension functions

What you'll do

  • Learn about pairs, triples, and hash maps in the REPL
  • Learn different ways of organizing constants
  • Write an extension function and an extension property

2. Task: Learn about pairs and triples

In this task, you learn about pairs and triples and destructuring them. Pairs and triples are premade data classes for 2 or 3 generic items. This can, for example, be useful for having a function return more than one value.

Suppose you had a List of fish, and a function isFreshWater() to check if the fish was a freshwater or saltwater fish. List.partition() returns two lists, one with the items where the condition is true, and the other for items where the condition is false.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Step 1: Make some pairs and triples

  1. Open the REPL (Tools > Kotlin > Kotlin REPL).
  2. Create a pair, associating a piece of equipment with what it is used for, then print the values. You can create a pair by creating an expression connecting two values, such as two strings, with the keyword to, then using .first or .second to refer to each value.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Create a triple and print it with toString(), then convert it to a list with toList(). You create a triple using Triple() with 3 values. Use .first, .second and .third to refer to each value.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

The above examples use the same type for all the parts of the pair or triple, but that is not required. The parts could be a string, a number, or a list, for example—even another pair or triple.

  1. Create a pair where the first part of the pair is itself a pair.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Step 2: Destructure some pairs and triples

Separating pairs and triples into their parts is called destructuring. Assign the pair or triple to the appropriate number of variables, and Kotlin will assign the value of each part in order.

  1. Destructure a pair and print the values.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Destructure a triple and print the values.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Note that destructuring pairs and triples works the same as with data classes, which was covered in a previous codelab.

3. Task: Comprehend collections

In this task you learn more about collections, including lists, and a new collection type, hash maps.

Step 1: Learn more about lists

  1. Lists and mutable lists were introduced in an earlier lesson. They're a very useful data structure, so Kotlin provides a number of built-in functions for lists. Review this partial list of functions for lists. You can find complete listings in the Kotlin documentation for List and MutableList.

Function

Purpose

add(element: E)

Add an item to the mutable list.

remove(element: E)

Remove an item from a mutable list.

reversed()

Return a copy of the list with the elements in reverse order.

contains(element: E)

Return true if the list contains the item.

subList(fromIndex: Int, toIndex: Int)

Return part of the list, from the first index up to but not including the second index.

  1. Still working in the REPL, create a list of numbers and call sum() on it. This sums up all the elements.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Create a list of strings and sum the list.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. If the element isn't something List knows how to sum directly, such as a string, you can specify how to sum it using .sumBy() with a lambda function, for example, to sum by the length of each string. The default name for a lambda argument is it and here it refers to each element of the list as the list is traversed.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. There's a lot more you can do with lists. One way to see the functionality available is to create a list in IntelliJ IDEA, add the dot, and then look at the auto-completion list in the tooltip. This works for any object. Try it out with a list.

7accafeefe61a724.png

  1. Choose listIterator() from the list, then go through the list with a for statement and print all the elements separated by spaces.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Step 2: Try out hash maps

In Kotlin, you can map pretty much anything to anything else using hashMapOf(). Hash maps are sort of like a list of pairs, where the first value acts as a key.

  1. Create a hash map that matches symptoms, the keys, and diseases of fish, the values.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. You can then retrieve the disease value based on the symptom key, using get(), or even shorter, square brackets [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Try specifying a symptom that isn't in the map.
println(cures["scale loss"])
⇒ null

If a key isn't in the map, trying to return the matching disease returns null. Depending on the map data, it may be common to have no match for a possible key. For cases like that, Kotlin provides the getOrDefault() function.

  1. Try looking up a key that has no match, using getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

If you need to do more than just return a value, Kotlin provides the getOrElse() function.

  1. Change your code to use getOrElse() instead of getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Instead of returning a simple default value, whatever code is between the curly braces {} is executed. In the example, else simply returns a string, but it could be as fancy as finding a webpage with a cure and returning it.

Just like mutableListOf, you can also make a mutableMapOf. A mutable map lets you put and remove items. Mutable just means able to change, immutable means unable to change.

  1. Make an inventory map that can be modified, mapping an equipment string to the number of items. Create it with a fish net in it, then add 3 tank scrubbers into the inventory with put(), and remove the fish net with remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

4. Task: Study constants

In this task, you learn about constants in Kotlin and different ways of organizing them.

Step 1: Learn about const vs. val

  1. In the REPL, try creating a numeric constant. In Kotlin, you can make top-level constants and assign them a value at compile time using const val.
const val rocks = 3

The value is assigned, and can't be changed, which sounds a lot like declaring a regular val. So what's the difference between const val and val? The value for const val is determined at compile time, where as the value for val is determined during program execution, which means, val can be assigned by a function at run time.

That means val can be assigned a value from a function, but const val cannot.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

In addition, const val only works at the top level, and in singleton classes declared with object, not with regular classes. You can use this to create a file or singleton object that contains only constants, and import them as needed.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Step 2: Create a companion object

Kotlin does not have a concept of class level constants.

To define constants inside a class, you have to wrap them into companion objects declared with the companion keyword. The companion object is basically a singleton object within the class.

  1. Create a class with a companion object containing a string constant.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

The basic difference between companion objects and regular objects is:

  • Companion objects are initialized from the static constructor of the containing class, that is, they are created when the object is created.
  • Regular objects are initialized lazily on the first access to that object; that is, when they are first used.

There is more, but all that you need to know for now is to wrap constants in classes in a companion object.

5. Task: Write extensions

In this task, you learn about extending the behavior of classes. It's very common to write utility functions to extend the behavior of a class. Kotlin provides a convenient syntax for declaring these utility functions: extension functions.

Extension functions allow you to add functions to an existing class without having to access its source code. For example, you could declare them in an Extensions.kt file that is part of your package. This doesn't actually modify the class, but it allows you to use the dot-notation when calling the function on objects of that class.

Step 1: Write an extension function

  1. Still working in the REPL, write a simple extension function, hasSpaces() to check if a string contains spaces. The function name is prefixed with the class it operates on. Inside the function, this refers to the object it is called on, and it refers to the iterator in the find() call.
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. You can simplify the hasSpaces() function. The this isn't explicitly needed, and the function can be reduced to a single expression and returned, so the curly braces {} around it aren't needed, either.
fun String.hasSpaces() = find { it == ' ' } != null

Step 2: Learn the limitations of extensions

Extension functions only have access to the public API of the class they're extending. Variables that are private can't be accessed.

  1. Try adding extension functions to a property marked private.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Examine the code below and figure out what it will print.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() prints GreenLeafyPlant. You might expect aquariumPlant.print() to print GreenLeafyPlant, too, because it was assigned the value of plant. But the type is resolved at compile time, so AquariumPlant gets printed.

Step 3: Add an extension property

In addition to extension functions, Kotlin also lets you add extension properties. Like extension functions, you specify the class you're extending, followed by a dot, followed by the property name.

  1. Still working in the REPL, add an extension property isGreen to AquariumPlant, which is true if the color is green.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

The isGreen property can be accessed just like a regular property; when accessed, the getter for isGreen is called to get the value.

  1. Print the isGreen property for the aquariumPlant variable and observe the result.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Step 4: Know about nullable receivers

The class you extend is called the receiver, and it is possible to make that class nullable. If you do that, the this variable used in the body can be null, so make sure you test for that. You would want to take a nullable receiver if you expect that callers will want to call your extension method on nullable variables, or if you want to provide a default behavior when your function is applied to null.

  1. Still working in the REPL, define a pull() method that takes a nullable receiver. This is indicated with a question mark ? after the type, before the dot. Inside the body, you can test if this is not null by using questionmark-dot-apply ?.apply.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. In this case, there is no output when you run the program. Because plant is null, the inner println() is not called.

Extension functions are very powerful, and most of the Kotlin standard library is implemented as extension functions.

6. Summary

In this lesson, you learned more about collections, learned about constants, and got a taste of the power of extension functions and properties.

  • Pairs and triples can be used to return more than one value from a function. For example: val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin has many useful functions for List, such as reversed(), contains(), and subList().
  • A HashMap can be used to map keys to values. For example: val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Declare compile-time constants using the const keyword. You can put them at the top level, organize them in a singleton object, or put them in a companion object.
  • A companion object is a singleton object within a class definition, defined with the companion keyword.
  • Extension functions and properties can add functionality to a class. For example: fun String.hasSpaces() = find { it == ' ' } != null
  • A nullable receiver allows you to create extensions on a class which can be null. The ?. operator can be paired with apply to check for null before executing code. For example: this?.apply { println("removing $this") }

7. Learn more

Kotlin documentation

If you want more information on any topic in this course, or if you get stuck, https://kotlinlang.org is your best starting point.

Kotlin tutorials

The https://play.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter, and a complete set of reference documentation with examples.

Udacity course

To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers.

IntelliJ IDEA

Documentation for the IntelliJ IDEA can be found on the JetBrains website.

8. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

Which one of the following returns a copy of a list?

add()

remove()

reversed()

contains()

Question 2

Which one of these extension functions on class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) will give a compiler error?

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

Question 3

Which one of the following is not a place where you can define constants with const val?

▢ at the top level of a file

▢ in regular classes

▢ in singleton objects

▢ in companion objects

9. Next codelab

Proceed to the next lesson:

For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."