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.
funmain(){vara:Int=10varb:String="Jeff"varc:Boolean=falsevard="abc"// inferred as a String
vare=5// inferred as Int
varf=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:
funmain(){valstr="Sam"for(cinstr){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):
funmain(){vals="abc"+1println(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.
funmain(){println("> Kotlin ${KotlinVersion.CURRENT}")valstr="abc"println("$str.length is ${str.length}")varn=5println("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:
funmain(){valobj="abc"if(objisString){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:
funmain(){valx="abc"if(x!isString)returnprintln("x=${x.length}")// x is automatically cast to String
valy="defghi"// y is automatically cast to string on the right-hand side of `||`
if(y!isString||y.length==0)returnprintln("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.
vara=0// type inferred as Int
a=5// a is mutable, so reassignment is ok
valb=1// type inferred as Int as well
b=2// error because b is immutable
varc: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 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
varname:String?=nullvarlength: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:
vallist: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:
vallist=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.
classTable<T>(t:T){varvalue=t}valtable1:Table<Int>=Table<Int>(5)valtable2=Table<Float>(3.14)Table(10)// type inference is supported, so this is a Table<Int>
A more complete example:
importjava.util.*classTimeline<T>(){valevents:MutableMap<Date,T>=mutableMapOf()funadd(element:T){events.put(Date(),element)}fungetLast():T{returnevents.values.last()}}funmain(){// explicit
valtimeline=Timeline<Int>()timeline.add(5)timeline.add(10)// we can also omit the Type if it can be inferred
valtimeline2=Timeline(65)// Timeline<Int>
}
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:
vall=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).
funmain(){vala=5valb=7// we don't return anything, so this is a statement
if(a>b){println("a=${a}")}else{println("b=${b}")}valnumber=-6// the value from each branch is considered a return value
// this is an expression that returns a result
valresult=if(number>0)"$number is positive"elseif(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#.
funmain(){valitems=listOf("apple","banana","kiwifruit")for(iteminitems){println(item)}for(indexinitems.indices){println("item $index is ${items[index]}")}for(cin"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.
funmain(){// invalid in Kotlin
// for (int i=0; i < 10; ++i)
// range provides the same funtionality
for(iin1..3){print(i)}println()// space out our answers
// descending through a range, with an optional step
for(iin6downTo0step2){print("$i ")}println()// we can step through character ranges too
for(cin'A'..'E'){print("$c ")}println()// Check if a number is within range:
valx=10valy=9if(xin1..y+1){println("fits in range")}}
while
while and do... while exist and use familiar syntax.
funmain(){vari=1while(i<=10){print("$i ")i++}}
when
when replaces the switch operator of C-like languages:
funmain(){valx=2when(x){1->print("x == 1")2->print("x == 2")else->print("x is neither 1 nor 2")}}
funmain(){valx=13valvalidNumbers=listOf(11,13,17,19)when(x){0,1->print("x == 0 or x == 1")in2..10->print("x is in the range")invalidNumbers->print("x is valid")!in10..20->print("x is outside the range")else->print("none of the above")}}
when as an expression also returns a value.
fundescribe(obj:Any):String=when(obj){1->"One""Hello"->"Greeting"isLong->"Long"!isString->"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].
throwException("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
valret=readExternalInput()}catch(e:IOException){// this block will get called to handle this exception
e.printStackTrace()}funreadExternalInput(){throwIOException("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
funmain(){println(sum1(1,2))println(sum2(3,4)))}// parameters which require type annotations
funsum1(a:Int,b:Int):Int{returna+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
funsum2(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
funsum(a:Int,b:Int){a+b// Kotlin knows that (Int + Int) -> Int
}// this is equivilant
funsum(a:Int,b:Int)=a+b// this works since we evaluate a single expression
funminOf(a:Int,b:Int)=if(a<b)aelseb
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
funmult(a:Int,b:Int=1):Int{returna*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!
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
funsum(varargnumbers:Int):Int{varsum:Int=0for(numberinnumbers){sum+=number}returnsum}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.
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.
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).
A Pair is a tuple of two values. Use var or val to indicate mutability. Theto keyword can be used to indicate a Pair.
funmain(){// mutable
varnova_scotia="Halifax Airport"to"YHZ"varnewfoundland=Pair("Gander Airport","YQX")varontario=Pair("Toronto Pearson","YYZ")ontario=Pair("Billy Bishop","YTZ")// reassignment is ok
// immutable, mixed types
valcanadian_exchange=Pair("CDN",1.38)// accessing elements
valcharacters=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.
funmain(){// define an immutable list
varfruits=listOf("advocado","banana")println(fruits.get(0))// advocado
// add elements
varmfruits=mutableListOf("advocado","banana")mfruits.add("cantaloupe")// sorted/sortedBy returns ordered collection
vallist=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:
A Map is an associative dictionary containing Pairs of keys and values.
funmain(){// immutable reference, immutable map
valimap=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)
valmmap=mutableMapOf(5to"d",6to"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)inimap){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"]
valasc=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.
valsequence=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
Tony Hoare invented the idea of a NULL reference. In 2009, he apologized for this, famously calling it his “billion-dollar mistake”. ↩︎