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.
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.
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.
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.
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:
- 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) { }
- 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 compilerMETA-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
Type | Size (bits) | Min value | Max value |
---|---|---|---|
Byte | 8 | -128 | 127 |
Short | 16 | -32768 | 32767 |
Int | 32 | -2,147,483,648 (-2 31) | 2,147,483,647 (2 31- 1) |
Long | 64 | -9,223,372,036,854,775,808 (-2 63) | 9,223,372,036,854,775,807 (2 63- 1) |
Floating Point Types
Type | Size (bits) | Significant bits | Exponent bits | Decimal digits |
---|---|---|---|---|
Float | 32 | 24 | 8 | 6-7 |
Double | 64 | 53 | 11 | 15-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)
}
}
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")
}
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"}")
}
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")
}
}
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
}
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
Operators
Kotlin supports a wide range of operators. The full set can be found on the Kotlin Language Guide.
+
,-
,*
,/
,%
- mathematical operators=
assignment operator&&
,||
,!
- logical ‘and,’ ‘or,’ ‘not’ operators==
,!=
— structural equality operators compare members of two objects for equality===
,!==
- referential equality operators are true when both sides point to the same object.[
,]
— indexed access operator (translated to calls ofget
andset
)
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
}
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)
}
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)
}
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
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
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
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
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
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
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
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
return
Kotlin has three structural jump expressions:
return
by default returns from the nearest enclosing function or anonymous functionbreak
terminates the nearest enclosing loopcontinue
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
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
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
}
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="#")) // #####
}
// *
// #
// ***
// #####
// #####
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
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]
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 Class | Description |
---|---|
Pair | A tuple of two values. |
Triple | A tuple of three values. |
List | An ordered collection of objects. |
Set | An unordered collection of objects. |
Map | An associative dictionary of keys and values. |
Array | An 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
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
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]
// []
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
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]
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
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
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)]
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)]
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
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, ...]
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]