CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Types & Mutability

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

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

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