Kotlin basics

Why Kotlin?

Kotlin is a modern, general-purpose programming language designed by JetBrains as a drop-in replacement for Java. It has full compatibility with Java source code, and is 100% compatible with that ecosystem.

Over time, it has extended far past it’s original goals. It’s seen broad industry support, and has proven to be a versatile language for full-stack development.

  • As of 2019, it’s Google’s recommended language for building Android applications. Android development, including third-party libraries, is done in Kotlin1.
  • It’s fully capable back-end language for building servers and services, and it’s compatibile with existing service frameworks e.g., Spring Boot, Ktor.
  • Finally, it’s designed as a cross-platform language, with native compilers for many platforms . We use it in this course for desktop and Android. iOS support is in beta2, and WASM/web is also on the horizon.
1

At Google I/O 2024, their developer conference, Google announced that more than 60% of the top 1000 apps on the Google Play Store are written in Kotlin.

2

We won’t be building iOS applications in this course, mainly because iOS support requires dedicated Apple hardware, which is not available to all students. Android development tools will work on Windows, macOS and Linux, so there are no barriers to entry.

Kotlin vs. Python
I don't think Kotlin is more difficult than other languages; we're just building complex applications!

Tip

The following sections introduce Kotlin, focusing on features that you will use in this course. Other recommended resources for learning Kotlin include:

For a more comprehensive list of resources, see the reading-list and sample code pages.

Compiling

The Kotlin programming language includes a set of compilers that can be used to target different platforms.

flowchart LR
    kotlin([Kotlin Code])
    jvm([Kotlin/JVM])
    native([Kotlin/Native])
    js([Kotlin/JS])
    wasm([Kotlin/WASM])
    kotlin --> jvm
    kotlin --> native
    kotlin --> js
    kotlin --> wasm
  • Kotlin/JVM compiles Kotlin to JVM bytecode, which can be interpreted on a Java virtual machine. This supports running Kotlin code anywhere a JVM is supported (typically desktop, server).
  • Kotlin/Native compiles Kotlin to native executables. This provides support for iOS and other targets.
  • Kotlin/JS transpiles Kotlin to JavaScript. The current implementation targets ECMAScript 5.1.
  • Kotlin/WASM adds support for WASM virtual machine standard, allowing Kotlin to run on the web.

Kotlin/JVM and Kotlin/Native for Android are the best-supported targets at this time, and what we will focus on in this course.

A Kotlin application can be compiled to JVM or native code. Kotlin’s application code looks a little like C, or Java. Here’s the world’s simplest Kotlin program, consisting of a single main method.

fun main() {
	val message="Hello Kotlin!"
	println(message)
}

To compile from the command-line, we can use the Kotlin compiler, kotlinc. By default, it takes Kotlin source files (.kt) and compiles them into corresponding class files (.class) that can be executed on the JVM.

$ kotlinc Hello.kt

$ ls
Hello.kt HelloKt.class

$ kotlin HelloKt
Hello Kotlin!

Notice that the compiled class is named slightly differently than the source file. If your code isn’t contained in a class, Kotlin wraps it in an artificial class so that the JVM (which requires a class) can load it properly. Later when we use classes, this won’t be necessary.

This example compiles Hello.kt into Hello.jar and then executes it:

$ kotlinc Hello.kt -include-runtime -d Hello.jar

$ ls
Hello.jar Hello.kt

$ java -jar Hello.jar
Hello Kotlin!

Most of the time, we won’t compile from the command-line; instead, we will use a build system to generate an installable package for our platform e.g., desktop or Android. See Gradle.

Modularity

Packages

When defining related classes or functions within an application, it’s best to group them together in a package.

Packages are meant to be a collection of related classes. e.g. graphics classes. They are conceptually similar to namepsaces in C++. We use them to enforce a clear separation of concerns within code.

Packages are named using a reverse DNS name, typically the reverse-domain name of company that owns the code, followed by a unique name related to the code’s functionality. e.g. com.sun.graphics for a graphics package developed at Sun Microsystems, or ca.uwaterloo.cs346 for code related to this course.

Info

Package names are always lowercase, and dot-separated with no underscores. This convention arose from the Java language, and is used in Kotlin as well.

To create and use a package, you need to:

  1. Add the package declaration to the top of a source file to assign that file to a namespace. Classes or modules in the same package have full visibility to each other.

For example, we have declared this file to be included in the cs.uwaterloo.cs346 package. This means that any classes in this file will be included in that package, and will be visible to any other functions or classes in that package, even if they are in different files.

package ca.uwaterloo.cs346

data class Point(x: Float, y: Float) { }
data class Vector(x: Float, y: Float, m: Float) { }
  1. Use the import keyword to bring classes from other packages into the current namespace. This allows you to use classes from other packages in your code.

For example, to use our Point class in another file, we would import it like this. Note that the Vector class isn’t imported, so it won’t be available in this file.

import ca.uwaterloo.cs346.Point

fun main() {
  val p1 = Point(5.0, 10.0)
  val p2 = Point(15.0, 20.0)
}

To include all classes in a package, you can use the * wildcard. This would make both Point and Vector visible.

import ca.uwaterloo.cs346.*

Modules

Modules serve a different purpose than packages: they are intended to expose permissions for external dependencies. Using modules, you can create higher-level constructs (modules) and have higher-level permissions that describe how that module can be reused. Modules are intended to support a more rigorous separation of concerns that you can obtain with packages.

A simple application would be represented as a single module containing one or more packages, representing different parts of our application.

JAR Files

A Kotlin application might consist of many classes, each compiled into it’s own .class file. To make these more manageable, the Java platform includes the jar utility, which can be used to combine your classes into a single archive file i.e. a jar file. Your application can be executed directly from that jar file. This is the standard mechanism in the Java ecosystem for distributing applications.

A jar file is just a compressed file (like a zip file) which has a specific structure and contents, and is created using the jar utility. This example compiles Hello.kt, and packages the output in a Hello.jar file.

$ kotlinc Hello.kt -include-runtime -d Hello.jar

$ ls
Hello.jar Hello.kt

The -d option tells the compiler to package all of the required classes into our jar file. The -include-runtime flag tells the compiler to also include the Kotlin runtime classes (which contain things like the Garbage Collector).

These classes are needed for all Kotlin applications, and they’re small, so you should always include them in your distribution (if you fail to include them, you app won’t run unless your user has Kotlin installed).

To run from a jar file, use the java command.

$ java -jar Hello.jar
Hello Kotlin!

In the same way that the Java compiler compiles Java code into IR code that will run on the JVM, the Kotlin compiler also compiles Kotlin code into compatible IR code. The java command will execute any IR code, regardless of which programming language and compiler was used to produce it.

Our JAR file from above looks like this if you uncompress it:

$ unzip Hello.jar -d contents
Archive:  Hello.jar
  inflating: contents/META-INF/MANIFEST.MF
  inflating: contents/HelloKt.class
  inflating: contents/META-INF/main.kotlin_module
  inflating: contents/kotlin/collections/ArraysUtilJVM.class
  ...

$ tree -L 2 contents/
.
├── META-INF
│   ├── MANIFEST.MF
│   ├── main.kotlin_module
│   └── versions
└── kotlin
    ├── ArrayIntrinsicsKt.class
    ├── BuilderInference.class
    ├── DeepRecursiveFunction.class
    ├── DeepRecursiveKt.class
    ├── DeepRecursiveScope.class
    ...

The JAR file contains these main features:

  • HelloKt.class – a class wrapper generated by the compiler
  • META-INF/MANIFEST.MF – a file containing metadata.
  • kotlin/ – Kotlin runtime classes not included in the JDK.

The MANIFEST.MF file is autogenerated by the compiler, and included in the JAR file. It tells the runtime which main method to execute. e.g. HelloKt.main().

$ cat contents/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: JetBrains Kotlin
Main-Class: HelloKt

Library support

Kotlin benefits from some of the best library support in the industry, since it can leverage any existing Java libraries as well as Kotlin standard libraries. This means that you can use any Java library in your Kotlin code, and you can also use any Kotlin library that has been developed.

Kotlin Standard Library

The Kotlin Standard Library is included with the Kotlin language, and contained in the kotlin package. This is automatically imported and does not need to be specified in an import statement.

Some of the features that will be discussed below are actually part of the standard library (and not part of the core language). This includes essential classes, such as:

  • Higher-order scope functions that implement idiomatic patterns (let, apply, use, etc).
  • Extension functions for collections (eager) and sequences (lazy).
  • Various utilities for working with strings and char sequences.
  • Extensions for JDK classes making it convenient to work with files, IO, and threading.

Java Standard Library

Kotlin is completely 100% interoperable with Java, so all of the classes available in Java/JVM can also be imported and used in Kotlin.

// import all classes in the java.io package
// this allows us to reference any classes in this namespace
import java.io.*

// we can also just import a single class
// this allows us to refer to just the ListView class in code
import javafx.scene.control.ListView

// Kotlin code calling Java IO libraries
import java.io.FileReader
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.IOException
import java.io.FileWriter
import java.io.BufferedWriter

if (writer != null) {
	writer.write(
  	row.toString() + delimiter +
    	s + row + delimiter +
      pi + endl
  )

Importing a class requires your compiler to locate the file containing these classes! The Kotlin Standard Library can always be referenced by the compiler, and as long as you’re compiling to the JVM, the Java class libraries will also be made available.

Third-Party Libraries

To use any other Java or Kotlin library, you will to add it to your Gradle dependencies in your build.gradle.kts. This makes them available to import in your source code

// build.gradle.kts

dependencies {
  implementation("com.github.ajalt.clikt:clikt:4.2.2")
}

Once the library is added to your dependencies, you can import it in your source code:

import com.github.ajalt.clikt.core.CliktCommand

fun process() {
  val command = object : CliktCommand() {
    override fun run() {
      echo("Hello, world!")
    }
  }
  command.main(arrayOf())
}

See Gradle > Dependency Management for more information on how to manage dependencies in Kotlin.

Kotlin Basics

Kotlin is a general-purpose, class-based object-oriented language. Although not a functional language, it includes some functional design elements (e.g. higher order functions). Syntactically, it is similar to other C-style programming languages, with a number of modern advances.

Practically, it is an excellent general-purpose language that can replace Java in production environments.

Types

A type system is the set of rules that are applied to expressions in a propgramming language language. We differentiate different type systems by the types of rules that they apply:

  • Strong typing: The language has strict typing rules, which are typically enforced at compile-time. The exact type of variable must be declared or fixed before the variable is used. This has the advantage of catching many types of errors at compile-time (e.g. type-mismatch).
  • Weak typing: These languages have looser typing rules, and will often attempt to infer types based on runtime usage. This means that some categories of errors are only caught at runtime.

Kotlin is a strongly typed language, where variables need to be declared, or the type known, at compile time. If a type isn’t strictly provided, Kotlin will infer the type at compile time (similar to ‘auto‘ in C++). The compiler is strict about this: if the type cannot be inferred at compile-time, an error will be thrown.

Variables

Kotlin uses the var keyword to indicate a variable, and Kotlin expects variables to be declared before use. Types are always placed to the right of the variable name. Types can be declared explicitly, but will be inferred if the type isn’t provided.

fun main() {
  var a:Int = 10
  var b:String = "Jeff"
  var c:Boolean = false

  var d = "abc"	   // inferred as a String
  var e = 5        // inferred as Int
  var f = 1.5      // inferred as Float
}

All standard data-types are supported, and unlike Java, all types are objects with properties and behaviours. This means that your variables are objects with methods! E.g. "10".toInt() does what you would expect.

Supported Types

Integers

TypeSize (bits)Min valueMax value
Byte8-128127
Short16-3276832767
Int32-2,147,483,648 (-2 31)2,147,483,647 (2 31- 1)
Long64-9,223,372,036,854,775,808 (-2 63)9,223,372,036,854,775,807 (2 63- 1)

Floating Point Types

TypeSize (bits)Significant bitsExponent bitsDecimal digits
Float322486-7
Double64531115-16

Boolean

The type Boolean represents boolean objects that can have two values: true and false. Boolean has a nullable counterpart Boolean? that also has the null value.

Built-in operations on booleans include:

  • || – disjunction (logical OR)
  • && – conjunction (logical AND)
  • !- negation (logical NOT)
  • || and && work lazily.

Strings

Strings are often a more complex data type to work with. In Kotlin, they are represented by the String type, and are immutable. Elements of a string are characters that can be accessed by the indexing operation: s[i], and you can iterate over a string with a for-loop:

fun main() {
  val str = "Sam"
  for (c in str) {
      println(c)
  }
}

click to run

You can concatenate strings using the + operator. This also works for concatenating strings with values of other types, as long as the first element in the expression is a string (in which case the other element will be case to a String automatically):

fun main() {
  val s = "abc" + 1
  println(s + "def")
}

click to run

Kotlin supports the use of string templates, so we can perform variable substitution directly in strings. It’s a minor but incredibly useful feature that replaces the need to concatenate and build up strings to display them.

fun main() {
  println("> Kotlin ${KotlinVersion.CURRENT}")

  val str = "abc"
  println("$str.length is ${str.length}")

  var n = 5
  println("n is ${if(n > 0) "positive" else "negative"}")
}

click to run

is and !is operator

To perform a runtime check whether an object conforms to a given type, use the is operator or its negated form !is:

fun main() {
  val obj = "abc"

  if (obj is String) {
    print("String of length ${obj.length}")
  } else {
    print("Not a String")
  }
}

click to run

In most cases, you don’t need to use explicit cast operators in Kotlin because the compiler tracks the is checks and explicit casts for immutable values and inserts (safe) casts automatically when needed:

fun main() {
  val x = "abc"
  if (x !is String) return
  println("x=${x.length}") // x is automatically cast to String

  val y = "defghi"
  // y is automatically cast to string on the right-hand side of `||`
  if (y !is String || y.length == 0) return
  println("y=${y.length}") // y must be a string with length > 0
}

click to run

Immutability

Kotlin supports the use of immutable variables and data structures (mutable means that it can be changed; immutable structures cannot be changed after they are initialized). This follows best-practices in other languages (e.g. use of final in Java, const in C++), where we use immutable structures to avoid accidental mutation.

  • var: a standard mutable variable that can be changed or reassigned.
  • val: an immutable variable that cannot be changed once initialized.
var a = 0       // type inferred as Int
a = 5           // a is mutable, so reassignment is ok

val b = 1	    // type inferred as Int as well
// b = 2	    // error because b is immutable

var c:Int = 10  // explicit type provided in this case

click to run

Operators

Kotlin supports a wide range of operators. The full set can be found on the Kotlin Language Guide.

NULL Safety

NULL is a special value that indicates that there is no data present (often indicated by the null keyword in other languages). NULL values can be difficult to work with in other programming languages, because once you accept that a value can be NULL, you need to check all uses of that variable against the possibility of it being NULL.

NULL values are incredibly difficult to manage, because to address them properly means doing constant checks against NULL in return values, data, and so on. They add inherent instability to any type system.

Tony Hoare invented the idea of a NULL reference. In 2009, he apologized for this, famously calling it his “billion-dollar mistake.”

In Kotlin, every type is non-nullable by default. This means that if you attempt to assign a NULL to a normal data type, the compiler is able to check against this and report it as a compile-time error. If you need to work with NULL data, you can declare a nullable variable using the ? annotation. Once you do this, you need to use specific ? methods. You may also need to take steps to handle NULL data when appropriate.

Conventions

  • By default, a variable cannot be assigned a NULL value.
  • ? suffix on the type indicates that it’s NULL-able.
  • ?. accesses properties/methods if the object is not NULL (“safe call operator”)
  • ?: elvis operator is a ternary operator for NULL data
  • !! override operator (calls a method without checking for NULL, bad idea)
fun main() {
	// name is nullable
	var name:String? = null

	// only returns value if name is not null
	var length = name?.length
	println(length) // null

	// elvis operator provides an `else` value
	length = name?.length ?: 0
	println(length) // 0
}

click to run

Generics

Generics are extensions to the type system that allows us to parameterize classes or functions across different types. Generics expand the reusability of your class definitions because they allow your definitions to work with many types.

We’ve already seen generics when dealing with collections:

val list: List<Int> = listOf(5, 10, 15, 20)

In this example, <Int> is specifying the type that is being stored in the list. Kotlin infers types where it can, so we typically write this as:

val list = listOf(5, 10, 15, 20)

We can use a generic type parameter in the place of a specific type in many places, which allows us to write code towards a generic type instead of a specific type. This prevents us from writing methods that might only differ by parameter or return type.

A generic type is a class that accepts an input of any type in its constructor. For instance, we can create a Table class that can hold differing values.

You define the class and make it generic by specifying a generic type to use in that class, written in angle brackets < >. The convention is to use T as a placeholder for the actual type that will be used.

fun main() {
	class Table<T>(t: T) {
	    var value = t
	}

	val table1: Table<Int> = Table<Int>(5)
	val table2 = Table<Float>(3.14f)
}

click to run

A more complete example:

import java.util.*

class Timeline<T>() {
    val events : MutableMap<Date, T> = mutableMapOf()

    fun add(element: T) {
        events.put(Date(), element)
    }

    fun getLast(): T {
        return events.values.last()
    }
}

fun main() {
	val timeline = Timeline<Int>()
	timeline.add(5)
	timeline.add(10)
}

click to run

Control Flow

Kotlin supports the style of control flow that you would expect in an imperative language, but it uses more modern forms of these constructs

if then else

if... then has both a statement form (no return value) and an expression form (return value).

fun main() {
  val a=5
  val b=7

  // we don't return anything, so this is a statement
  println("a=$a, b=$b")
  if (a > b) {
      println("a is larger")
  } else {
      println("b is larger")
  }

  val number = 6

  // the value from each branch is considered a return value
  // this is an expression that returns a result
  println("number=$number")
  val result =
    if (number > 0)
      "$number is positive"
    else if (number < 0)
      "$number is negative"
    else
      "$number is zero"

  println(result)
}

// a=5, b=7
// b is larger
// number=6
// 6 is positive

click to run

This is why Kotlin does not have a ternary operator: if used as an expression serves the same purpose.

for in

A for in loop steps through any collection that provides an iterator. This is equivalent to the for each loop in languages like C#.

fun main() {
  val items = listOf("apple", "banana", "kiwifruit")
  for (item in items) {
  	println(item)
  }

  for (index in items.indices) {
  	println("item $index is ${items[index]}")
  }

  for (c in "Kotlin") {
    print("$c ")
  }
}

// apple
// banana
// kiwifruit
// item 0 is apple
// item 1 is banana
// item 2 is kiwifruit
// K o t l i n

click to run

Kotlin doesn’t support a C/Java style for loop. Instead, we use a range collection .. that generates a sequence of values.

fun main() {
  // invalid in Kotlin
  // for (int i=0; i < 10; ++i)

  // range provides the same funtionality
  for (i in 1..3) {
    print(i)
  }
  println() // space out our answers

  // descending through a range, with an optional step
  for (i in 6 downTo 0 step 2) {
    print("$i ")
  }
  println()

  // we can step through character ranges too
  for (c in 'A'..'E') {
    print("$c ")
  }
  println()

  // Check if a number is within range:
  val x = 10
  val y = 9
  if (x in 1..y+1) {
    println("fits in range")
  }
}

// 123
// 6 4 2 0
// A B C D E
// fits in range

click to run

while

while and do... while exist and use familiar syntax.

fun main() {
  var i = 1
  while ( i <= 10) {
    print("$i ")
    i++
  }
}

// 1 2 3 4 5 6 7 8 9 10

click to run

when

when replaces the switch operator of C-like languages:

fun main() {
  val x = 2
  when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> print("x is neither 1 nor 2")
  }
}

// x == 2

click to run

fun main() {
    val x = 13
    val validNumbers = listOf(11,13,17,19)

    when (x) {
    	0, 1 -> print("x == 0 or x == 1")
    	in 2..10 -> print("x is in the range")
    	in validNumbers -> print("x is valid")
    	!in 10..20 -> print("x is outside the range")
    	else -> print("none of the above")
  }
}

// x is valid

click to run

We can also return a value from when. Here’s a modified version of this example:

fun main() {
    val x = 13
    val validNumbers = listOf(11,13,17,19)

    val response = when (x) {
        0, 1 -> "x == 0 or x == 1"
        in 2..10 -> "x is in the range"
        in validNumbers -> "x is valid"
        !in 10..20 -> "x is outside the range"
        else -> "none of the above"
    }
    println(response)
}

// x is valid

click to run

When is flexible. To evaluate any expression, you can move the comparison expressions into when statement itself:

fun main() {
    val x = 13

    val response = when {
        x < 0 -> "negative"
        x >= 0 && x <= 9 -> "small"
        x >=10 -> "large"
        else -> "how do we get here?"
    }
    println(response)
}

// large

click to run

return

Kotlin has three structural jump expressions:

  • return by default returns from the nearest enclosing function or anonymous function
  • break terminates the nearest enclosing loop
  • continue proceeds to the next step of the nearest enclosing loop

Functions

Functions are preceded with the fun keyword. Function parameters require types, and are immutable. Return types should be supplied after the function name, but in some cases may also be inferred by the compiler.

Named Functions

Named functions has a name assigned to them that can be used to invoke them directly (this is the expected form of a “function” in most cases, and the form that you’re probably expecting).

// no parameters required
fun main() {
    println(sum1(1, 2))
    println(sum1(3,4))
}

// parameters which require type annotations
fun sum1(a: Int, b: Int): Int {
    return a + b
}

// return types can be inferred based on the value you return
// it's better form to explicitly include the return type in the signature
fun sum2(a: Int, b: Int) {
    a + b // Kotlin knows that (Int + Int) -> Int
}

// 3
// 7

click to run

Single-Expression Functions

Simple functions in Kotlin can sometimes be reduced to a single line aka a single-expression function.

// previous example
fun sumOf(a: Int, b: Int):Int {
    return a + b
}

// this works since we evaluate a single expression
fun minOf(a: Int, b: Int) = if (a < b) a else b

fun main() {
    println(sumOf(5,10))
    println(minOf(10,20))
}

// 15
// 10

click to run

Default arguments

We can use default arguments for function parameters. When called, a parameter with a default value is optional; if the caller does not provide the value, then the default will be used.

// Second parameter has a default value, so it’s optional
fun mult(a:Int, b:Int = 5): Int {
	return a * b
}

fun main() {
	println(mult(1)) // a=1, b=5 default
	println(mult(5,2)) // a=5, b=2
	// mult() would throw an error, since `a` must be provided
}

click to run

Named parameters

You can (optionally) provide the parameter names when you call a function. If you do this, you can even change the calling order!

fun repeat(str:String="*", count:Int=1):String {
    return str.repeat(count)
}

fun main() {
	println(repeat()) // *
	println(repeat(str="#")) // *
	println(repeat(count=3)) // ***
	println(repeat(str="#", count=5)) // #####
	println(repeat(count=5, str="#")) // #####
}

// *
// #
// ***
// #####
// #####

click to run

Variable-length arguments

Finally, we can have a variable length list of arguments:

fun sum(vararg numbers: Int): Int {
    var sum = 0
    for(number in numbers) {
        sum += number
    }
  return sum
}

fun main() {
    println(sum(1)) // 1
    println(sum(1,2,3)) // 6
    println(sum(1,2,3,4,5,6,7,8,9,10)) // 55
}

// 1
// 6
// 55

click to run

Collections

A collection is a finite group of some variable numbers of items (possibly zero) of the same type. Objects in a collection are called elements.

Collections in Kotlin are contained in the kotlin.collections package, which is part of the Kotlin Standard Library.

These collection classes exist as generic containers for a group of elements of the same type e.g. List<Int> would be an ordered list of integers. Collections have a finite size, and are eagerly evaluated.

Kotlin offers functional processing operations (e.g. filter, map and so on) on each of these collections.

fun main() {
  val list = (1..10).toList() // generate list of 1..10
  println( list.take(5).map{it * it} ) // square the first 5 elements
}

// [1, 4, 9, 16, 25]

click to run

Under-the-hood, Kotlin uses Java collection classes, but provides mutable and immutable interfaces to these classes. Kotlin best-practice is to use immutable for read-only collections whenever possible (since mutating collections is often very costly in performance).

{.compact}

Collection ClassDescription
PairA tuple of two values.
TripleA tuple of three values.
ListAn ordered collection of objects.
SetAn unordered collection of objects.
MapAn associative dictionary of keys and values.
ArrayAn indexed, fixed-size collection of objects.

A tuple is a data structure representing a sequence of n elements.

Pair

A Pair is a tuple of two values. Use var or val to indicate mutability. Theto keyword can be used to indicate a Pair.

fun main() {
  // mutable
  var nova_scotia = "Halifax Airport" to "YHZ"
  var newfoundland = Pair("Gander Airport", "YQX")
  var ontario = Pair("Toronto Pearson", "YYZ")
  ontario = Pair("Billy Bishop", "YTZ") // reassignment is ok

  // accessing elements
  val canadian_exchange = Pair("CDN", 1.38)
  println(canadian_exchange.first) // CDN
  println(canadian_exchange.second) // 1.38

  // destructuring
  val (first, second) = Pair("Calvin", "Hobbes") // split a Pair
  println(first) // Calvin
  println(second) // Hobbes
}

// CDN
// 1.38
// Calvin
// Hobbes

click to run

Pairs are extremely useful when working with data that is logically grouped into tuples, but where you don’t need the overhead of a custom class., e.g. Pair for 2D points.

List

A List is an ordered collection of objects.

fun main() {
  // define an immutable list
  var fruits = listOf( "advocado", "banana")
  println(fruits.get(0))
  // advocado

  // add elements
  var mfruits = mutableListOf( "advocado", "banana")
  mfruits.add("cantaloupe")
  mfruits.forEach { println(it) }

  // sorted/sortedBy returns ordered collection
  val list = listOf(2,3,1,4).sorted() // [1, 2, 3, 4]
  list.sortedBy { it % 2 } // [2, 4, 1, 3]

  // groupBy groups elements on collection by key
  list.groupBy { it % 2 } // Map: {1=[1, 3], 0=[2, 4]}

  // distinct/distinctBy returns unique elements
  listOf(1,1,2,2).distinct() // [1, 2]
}

// advocado
// advocado
// banana
// cantaloupe

click to run

Set

A Set is a generic unordered collection of unique elements (i.e. it does not support duplicates, unlike a List which does). Sets are commonly constructed with helper functions:

fun main() {
	val numbersSet = setOf("one", "two", "three", "four")
    println(numbersSet)

	val emptySet = mutableSetOf<String>()
    println(emptySet)
}

// [one, two, three, four]
// []

click to run

Map

A Map is an associative dictionary containing Pairs of keys and values.

fun main() {
  // immutable reference, immutable map
  val imap = mapOf(Pair(1, "a"), Pair(2, "b"), Pair(3, "c"))
  println(imap)
  // {1=a, 2=b, 3=c}

  // immutable reference, mutable map (so contents can change)
  val mmap = mutableMapOf(5 to "d", 6 to "e")
  mmap.put(7,"f")
  println(mmap)
  // {5=d, 6=e, 7=f}

  // lookup a value
  println(mmap.get(5))
  // d

  // iterate over key and value
  for ((k, v) in imap) {
    print("$k=$v ")
  }
  // 1=a 2=b 3=c

  // alternate syntax
  imap.forEach { k, v -> print("$k=$v ") }
  // 1=a 2=b 3=c

  // `it` represents an implicit iterator
  imap.forEach {
    print("${it.key}=${it.value} ")
  }
  // 1=a 2=b 3=c
}

// {1=a, 2=b, 3=c}
// {5=d, 6=e, 7=f}
// d
// 1=a 2=b 3=c 1=a 2=b 3=c 1=a 2=b 3=c

click to run

Functions

Collection classes (e.g. List, Set, Map, Array) have built-in functions for working with the data that they contain. These functions frequently accept other functions as parameters.

Filter

filter produces a new list of those elements that return true from a predicate function.

fun main() {
	val list = (1..100).toList()
	val filtered = list.filter { it % 5 == 0 }
    println(filtered)
	// 5 10 15 20 ... 100

	val below50 = filtered.filter { it in 0..49 }
    println(below50)
	// [5, 10, 15, 20]
}

// [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]
// [5, 10, 15, 20, 25, 30, 35, 40, 45]

click to run

Map

map produces a new list that is the results of applying a function to every element that it contains.

fun main() {
	val list = (1..100).toList()
	val doubled = list.map { it * 2 }
    println(doubled)
}

// 2 4 6 8 ... 200

click to run

Reduce

reduce accumulates values starting with the first element and applying an operation to each element from left to right.

fun main() {
	val strings = listOf("a", "b", "c", "d")
	println(strings.reduce { acc, string -> acc + string }) // abcd
}

// abcd

click to run

Zip

zip combines two collections, associating their respective pairwise elements.

fun main() {
	val foods = listOf("apple", "kiwi", "broccoli", "carrots")
	val fruit = listOf(true, true, false, false)
    println(fruit)

	val results = foods.zip(fruit)
	println(results)
}

// [true, true, false, false]
// [(apple, true), (kiwi, true), (broccoli, false), (carrots, false)]

click to run

A more realistic scenario might be where you want to generate a pair based on the results of the list elements:

fun main() {
	val list = listOf("123", "", "456", "def")
	val exists = list.zip(list.map { !it.isBlank() })
    println(exists)

	val numeric = list.zip(list.map { !it.isEmpty() && it[0] in ('0'..'9') })
    println(numeric)
}

// [(123, true), (, false), (456, true), (def, true)]
// [(123, true), (, false), (456, true), (def, false)]

click to run

ForEach

forEach calls a function for every element in the collection.

fun main() {
	val fruits = listOf("advocado", "banana", "cantaloupe" )
	fruits.forEach { print("$it ") }
}

// advocado banana cantaloupe

click to run

We also have helper functions to extract specific elements from a list.

Take

take returns a collection containing just the first n elements. drop returns a new collection with the first n elements removed.

fun main() {
	val list = (1..50)
	val first10 = list.take(10)
	println(first10)

	val last40 = list.drop(10)
	println(last40)
}

// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// [11, 12, 13, 14, 15, 16, 17, 18, 19, ...]

click to run

First, Last, Slice

first and last return those respective elements. slice allows us to extract a range of elements into a new collection.

fun main() {
	val list = (1..50)
	val even = list.filter { it % 2 == 0 }

	println(even.first()) // 2
	println(even.last()) // 50
	println(even.slice(1..3)) // 4 6 8
}

// 2
// 50
// [4, 6, 8]

click to run