CS349 User Interfaces
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Fundamentals

Types

Programming languages can take different approaches to enforcing how types are managed.

  • Strong typing: The language has strict typing rules, which typically enforced at compile-time. The exact type of a 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 before they are used. Kotlin also supports type infererence. If a type isn’t 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.

Integer Types

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, and deserve a callout. 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 operators

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 - this is a standard mutable variable that can be changed or reassigned.
  • val - this is 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.

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 on1. They add inherent instability to any type system.

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 [ed. a nullable version of a type is actually a completely different type]. 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)
// name is nullable, length is not
var name:String? = null 
var length:Int  = 0

// only assign length if not null
length = name?.length ?: name?.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 a 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.

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

val table1: Table<Int> = Table<Int>(5)
val table2 = Table<Float>(3.14)
Table(10) // type inference is supported, so this is a Table<Int>

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() {
    // explicit
    val timeline = Timeline<Int>()
    timeline.add(5)
    timeline.add(10)
  
    // we can also omit the Type if it can be inferred
    val timeline2 = Timeline(65) // Timeline<Int>
}

Functions

Functions can also work with generic types2.

fun <T> singletonList(item: T): List<T> {
    // return a list of items of type T
}

fun <T> T.basicToString(): String { // extension function
    // apply to any type that is provided.
}

To call a function:

val l = singletonList<Int>(1)
TypeErasure
The type safety checks that Kotlin performs for generic declaration usages are done at compile time. At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of Foo<Bar> and Foo<Baz?> are erased to just Foo<*>. – https://kotlinlang.org/docs/generics.html#type-erasure

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
  if (a > b) { 
    println("a=${a}") 
  } else { 
    println("b=${b}") 
  }

  val number = -6

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

  println(result)
}
Packages
This is why Kotlin doesn’t 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 ")
  }
}

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")
  }
}

while

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

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

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") 
  }
}
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")
  }
}

when as an expression also returns a value.

fun describe(obj: Any): String =
  when (obj) { 
    1 -> "One"
    "Hello" -> "Greeting"
    isLong -> "Long"!is String -> "Not a string"
    else -> "Unknown"
  }

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

Exceptions

Exception handling is the notion that a function that fails can choose to throw an exception: a rich structure containing information that can be used further up the call stack by the calling function (or a function above that) to handle the error. Exceptions assume that someone else aside from the failing function should handle the error.

There are number of predefined exceptions, all of which are descended from java.lang.Exception. e.g. IOException, DataFormatException, BadStringOperationException and many more.

Functions that you write can choose to generate an exception using the throw keyword [ed. the more common way is to call some function in a Java or Kotlin library to open a file or something similar, which can throw exceptions on error as well].

throw Exception("Hi There!")

The calling function is expected to watch for, and catch an exception, use try... catch:

try {
	// calling this function will generate an exception
  val ret = readExternalInput()
} catch (e: IOException) {
	// this block will get called to handle this exception
  e.printStackTrace()
} 

fun readExternalInput() {
  throw IOException("This is an error!")
}

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 function have 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(sum2(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
}

Single-Expression Functions

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

// previous example
fun sum(a: Int, b: Int) {
  a + b // Kotlin knows that (Int + Int) -> Int
}

// this is equivilant
fun sum(a: Int, b: Int) = a + b

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

Function Parameters

Default arguments

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

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

mult(1) // 1 
mult(5,2) // 10 
mult() // error 

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(s:String="*", n:Int=1):String {
    return s.repeat(n)
}

println(repeat()) // *
println(repeat(n=3)) // ***
println(repeat(s="#", n=5)) // #####

Variable-length arguments

Finally, we can have a list of variable length, whose length is evaluated at runtime.

// Variable number of arguments can be passed!
// Arguments in the list need to have the same type 

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

sum(1) // 1
sum(1,2,3) // 6 
sum(1,2,3,4,5,6,7,8,9,10) // 55

Collections

A collection is a finite group of some variable number 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 exists as generic containers for a group of elements of the same type e.g. List 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()
  println( list.take(5).map{it* it} )
}

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).

Collection Class Description
Pair A tuple3 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.

Examples

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

  // immutable, mixed types
  val canadian_exchange = Pair("CDN", 1.38) 

  // accessing elements
  val characters = Pair("Tom", "Jerry") 
  println(characters.first) 
  println(characters.second) 

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

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.

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")

  // 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] 
}

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:

val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()

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
}

Arrays are indexed, fixed-sized collection of objects and primitives. We prefer other collections, but these are offered for legacy and compatibility with Java.

// Create using the `arrayOf()` library function
arrayOf(1, 2, 3)

// Create using the Array class constructor 
// Array<String> ["0", "1", "4", "9", "16"] 
val asc = Array(5) { 
	i -> (i*i).toString()
} 
asc.forEach { println(it) } 

You can access array elements through using the [] operators, or the get() and set() methods.

Sequences

A sequence behaves similarly to a collection but is more related to a stream than a collection. It does not implement iterable and you cannot iterate over it. Sequences are also infinite, and lazily evaluated.

val sequence = generateSequence(0) { it + 1 } // infinite!
sequence
	.filter { it.isOdd() }	// 1 3 5 7 9 ...
	.map { it * it } // 1 9 25 49 81 ...
	.take(3) // 1 9 25 .... this is actually where evalutions are done

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

  2. Examples taken from Kotlin documentation: https://kotlinlang.org/docs/generics.html#generic-functions ↩︎

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