User Tools

Site Tools


elements_of_kotlin

Table of Contents

Elements of Kotlin

Elements of Kotlin - by Mark L. Murphy

Outline

Introducing Kotlin

Why?

  • 2017 - Kotlins popularity exploded as Google announced support for Kotlin for Android apps
  • originally created as replacement for Java
  • Kotlin is designed to be more concise as Java
    • e.g. semicolon is optional in Kotlin

...And Why Not?

  • Kotlin is a complex language
  • Kotlin lends itself to be terse
    • “concise” → short but understandable
    • “terse” → short but not understandable

What You Need to Know

  • class-based language
    • like Java
    • JavaScript previously was not, but now it is moved in this direction
  • functions
    • (“methods” in Java)
  • properties
    • (“fields” in Java)
  • variables
    • save working data inside functions

Look At All the Kotlins!

  • Runtime environments to run compiled Kotlin code:
    • Kotlin/JVM
      • most used environment
    • Kotlin/JS
      • Kotlin “compiled” into JavaScript code
        • ⇒ simple JavaScript (but not really readable by humans :))
    • Kotlin/Native
      • mostly used in iOS environment
    • Kotlin/Common
      • not really an environment
      • it is a library that limits Kotlin code to guarantee that this code can be used all on other platforms (JVM, JS etc)
    • Kotlin/Multiplatform
      • a way of setting up a project such that some core app logic is written in a Kotlin/Common library, while other “modules” are set up for specific runtime environments.
      • used for example to create one code base for Android and iOS apps

Getting Kotlin, Normally

  • normally it will be done by IDE (for Android → Android Studio)
    • there are Gradle plugins for it etc
  • it is also possible to download a standalone Kotlin compiler

Introducing the Klassbook

  • An easy way to experiment with a programming language is to use a REPL.
    • Read typed-in source code,
    • Evaluate it,
    • Print the results of that evaluation in the tool
    • Loop back to process more input.
  • Klassbook is a REPL set up for use with this book.

How This Book is Structured

  • This book has two objectives:
    • Help you start writing basic Kotlin code
    • Help you start reading advanced Kotlin code
  • This book is divided into two major sets of chapters:
    • core chapters
      • basic Kotlin syntax
      • mostly used Kotlin idioms
    • “WTF?” chapters
      • advanced Kotlin

A Few “Hello, World!” Examples

…

So, What Does This Do?

…

  • HelloWorld().speak()
    • In Kotlin: just use the class name like a function. So, HelloWorld() returns an instance of a HelloWorld object. There is no need for a keyword, the way Java, Ruby, or JavaScript uses new.
  • Kotlin is a “strongly typed” language: everything in Kotlin is tied to some type.

Using the Klassbook

…

Running These Snippets in an IDE

  • Android Studio and IntelliJ IDEA each has a Kotlin REPL, available from the Tools > Kotlin > Kotlin REPL main menu option
  • In Android Studio is also possible to use scratch files to run short examples

Basic Types and Expressions

Kotlin’s basic types are modeled somewhat after Java, as Kotlin’s original target environment was the JVM.

But there are also some differences.

Basic Types and “Objectness”

  • In Kotlin everything is an object
    • In Java: basic types are “primitives”, but have object counterparts
      • e.g. int → Integer

Numbers

  • There are four integral types:
    • Byte (8-bit representation)
    • Short (16 bits)
    • Int (32 bits)
    • Long (64 bits)
  • Two floating-point types:
    • Float (32 bits)
    • Double (64 bits)
  • Number Literals
    • by default Int or Double
    • if you want a Long, use L suffix
      • 12345L
    • if you want a Float, use F suffix
      • 1.2345f
    • For longer numbers, if you like, you can use underscores for the “thousands separator”:
      • 1_234
      • Technically, those underscores can go anywhere, so 1_2_3_4 is also perfectly valid.
    • For integral types, the default representation is decimal format.
      • hexadecimal by using an 0x prefix
      • 0xFFA4C639
      • binary literals by using an 0b prefix
        • 0b10110100
  • Mathematical Expressions
    • + for addition
    • - for subtraction
    • * for multiplication
    • / for division
    • % for the remainder after division (“modulo”)

Boolean

  • true or false
  • Boolean operators:
    • && → AND
    • || → OR
    • ! → NOT

Strings

  • String Quoting Options
    • two possible options:
      • “ - double-quotes
        • like in Java
          • cannot contain newlines
          • special characters, such as newlines or tabs, need to be indicated via “escape sequences”
            • \n, \t etc.
      • ”“” - triple-quotes
        • does not support escape sequences but does support embedded newlines, tabs, and anything else
        • this allows you to directly express strings that otherwise would be a mess of text and escape sequences, resulting in more readable code.
        • but indents then become a problem
          • use trimMargin() function on String to eliminate unwanted indentation.
  • String Expressions
    • Concatenation: “one” + “two”
    • Kotlin also supports “string interpolation”
      • “The value of count is $count”
      • “The value of count is ${count+2}”

Characters

  • Use single quotes to declare a single character:
    • println('A')

A Quick Note About Equality

  • == → means “content equality”
    • (like equals() in Java)
  • === → means “instance equality”
    • (like == in Java)
  • What you get as the result of an instance equality check (===) is determined a bit by Kotlin but mostly by the Kotlin environment.
val value = "something"
println(value.toUpperCase().toLowerCase() == value)   // prints true
println(value.toUpperCase().toLowerCase() === value)  // prints false in Kotlin/JVM and true in Kotlin/JS

Variables… Whether They Vary or Not

Declaring Variables

  • Do you need a keyword, like var, or not?
    • → yes
  • Do you need a data type, like int, or not?
    • → not always
  • Do you need to provide an initializer to establish the variable’s value, or not?
    • → not always
  • Typeless Declaration
    • var myVar = 5
      • myVar::class will be kotlin.Int (in Kotlin/JVM environment)
  • Typed Declaration
    • var count: Long = 5

Declaring Read-Only Variables

  • Declare “read-only” variables with keyword val
  • Reassigning value to a val will result in an exception
  • Prefer val Over var
    • use var only if it is necessary

String Interpolation

  • For a simple variable reference, you can skip the braces and just use $:
  val count = 5
  println("The value of count is $count")

Arbitrary Kotlin expressions can be used with ${} syntax:

val count = 3
println("The value of count is not ${count+2}")

More Operators

  • Mathematical expressions
    • +, -, /, % etc
  • Increment and decrement
    • postincrement++, ++preincrement
    • postdecrement–, –predecrement
  • Augmented Assignments
    • +=, -=, *=, /=, %=
    • example: count += 2
  • Unary Operators (inverting)
    • -count (negation)
    • !count (true will be false and vice versa)

No Automatic Number Conversions

This will not work in Kotlin:

val thisIsInt = 5
val thisIsLong : Long = thisIsInt

(but it works in Java)

In Kotlin the code should be:

val thisIsInt = 5
val thisIsLong : Long = thisIsInt.toLong()

Hey, What About ?

  • We can assign null to a variable whose type ends in ?, and we cannot assign null to a variable whose type does not end in ?.
var something : Boolean? = null
println("something was: $something")
 
something = true
println("something is now: $something")

Functions

Functions with Parameters

fun lessTrivial(left: Int, right: Int) {
  println(left + right)
}

At first the parameter name and then colon and the parameter type.

Functions with Return Types

fun simpleReturn(left: Int, right: Int) : Int {
  return left + right
}
  • Return type is at the end after the colon and before the curly brace (Int in this example).
  • Functions without return keyword returns nothing (void in Java), but technically seen they return Unit.

Local Variables

fun actLocally(left: Int, right: Int) : Int {
  val result = left + right
 
  return result
}

Fancier Functions

Expression bodies:

fun expressionBody(left: Int, right: Int) = left + right

Expression Bodies and Types:

fun expressionBody(left: Int, right: Int):Number = left + right

Number is a super type of Int and sometimes it could be necessary to use it instead of Int.

Default parameter values:

fun main() {
  println(increment(1))
}
 
fun increment(base: Int, amount: Int = 1) = base + amount

Named parameters:

fun main() {
  println(increment(base = 1))
  println(increment(amount = 10, base = 1))
  println(increment(1, amount = 10))
}
 
fun increment(base: Int, amount: Int = 1) = base + amount

It is a simple example and it does not make much sense. But sometimes it is really helpful. For example:

fun main() {
  val cookieHeader = iCanHazCookie(name = "foo", value = "bar", path = "/")
 
  // or even:
  cookieHeader = iCanHazCookie("foo", "bar", path = "/")
}
 
 
 
fun iCanHazCookie(name: String,
                  value: String,
                  maxAge: Long? = null,
                  expires: LocalDateTime? = null,
                  domain: String? = null,
                  path: String? = null,
                  secure: Boolean = false,
                  httpOnly: Boolean = false) : String {
  // code to generate an HTTP Set-cookie header goes here
  // ...
}

Traditionally, named parameters had to appear after all positional parameters. Kotlin 1.4 relaxed that restriction somewhat. You can use names for any parameters, but up through the last positional parameter, the positions (not the names) are what matter.

Collections and Lambdas

Major collection types are like in the other popular modern languages:

  • arrays or lists
  • sets (collections of distinct unequal objects)
  • maps (collections of key/value pairs)

Major Collection Types and Creation Functions

Kotlin has the aforementioned roster of collection types, and they form the most common collection types that you will use.

Kotlin/JVM also has access to other standard Java collection types, like LinkedHashMap, which we will discuss later in this chapter. However, usually you will use those only if there is no native Kotlin equivalent.

  • Arrays
    • the simplest way to create an array:
      • val things = arrayOf(“foo”, “bar”, “goo”)
    • Typed arrays:
      • BooleanArray, ByteArray, CharArray, DoubleArray, FloatArray,IntArray, LongArray, ShortArray
      • They have corresponding utility functions to create instances, such as intArrayOf():
        • val things = intArrayOf(1, 3, 3, 7)
    • Lists
      • can be created like Arrays:
        • listOf(“foo”, “bar”, “goo”)
    • Sets
      • is a one-dimensional collection in which only distinct objects will be included.
        • For example, if you try adding the four numbers 1, 3, 3, 7 to a list, you would have four entries, but adding them to a set results in a three-element set holding 1, 3, 7. The duplicate 3 value is ignored.
      • creation:
        • val things = setOf(1, 3, 3, 7)
        • (will contain only 1,3,7)
      • As with List, Set is an interface. The concrete implementation that you will get from setOf() is a LinkedHashSet
      • And, as with lists, setOf() creates an immutable Set
    • Maps
      • To create a hard-coded map — the way we have created hard-coded lists and sets — you can use the mapOf() utility function. This takes a comma-delimited list of key-value pairs, where the key and value are separated by the to keyword
      • mapOf(“key” to “value”, “other-key” to “other-value”)
      • it is also possible to mix these constructs (e.g. use Lists as values for Maps)
        • mapOf(“odd” to listOf(1, 3, 5, 7, 9), “even” to listOf(2, 4, 6, 8))

Basic Usage

  • [] Syntax
    • To retrieve values from a collection (other than a Set), you can use []. For example:
      • thingsArray[0]
  • Typical Stuff
    • size()
    • isEmpty(), for a quick Boolean value based on whether the collection is empty
    • contains(), which will return true if the collection contains the requested item (for maps, this checks the keys — use containsValue() to check the values)
    • For lists and arrays, you have things like:
      • first(), to get the first element
      • last(), to get the last element
      • firstOrNull(), to get the first element or null if the collection is empty
      • And so on
    • Conversion Functions
      • Array offers
        • toList()
        • toSet()
      • List offers
        • toSet()
        • toTypedArray()
      • Set offers
        • toList()
        • toTypedArray()

Immutability and Collections

Array objects that we create with arrayOf() are mutable. Not only can we use [] syntax to retrieve a value based on its index, but we can use []= syntax to replace a value based on its index.

val thingsArray = arrayOf("foo", "bar", "goo")
 
thingsArray[1] = "something completely different"

However, this fails if you try it with a List created using listOf():

val thingsList = listOf("foo", "bar", "goo")
 
thingsList[1] = "something completely different"

A List is immutable: you cannot replace its members with other objects.

If you need a list where you can replace its members, you can use mutableListOf():

val thingsList = mutableListOf("foo", "bar", "goo")
 
thingsList[1] = "something completely different"

The same distinction holds true for Map. A Map is immutable, but a MutableMap created via mutableMapOf() can be modified.

Introducing Lambda Expressions

  • Lambda expressions are important to do the following:
    • Looping over the elements
    • Finding or filtering elements that match certain criteria
    • Providing comparison logic for sorting elements
    • …
  • What is a Lambda Expression?
    • A simple way to start thinking about lambda expressions is to consider them just as a set of Kotlin statements wrapped in curly braces:
      • val lambda = { println(“Hello, world!”) }
      • A lambda expression itself is an object.
        • You can hold onto them in variables,
        • pass them as function parameters,
        • and do everything else that you might do with other types of objects.
      • If you receive a lambda expression, and you want to run it, call invoke():
val lambda = { println("Hello, world!") }
 
lambda.invoke()
  • Where Do We Use Lambda Expressions?
    • At minimum, lambda expressions replace a lot of the “listener” or “callback” patterns that you might have used in Java.
  • Lambda Expressions, Parameters, and Return Values
    • Like functions, lambda expressions can take parameters:
      • val squarifier = { x: Int -> x * x }
         
        println(squarifier.invoke(3))
      • single-parameter lambdas are very common. For that, Kotlin offers a shorthand syntax: it. If the Kotlin compiler can determine the data type for the parameter, you can skip the parameter declaration and just refer to the parameter as it.
      • A lambda expression will return whatever its last statement does.

Common Collection Operations

  • Iteration (forEach() and for)
    • forEach()
      • val things = listOf("foo", "bar", "goo")
         
        things.forEach { println(it) }
      • forEach() takes a lambda expression as a parameter. It may not look like a parameter, again due to the lack of parentheses. The same code:
        • val things = listOf("foo", "bar", "goo")
           
          things.forEach( { println(it) } )
      • The lambda expression takes a single parameter. The type of the parameter is determined by the definition of forEach() — just as a function can declare a parameter as being an Int or a String, it can declare a parameter as being a lambda expression that itself takes a certain parameter.
    • for
      • val things = listOf("foo", "bar", "goo")
         
        for (thing in things) {
          println(thing)
        }
      • forEach() is somewhat more popular as for, as it can be used in chained expressions.
    • filter()
      • filter() applies a filtering lambda expression to a collection, returning a new collection that contains the subset of items from the original collection. If the lambda expression returns true for an item, that item is included in the result collection, otherwise it is not.
      • val things = listOf("foo", "bar", "goo")
         
        things.filter { it[1]=='o' }.forEach { println(it) }
    • map()
      • filter() passes along a strict subset of the items in the collection, but the items themselves are unchanged. map(), by contrast, passes along each item from the collection, transformed by a lambda expression that you supply:
      • val things = listOf("foo", "bar", "goo")
         
        things
          .map { it.toUpperCase() }
          .forEach { println(it) }
      • whereas filter() takes a collection and returns a subset of its items, map() takes a collection and returns a transformed representation of its items.

Varargs

fun main() {
  reciprocate(1, 2, 3, 5, 7)
}
 
fun reciprocate(vararg values: Int) {
  values
    .map { 1.0 / it }
    .forEach { println(it) }
}

In addition, Kotlin offers special support for an existing Array. If you already have an Array with the values that you want to supply, but the function that you are calling uses vararg (instead of an Array parameter), you can use the “spread operator”

fun main() {
  val things = arrayOf("foo", "bar", "goo")
 
  capped(*things).forEach { println(it) }
}
 
fun capped(vararg strings: String) = strings.map { it.toUpperCase() }

Things get a bit strange with types that map to primitives, though. vararg expects the dedicated array types to be used with the spread operator. So, in the following example, the reciprocate() function takes a vararg of Int. For the spread operator to work, we need to use an IntArray, not a regular Array:

fun main() {
  val primes = intArrayOf(1, 2, 3, 5, 7)
 
  reciprocate(*primes)
}
 
fun reciprocate(vararg values: Int) {
  values
    .map { 1.0 / it }
    .forEach { println(it) }
}

Other JVM Collections

If you are using Kotlin/JVM — for example, if you are using Kotlin for ordinary Android app development — you are welcome to use other JVM collection types, such as LinkedList. On the whole, you should stick with Kotlin collection types where possible, as the Kotlin types are more “natural” for use in Kotlin. But sometimes you will find a Java class that fits your scenario better, such as using a LinkedHashMap to implement an LRU-style cache.

get() and [] Syntax

In reality, [] syntax is available for anything that offers a one-parameter get() function. When Kotlin encounters [] syntax, it simply replaces that with the corresponding get() call.

If, When, and While

If

val i = 3
 
if (i > 10) {
  println("something")
}
else {
  println("something else")
}

else is optional:

val i = 3
 
if (i > 10) {
  println("something")
}
 
println("the if is done")

For single-line conditional work, you can skip the braces:

val i = 3
 
if (i>10) println("something") else println("something else")

else if is also possible:

val i = 3
 
if (i > 10) {
  println("something")
}
else if (i > 2) {
  println("something else")
}
else {
  println("and now for something completely different")
}

However, as you will see in the next section, the convention in Kotlin development is to use when instead of if for such scenarios.

When

Basic syntax:

val thingy = "foo"
 
when (thingy) {
  "foo" -> println("something")
  "bar" -> println("something else")
  else -> println("and now for something completely different")
}

However, you cannot mix types, unless == happens to work to compare them.

It is possible to skip the parameter to the when. In that case, each branch has its own Boolean expression, and the first that evaluates to true is used:

val i = 3
 
when {
  i > 10 -> println("something")
  i > 2 -> println("something else")
  else -> println("and now for something completely different")
}

Also we can consolidate multiple branches:

val thingy = "foo"
 
when (thingy) {
  "foo", "goo" -> println("something")
  "bar", "baz", "frobozz" -> println("something else")
  else -> println("and now for something completely different")
}

For numbers, you can use .. syntax and in to set up a “range” and test against it:

val i = 3
 
when (i) {
  in 3..10 -> println("something")
  in 1..2 -> println("something else")
  else -> println("and now for something completely different")
}

However, these simplifications only work when you supply a value to when, as opposed to leaving when empty and using boolean expressions for the branches.

The value for comparison does not need to be a constant — any expression will also work:

val thingy = "bar"
val otherThingy = "BAR"
 
when (thingy) {
  "foo", "goo" -> println("something")
  otherThingy.toLowerCase() -> println("something else")
  else -> println("and now for something completely different")
}

While

var i = 0
 
while (i < 10) {
  i++
  println(i)
}

However, while is not as common in Kotlin as it is in other languages. More often we use operations on collections (e.g., forEach()) rather than while.

If/When As Expressions

Kotlin lacks ternary support, but more than makes up for it by allowing if to serve in the same role:

val i = 3
 
val j = if(i < 2) 2 else i

Also possible with braces:

val bar = 5
val foo = if (bar > 10) {
  10
}
else {
  bar
}
 
println(foo)

The value of the last statement of the block is what that block evaluates to. In this case, each of those blocks is a single statement, but if we had several statements in a block, while they are all executed, the value from the last statement is used for the result (assuming that block is used, based on the conditional expression).

when can also be used as an expression:

val i = 3
 
val message = when (i) {
  in 3..10 -> "something"
  in 1..2 -> "something else"
  else -> "and now for something completely different"
}
 
println(message)

In the situations, when if or when is used as expression, they must be exhaustive (the expression should work for all cases!). If you try a non-exhaustive if or when for an expression, such as:

val i = 3
 
val message = when (i) {
  in 3..10 -> "something"
  in 1..2 -> 5
}
 
println(message)

…you will get a compiler error. Because it is not clear what should be done in else case.

Bustin’ Out

You can always use return to exit the entire function that you are in. Beyond that, though, there are two loop control statements that you can use: break and continue. These work much like their counterparts in Java and other programming languages.

  • break says “abandon the loop”
  • continue says “skip the rest of this pass through the loop, and move along to the next pass through the loop”

Basic Classes

Basic Classes

A class does not have to contain any of that — this is a valid Kotlin class definition:

class Foo {
 
}

This is also valid:

class Foo

Having a class with no body seems odd. However, in Kotlin, you will occasionally see that sort of thing, particularly with data classes and sealed classes. However, most ordinary classes will have properties and functions.

Creating Instances of Classes

To create an instance of a class it is not necessary to use new() like in Java. Simply call the class like function:

class Foo
 
fun main() {
  val foo = Foo()
 
  println(foo)
}

Packages

The syntax to declare package is Java-like:

package bar.goo
 
class Foo
 
val foo = Foo()

Common Contents

Functions

class Foo {
  fun something() {
    println("Hello, world!")
  }
}
 
fun main() {
  val foo = Foo()
 
  foo.something()
}

Properties

Classes can also have properties:

class Foo {
  var count = 0
 
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}
 
fun main() {
  val foo = Foo()
 
  foo.something()
  foo.something()
  foo.something()
 
  println("the final count was ${foo.count}")
}

this

As with Java, Kotlin uses this as a pseudo-property to refer to the instance of the object in whose function you happen to be running.

class Foo {
  var count = 0
 
  fun something() {
    this.count += 1
    println("something() was called $count times")
  }
}

Constructors

The way you will normally see Kotlin classes have a constructor is by making the actual class name itself take the constructor parameters:

class Foo(var count: Int) {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}
 
fun main() {
  val foo = Foo(0)
 
  foo.something()
  foo.something()
  foo.something()
 
  println("the final count was ${foo.count}")
}

The constructor syntax shown above is really a shorthand for using the constructor keyword:

class Foo constructor(var count: Int) {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}
// ...

Init Blocks

Syntax-wise, an init block resembles a Java static block: a set of braces-wrapped statements preceded by a keyword. In Java, the static block is executed once for the class and is used to initialize complex static fields. In Kotlin, an init block is executed once for each instance and serves as a baseline constructor body:

class Foo(val sillyCount: String) {
  var count = 0
 
  init {
    count = sillyCount.toInt()
  }
 
  fun something() {
    count += 1
    println("something() was not called $count times")
  }
}
 
 
fun main() {
  val foo = Foo("7")
 
  foo.something()
  foo.something()
  foo.something()
 
  println("the final count was ${foo.count}")
}

init blocks have a couple of other distinctions, particularly when compared to Java/Ruby constructors:

  • You can have more than one init block. They are executed in the order that they appear in the class.
  • An init block can only reference properties that are declared before it in the class, not ones declared after it. As a result, an init block often appears after the list of properties, so the init block can reference any of the properties.
  • Sometimes, you will see constructor parameters that do not have the var or val keyword. They just have the parameter name and type. They are still constructor parameters, but they are not properties. You cannot refer to them from the functions of your class. You can refer to them from init blocks, though.

Having Multiple Constructors

The primary constructor appears in the class declaration itself, as we have seen. Other constructors, if they exist, are called “secondary constructors”, and they somewhat resemble ordinary functions:

class Foo(var count: Int) {
 
  constructor(sillyCount: String) : this(sillyCount.toInt()) {
    // could have code here if needed
  }
 
  fun something() {
    count += 1
    println("something() was not called $count times")
  }
}
 
 
fun main() {
  val foo = Foo("7")
 
  foo.something()
  foo.something()
  foo.something()
 
  println("the final count was ${foo.count}")
}

A primary constructor has no function body — any such code goes into an init block. Secondary constructors, though, can have bodies, the way that functions do, but only if needed.

Secondary constructors must arrange to call the primary constructor. This is handled by having a this() call after the constructor parameter list, separated from that list via a colon. The this() call needs to match the parameter list of the primary constructor — in the example shown above, the secondary constructor needs to provide a Int to the primary constructor.

Default Parameter Values

Kotlin supports default parameter values in constructors:

 
class View(
  context: Context,
  attrs: AttributeSet? = null,
  defStyleAttr: Int = 0,
  defStyleRes: Int = 0
) {
  // two tons of code
}

Here, the latter three parameters all have default values defined, giving us the ability to call the constructor with one parameter, all four parameters, or combinations in between.

Inheritance

Kotlin uses colons to indicate inheritance:

class Foo(var count: Int) : Any() {
  fun something() {
    count += 1
    println("something() was called $count times")
  }
}

(the Java counterpart is the keyword extends)

Chaining the super class contructor:

open class Base(val tag: String)
 
class Foo(var count: Int) : Base("Foo Example") {
  fun something() {
    count += 1
    println("$tag: something() was called $count times")
  }
}

In Java, by default, any class can be extended by a subclass. To prevent a class from being extended, you have to mark it as final.

Kotlin goes the opposite route. Kotlin classes cannot be extended by default. You have to mark classes that can be extended using open. Failure to do that results in an error.

Overriding Functions

  • A subclass can call functions that it inherits from its superclass.
  • A subclass can override functions of its superclasses… at least, if those too are marked as open.
open class Base(val tag: String) {
  open fun gimmeTheTag() = tag
}
 
class Foo(var count: Int) : Base("Foo Example") {
  override fun gimmeTheTag() = "Not the Original Tag"
 
  fun something() {
    count += 1
    println("${gimmeTheTag()}: something() was called $count times")
  }
}
  • By default, open is transitive. Once a function is marked as open, it is open for all subclasses down the hierarchy, regardless of whether intervening classes override the function or not.
    • unless the class which overrides the functions marks its version of that function as final

Overriding Properties

It is also possible for a class to declare a property as open, allowing subclasses to override it.

open class Base(open val tag: String) {
  open fun gimmeTheTag() = tag
}
 
open class Intermezzo(override val tag: String) : Base(tag)
 
class Foo(var count: Int) : Intermezzo("Foo Example") {
  override fun gimmeTheTag() = "Not the Original Tag"
 
  fun something() {
    count += 1
    println("${gimmeTheTag()}: something() was called $count times")
  }
}

Chaining to Superclass Functions

open class Base(val tag: String) {
  open fun gimmeTheTag() = tag
}
 
class Foo(var count: Int) : Base("Foo Example") {
  override fun gimmeTheTag() = super.gimmeTheTag() + "-Extended"
 
  fun something() {
    count += 1
    println("${gimmeTheTag()}: something() was called $count times")
  }
}

Inheritance Tests

Kotlin has syntax similar to Java's instanceof , with a shorter keyword: is. So, we can do this:

open class Animal
 
class Frog : Animal()
 
class Axolotl : Animal()
 
fun main() {
  val critter: Animal = Frog()
 
  if (critter is Frog) println("Ribbit!") else println("Ummm... whatever noise an axolotl makes!")
}

In Kotlin, we can use is not only as part of an expression, but we can use it as a test for a when clause. We can rewrite the previous example using a when:

open class Animal
 
class Frog : Animal()
 
class Axolotl : Animal()
 
fun main() {
  val critter: Animal = Frog()
 
  when (critter) {
    is Frog -> println("Ribbit!")
    else -> println("Ummm... whatever noise an axolotl makes!")
  }
}

Casting

In Kotlin, casting is performed by using the as keyword:

val critter: Animal = Frog()
val kermit = critter as Frog

Inside of if and when blocks, if we know that a variable is really of some subtype, the compiler lets us skip the casts and allows us to refer to the variable as the subtype, even though it was declared as a supertype.

open class Animal
 
class Frog : Animal() {
  fun hop() = println("Hop!")
}
 
class Axolotl : Animal() {
  fun swim() = println("Swish!")
}
 
fun main() {
  val critter: Animal = Frog()
 
  when (critter) {
    is Frog -> critter.hop()
    is Axolotl -> critter.swim()
  }
}

In a Java-style world, this would not compile. critter is an Animal, and Animal has neither hop() nor swim(). However, Kotlin realizes that the is Frog code will only be executed if critter is a Frog, so it allows us to call hop() on critter, as if critter were declared as a Frog instead of as an Animal. Similarly, we can call swim() on critter if it is Axolotl.

But this works only if compiler is sure. This code will not compile, because it is unclear in else whether critter is really Axolotl:

//...
when (critter) {
  is Frog -> critter.hop()
  else -> critter.swim()
}
//...

Comments and Documentation

Basic Comment Syntax

All is like in Java. With one exception:

The biggest — and perhaps only — difference between Kotlin comments and what you might do in Java is that multi-line comments can be nested in Kotlin.

Introducing KDoc

Kotlin’s equivalent to JavaDoc is KDoc.

Where KDoc breaks from JavaDoc is in terms of text formatting. KDoc uses Markdown.

What Markdown lacks is a way to link to things inside the same document. KDoc extends Markdown linking syntax, allowing you to reference properties, functions, and similar named things by using simple bracket syntax:

/**
 * This is the summary of your Kotlin class. It should explain, in a single
 * paragraph, the role of this class in your project.
 *
 * This class has a single function: [something].
 *
 * You can also link to other classes, such as [kotlin.String], using the
 * fully-qualified class name.
 *
 * And, of course, you can [link to external content](https://commonsware.com).
 */

As with JavaDoc, KDoc supports “block tags” for documenting sub-elements of class, function, etc. For example, you can use @param to document a function parameter, @return to document a function return value, and so forth.

Just as Java has a javadoc program to generate JavaDocs, Kotlin has a similar tool: Dokka.

Properties

Initialization

Properties need to be initialized at some point.

Initialization when declared (count):

class Foo(sillyCount: String) {
  var count = sillyCount.toInt()
 
  fun something() {
    count += 1
    println("something() was not called $count times")
  }
}

In an init block:

class Foo(sillyCount: String) {
  var count: Int
 
  init {
    count = sillyCount.toInt()
  }
  // ...
}

Initialization “sometime later”:

class Foo {
  var count = 0
  lateinit var label: String
 
  fun defineTag(tag: String) {
    label = tag
  }
 
  fun something() {
    count += 1
    println("$label: something() was called $count times")
  }
}

But in this case the function something() must be called after defineTag(). Where possible, you should try to avoid lateinit, as it is easy to make mistakes and try using the lateinit property before it is initialized.

Also note that lateinit cannot be used for types based on primitives, so you cannot have a lateinit Int, Boolean, etc.

Yet another possibility is to use by lazy:

class Foo(val rawLabel: String) {
  var count = 0
  val label: String by lazy { println("initializing via lazy!"); rawLabel.toUpperCase() }
 
  fun something() {
    count += 1
    println("$label: something() was called $count times")
  }
}

Here, label is initialized lazily. Whenever we first attempt to use label, the lambda expression will be executed, and that value is what we see as the value of label.

One more way to initialize property is to use a function like property. In Kotlin it is possible to call function without parethesis (e.g. forEach). Such functions are named “fake properties”. They will be discussed later in this book.

Constants

Kotlin has a const keyword, which you can use to declare certain val properties as constants. This is somewhat reminiscent of final in Java. val is already a constant but there are some cases for using const in Kotlin. These cases will be discussed later.

Getting Down in the Weeds

A Kotlin property really represents the combination of these three elements:

  • a field
  • a getter function
  • a setter function

It is something like JavaBeans, but without the need to implement standard getter and setter.

Some advanced Kotlin syntax — such as the “fake properties” alluded to earlier — take advantage of this, overriding the default getter and/or setter to do something else. But in the most cases the standard is enough.

Visibility and Scope

  • Visibility: What is allowed to see these classes, functions, and properties?
  • Scope: Where do these classes, functions, and properties actually exist?

Visibility

  • public
    • Kotlin takes the public-by-default route. Everything that is defined by default is visible to anything else.
  • private
    • private limits access to instances of its class, and nothing else.
    • private is not just available for aspects of classes. Classes themselves can be private. Properties and functions defined outside of classes can also be marked as private. In these cases, the determination of what can and cannot access the private items is based on the file:
      • private top-level things are visible to other things in the same Kotlin source file
      • private top-level things are inaccessible to other things in other Kotlin files
  • protected
    • The protected visibility modifier works a lot like private, but it also allows subclasses to access the property or function.
    • protected is defined as “accessible by this class and its subclasses”, so it only makes sense in the context of classes
  • package
    • Kotlin does not offer package-private visibility.
  • internal
    • anything marked as internal is visible to code in the same module but is not visible to code in other modules.
      • strange → the meaning of the “module” depends on the context in which project is used
    • So, if you plan on publishing some form of Kotlin library, internal will be important.
      • On a large project that you have divided into modules, internal can be useful.
      • For small single-module projects, internal is pointless.

Scope

  • Global
    • Top-level definitions in a Kotlin source file are global in scope.
    • Immutable globals — classes, val, etc. — will exist for the life of the process that is executing the Kotlin code.
    • Mutable globals — such as var — have references that will live forever, but the objects that those references reference might come and go as the value of that reference changes.
  • Instance
    • Properties defined inside of a class have “instance” scope. Each instance of the enclosing class will have its own set of instance properties, independent of any other instance of that same class.
  • Local
    • Local to Functions
    • Local to Blocks/Lambdas

Abstract Classes and Interfaces

The Objective: Contracts

Abstract Classes

Interfaces

Which Do You Use?

Data Class

The object Keyword

Nested Objects and Classes

Enums and Sealed Classes

Generics

Exceptions

Annotations

Nullability

Scope Functions

Functional Programming

Extension Functions

Java Interoperability

Changes in Newer Kotlin Versions

Custom Accessors

Extension Properties

Escaping Keywords

Escaped Method Names

Property Delegates

Class Delegation

Constants

Abstract Properties

Covariance in Generics

Contravariance in Generics

Anonymous Functions

Local Functions

Local Types

Inline Functions

Inline Classes

Reified Type Parameters

noinline and crossinline

Receivers in Function Types

Renamed Imports

Operator Overloading

Infix Functions

Destructuring Declarations

Labeled Returns

Nothing

Types of Keywords

elements_of_kotlin.txt · Last modified: by kalaschnikow