Kotlin Basics

Introduction to the Kotlin language and basic concepts

Updated: 20 October 2023

From this Udacity course

About Kotlin

Kotlin is a statically typed languge developed by JetBrains that makes use of:

  • Type inferrence
  • Lambdas
  • Corutines
  • Properties

Kotlin is also officially supported for building Androind Apps

The language also differentiates between Nullable and NonNullable data as well as removing a lot of boilerplate code

Kotlin also can interoperate with Java bidirectionally

Prerequisites

  • A JAVA/Kotlin IDE such as IntelliJ IDEA or Android Studio if you’re working on an Android App
  • JDK

For some basic stuff you can use the Kotlin Playground

Setting Up a Project

  1. New Project
  2. Kotlin > JVM | IDEA

From a toolbar you can go to tools > kotlin > REPL

Hello World

A Hello World Program may look like the following:

1
fun printHello () {
2
println ("Hello World")
3
}
4
5
printHello()

We can see that a function is defined with fun and the only other notable feature in the above code is the lack of semicolons at the end of the line

Operators and Variables

Operators

OperationNameFunction Representation
n + mAdditionn.plus(m)
n - mSubtractionn.minus(m)
n * mMultiplicationn.times(m)
n / mDivisionn.div(m)
n == mEqualsn.equals(m)
n != mNotEquals

Basic types have default operators and functions that can be overloaded as well as allows you to convert basic types automatically however numbers will not implicitly convert as this can cause problems and they must be explicitly cast

Variables

Kotlin has immutable and mutable variables

You can create an immutable variable with val and a mutable variable with var

1
var changeable = 0
2
changeable = 1
3
4
val unchangeable = 1

Note that val only makes the reference immutable and not the properties of the object, so we can still call methods on that object

Although types are inferred we can state them explicitly as well:

1
val b: Byte = 1

Regardless, types are static at compile time

By default variables cannot be null, this is to avoid null pointer exceptions, we can use the ? in the type to state that a variable is nullable

1
val imNotNullable: int = null // will throw an errro
2
val imNullable: int? = null // will work fine

You can use the Not Null Assertion !! operator to throw the null exception if the result is null if you want to do your own null checking

1
myVal!!.functionThatDoesNotExist() // will throw

You can rather use the Elvis ? operator to do null checking and defaulting

1
myVariable?.doThing() ?: 0

If myVariable is null then it will return 0 otherwise it will return the function result

Constants

We can create constants using const val at a top level and classes declared using object because these are evaluated at compile time

Constants can be created at the top level of a file with:

1
const CONSTANT1= 1

Or as object singleton properties

1
object Constants {
2
const val CONSTANT2 = 2
3
}

Or wrapped in a companion object within a class

1
class MyClass {
2
companion object {
3
const val CONSTANT3 = 3
4
}
5
}

Strings

We can define strings using the "..." as normal, additionally we can use the $ sign in a string to display a value inline

1
val fish = 12
2
3
"I have $fish fishies"
1
val cows = 12
2
3
"I have $(fish + cows) animals in total"

Conditions

If-Else

We can use If-Else as follows:

1
if (fish > 1)
2
println("Good")
3
else
4
println("Bad")

If we have multiple statements after the condition we can use braces:

1
if (fish > 1) {
2
println("Statement 1")
3
println("Statement 2")
4
}
5
else {
6
println("Statement 3")
7
println("Statement 4")
8
}

Additionally we can also make use of ranges in our If conditions using the .. notation:

1
if (fish in 1..20)
2
println("Wow")

Switch / When

Kotlin also has switch statements, which make use of the when keyword:

1
when (fish) {
2
0 -> println("No Fish")
3
1 -> println("Eh")
4
in 2..20 -> println("Great")
5
else -> println("WOAH")
6
}

If we use a when without a value, it functions like an If-Else statement:

1
when {
2
fish == 0 -> println("No Fish")
3
fish == 1 -> println("Eh")
4
fish in 2..20 -> println("Great")
5
else -> println("WOAH")
6
}

Data Collections

Arrays

We can create an Array with

1
val myArr = arrayOf("val1", "val2")

We can also mix the data types in the array with:

1
val myArr = arrayOf("val1", 2)

You can also loop over arrays while getting the index like the following:

1
for ((index, element) in myArr.withIndex()) {
2
println("Index: $index, Element: $element")
3
}

Lists

We can create lists using:

1
val myList = listOf("val1", "val2")

If we want the list to be mutable, we can do this using:

1
val myMutableList = mutableListOf("val1", "val2")

Similar constructor functions exist for other collection types with the concept of mutable and immutable collection types

Ranges

You can also create ranges, we can use the following methods of creating a range:

1
val digits = 1..10
2
val reverse = 10 downTo 1
3
val stepped = 1..10, step 2
4
val mixed = 103 downTo 15 step 5

We can then make use of these in a for loop, for example:

1
for (i in 103 downTo 15 step 5) println(i)

Additionally we can also sort of create a sort of array of range values using the following:

1
val myData = Array(10){ 1000.0.pow(it) }

Which will result in a 10 item array with each element being 1000^index

Sequences

Kotlin also has a datatype known as a Sequence which is like a lazy list, the values in this are only evaluated when read and not calculated immediately when operated on

Maps

Maps are essentially key-value pairs that make use of pairs (see later)

We can create a Map of some data with mapOf

1
val colours = mapOf(
2
"sky" to "blue",
3
"fire" to "red",
4
"grass" to "green"
5
)

We can then access the elements of the map with:

1
colours.get("sky")
2
colours["sky"]

If we want to retrieve a value and provide a default value if the key does not exist we can use getOrElse with a default value as a lambda

1
colours.getOrElse("sad") { "no colour found" }

Maps are immutable by default, we can make a mutable map with mutableMapOf

Pairs

Pairs allow us to define a pair of data that are mapped to each other in some way, these can also be chained

We can create aa pair with the to keyword

1
val pair = "val1" to "val2"

This results in the pair (val1, val2), we can access the first and second elements with pair.first and pair.second

We can chain them as well, which is the equivalent of wrapping them in parenthesis

1
val chained = "val1" to "val2" to "val3"

This is equal to:

1
val chained = ("val1" to "val2") to "val3"

Which yields a pair within a pair ((val1, val2), val3). We can also destructure these the same as we would with a data object

These are useful for returning multiple pieces of data from a function and destructuring it on the receiving end

You also have triples that can be created with Triple(el1, el2, el3)

Functions

Main

The main function has all the main parts of a Kotlin function

1
fun main(args: Array<String>) {
2
println("Hello, world!")
3
}

A function uses the fun keyword to define the function and the function arguments with their types are in the parenthesis

Every function has a return type, in the above the function does not return any value and hence returns unit

If a function returns a unit we do not need to state it explicitly

We can create a new Kotlin file in the src directory called main.kt with the above main function contents

The main function is the default entrypoint for a Kotlin application. Click the green run icon next to the line numbers on the left to run the function

Defining and Using Functions

A function to return the day of the week as text can look something like:

1
fun getDayOfWeek (): String {
2
val date = LocalDate.now()
3
val day = date.dayOfWeek
4
5
return when (day) {
6
DayOfWeek.MONDAY ->
7
"Monday"
8
DayOfWeek.TUESDAY ->
9
"Tuesday"
10
DayOfWeek.WEDNESDAY ->
11
"Wednesday"
12
DayOfWeek.THURSDAY ->
13
"Thursday"
14
DayOfWeek.FRIDAY ->
15
"Friday"
16
DayOfWeek.SATURDAY ->
17
"Saturday"
18
DayOfWeek.SUNDAY ->
19
"Sunday"
20
else ->
21
"Something else somehow"
22
}
23
}

We can see the Sting return type written after the function signature, we can use this in our main function with:

1
fun main(args: Array<String>) {
2
val day = getDayOfWeek()
3
println("Today is $day")
4
}

We can run the main function with arguments as well using the Debug Configuration in IntelliJ

From the above we can see that everything in Kotlin is a value, even the result of a when or if statement

Default Values

Function params can also have a default value, for example:

1
fun addNumbers(x: Int = 0, y: Int = 2) : Int {
2
return x + y
3
}

Aside from default params as values we can also make use of this by calling a function

1
fun addNumbers(x: Int = getInitValue(), y: Int = 2) {
2
return x + y
3
}

When defining a function like the above we should avoid using expensive function calls as these are evaluated at call time

Single Line Functions

Single expression functions like the above can also be defined like this:

1
fun addNumbers(x: Int = getInitValue(), y: Int = 2) : Int = x + y

In the case of a single expression return the type can be left out as it can be inferred

1
fun addNumbers(x: Int = getInitValue(), y: Int = 2) = x + y

We can then just use that like we would any other function

Lambdas

A Lambda, also known as an anonymous function, and is an expression that makes a function. These can take arguments or not. The syntax is seen below:

1
val printHello= { println("Hello, World!") }
2
val addFive = { x: Int -> x + 5 }
3
val minusFive = { x: Int -> x - 5 }
4
val addAny = { x: Int, y: Int -> x + y }

If we want to define a variable that holds a function we can state the type using the (T) -> U Syntax

1
val printHello: () -> Unit = { println("Hello, World!") }
2
val addFive: (Int) -> Int = { x: Int -> x + 5 }
3
val minusFive: (Int) -> Int = { x: Int -> x - 5 }
4
val addAny: (Int,Int) -> Int = { x: Int, y: Int -> x + y }

Higher Order Functions

Higher Order Functions are functions that can take other functions, an example of this is the built-in with function. We can create a function that takes another function as an operator, we usually use block to reference the function we are receiving:

1
fun operate(x: Int, block: (Int) -> Int) = blobk(x)
2
3
val addResult = operate(12, addFive)
4
val minusResult = operate(12, minusFive)

Functions can also return other functions, for example we can create a function that builds a generic adder:

1
fun buildAdder(additive: Int) : (Int) -> Int {
2
val adder = { x: Int -> x + additive }
3
return adder
4
}
5
6
val addTwelve = buildAdder(12)
7
ans = addTwelve(10)

There are a lot of other cool things about functions and small syntactical changes that can be used when mixing them together but this should be relatively straightforward

We can make use for the above idea to create the HOCs as extension functions

1
fun operate (
2
additive: Int, block: Int.() -> Int
3
) {
4
int.block()
5
}

Some other built-in HOC’s are run which runs a lambda and returns the result and apply which calls a function on an object and returns the updated object, and let which is used for chaingin functions and getting their results

1
val newHouse = House(isFancy = true)
2
.apply {
3
garages = 2
4
extend(4, 1)
5
}
6
7
print("${ newHouse.price } ${ newHouse.size } ${ newHouse.garages }")
8
9
val priceWithTax = newHouse
10
.let { it.price }
11
.let { it * 1.2 }

You can see that apply is really useful for initializing an object and let is useful for essentially summarizing an object or doing some operations with it

Inlines

Every time we call a lambda Kotlin creates a new lambda object instance, there can be a lot of overhead to create the function instance. We can instead use inline to tell the compiler to inline a function call where it is used (similar to C++)

We can define an inline function by simply adding the inline keyword before fun:

1
inline fun addStuff(x: Int, y: Int): Int {
2
return x + y
3
}

Single Abstract Method (SAM)

SAMs are essentially interfaces with a single methods on them

In Kotlin we can pass lambdas to Java functions that require SAMs

OOP

Create a Class

We typically organize classes into packages. Right Click on src > New Package then you can fill in the package name. Thereafter create a new Kotlin file in the package directory

You can define a new class with the class keyword. A class name typically defines the class name. A function does not need to have a body to be valid:

1
package Functionality
2
3
class House

We can add properties to the House class with:

1
package Functionality
2
3
class House {
4
val size = 1200
5
val rooms = 3
6
val garages = 2
7
}

Using this means that the properties in our class are constants

We can then create an instance of a house and read its properties with:

1
val house = House()
2
val size = house.size

Kotlin automatically creates getters and setters for properties

To make properties changeable we can change val to var

We can also create a custom getter like so:

1
val price : Int
2
get() = size + rooms + garages

By default everything is public

We can also create a default constructor which will set the initial values in the parenthesis of the class declaration

1
class House (
2
val size: Int = 1200,
3
val rooms: Int = 3,
4
val garages: Int = 2
5
) {
6
val price : Int
7
get() = size + rooms + garages
8
}

Additional constructors can be created as well using the constructor keyword in the class, such as this one:

1
class House (
2
var size: Int = 1200,
3
var rooms: Int = 3,
4
var garages: Int = 2
5
) {
6
val price : Int
7
get() = size + rooms + garages
8
9
constructor(kind: Int): this() {
10
size = kind * 3
11
rooms = kind * 2
12
garages = kind
13
}
14
}

The new constructor must make a call a constructor with this(), we can also set the parameters like so:

1
class House (
2
val size: Int = 1200,
3
val rooms: Int = 3,
4
val garages: Int = 2
5
) {
6
val price : Int
7
get() = size + rooms + garages
8
9
constructor(kind: Int): this(
10
kind * 3,
11
kind * 2,
12
kind
13
)
14
}

We can also place logic for a default constructor in the init block, for example:

1
class House (
2
val rooms: Int = 3,
3
val garages: Int = 2,
4
isFancy: Boolean
5
) {
6
val size: Int
7
init {
8
size = if (isFancy) rooms * 1000
9
else rooms * 500
10
}
11
12
val price : Int
13
get() = size + rooms + garages
14
15
constructor(kind: Int): this(
16
kind * 3,
17
kind * 2,
18
false
19
)
20
}

We can have multiple init blocks they are executed in the order they are defined in the class

1
class MyClass {
2
init {
3
println("I run before any properties are initiailzed")
4
}
5
6
val size = 12
7
8
init {
9
println("I run after size is initialized")
10
}
11
12
val height = 14
13
14
init {
15
println("I'll run last")
16
}
17
}

In Kotlin we typically try to avoid using multiple constructors, if we need to do something like that we will create a helper function outside our class that we can use to set up a constructor

Inheritence

The default class inherits from Any, however to inherit from a class we use the word open to say that a class or property can be inherited or overriden

1
open class House (...) {
2
...
3
4
open val price : Int
5
get() = size + rooms + garages
6
7
...
8
}

We can create a new class called FancyHouse like:

1
class FancyHouse(
2
val pools: Int = 1,
3
rooms: Int,
4
garages: Int
5
): House(rooms, garages, isFancy = true) {
6
override val price: Int
7
get() = (size + rooms + garages + pools) * 2
8
}

Interfaces

Kotlin allows us two forms of inheritence: Interfaces and Abstracts. Interfaces cannot have a constructor or any logic

Interfaces use the interface keyword and can essentially only define the signatures for the different properties

1
interface ICanRenovate {
2
var rooms: Int
3
var garages: Int
4
fun extend(roomsToAdd: Int, garagesToAdd: Int)
5
}

Next we can inherit from the ICanRenovate class in our House class. this requires us to set implementations for the rooms, garages, and extend properties and add ICanRenovate after the constructor params

1
open class House (
2
override var rooms: Int = 3,
3
override var garages: Int = 2,
4
isFancy: Boolean
5
): ICanRenovate {
6
val size: Int
7
init {
8
size = if (isFancy) rooms * 1000
9
else rooms * 500
10
}
11
12
override fun extend(roomsToAdd: Int, garagesToAdd: Int) {
13
rooms += roomsToAdd
14
garages += garagesToAdd
15
}
16
17
open val price : Int
18
get() = size + rooms + garages
19
20
constructor(kind: Int): this(
21
kind * 3,
22
kind * 2,
23
false
24
)
25
}

Abstract

We can add an abstract class for some predefined functionality, let’s create one called Livable with a population

1
abstract class Livable() {
2
abstract val population: Int
3
}

The abstract keyword for the population property allows it to be overriden when inherited

We can then update our House class to include this with:

1
open class House (
2
override var rooms: Int = 3,
3
override var garages: Int = 2,
4
isFancy: Boolean
5
): Livable(), ICanRenovate {
6
override val population: Int = 4
7
...
8
}

The house class now has the population property as well as a result of the inheritance

Using Inherited Classes

We can specify the inherited classes as parameters to functions where we would like to use some specific functionality, for example in a function where we want to use the extend function we can just ask for ICanRenovate

1
fun extendItem(item: ICanRenovate) = item.extend(1, 1)

Kotlin also allows you to define preset classes that can be delegated for inheritence which allows us to implement certain functionality in an instance that can be reused

Singletons

We can create a class that can only have a single instance with the object keyword -> AKA singleton

We can create an Interface called IHasColour with a colour property

1
interface IHasColour {
2
val colour: String
3
}

An object called HouseColour can then implement that interface and set the implementation for it

1
object HouseColour: IHasColour {
2
override val colour: String = "Beige"
3
}

We can then update a class to implement this functionality using the Interface by Singleton structure in the definition

1
open class House (
2
...
3
): Livable(), ICanRenovate, IHasColour by HouseColour {
4
...
5
}

Interface delegation allows us to use composition to plug in select functionality and should be considered for the kinds of usecases that we would use abstract classes for in other languages

Data Classes

Often we have classes that are defined just for storing data, we can use a data class in Kotlin for doing that

1
data class Address(val number: Int, val street: String)

The data class has an automatic toString and equals method for proper equality checking as well as the copy method which can copy objects

We can also decompose the values from the class using the following syntax:

1
val postal = Address(24, "Fun Street")
2
val ( postalNumber, postalStreet ) = postal

The number of values in the decomposition must match the number of properties and are defined based on the order they are in the class

Enums

In Kotlin Enums are defined using the enum keyword and they can haver properties and methods

1
enum class Suburb {
2
SUB_1,
3
SUB_2,
4
SUB_3
5
}

Sealed Class

A sealed class is a class that can only be used within the same file. These classes are static at compile time as well as all its references this means that the compiler can do additional safety checking that wouldn’t otherwise be possible

Extenstion Methods

Extension methods are functions that extend functionality of a class without modifying the class itself. Inside of the function this refers to the current object instance

We can declare the function using the dot notation for a function name. For example we can create an extension of String with:

1
fun String.hasSpaces(): Boolean {
2
return this.find { it == ' ' } != null
3
}
4
5
"Hello World".hasSpaces() // true

We can also leave out the this if there is no ambiguity in the scope, as well as make this a single line

1
fun String.hasSpaces() = find { it == ' ' } != null

These functions don’t have access to private members and are based on only on private members

Extension functions can also be used on getters and setters for properties, for example:

1
val String.isApple: Boolean
2
get() = this == "Apple"

Generic Classes

Kotlin also allows us to create generics using a similar notation to other languages, such as MyClass<T> which is a generic Class of T, we can also do the same for function arguments

1
class MyClass<T>(val propKey: String, val propVal: T)

We can create an instance of the class with:

1
val myData = MyClass<Int>("hello", 12)

Additionally we can also create Generic Function with:

1
fun <T> printAndReturn(data: T): T {
2
println(data)
3
return data
4
}

And the function can be called with either an inferred or explicit type:

1
printAndReturn("hello")
2
printAndReturn<String>("hello")

By default T is nullable, if we want to be non-nullable we can specify it with the Any type:

1
class MyClass<T: Any>(val propKey: String, val propVal: T: Any)

We can alternatively specify a base class to use as well, for example say we have a class MyClass that other elemens have extended, we can do something more like:

1
class MyNewClass<T: MyClass>(
2
val propKey: String, val propVal: T: MyClass
3
)

We can also define in which can only be passed into something and out types which can only be returned as a result of a function or passed into a constructor.

1
class MyNewClass<in T: MyClass>( ... )

We can also define an out type using the same kind of notation

The IDE should point out when these are needed though

Sometimes you may need to tell the compiler that a type is a real type, this does something with the runtime that I don’t completely understand but essentially you need to use inline before the function and reified before the types if you want to access the types themselves:

1
inline fun <reified T: MyClass> isTypeValid(data: MyClass)
2
= data is T

We can use generics for extension methods as well

Annotations

Annotations are used by the compiler and many are supplied with the language itself and are generally used when interoperating with Java

Annotations come before the thing that is annotated

Below we can see a class definition for the most basic annotation. It doesn’t do much other than be annotated

1
annotation class ImAnnotated
2
3
@ImAnnotated
4
class MyClass

If an annotation is targeting a property we can specify if it is only allowed to be used on getters or setters

1
annotation class ImAnnotated
2
3
@Target(AnnotationTarget.PROPERTY_GETTER)
4
annotation class OnGet
5
6
@Target(AnnotationTarget.PROPERTY_SETTER)
7
annotation class OnSet
8
9
@ImAnnotated
10
class MyClass {
11
@get: OnGet
12
val data1: Int = 0
13
14
@set: OnSet
15
var data2: Int = 0
16
}

Labelled Breaks

These allow us to break out of a loop in a more controlled manner by breaking out to the block outside of the label, they are defined with @labelName

1
mainLoop@for (i in 1..100) {
2
for (j in 1..10) {
3
if (i > 10) break@mainLoop
4
else println("i: $i, j: $j")
5
}
6
}

The code above will run until i > 10 and then completely exit both for loops, normally we would do something like this with two breaks for example to break out of each loop individually