Skip to content

3. The Basics of the Kotlin Language

3.1. Introduction

We’ll start by treating Kotlin as a traditional programming language. We’ll cover classes in the next chapter. A program consists of two things:

  • data;
  • the instructions that manipulate them.

We generally strive to separate data from instructions:

Image

All the examples in this chapter are part of the Gradle project in the [03-basics] folder of the course code. As noted in Chapter 2, “Setting Up the Development Environment,” each program is contained in a file located within a package named after the program in lowercase: the program [Basics04.kt] is located in the folder [src/main/kotlin/basics04] and begins with the line [package basics04]. You can run it by clicking the green triangle next to IntelliJ IDEA, located in the margin next to the [main] function, or from the command line, from the project folder, using the command [gradlew -q run -Pmain=basics04.Basics04Kt] ([./gradlew] on Linux and macOS). The [-q] (“quiet”) option suppresses Gradle messages: only the program’s output remains. We will explain the name [basics04.Basics04Kt] in the next section. The examples run using French language and formatting conventions (the “locale” fr-FR), just as they would on a French Windows machine. We’ll see that this only affects numbers and dates formatted with a specific format: simply converting a floating-point number to a string always uses a decimal point.

3.2. The Minimal Program

3.2.1. The high-level main function

The program [Basics01.kt] is as follows:

// minimal program: a top-level main function, written outside any class
package basics01

fun main() {
    println("Hello world!")
    // the Java form remains usable: the out field of the System class, then its println method
    System.out.println("Hello world!")
    // listOf (package kotlin.collections) can be used without an import
    val words = listOf("Hello", "the", "world")
    println(words)
}

Results:

1
2
3
4
> gradlew -q run -Pmain=basics01.Basics01Kt
Hello world!
Hello world!
[Hello, the, world]

Comments:

  • Line 2: the program's package. A package groups related declarations, just as a folder groups files. The full name of everything declared in this file begins with [basics01.]. In Kotlin, unlike in Java, the file’s directory is not dictated by the package name; however, it is common practice—which we follow—to make them match;
  • Line 4: The function [main] is the program’s entry point. The keyword [fun] declares a function; [main()] has no parameters and returns no value. The function is written directly in the file, outside of any class: it is a top-level function. The body of the function is enclosed in curly braces;
  • line 5: the function [println] prints a line to the screen. It is also a top-level function, declared in the [kotlin.io] package of the Kotlin standard library;
  • line 7: the Java version of the same operation. [System] is a class of JDK, [out] is one of its fields, representing standard output (the screen), and [println] is a method of this object. Kotlin directly uses all Java classes: this is one of its major advantages. The Kotlin function [println] is, in fact, just a shorthand for [System.out.println]; this is the one we will use;
  • Line 9: [words] is a list of strings; lists will be covered in Chapter 5. The keyword [val] declares a variable whose value will not change; we did not specify its type: the compiler infers it from the value, in this case [List<String>]. The function [listOf] belongs to the package [kotlin.collections], which can be used without an import statement (see below);
  • Line 10: A list is displayed with its elements enclosed in square brackets;
  • statements do not end with a semicolon: a line break is sufficient. The semicolon is only used to write multiple statements on a single line. Lines beginning with // are comments, which are ignored by the compiler.

With two command-line arguments, which are passed to Gradle via the [--args] option:

1
2
3
4
> gradlew -q run -Pmain=basics01.Basics01Kt --args="a0 a1"
Hello world!
Hello world!
[Hello, the, world]

The arguments are ignored because the [main()] function has no parameters to receive them. To use them, declare the function as [fun main(args: Array<String>)]: the [args] string array will then contain the command-line arguments. We’ll come back to this in the “Main Program Arguments” section.

3.2.2. The main function and its arguments

The program [Basics02.kt] is as follows:

1
2
3
4
5
6
7
8
9
// the main function with a parameter: the command-line arguments
package basics02

fun main(args: Array<String>) {
    println("Hello world!")
    println("Number of arguments received: ${args.size}")
    // contentToString builds a string with the elements of an array
    println("Arguments: ${args.contentToString()}")
}

Results:

1
2
3
4
> gradlew -q run -Pmain=basics02.Basics02Kt --args="a0 a1"
Hello world!
Number of arguments received: 2
Arguments: [a0, a1]

Comments:

  • Line 4: The [main] function receives command-line arguments in the [args] parameter. In Kotlin, a parameter’s type is written after its name, separated by a colon: [args: Array<String>] reads as “[args], of type array of strings.” This is equivalent to [String[] args] in Java;
  • line 6: the number of elements in the array is [args.size] (the Java equivalent of [args.length]). In the string, [${args.size\}] is replaced by the value of the expression enclosed in curly braces: this is a string template, which we will discuss in more detail later;
  • Line 7: The function [contentToString] constructs a string using the elements of an array. It replaces the Java method [Arrays.toString] and, like the previous function, does not require any imports.

We haven't written any classes. However, the Java virtual machine (JVM), which runs the program, only recognizes classes. The Kotlin compiler therefore organizes the high-level declarations in a file into a class that it generates itself, named after the file followed by [Kt]: for the file [Basics02.kt] in the package [basics02], this is the class [basics02.Basics02Kt]. This is why the execution command refers to the program as [-Pmain=basics02.Basics02Kt]. You can verify this by compiling the file with the command-line compiler [kotlinc] (Chapter 2), then examining the generated class with the [javap] tool from the JDK package:

> kotlinc Basics02.kt -d classes
> ls classes/basics02
Basics02Kt.class
> kotlinr -cp classes basics02.Basics02Kt a0 a1
Hello world!
Number of arguments received: 2
Arguments: [a0, a1]
> javap classes/basics02/Basics02Kt.class
Compiled from "Basics02.kt"
public final class basics02.Basics02Kt {
  public static final void main(java.lang.String[]);
}
> java -cp classes basics02.Basics02Kt a0 a1
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
    at basics02.Basics02Kt.main(Basics02.kt)
  • [kotlinc] generated a single intermediate code file (“bytecode”), [Basics02Kt.class], in the package directory;
  • the command [kotlinr] (the Kotlin launcher, Chapter 2) executes this class by adding the Kotlin standard library ([kotlin-stdlib.jar]) to the classpath;
  • [javap] shows that the generated class is [public final] and that the function [main] has become a method [public static] within it: exactly the method [public static void main(String[] args)] that JVM expects. For JVM, a Kotlin program is just another Java class;
  • when launched with the simple command [java], the class fails: the generated code calls on classes from the Kotlin standard library (in this case, [kotlin.jvm.internal.Intrinsics], which, among other things, checks that the parameters are not equal to [null]), which is missing from the classpath. Gradle and IntelliJ IDEA add it automatically; a standalone Kotlin program must include it (option [-include-runtime] of [kotlinc], Chapter 2, or Gradle scripts, see below).

For the program [Basics01.kt], whose function [main] has no parameters, the compiler also generates a method [main(String[])] that simply calls the first one. Gradle places the classes it compiles in the project’s [build/classes/kotlin/main] folder; you can examine them there after a compilation ([gradlew build]):

1
2
3
4
5
6
> javap -cp build/classes/kotlin/main basics01.Basics01Kt
Compiled from "Basics01.kt"
public final class basics01.Basics01Kt {
  public static final void main();
  public static void main(java.lang.String[]);
}

A comparison with Java 25 is instructive. Java 25 also allows you to write a program without declaring a class (a “compact source file,” with a method named [void main()]): the Java compiler then wraps the file’s contents in an implicit class bearing the file’s name. Kotlin has done the same thing since its first version, with two differences:

  • the generated class is named [Basics02Kt] rather than [Basics02], which leaves the name [Basics02] available for a class the programmer might want to write themselves in the same file. The annotation ***[@file:JvmName("ClassName")]***, placed at the top of the file, allows you to choose a different name;
  • high-level functions are compiled into [static] methods: no object is created to call [main], whereas the [void main()] method in a compact Java file is an instance method. Most importantly, in Kotlin, this syntax is not limited to small programs: top-level functions, variables, and constants are commonly used in all programs, alongside classes.

The function [main] can only take the two forms encountered: [fun main()] and [fun main(args: Array<String>)]. It does not need to be declared public: in Kotlin, everything is public by default.

3.2.3. Default imports

The program [Basics01.kt] uses the functions [println] and [listOf], and the program [Basics02.kt] uses the function [contentToString], without any [import] directive. This is possible because every Kotlin file automatically imports the following packages from the standard library:

import kotlin.*
import kotlin.annotation.*
import kotlin.collections.*
import kotlin.comparisons.*
import kotlin.io.*
import kotlin.ranges.*
import kotlin.sequences.*
import kotlin.text.*
// and, for the JVM:
import java.lang.*
import kotlin.jvm.*

This provides the basic types ([Int], [String]... from the [kotlin] package), lists, sets, and dictionaries ([kotlin.collections]), console input and output ([kotlin.io]), intervals ([kotlin.ranges]), string functions ([kotlin.text]), as well as classes from the Java package [java.lang] ([System], [Math], [Thread]...). Any other declarations must be imported at the top of the file, immediately after the line [package]: one directive per class or function ([import java.math.BigDecimal]) or one directive per package ([import java.time.*]). There is no equivalent to the [import module java.base;] directive from Java 25, which a compact Java file automatically receives: A Kotlin program that uses the classes [BigDecimal] or [LocalDate] from JDK must import them. A note on syntax: [import] directives, like other statements, do not end with a semicolon.

3.3. Kotlin Data Types

Kotlin uses the following data types:

  • integers, both signed and unsigned;
  • real numbers;
  • decimal numbers;
  • characters and strings;
  • Booleans;
  • objects.

3.3.1. Predefined data types

Kotlin type
Corresponding Java type
Data represented
Literal suffix
Encoding
Value range
[Char]
[char] / [Character]
character
 
2 bytes
Unicode character (UTF-16), from '\u0000' to '\uFFFF'
[Byte]
[byte] / [Byte]
integer
 
1 byte
[-2⁷, 2⁷-1] or [-128, 127]
[Short]
[short] / [Short]
integer
 
2 bytes
[-2¹⁵, 2¹⁵-1] or [-32768, 32767]
[Int]
[int] / [Integer]
integer
 
4 bytes
[-2³¹, 2³¹-1] or [-2147483648, 2147483647]
[Long]
[long] / [Long]
integer
L
8 bytes
[-2⁶³, 2⁶³-1] or [-9223372036854775808, 9223372036854775807]
[UByte], [UShort], [UInt], [ULong]
(no equivalent)
unsigned integer
U, UL
1, 2, 4, 8 bytes
[0, 2⁸-1], [0, 2¹⁶-1], [0, 2³²-1], [0, 2⁶⁴-1]
[Float]
[float] / [Float]
actual number
F
4 bytes
[1.4 10⁻⁴⁵, 3.4 10³⁸] in absolute value, 7 significant digits
[Double]
[double] / [Double]
actual number
 
8 bytes
[4.9 10⁻³²⁴, 1.8 10³⁰⁸] in absolute value, 15 to 16 significant digits
[Boolean]
[boolean] / [Boolean]
Boolean
 
not defined
[true], [false]
[String]
[String]
character string
  
reference to a Unicode character sequence
[BigDecimal] (Java class)
[BigDecimal]
decimal number
 
variable
exact decimal number, of any precision
[Any]
[Object]
object reference
  
reference to an object of any type

Java distinguishes between the eight primitive types ([int], [double]...), whose values are not objects, and reference types (classes), and it associates each primitive type with a wrapper class ([Integer] for [int]...). Kotlin removes this distinction: in Kotlin, every value is an object. The type [Int] is a class that has methods; you can write [3.plus(4)] or [3.toString()], which Java does not allow for the literal 3 of type [int]. Type names all begin with a capital letter, just like class names.

This has no impact on runtime performance. The compiler translates a variable of type [Int] into a variable of the primitive type [int] or JVM whenever possible, and uses the wrapper class *[java.lang.Integer] only when an object is required: when the value is passed to a parameter of type [Any] (the Kotlin equivalent of Java’s [Object] class), stored in a list, or when it can be of type [null] (see below). Boxing and unboxing are therefore still present, but this is handled by the compiler, and the Kotlin programmer does not need to worry about it. There is no [Integer] type in Kotlin: there is only [Int]*.

The sizes in bytes are given by the constants [Int.SIZE_BYTES], [Long.SIZE_BYTES]... (and [Int.SIZE_BITS]... in bits), the range bounds by [Int.MIN_VALUE] and [Int.MAX_VALUE], etc.

Unlike Java, Kotlin has unsigned integers: [UByte], [UShort], [UInt], and [ULong], which are the same size as their signed counterparts but represent only positive or zero values. Their literals are suffixed with U ([42U]) or UL ([42UL]). They are primarily used to manipulate binary data (file bytes, addresses, colors); common calculations are performed using signed types. Kotlin also lacks a decimal type: amounts are calculated using the [BigDecimal] class from the Java package [java.math], which we’ll discuss a bit later.

The program [Basics03.kt] illustrates these various points:

// the basic types: Kotlin name, Java class and size in bytes
package basics03

import java.math.BigDecimal

fun main() {
    val integer: Int = 2
    val fl: Float = 10.5F
    val d: Double = -4.6
    val s: String = "test"
    val l: Long = 1000
    val bt: Byte = 5
    val sh: Short = -4
    val c: Char = 'A'
    val b: Boolean = true
    val dec: BigDecimal = BigDecimal("10.67")
    // in a string, $name and ${expression} are replaced by their value
    println("Type of integer[$integer] : [${typeName(integer)},${Int.SIZE_BYTES}]")
    println("Type of fl[$fl] : [${typeName(fl)},${Float.SIZE_BYTES}]")
    println("Type of d[$d] : [${typeName(d)},${Double.SIZE_BYTES}]")
    println("Type of s[$s] : [${typeName(s)}]")
    println("Type of l[$l] : [${typeName(l)},${Long.SIZE_BYTES}]")
    println("Type of bt[$bt] : [${typeName(bt)},${Byte.SIZE_BYTES}]")
    println("Type of sh[$sh] : [${typeName(sh)},${Short.SIZE_BYTES}]")
    println("Type of c[$c] : [${typeName(c)},${Char.SIZE_BYTES}]")
    println("Type of b[$b] : [${typeName(b)}]")
    println("Type of dec[$dec] : [${typeName(dec)}]")
    // the literal 3 is an object: it has methods
    println("The literal 3 : ${3::class.simpleName}, 3.plus(4)=${3.plus(4)}, 3.toString()=${3.toString()}")
    // extreme values
    println("Int : [${Int.MIN_VALUE}, ${Int.MAX_VALUE}]")
    println("Long : [${Long.MIN_VALUE}, ${Long.MAX_VALUE}]")
    println("Double : [${Double.MIN_VALUE}, ${Double.MAX_VALUE}]")
    // unsigned integers: UByte, UShort, UInt, ULong
    val u: UInt = 4_000_000_000U
    println("u=$u, UInt : [${UInt.MIN_VALUE}, ${UInt.MAX_VALUE}], ULong.MAX_VALUE=${ULong.MAX_VALUE}")
    val minusOne = -1
    println("-1 read as an unsigned integer: ${minusOne.toUInt()}")
}

// returns the Kotlin name and the Java class name of the object received
fun typeName(o: Any): String {
    return "${o::class.qualifiedName},${o.javaClass.name}"
}

Results:

> gradlew -q run -Pmain=basics03.Basics03Kt
Type of integer[2] : [kotlin.Int,java.lang.Integer,4]
Type of fl[10.5] : [kotlin.Float,java.lang.Float,4]
Type of d[-4.6] : [kotlin.Double,java.lang.Double,8]
Type of s[test] : [kotlin.String,java.lang.String]
Type of l[1000] : [kotlin.Long,java.lang.Long,8]
Type of bt[5] : [kotlin.Byte,java.lang.Byte,1]
Type of sh[-4] : [kotlin.Short,java.lang.Short,2]
Type of c[A] : [kotlin.Char,java.lang.Character,2]
Type of b[true] : [kotlin.Boolean,java.lang.Boolean]
Type of dec[10.67] : [java.math.BigDecimal,java.math.BigDecimal]
The literal 3 : Int, 3.plus(4)=7, 3.toString()=3
Int : [-2147483648, 2147483647]
Long : [-9223372036854775808, 9223372036854775807]
Double : [4.9E-324, 1.7976931348623157E308]
u=4000000000, UInt : [0, 4294967295], ULong.MAX_VALUE=18446744073709551615
-1 read as an unsigned integer: 4294967295

Comments:

  • Line 4: The class [BigDecimal] is a subclass of JDK; it must be imported;
  • line 7: declaration of a variable [integer] of type [Int] initialized to 2. In Kotlin, a variable’s type is written after its name, separated by a colon. We’ll see that the type can usually be omitted;
  • line 8: the suffix F is required: the literal 10.5 is of type [Double], and the compiler refuses to assign it to a variable of type [Float];
  • line 11: the literal 1000 is accepted for a variable of type [Long], because the compiler knows that it is a constant that fits within the type. The same applies to lines 12–13 for the types [Byte] and [Short]. However, as we will see, Kotlin never implicitly converts a variable from one numeric type to another;
  • line 16: a [BigDecimal] number is an object; here, it is created from a string, which guarantees an exact value. Kotlin does not have a [new] operator: an object is created by calling its class constructor as a function;
  • line 18: In a string, [$integer] is replaced by the value of the variable [integer], and [${typeName(integer)\}] by the value of the expression written between the curly braces. These string templates are the standard way to construct a string in Kotlin; we’ll return to this in the next section. [Int.SIZE_BYTES] is the size in bytes of a [Int];
  • Line 18: The function [typeName] (lines 42–43) expects a parameter of type [Any]. The parameter is therefore an object, and the value [integer] is wrapped in a [java.lang.Integer] object. The function displays two names: [o::class.qualifiedName] is the full type name for Kotlin ([kotlin.Int]), and [o.javaClass.name] is the name of the object’s class for JVM ([java.lang.Integer]). We can see that [kotlin.Int], [kotlin.Double], [kotlin.String]... are represented at runtime by Java classes;
  • line 21: the size of a string in memory is not accessible: [String] does not have a constant [SIZE_BYTES];
  • line 26: the size of a boolean is not defined by JVM: [Boolean] does not have a constant [SIZE_BYTES];
  • line 27: [BigDecimal] is a Java class: its two names are identical;
  • line 29: the literal 3 is an object of type [Int]: [3::class.simpleName] is the short name of its type, and its methods can be called [plus] and [toString]. We will see in Chapter 4 that the + operator is simply another way of writing the method [plus];
  • lines 31–33: the constants [MIN_VALUE] and [MAX_VALUE] specify the bounds of the value domain. Note: For real numbers, [Double.MIN_VALUE] is the smallest strictly positive value (4.9 × 10⁻³²⁴) and not the largest negative number, which is [-Double.MAX_VALUE];
  • line 35: a 32-bit unsigned integer. The value 4,000,000,000 exceeds the range of [Int], but not that of [UInt], which extends up to 2³²-1;
  • lines 37–38: -1 is encoded as 32 bits set to 1; the [toUInt] function interprets them as an unsigned integer, which is equal to 2³²-1;
  • Lines 42–43: A high-level function from the file. The return type is specified after the list of parameters: [: String]. It returns ([return]), a character string. Functions are described in the section “Passing Parameters to a Function.”

Note that real numbers are displayed with a decimal point (10.5, -4.6), even though the program runs in French. Converting a number to a string—whether using a template, concatenation, or the [toString] method—does not depend on the user’s language: it always produces a decimal point. Only numeric formats such as %f, which we will discuss later, follow the conventions of the language.

3.3.2. Notation for Literal Data

Data
Examples of literals
[Int] integer (32 bits)
145, -7, 0xFF (hexadecimal), 0b1010_1010 (binary)
Integer [Long] (64 bits), suffix L
100000L, 1_000_000_000_000L, 3_000_000_000 (too large for a [Int])
unsigned integer [UInt], [ULong], suffix U, UL
255U, 0xFFFF_FFFFU, 1UL
real [Double]
134.789, -45E-18 (-45 × 10⁻¹⁸), 10.0
real [Float], suffix F or f
134.789F, -45E-18F
decimal number [BigDecimal]
no literal: [BigDecimal("100000.50")]
character [Char]
'A', 'b', '\n', '\u00e9'
string [String]
"today", "c:\\chap1", "$name is ${age + 10\} years old", raw string """c:\chap1"""
boolean [Boolean]
[true], [false]
date
[LocalDate.of(1954, 10, 13)] (year, month, day) for 10/13/1954

In the source code, floating-point numbers are always written with a decimal point. As in Java, integers can be written in hexadecimal (prefix [0x]) or binary (prefix [0b]), and groups of digits can be separated by the _ character to improve readability: [1_000_000] is the integer one million. Kotlin differs from Java in a few ways, which eliminate just as many pitfalls:

  • an integer literal without a suffix is of type [Int] if it fits within that type, and of type [Long] otherwise: [3_000_000_000] is a [Long]. Java rejects this literal without the suffix L;
  • the suffix L must be written in uppercase: the compiler rejects [100l] with the message [use 'L' instead of 'l'], because l looks too much like the digit 1;
  • there is no octal notation. The Java pitfall, where 017 equals 15, does not exist: the compiler rejects 017 with the message [leading zeros are not allowed in integer literals];
  • there is no “D” suffix for floating-point numbers: a literal containing a decimal point or an exponent is [Double]; 10.0 is therefore written as 10.0 and not 10D;
  • Unsigned integers have their own suffixes, U and UL.

A string literal may contain escape sequences introduced by the \ character: "\n" represents the end-of-line character and not the sequence of the two characters \ and n, "\t" represents a tab, "\"" represents a double quote, "\\" the \ character itself, "\$" the $ character (see below), and "\u00e9" the Unicode character with hexadecimal code 00E9. To write the Windows path c:\chap1\paragraph3, you must therefore write "c:\\chap1\\paragraph3". Java file classes also accept the / separator on Windows: "c:/chap1/paragraph3" refers to the same file.

To construct a string from multiple values, Kotlin offers three methods:

  • string templates: In a string, [$name] is replaced by the value of the variable [name], and [${expression\}] by the value of any expression: ["in 10 years he will be ${age + 10\}"]. This is the most readable and most widely used method. It is equivalent to C#’s interpolated strings, such as [$"..."]; Java, on the other hand, does not have this feature (the “string templates” proposed as a preview in Java 21 and 22 have been removed);
  • Concatenation using the + operator: if the left operand is a string, the right operand is converted to a string, regardless of its type. Unlike in Java, the left operand must be a string: [10 + " years"] is rejected by the compiler because the type [Int] does not have a + operator that accepts a string;
  • the formats: ["format".format(value1, value2, ...)] or [String.format(format, value1, value2, ...)]. These are the Java formats, with the same specifiers, replaced in order by the following values: %s (any value converted to a string), %d (an integer), %f (a floating-point number), %.2f (a floating-point number with two decimal places), %x or %X (an integer in hexadecimal), %,d (an integer with a thousands separator), %e (scientific notation), %n (a line break), %10s (a string right-aligned to 10 characters), %-10s (left-aligned)... Numeric formats follow the conventions of the user’s language. They are used when you need to control the presentation of numbers (number of decimal places, alignment); otherwise, the template is sufficient.

Finally, Kotlin offers “raw strings”: a string delimited by three quotation marks, such as ["""..."""], can span multiple lines and contain quotation marks; most importantly, escape sequences are not interpreted in raw strings: the \ character is treated as-is, so you would write ["""c:\chap1"""]. This is very useful for writing JSON, SQL, HTML, or regular expressions (Chapter 5). Raw strings support templates. Leading spaces are preserved as-is; to indent the text with the code, apply the function [trimIndent()] to the string, which removes the spaces common to all lines as well as the first and last lines if they are empty, or the function [trimMargin()], which removes everything preceding the | character (the “margin”). Java 15 text blocks were designed based on this model; they automatically remove common indentation but interpret escape sequences.

The program [Basics04.kt] demonstrates these different notations:

// the notation of literal data
package basics04

import java.math.BigDecimal
import java.time.LocalDate
import java.time.format.DateTimeFormatter

fun main() {
    // integers: decimal, hexadecimal (0x), binary (0b), separator _
    val i1 = 145; val i2 = -7; val i3 = 0xFF; val i4 = 0b1010_1010
    val l1 = 100000L; val l2 = 1_000_000_000_000L; val l3 = 3_000_000_000
    println("integers: $i1 $i2 $i3 $i4 $l1 $l2 $l3")
    // unsigned integers: suffix U
    val u1 = 255U; val u2 = 0xFFFF_FFFFU
    println("unsigned: $u1 $u2")
    // reals
    val d1 = 134.789; val d2 = -45E-18; val d3 = 10.0
    val f1 = 134.789F
    val m1 = BigDecimal("100000.50")
    println("reals: $d1 $d2 $d3 $f1 $m1")
    // characters and booleans
    val c1 = 'A'; val c2 = '\u00e9'
    val b1 = true
    println("characters: $c1 $c2, boolean: $b1")
    // a date: (year, month, day)
    val birth = LocalDate.of(1954, 10, 13)
    println("date: $birth")

    // the \ character introduces an escape sequence
    val path = "c:\\chap1\\paragraph3"
    println(path)
    println("line 1\nline 2\t[tab]")
    // the $ character is written \$ in a string
    println("price: 10 \$, quotes: \"yes\"")

    // building a string: template, concatenation or format
    val name = "dupont"
    val age = 23
    println("$name is $age years old, in 10 years he will be ${age + 10}")
    println(name + " is " + age + " years old, in 10 years he will be " + (age + 10))
    println(String.format("%s is %d years old, in 10 years he will be %d", name, age, age + 10))
    // the format function applies to the format string itself
    println("d1=%.1f, i3 in hexadecimal=%X, l2=%,d".format(d1, i3, l2))
    println("i4 in binary=${i4.toString(2)}, name in upper case=${name.uppercase()}")
    println("date=${birth.format(DateTimeFormatter.ofPattern("EEEE d MMMM yyyy"))}")

    // raw string: no escape sequences, no quotes to escape
    val json = """
        {
          "name": "dupont",
          "path": "c:\chap1"
        }
        """.trimIndent()
    println(json)
    // a raw string accepts templates
    val json2 = """{ "name": "$name", "age": $age }"""
    println(json2)
    // trimMargin: the margin stops at the | character
    val text = """
        |A raw string
        |   on several lines, costing 10 ${'$'}
        """.trimMargin()
    println(text)
}

Results:

> gradlew -q run -Pmain=basics04.Basics04Kt
integers: 145 -7 255 170 100000 1000000000000 3000000000
unsigned: 255 4294967295
reals: 134.789 -4.5E-17 10.0 134.789 100000.50
characters: A é, boolean: true
date: 1954-10-13
c:\chap1\paragraph3
line 1
line 2    [tab]
price: 10 $, quotes: "yes"
dupont is 23 years old, in 10 years he will be 33
dupont is 23 years old, in 10 years he will be 33
dupont is 23 years old, in 10 years he will be 33
d1=134,8, i3 in hexadecimal=FF, l2=1 000 000 000 000
i4 in binary=10101010, name in upper case=DUPONT
date=mercredi 13 octobre 1954
{
  "name": "dupont",
  "path": "c:\chap1"
}
{ "name": "dupont", "age": 23 }
A raw string
   on several lines, costing 10 $

Comments:

  • lines 4–6: the classes from JDK used by the program are imported;
  • Line 10: Four declarations on a single line, separated by semicolons. [0xFF] is 255 and [0b1010_1010] is 170. The variable types are not specified: the compiler infers them from the values, in this case [Int];
  • line 11: the suffix L turns 100000 into a [Long]. The literal 3_000_000_000, which is too large for a [Int], is a [Long] without the need for a suffix;
  • line 14: two unsigned integers of type [UInt]. 0xFFFF_FFFFU, which has all 32 bits set to 1, is equal to 2³²-1;
  • line 17: -45E-18 is displayed as -4.5E-17: this is the same value, normalized. The floating-point number 10.0 is displayed as 10.0: converting a [Double] to a string always produces at least one decimal place;
  • line 19: a [BigDecimal] retains the number of decimal places with which it was written: 100000.50, not 100000.5;
  • line 22: the sequence \u00e9 denotes the Unicode character with hexadecimal code 00E9, i.e., é;
  • line 26: a date. The class [LocalDate] (package [java.time] of JDK) represents a date without a time. Kotlin does not have its own date class: it uses Java’s. Its conversion to a string uses the international format ISO 8601 (year-month-day), regardless of the language. We’ll cover dates in Chapter 5;
  • line 30: each \\ represents a single \ character;
  • line 32: a line break and a tab;
  • line 34: the sequences \$ and \";
  • line 39: a template. [$name] and [$age] are replaced by the values of the variables, and [${age + 10\}] by the value of the expression. Curly braces are required whenever what follows the $ is not a simple variable name;
  • line 40: the same text constructed by concatenation. The parentheses around [(age + 10)] are essential: without them, the + operators would be evaluated from left to right, the string would first receive 23 and then 10, and the result would be “in 10 years he will be 2310.” The template does not have this pitfall;
  • line 41: the same text constructed using a format;
  • line 43: a few format specifiers: %.1f (one decimal place), %X (hexadecimal in uppercase), %,d (thousands separator). These formats follow French conventions: a decimal comma and, as the thousands separator, a thin non-breaking space. The [format] function from the Kotlin library is applied to the format string itself; it calls the Java function [String.format];
  • line 44: the function [toString(base)] converts an integer to a string in any base, here 2; the function [uppercase()] converts the string to uppercase. Note that a template can contain function calls;
  • line 45: a custom date format: day of the week (EEEE), day (d), month in words (MMMM), year (yyyy). The names of the days and months are in French, the user’s language. Note that the template can contain quotation marks inside curly braces;
  • Lines 48–53: a raw string spanning multiple lines. The quotation marks are written as-is, and \chap1 did not need to be written as \\chap1. The [trimIndent()] function removed the eight spaces common to all lines, as well as the first line (which is empty, immediately after the opening quotation marks) and the last line (which contains only spaces before the closing quotation marks);
  • line 56: a raw string on a single line, which avoids having to escape the quotation marks in JSON, with two templates;
  • lines 59–62: the [trimMargin()] function removes, from each line, the spaces and the | character that mark the margin: the three spaces placed after the | are retained. Since escape sequences are not interpreted in a raw string, the $ character is written as [${'$'\}]: a template whose expression is the '$' character. Here, since the $ is followed by a newline, it could have been written on its own; however, [$name] would be replaced by the value of [name]. Since Kotlin 2.2, you can also prefix the string with multiple $ signs: in [$$"""{ "$schema": "$$name" \}"""], two $ signs are required to introduce a template, and [$schema] remains unchanged.

3.3.3. Data Declaration

3.3.3.1. Role of Declarations

A program manipulates data characterized by a name and a type. This data is stored in memory. When the program is compiled, the compiler assigns each piece of data a memory location characterized by an address and a size. It does this based on the declarations made by the programmer. Furthermore, these declarations allow the compiler to detect programming errors. Thus, the operation [x = x * 2] will be declared invalid if [x] is a character string.

3.3.3.2. Read-Only and Mutable Variables

Kotlin has two keywords for declaring a variable:

  • [val] (“value”) declares a read-only variable: it is assigned a value only once and cannot be changed afterward. This is equivalent to a Java variable [final];
  • [var] (“variable”) declares a mutable variable, which can be assigned new values.

The syntax is as follows:

val name: Type = value
var name: Type = value

The type is usually omitted: the compiler then infers it from the value (type inference). The Kotlin keyword [var] therefore does not have the same meaning as in Java 10: in Java, [var] means “inferred type”; in Kotlin, it means “mutable,” and the type can be inferred with [val] just as with [var]. A variable declared without being initialized must have an explicit type; it cannot be used until it has been assigned a value (message [variable 'b' must be initialized]).

The convention in Kotlin is to declare all variables using [val], and to use [var] only when the variable’s value is actually expected to change. This improves the program’s readability: when reading ***[val total = ...]***, you know that *[total] will retain that value. IntelliJ and IDEA also help identify any variable named [var] that might actually be [val]*.

Kotlin distinguishes between uppercase and lowercase letters: the variables [END] and [end] are different. By convention, a variable name begins with a lowercase letter, and each subsequent word begins with an uppercase letter: [priceExclTax], [nbChildren].

3.3.3.3. Declaring Constants

A variable such as [val] does not change its value, but its value may not be known until runtime ([val today = LocalDateTime.now()]). A constant, strictly speaking, is a value known at compile time. Kotlin declares it using the keywords [const val]:

const val NAME = value

A constant [const val] is declared at the top level of the file (outside of functions), or within a [object] or a [companion object], which we will cover in Chapter 4. It can only be of a basic type ([Int], [Double], [Boolean]...) or of type [String], and its value must be computable by the compiler. For example:

const val VAT_RATE = 0.2

The compiler then replaces every occurrence of [VAT_RATE] with its value, just as the Java compiler does for a [static final] field initialized with a constant. The following attempts are rejected:

1
2
3
4
5
const val START = LocalDate.of(2026, 1, 1)
error: const 'val' has type 'LocalDate!'. Only primitive types and 'String' are allowed.
fun main() {
    const val N = 3
error: modifier 'const' is not applicable to 'local variable'.

(the exclamation point in [LocalDate!] will be explained in the next section). Why declare constants?

  • The program is easier to read if the constant is given a meaningful name;
  • it is easier to modify the program if the “constant” needs to be changed. Thus, if the VAT rate changes to 0.25, the only modification needed is to the statement that defines its value: [const val VAT_RATE = 0.25]. If 0.2 had been used explicitly in the program, numerous statements would need to be modified.

By convention, as in Java, a constant’s name is written in uppercase letters, with words separated by the _ character: [VAT_RATE], [MAX_VALUE]. A local variable, such as [val], retains a regular name.

The program [Basics05.kt] is as follows:

// constants and variables
package basics05

import java.time.LocalDateTime

// a top-level constant: const val, name in upper case
const val VAT_RATE = 0.2

fun main() {
    // a read-only variable: val
    val currency = "euros"
    // a mutable variable: var
    var priceExclTax = 100.0
    println("Price incl. tax: ${priceExclTax * (1 + VAT_RATE)} $currency")
    priceExclTax = 250.0
    println("Price incl. tax: ${priceExclTax * (1 + VAT_RATE)} $currency")

    // an explicitly typed piece of data
    val i: Int = 2
    println("Type of val i: Int = 2 : ${typeName(i)}")
    // implicitly typed data: the type is inferred from the value
    val j = 3
    println("Type of val j = 3 : ${typeName(j)}")
    val today = LocalDateTime.now()
    println("Type of val today : ${typeName(today)}")
    val x = 3.5
    val y = 3.5F
    val z = 10L
    val name = "dupont"
    println("x : ${typeName(x)}, y : ${typeName(y)}")
    println("z : ${typeName(z)}, name : ${typeName(name)}")
    // a variable declared without an initial value: its type must be written
    val b: Int
    val a = 1
    val c = 3
    b = a + c
    println("a=$a, b=$b, c=$c")
}

// returns the short name and the full name of the type of the object received
fun typeName(o: Any): String = "${o::class.simpleName},${o::class.qualifiedName}"

Results:

1
2
3
4
5
6
7
8
9
> gradlew -q run -Pmain=basics05.Basics05Kt
Price incl. tax: 120.0 euros
Price incl. tax: 300.0 euros
Type of val i: Int = 2 : Int,kotlin.Int
Type of val j = 3 : Int,kotlin.Int
Type of val today : LocalDateTime,java.time.LocalDateTime
x : Double,kotlin.Double, y : Float,kotlin.Float
z : Long,kotlin.Long, name : String,kotlin.String
a=1, b=4, c=3

Comments:

  • line 7: a top-level constant. The compiler classifies it in the [Basics05Kt] class as a [public static final double] field;
  • line 11: a read-only local variable, of type [String] (inferred);
  • line 13: a mutable variable. The literal 100.0 gives [priceExclTax] the type [Double]. With [var priceExclTax = 100], the variable would be of type [Int], and the assignment of 250.0 on line 15 would be rejected;
  • line 14: the result is a [Double], displayed with at least one decimal place: 120.0;
  • line 15: [priceExclTax] is assigned a new value;
  • line 19: an explicitly typed data value;
  • line 20: the function [typeName] (line 41) displays the short name ([simpleName]) and the full name ([qualifiedName]) of its parameter’s type;
  • line 22: an implicitly typed value. Because 3 is of type [Int], [j] will be of type [Int];
  • line 24: Because [LocalDateTime.now()] (the current date and time) is of type [LocalDateTime], [today] will be of type [LocalDateTime]. Note that the program calls a [static] method of a Java class, just as in Java: [LocalDateTime.now()];
  • lines 26–29: the inferred type depends on how the literal is written: 3.5 is a [Double], 3.5F is a [Float], 10L is a [Long];
  • Line 33: a variable declared without an initial value; its type is required. Although it is declared as [val], it can be assigned a value later (line 36), but only once. Kotlin does not allow declaring multiple variables in a single statement, as in Java’s [int a = 1, b, c = 3;]: you write one declaration per line, or multiple declarations separated by semicolons, as in the previous program;
  • Line 41: A function whose body is reduced to a single expression can be written using the equals sign (=) instead of curly braces and the expression body. You could even omit the return type, which the compiler would infer from the expression.

A variable [val] cannot be assigned a new value, and a variable [var] cannot change its type: its type was fixed by its declaration. Thus, the following statements are rejected by the compiler:

1
2
3
4
5
6
val today = LocalDateTime.now()
today = LocalDateTime.now()
error: 'val' cannot be reassigned.
var counter = 0
counter = "one"
error: assignment type mismatch: actual type is 'String', but 'Int' was expected.

3.3.4. Null Safety

It is sometimes useful to be able to express the absence of a value: an unknown age, an empty field in a database, or an unsuccessful search. In Java, any variable of a reference type can have the value [null], which means “no object.” Attempting to access a member of such a variable—for example, its method [length()]—causes a runtime error, a [NullPointerException]: This is the most common error in Java, because the Java compiler does not distinguish between a reference that can be [null] and one that cannot. To express the absence of a value without [null], Java 8 added the [Optional<T>] class, a “box” that either contains a value or is empty; and for a number, Java uses the wrapper class [Integer], which can hold [null], instead of the primitive type [int].

Kotlin resolves this issue within the type system itself, and this is one of its major contributions. A regular type does not allow the null value: a variable of type [String] always contains a string, and a variable of type [Int] always contains an integer. To allow [null], you must explicitly indicate this by appending a question mark to the type name: [String?] is the “string or null” type, and [Int?] is the “integer or null” type. These are known as “nullable types.” The compiler then verifies that a member of a nullable value cannot be used without first handling the case [null]:

1
2
3
4
5
6
7
val name: String = null
error: null cannot be a value of a non-null type 'String'.
val firstName: String? = firstNameOf("martin")
println(firstName.length)
error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
val n: Int = "12".toIntOrNull()
error: initializer type mismatch: expected 'Int', actual 'Int?'.

Kotlin offers several ways to use a nullable value:

  • the [if (x != null)] test: in the branch where the test is true, the compiler knows that [x] is not equal to [null], and treats it as a value of type [String] (and not [String?]). This is known as a “smart cast”;
  • the safe call [x?.length]: if [x] is equal to [null], the expression evaluates to [null] instead of causing an error; otherwise, it evaluates to [x.length]. The result is therefore of type [Int?];
  • the Elvis operator [x ?: defaultValue]: it evaluates to [x] if [x] is not equal to [null], and to [defaultValue] otherwise (its name comes from the resemblance of the “?” symbol to the singer’s hairstyle, viewed from the side). It is often combined with the safe call: [x?.length ?: 0];
  • the **let** function: ***[x?.let { ... \\}] executes the block between curly braces only if *[x] is not equal to *[null]*; within the block, the value is called [it] and is of a non-nullable type. The block is a lambda**, a concept we will study in Chapter 7;
  • the assertion [x!!]: the programmer asserts that [x] is not equal to [null]. If this is true, [x!!] is of type [String]; otherwise, a [NullPointerException] is thrown. It should be used as little as possible: it is a deliberate return to Java’s behavior.

The ?, ?:, and !! operators are discussed in more detail in the “Null Operators” section. C# has adopted a similar approach with its “nullable reference types” (C# 8), in the form of warnings; in Kotlin, these are compilation errors.

In practice, the [NullPointerException] error is rare in Kotlin. It can now only occur in a few cases: the use of !!, an explicitly thrown exception, and, most importantly, the use of Java code. The Kotlin compiler does not know whether a Java method might return [null]: it therefore assigns it a “platform type,” denoted as [String!] in compiler messages (it cannot be written in the code), which can be used as a [String] or as a [String?]. The responsibility then falls on the programmer. When a Java method is known to return [null], its result is stored in a nullable variable ([val path: String? = System.getenv("X")]): the compiler then resumes its checks. (Java methods annotated as [@Nullable] or [@NonNull]—for example, with annotations from the JSpecify project—are assigned a specific Kotlin type, not a platform type.)

What happens to the wrapper classes and [Optional]? A [Int] is compiled into [int], and a [Int?] into [java.lang.Integer], since only a reference can be of type [null]: The wrapper classes are there, but the compiler handles them on its own. The [Optional] class remains usable, since it is a Java class, but it is of little use in Kotlin: a [String?] says the same thing as a [Optional<String>], without creating an additional object, and the operators ?. and ?: replace its methods [map] and [orElse]. Functions in the Kotlin library that may return nothing return [null]: [toIntOrNull()], [firstOrNull()], [readlnOrNull()]...

We’ll encounter nullable types right from the first examples, because the function [readlnOrNull()], which reads a line typed on the keyboard, returns [null] when there is nothing left to read (end of the input stream). The program [Basics06.kt] is as follows:

// null safety
package basics06

// a function that may return nothing: the type of its result is String?
fun firstNameOf(name: String): String? = if (name == "dupont") "jean" else null

fun main() {
    // a String cannot be null; a String? can
    val firstName1: String? = firstNameOf("dupont")
    val firstName2: String? = firstNameOf("martin")
    println("firstName1=$firstName1, firstName2=$firstName2, firstName2 is null: ${firstName2 == null}")
    // after the test, firstName1 is of type String: its length property can be used
    if (firstName1 != null) {
        println("firstName1 has ${firstName1.length} characters")
    }
    // the safe call ?. returns null if the object is null
    println("lengths: ${firstName1?.length}, ${firstName2?.length}")
    // the Elvis operator ?: provides a replacement value
    println("firstName2: ${firstName2 ?: "unknown"}, length: ${firstName2?.length ?: 0}")
    // let: the block is executed only if the value is not null; inside, it is called it
    firstName1?.let { println("let: $it in upper case: ${it.uppercase()}") }
    firstName2?.let { println("this text is not displayed") }

    // an Int? is represented by a java.lang.Integer, an Int by an int
    val age: Int? = "23".toIntOrNull()
    val unknownAge: Int? = "twenty".toIntOrNull()
    println("age=$age, unknownAge=$unknownAge, (unknownAge ?: 0) + 1 = ${(unknownAge ?: 0) + 1}")

    // the !! assertion: "this value is not null", otherwise NullPointerException
    println("firstName1!!.length=${firstName1!!.length}")
    try {
        println(firstName2!!.length)
    } catch (e: NullPointerException) {
        println("!! applied to null: ${e::class.qualifiedName}")
    }

    // a Java method: the compiler does not know whether the result can be null
    val path = System.getenv("UNKNOWN_VARIABLE")
    println("path=$path, length: ${path?.length}")
    try {
        // accepted by the compiler, but NullPointerException at runtime
        println(path.length)
    } catch (e: NullPointerException) {
        println("path.length: ${e.message}")
    }

    // readlnOrNull returns null if the input stream is closed
    print("Type a line: ")
    val line = readlnOrNull()
    if (line == null) {
        println()
        println("No line: the input stream is closed")
    } else {
        println("You typed [$line]: ${line.length} character(s)")
    }
}

Results when entering text via the keyboard:

> gradlew -q run -Pmain=basics06.Basics06Kt
firstName1=jean, firstName2=null, firstName2 is null: true
firstName1 has 4 characters
lengths: 4, null
firstName2: unknown, length: 0
let: jean in upper case: JEAN
age=23, unknownAge=null, (unknownAge ?: 0) + 1 = 1
firstName1!!.length=4
!! applied to null: java.lang.NullPointerException
path=null, length: null
path.length: Cannot invoke "String.length()" because "path" is null
Type a line: hello
You typed [hello]: 5 character(s)

Results when the user presses Ctrl-D (Ctrl-Z followed by Enter on Windows), which closes the input stream:

> gradlew -q run -Pmain=basics06.Basics06Kt
firstName1=jean, firstName2=null, firstName2 is null: true
firstName1 has 4 characters
lengths: 4, null
firstName2: unknown, length: 0
let: jean in upper case: JEAN
age=23, unknownAge=null, (unknownAge ?: 0) + 1 = 1
firstName1!!.length=4
!! applied to null: java.lang.NullPointerException
path=null, length: null
path.length: Cannot invoke "String.length()" because "path" is null
Type a line:
No line: the input stream is closed

Comments:

  • Line 5: a function that returns a first name, such as [null]. The type of its result, [String?], informs users of this. The expression [if (...) "jean" else null] is a [if] that returns a value; we’ll come back to this;
  • lines 9–10: two variables of type [String?]. [firstName2] is equal to [null]. The type could have been omitted: it would have been inferred from the type of the result of [firstNameOf];
  • line 11: a template displays [null] for a null value; the value [null] is tested using the == operator;
  • lines 13–14: within the [if] block, [firstName1] is of type [String]: its property [length] (the length of the string) can be used safely. Outside the block, [firstName1.length] would be rejected;
  • line 17: the safe call. [firstName2?.length] is equal to [null];
  • line 19: the Elvis operator. [firstName2 ?: "unknown"] is "unknown," and [firstName2?.length ?: 0] is 0;
  • lines 21–22: the function [let]. The first block is executed with [it] set to "jean"; the second is not;
  • lines 25–26: The function [toIntOrNull()] converts a string to an integer and returns [null] if the string does not represent an integer. The type [Int?] is compiled to [java.lang.Integer];
  • Line 27: You cannot perform calculations with a [Int?]; [unknownAge + 1] would be rejected. The Elvis operator returns a [Int]: 0 if [unknownAge] is equal to [null]. Unboxing a null [Integer]—a classic Java pitfall—is thus impossible;
  • line 30: the !! assertion applied to a non-null value: it returns the value, of type [String];
  • lines 31–34: the !! assertion applied to *[null] throws a *[NullPointerException]. The structure [try / catch]*, which catches the error, is described later; here, the full class name of the exception is displayed. [NullPointerException]* is a Java class, which Kotlin refers to by the same name;
  • line 38: the Java method [System.getenv] returns the value of a system environment variable, or [null] if it does not exist. Its result is of the platform type [String!]: [path] is therefore of this type;
  • line 39: when used as a [String?], [path] behaves correctly;
  • line 42: when used as a [String], it is accepted by the compiler but causes a [NullPointerException] at runtime. The exception message (property [message]), generated by JVM, specifies exactly which expression was [null]. If we had written [val path: String? = System.getenv(...)], the compiler would have rejected that line;
  • line 48: the function [print] writes to the screen without moving to the next line; user input will follow the message;
  • line 49: the function [readlnOrNull()] reads the line typed on the keyboard. It returns a value of type [String?]: [null] if the input stream is closed;
  • line 50: the value [null] is checked;
  • line 54: in the branch [else], [line] is of type [String]: [line.length] can be used safely.

3.3.5. Conversions Between Numbers and Strings

Conversion
Function
number -> string
[number.toString()], a template such as ["$number"], ["" + number], or a format such as ["%d".format(number)]
string -> int
[string.toInt()], [string.toIntOrNull()]
string -> Long
[string.toLong()], [string.toLongOrNull()]
string -> Double
[string.toDouble()], [string.toDoubleOrNull()]
string -> Float
[string.toFloat()], [string.toFloatOrNull()]
string -> BigDecimal
[string.toBigDecimal()], [string.toBigDecimalOrNull()]
string -> Boolean
[string.toBoolean()], [string.toBooleanStrictOrNull()]

These conversion functions are part of the Kotlin library (package [kotlin.text]) and are called directly on the string itself: ["10".toInt()] rather than the Java function [Integer.parseInt("10")], which remains usable. These are extension functions: functions written outside the [String] class, but called just like its methods. We’ll learn how to write them in Chapter 4.

Converting a string to a number may fail if the string does not represent a valid number. A function like [toInt()] then generates an error called an exception—in this case, of type [NumberFormatException]—which terminates the program if it is not handled. This error can be handled by the following [try / catch] clause:

1
2
3
4
5
6
try {
    call of the function likely to throw the exception
} catch (e: NumberFormatException) {
    handle the exception e
}
next statement

If the function does not throw an exception, execution proceeds to the next statement; otherwise, it proceeds to the body of the [catch] clause, then to the next statement. We will revisit exception handling in the section dedicated to it.

Kotlin also provides, for each conversion, a version that does not throw an exception: [toIntOrNull()], [toDoubleOrNull()]... return [null] if the conversion fails. Their return values are nullable ([Int?], [Double?]...): the compiler therefore requires that the failure be handled. When combined with the Elvis operator, they allow you to write a conversion with a default value in a single line: [input.toIntOrNull() ?: 0]. Java has no equivalent; the Java course wrote its own method, which returned a [OptionalInt].

These functions accept a leading + or - sign, but no spaces or thousand separators: use the function [trim()] to remove leading and trailing spaces from an input before converting it.

The program [Basics07.kt] demonstrates several techniques for converting between numbers and strings. The function [display] displays the value of its parameter on the screen:

// conversions between numbers and character strings
package basics07

import java.text.NumberFormat
import java.util.Locale

fun main() {
    // data
    val i = 10
    val l = 100000L
    val f = 45.78F
    val d = -14.98

    // number --> string
    display(i.toString())
    display(l.toString())
    display("$f")
    display("" + d)

    // boolean --> string
    val b = false
    display(b.toString())

    // string --> Int
    var i1 = "10".toInt()
    display(i1.toString())
    try {
        i1 = "10.67".toInt()
        display(i1.toString())
    } catch (e: NumberFormatException) {
        display("Error: ${e.message}")
    }

    // string --> Long
    var l1 = "100".toLong()
    display(l1.toString())
    try {
        l1 = "10.675".toLong()
        display(l1.toString())
    } catch (e: NumberFormatException) {
        display("Error: ${e.message}")
    }

    // string --> Double: the decimal separator is always the dot
    var d1 = "100.87".toDouble()
    display(d1.toString())
    try {
        d1 = "abcd".toDouble()
        display(d1.toString())
    } catch (e: NumberFormatException) {
        display("Error: ${e.message}")
    }

    // string --> Float
    var f1 = "100.87".toFloat()
    display(f1.toString())
    try {
        f1 = "100,87".toFloat()
        display(f1.toString())
    } catch (e: NumberFormatException) {
        display("Error: ${e.message}")
    }

    // string --> BigDecimal
    val m1 = "100.87".toBigDecimal()
    display(m1.toString())

    // string --> Boolean
    display("${"true".toBoolean()} ${"yes".toBoolean()} ${"yes".toBooleanStrictOrNull()}")

    // conversions without exceptions: the xxxOrNull functions return null on failure
    val i2 = "123".toIntOrNull()
    if (i2 != null) {
        display("\"123\".toIntOrNull() succeeded: $i2")
    }
    val i3 = "12x".toIntOrNull()
    if (i3 == null) {
        display("\"12x\".toIntOrNull() failed")
    }
    display("with a default value: ${"12x".toIntOrNull() ?: -1}")
    // spaces are not accepted: they are removed with trim()
    display("[ 42 ] : ${" 42 ".toIntOrNull()}, ${" 42 ".trim().toIntOrNull()}")

    // the user's language (the "locale") only plays a part in formats
    display("Current locale: ${Locale.getDefault().toLanguageTag()}")
    display("%.2f".format(d1))
    display("%.2f".format(Locale.ROOT, d1))
    // reading a number written the French way: Java's NumberFormat class
    // parse can throw a ParseException: Kotlin does not force us to handle it
    val number = NumberFormat.getInstance(Locale.FRANCE).parse("100,87")
    display("NumberFormat: ${number.toDouble()}")
}

// a function that displays its parameter
fun display(s: String) {
    println("S=$s")
}

Results:

> gradlew -q run -Pmain=basics07.Basics07Kt
S=10
S=100000
S=45.78
S=-14.98
S=false
S=10
S=Error: For input string: "10.67"
S=100
S=Error: For input string: "10.675"
S=100.87
S=Error: For input string: "abcd"
S=100.87
S=Error: For input string: "100,87"
S=100.87
S=true false null
S="123".toIntOrNull() succeeded: 123
S="12x".toIntOrNull() failed
S=with a default value: -1
S=[ 42 ] : null, 42
S=Current locale: fr-FR
S=100,87
S=100.87
S=NumberFormat: 100.87

Comments:

  • lines 9–12: the data to be converted;
  • lines 15–18: four ways to convert a number to a string. The third uses a template, and the last concatenates an empty string with the number. The template is the most common method in Kotlin;
  • line 22: a boolean is displayed as [true] or [false];
  • lines 25–31: We handle the exception that [toInt()] may throw. The variable [i1] is declared alongside [var] because it receives a new value in [try]. [e.message] is the error message associated with the [e] exception (the Java method [getMessage()], viewed in Kotlin as a property). These are the messages from the Java methods called by the Kotlin functions: they are in English;
  • line 45: [toDouble()] always expects a decimal point, regardless of the user’s language: the string "100.87" is interpreted the same way on all machines. This is the correct way to read numbers from files or in program-to-program exchanges;
  • line 58: Consequently, the decimal comma is rejected;
  • line 65: converting a string to [BigDecimal]; this also expects a decimal point. It is equivalent to [BigDecimal("100.87")];
  • line 69: [toBoolean()] returns [true] for the string "true" (case-insensitive) and [false] for any other string, including "yes". [toBooleanStrictOrNull()] accepts only “true” and “false,” and returns [null] for everything else;
  • Lines 72–78: Conversions without exception. [i2] and [i3] are of the same type as [Int?]; After the [i2 != null] test, [i2] can be used as a [Int]. In a string, a quotation mark is written as \";
  • line 80: a conversion with a default value, using the Elvis operator. The template contains a string, "12x", which is itself enclosed in quotation marks: this is allowed inside curly braces;
  • line 82: " 42 " is not converted; after [trim()], it is converted;
  • line 85: [Locale.getDefault()] returns the program’s locale—that is, the user’s language and country, derived from the system settings: here, fr-FR;
  • line 86: a format uses the current locale: the real number is written with a decimal comma;
  • line 87: the function [format] accepts a locale as its first parameter. [Locale.ROOT] is a neutral locale that uses a period as the decimal separator, regardless of the machine. It is used to write formatted numbers to files or in inter-program exchanges, which must not depend on the user’s language;
  • line 90: To read a number written according to French conventions, use a [NumberFormat] object (from the [java.text] package of JDK) obtained for the [Locale.FRANCE] locale. Its method [parse] returns a [Number] object and may throw a [ParseException] exception. In Java, this is a checked exception: the compiler requires that it be handled. Kotlin does not have checked exceptions: the compiler leaves it up to the programmer to decide whether or not to handle the exception. We’ll come back to this in the section on exceptions;
  • line 91: [toDouble()] converts [Number] to [Double];
  • Lines 95–96: a high-level function. Functions can be written in any order, before or after [main]. It does not return a result: the return type is omitted (it is actually of type [Unit], the equivalent of Java’s [void]).

Note that the functions [toInt()], [toDouble()]... and [toString()] ignore the user’s language (decimal point), whereas the formats and the [NumberFormat] class respect it. A program that reads numbers must always know which conventions were used to write them.

3.3.6. Data Arrays

A Kotlin array is an object that allows data of the same type to be grouped under a single identifier. Kotlin distinguishes between two types of arrays:

  • the generic type [Array<T>], an array of elements of type [T]: [Array<String>] is an array of strings. It corresponds to Java’s array of [T[]] objects ([String[]]);
  • specialized types for basic types: [IntArray], [LongArray], [DoubleArray], [BooleanArray], [CharArray]... A [IntArray] corresponds to the Java array [int[]]: its elements are primitive values, arranged side by side in memory. One could write [Array<Int>], but that would be an array of [Integer] ([Integer[]]) objects, which is bulkier and slower: for numbers, specialized arrays are preferred.

There are several ways to create an array:

1
2
3
4
5
val t1 = arrayOf("a", "b", "c")         // initialized Array<String>
val t2 = intArrayOf(0, 10, 20, 30)      // initialized IntArray
val t3 = IntArray(n)                    // n integers equal to 0
val t4 = Array(n) { i -> value }        // n elements, computed from their index
val t5 = arrayOfNulls<String>(n)        // Array<String?> of n elements equal to null

Kotlin does not have a [new] operator or [{0, 10, 20\}] syntax for creating an array: everything is done using functions ([arrayOf], [intArrayOf]...) or constructors ([IntArray(n)], [Array(n) { ... \}]). A [IntArray(n)] contains n zeros, while a [BooleanArray(n)] contains n values of [false]. However, there is no [Array<String>(n)] whose elements would be [null]: this would violate null safety, since the elements are of type [String]. You must either specify the value of each element or explicitly request an array of nullable elements using [arrayOfNulls<String>(n)], of type [Array<String?>].

The number of elements in an array is its property [size] (the Java field [length]). The syntax [array[i]] refers to data item number i, where i belongs to the interval [0, size-1]. Any reference to data [array[i]] where i is not in this range causes a [ArrayIndexOutOfBoundsException] exception. The property [indices] is the range of valid indices (from 0 to size-1), and [lastIndex] is the last index (size-1). An array cannot change size after it is created: for a growing collection, a list is used (Chapter 5). A variable [val] that refers to an array cannot refer to another array, but the elements of the array remain modifiable.

Kotlin also does not have true two-dimensional arrays: an array with two indices is an array of arrays, for example, a [Array<DoubleArray>]. The syntax [array[i][j]] refers to element j in row i. [array.size] is the number of rows, and [array[i].size] is the number of elements in row i. Since each row is an independent array, the rows can have different lengths (this is referred to as a “staircase array”).

The Kotlin library offers a wide range of array functions that replace the methods of the [java.util.Arrays] class and go far beyond them: [contentToString()], [contentDeepToString()], [contentEquals()], [first()], [last()], [getOrNull(i)], [copyOf()], [copyOfRange(start, end)], [sliceArray(range)], [sort()], [sorted()], [fill()], [sum()], [max()], [average()], [contains()] (written using the operator [in])... For the most part, these are the same as those in the lists, which we will examine in Chapter 5.

The program [Basics08.kt] combines these different arrays:

// arrays
package basics08

fun main() {
    // an initialized array of integers: an IntArray (a Java int[])
    val integers = intArrayOf(0, 10, 20, 30)
    for (i in integers.indices) {
        println("integers[$i]=${integers[i]}")
    }

    // a 2-dimensional array: an array of arrays of the same length
    val reals = arrayOf(doubleArrayOf(0.5, 1.7), doubleArrayOf(8.4, -6.0))
    for (i in reals.indices) {
        for (j in reals[i].indices) {
            println("reals[$i][$j]=${reals[i][j]}")
        }
    }
    println("reals=${reals.contentDeepToString()}")

    // an array of arrays of different lengths (jagged array)
    // Array(n) { i -> ... } computes the value of each element from its index
    val names = Array(3) { i -> Array(i + 1) { j -> "name$i$j" } }
    for (i in names.indices) {
        for (j in names[i].indices) {
            println("names[$i][$j]=${names[i][j]}")
        }
    }

    // arrays created without initial values
    val flags = BooleanArray(3)
    val strings = arrayOfNulls<String>(2)
    println("flags[0]=${flags[0]}, strings[0] is null: ${strings[0] == null}")

    val values = intArrayOf(0, 10, 20, 30, 40)
    // contentToString builds a string with the elements of the array
    println("values=${values.contentToString()}, size=${values.size}, lastIndex=${values.lastIndex}")
    // the first, the last and the second-to-last element
    println("first=${values.first()}, last=${values.last()}")
    println("second-to-last=${values[values.size - 2]}, ${values[values.lastIndex - 1]}")
    // a part of the array: the elements at indices 1 and 2 (the end bound is excluded)
    val extract = values.copyOfRange(1, 3)
    println("copyOfRange(1, 3)=${extract.contentToString()}")
    // the elements at indices 2 to lastIndex (closed range)
    val end = values.sliceArray(2..values.lastIndex)
    println("sliceArray(2..lastIndex)=${end.contentToString()}")
    // the extract is a copy: the original array is not modified
    extract[0] = 100
    println("extract[0]=${extract[0]}, values[1]=${values[1]}")
    // a few functions of the Kotlin library
    println("sum=${values.sum()}, max=${values.max()}, average=${values.average()}")
    println("20 in values: ${20 in values}, 25 in values: ${25 in values}")

    // an array of objects: Array<String>
    val friends = arrayOf("paul", "hélène", "jacques")
    println("first friend=${friends.first()}, last friend=${friends.last()}, friends.getOrNull(5)=${friends.getOrNull(5)}")
    // an index outside the bounds of the array
    try {
        println(friends[3])
    } catch (e: ArrayIndexOutOfBoundsException) {
        println("friends[3]: ${e.message}")
    }
}

Results:

> gradlew -q run -Pmain=basics08.Basics08Kt
integers[0]=0
integers[1]=10
integers[2]=20
integers[3]=30
reals[0][0]=0.5
reals[0][1]=1.7
reals[1][0]=8.4
reals[1][1]=-6.0
reals=[[0.5, 1.7], [8.4, -6.0]]
names[0][0]=name00
names[1][0]=name10
names[1][1]=name11
names[2][0]=name20
names[2][1]=name21
names[2][2]=name22
flags[0]=false, strings[0] is null: true
values=[0, 10, 20, 30, 40], size=5, lastIndex=4
first=0, last=40
second-to-last=30, 30
copyOfRange(1, 3)=[10, 20]
sliceArray(2..lastIndex)=[20, 30, 40]
extract[0]=100, values[1]=10
sum=100, max=40, average=20.0
20 in values: true, 25 in values: false
first friend=paul, last friend=jacques, friends.getOrNull(5)=null
friends[3]: Index 3 out of bounds for length 3

Comments:

  • line 6: an initialized array of integers, of type [IntArray];
  • line 7: a loop of type [for] iterates through the array indices, from 0 to [integers.lastIndex]. The loop of type [for] is presented later;
  • Line 8: In a template, [$i] is the index value and [${integers[i]\}] is the element value. Curly braces are required for the expression [integers[i]]; the square bracket that follows [[$i] does not require them, since it cannot be part of a variable name;
  • line 12: a two-dimensional array (2 rows, 2 columns) of type [Array<DoubleArray>]. You must write -6.0 and not -6: the function [doubleArrayOf] expects [Double], and Kotlin does not convert the integer -6 to a floating-point number (Java does);
  • lines 13–14: [reals.indices] iterates over the rows, [reals[i].indices] iterates over the columns of row i;
  • line 18: [contentDeepToString()] displays an array of arrays;
  • line 22: a 3-row staircase array. The constructor [Array(3) { i -> ... \}] creates an array of 3 elements and calculates the value of each by executing the block between curly braces with the element’s index [i]. Each element here is itself an array of strings ([i + 1]), whose element with index [j] is "name" followed by i and j. The block between curly braces is a lambda (Chapter 7); for now, it’s enough to read it as “for index i, the value is...”. In Java, it took three steps: create the array of rows, create each row, then populate the elements;
  • line 30: an array of 3 booleans, all of which are [false];
  • line 31: an array of 2 nullable strings, all of which are [null];
  • line 36: the function [contentToString()] constructs a string from the array’s elements. Displaying the array directly ([println(values)]) would result, as in Java, in text like [I@6328d34a (the array type and a numeric code), rather than its contents;
  • lines 38–39: the first, last, and second-to-last elements. The functions [first()] and [last()] replace the expressions [values[0]] and [values[values.size - 1]]. Kotlin does not have an index counted from the end of the array (the C# equivalent of [^1]);
  • line 41: the elements at indices 1 and 2. As with [Arrays.copyOfRange] in Java, the end boundary is excluded;
  • line 44: the elements at indices 2 through [lastIndex]. [2..values.lastIndex] is a range: the set of integers from 2 to 4, inclusive. The function [sliceArray] returns a new array consisting of the elements whose indices belong to the range. Ranges are introduced with the loop [for];
  • lines 47–48: [copyOfRange] creates a copy; modifying [extract] does not modify [values];
  • line 50: the sum, maximum, and average of the elements. The average is a [Double];
  • line 51: the operator [in] checks whether a value is present in the array;
  • line 54: an array of objects, of type [Array<String>];
  • line 55: [friends.getOrNull(5)] returns [null] instead of throwing an exception when the index is out of bounds;
  • lines 58–60: index 3 does not exist: a [ArrayIndexOutOfBoundsException] is thrown, with a message indicating the incorrect index and the array size.

3.4. Kotlin’s basic statements

There are two types:

  • elementary instructions executed by the computer;
  • instructions that control the flow of the program.

Basic instructions become clear when considering the structure of a microcomputer and its peripherals:

Image

  • 1: reading information from the keyboard;
  • 2: processing information;
  • 3: writing information to the screen.

3.4.1. Writing to the Screen

There are various instructions for writing to the screen:

1
2
3
4
println(expression)
print(expression)
System.out.println(expression)
System.err.println(expression)

where [expression] is any data that can be converted to a character string for display on the screen. All objects have a method [toString()] that is used to perform this conversion. The function [println] writes the value and then moves to the next line; the function [print] does the same thing without moving to the next line; [println()], without any parameters, simply moves to the next line. For formatted output, use [println("format".format(value1, ...))], or use the Java method [System.out.printf(format, value1, ...)].

The Kotlin functions [println] and [print] (in the [kotlin.io] package) write to the Java stream [System.out]. The [System] class provides access to two write streams via its static fields [out] and [err], which are of type [PrintStream]:

  • [System.out.println()] writes to the stream [out], the standard output, which is usually associated with the screen;
  • [System.err.println()] writes to the [err] stream, the error output, which is also usually associated with the screen.

The Kotlin library does not have a function to write to the error output: [System.err.println] is used directly. The streams [out] and [err] can be redirected to files at runtime, as we’ll see shortly.

3.4.2. Reading Data Entered from the Keyboard

The data stream from the keyboard is represented by the Java field [System.in], which is of type [InputStream]. This is a byte stream, which is not very convenient to use directly. The Kotlin library provides two functions that read a line of text from this stream:

val line: String = readln()
val line: String? = readlnOrNull()

The line typed on the keyboard, without the line-end character, is stored in the variable [line] and can then be processed by the program. The two functions differ only in their behavior at the end of the input stream (when the user presses Ctrl-D, or Ctrl-Z followed by Enter on Windows, or when the file redirected to the input stream is exhausted):

  • [readln()] then throws a [ReadAfterEOFException] exception; it therefore always returns a string of type [String];
  • [readlnOrNull()] then returns [null]; its result is of type [String?], which requires handling this case, as the program [Basics06.kt] did.

These functions do not display a prompt: this is specified beforehand using [print]. In older programs, you may also encounter the function [readLine()], which behaves like [readlnOrNull()], as well as the Java classes [BufferedReader] and [Scanner], which are still usable. The [System.in] stream can be redirected to a file, just like the [out] and [err] streams. Finally, note the Java class [Console], derived from [System.console()], whose method [readPassword] reads a password without displaying it.

3.4.3. Example of Input/Output

The program [Basics09.kt] illustrates keyboard and screen input/output operations:

// keyboard / screen input-output
package basics09

fun main() {
    // writing to the System.out stream
    val obj = Any()
    println(obj)

    // writing to the System.err stream
    val i = 10
    System.err.println("i=$i")
    System.err.printf("i=%d%n", i)

    // reading a line typed on the keyboard, after displaying a prompt
    print("Type a line: ")
    val line = readln()
    println("line=$line")
}

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=basics09.Basics09Kt
java.lang.Object@2b2fa4f7
i=10
i=10
Type a line: here I am
line=here I am

Comments:

  • Line 6: [obj] is a reference to an object of class [Any], the root class of all Kotlin classes ([Object] in Java). The object is created by calling the [Any()] constructor, without [new];
  • Line 7: [obj] is printed to the screen. The [obj.toString()] method is called to retrieve the text to be displayed;
  • line 11: [err] is written to the stream;
  • line 12: the same thing, but with a format. The first parameter, "i=%d%n," is the display format; the other parameters are the values to be displayed: %d is replaced by the value of [i], and %n by the system's line-end character (\r\n on Windows, \n on Linux, and macOS);
  • line 15: a prompt is displayed without moving to the next line;
  • line 16: reads the line typed on the keyboard. [line] is of type [String].

In the results:

  • line 2: the output generated by line 7 of the code. The [toString()] method of the [Any] class returns the full class name of the object, followed by the @ character and a hexadecimal numeric code (its “hash code”), which may vary from one execution or machine to another. We can see that the Kotlin class *[Any]* is, at runtime, indeed the class *[java.lang.Object]*. Classes generally override this method to display something more useful, as we will do in Chapter 4;
  • lines 3–4: the two writes to the stream [err] appear on the screen just like those from the stream [out].

If the user closes the input stream instead of typing a line, [readln()] throws an exception, which terminates the program:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=basics09.Basics09Kt
java.lang.Object@2b2fa4f7
i=10
i=10
Type a line: Exception in thread "main" kotlin.io.ReadAfterEOFException: EOF has already been reached
    at kotlin.io.ConsoleKt.readln(Console.kt:152)
    at basics09.Basics09Kt.main(Basics09.kt:16)
    at basics09.Basics09Kt.main(Basics09.kt)

The exception message is followed by the call stack (“stack trace”): the list of functions that were executing at the time of the error, with the most recent at the top, along with the file and line number for each. It shows that the function [readln] (file [Console.kt] from the Kotlin library) was called on line 16 of the file [Basics09.kt], via the [main] method of the [Basics09Kt] class, which was itself called by the [main(String[])] method generated by the compiler. (With [gradlew run], Gradle also reports that the [run] task failed; its messages are not reproduced here.)

3.4.4. I/O Redirection

Windows, Linux, and macOS recognize three standard devices:

  • the standard input device, which by default refers to the keyboard and is numbered 0;
  • the standard output device, which by default refers to the screen and is numbered 1;
  • the standard error device, which by default refers to the screen and is numbered 2.

On the JVM, the write stream [System.out] (associated with the functions [println] and [print]) writes to device 1, the write stream [System.err] writes to device 2, and the read stream [System.in] (associated with the functions [readln] and [readlnOrNull]) reads data from device 0.

When you run a program in a command window, you can specify which devices will be 0, 1, and 2 for the program being executed. After the program arguments, you can redirect the standard input/output devices to files:

Redirection
Meaning
0<in.txt or <in.txt
Standard input stream No. 0 is redirected to the file in.txt. In the program, the stream [System.in] will therefore read its data from the file in.txt
1>out.txt or >out.txt
redirects output #1 to the file out.txt. In the program, the stream [System.out] will therefore write its data to the file out.txt
1>>out.txt or >>out.txt
Same as above, but the written data is appended to the current contents of the file out.txt
2>err.txt
Redirects output #2 to the file err.txt. In the program, the stream [System.err] will therefore write its data to the file err.txt
2>>err.txt
Same as above, but the written data is appended to the current contents of the file err.txt
1>out.txt 2>err.txt
Devices 1 and 2 are both redirected to files

Note that to redirect a program’s input/output streams to files, the program does not need to be modified: the operating system determines the nature of devices 0, 1, and 2. Consider the following program, [Basics10.kt]:

// redirections of the input-output streams
package basics10

fun main() {
    // reading the System.in stream
    val data = readln()
    // writing to the System.out stream
    println("writing to the out stream: $data")
    // writing to the System.err stream
    System.err.println("writing to the err stream: $data")
}

Redirections apply to the program launched by the command entered at the prompt. With ***[gradlew run]***, this program is Gradle itself, which then passes the streams to the Kotlin program: the redirection generally works, but the files may also receive messages from Gradle (warnings, compilation errors, etc.). To observe the program’s behavior on its own, it’s best to run it outside of Gradle. As we saw in Chapter 2, the task [installDist] prepares an installable version of the project in the [build/install/basics] folder (whose name, [basics], is set by the file [settings.gradle.kts]): a folder named [lib], which contains the project’s class archive and the Kotlin standard library, and a folder named [bin], which contains the launch scripts [basics] (Linux, macOS) and [basics.bat] (Windows). The script launches the class specified by the file [build.gradle.kts] when it is created; therefore, the program is selected using the command [installDist]:

> gradlew -q installDist -Pmain=basics10.Basics10Kt

Here is a session in a Linux terminal (bash), in the project folder:

> echo test > in.txt
> cat in.txt
test
> build/install/basics/bin/basics < in.txt > out.txt 2> err.txt
> cat out.txt
writing to the out stream: test
> cat err.txt
writing to the err stream: test
> build/install/basics/bin/basics < in.txt 2> err.txt
writing to the out stream: test
> build/install/basics/bin/basics < in.txt > out.txt
writing to the err stream: test

Comments:

  • line 1: We insert the string "test" into the file in.txt;
  • line 2: display the contents of the file in.txt for verification;
  • line 4: run the program. The stream [System.in] is redirected to the file in.txt, the stream [System.out] to the file out.txt, and the stream [System.err] to the file err.txt. The execution does not result in any output;
  • lines 5–6: The contents of the file out.txt show that the file in.txt was read and that writing to the stream [out] was redirected to out.txt;
  • lines 7–8: similar verification for the file err.txt;
  • lines 9–10: only the [err] stream is redirected: the screen displays only the write operation to the [out] stream;
  • lines 11–12: only the [out] stream is redirected: the screen displays only the write operation on the [err] stream.

It is clear that the streams [out] and [err] do not write to the same devices, since they could be redirected separately. The [err] stream is intended for error messages: this allows you to redirect a program’s output to a file while still seeing its error messages on the screen.

The operators >, >>, and 2> are written the same way in the Windows command prompt ([cmd]), in PowerShell, and in bash. The < operator is recognized by [cmd] and bash, but not by PowerShell, which reserves it for future use: in PowerShell, a file is sent to a program’s standard input using a pipe (|). The following commands, provided for illustrative purposes (they were not executed for this course), do on Windows what the bash session above does:

rem Windows command prompt (cmd)
echo test>in.txt
build\install\basics\bin\basics <in.txt >out.txt 2>err.txt
type out.txt
type err.txt

# PowerShell
"test" > in.txt
Get-Content in.txt | build\install\basics\bin\basics > out.txt 2> err.txt
Get-Content out.txt
Get-Content err.txt

3.4.5. Assigning the value of an expression to a variable

Here we are interested in the operation [variable = expression]. The expression can be arithmetic, relational, Boolean, string, etc.

3.4.5.1. Interpretation of the assignment operation

The operation [variable = expression] proceeds as follows:

  • The right-hand side of the assignment is evaluated: the result is a value V;
  • the value V is assigned to the variable.

In Java, as in C and C#, an assignment is itself an expression, whose value is V: so we can write [V1 = V2 = expression], which assigns the value of the expression to V2 and then to V1, or [if ((n = read()) > 0)], which both assigns and evaluates at the same time. In Kotlin, assignment is a statement, not an expression: it has no value and cannot appear where a value is expected. The compiler therefore rejects:

1
2
3
var v2 = 0
val v1 = (v2 = 7 * 6)
error: only expressions are allowed in this context.

Multiple assignments like [V1 = V2 = expression] are rejected for the same reason: they involve writing two assignments. This choice eliminates a common source of errors in C and Java: confusion between the assignment operator = and the comparison operator == in a test like [if (x = 0)] becomes impossible.

3.4.5.2. Arithmetic Expressions

The operators used in arithmetic expressions are as follows:

Operator
Role
+
addition
-
subtraction
*
multiplication
/
division: the result is the exact quotient if at least one of the operands is a real number. If both operands are integers, the result is the integer quotient. Thus, 5/2 equals 2 and 5.0/2 equals 2.5
%
remainder of division, regardless of the type of operands. The remainder has the same sign as the dividend: -5 % 2 equals -1. The % operator is another notation for the function [rem] (“remainder”): [a % b] is equivalent to [a.rem(b)]. The function [mod] returns the remainder in the mathematical sense, with the sign of the divisor: [(-5).mod(2)] equals 1

In Kotlin, all these operators are, in fact, alternative ways of writing functions: [a + b] is equivalent to [a.plus(b)], [a - b] to [a.minus(b)], [a * b] is equivalent to [a.times(b)], and [a / b] is equivalent to [a.div(b)]. The compiler, of course, translates these operations on the base types into the arithmetic instructions of the JVM class, without any function calls. However, this mechanism allows other classes to be equipped with these operators (operator overloading, Chapter 4); the Kotlin library has done this, in particular, for the Java class [BigDecimal].

There are various mathematical functions. Kotlin groups them in the [kotlin.math] package, which must be imported ([import kotlin.math.*] or [import kotlin.math.sqrt]...). Here are a few of them:

Function
Role
[sqrt(x)]
square root
[cos(x)], [sin(x)], [tan(x)]
cosine, sine, tangent (angle in radians)
[x.pow(y)]
x to the power of y (y is a real number or an integer)
[exp(x)]
exponential
[ln(x)], [log10(x)], [log(x, base)]
natural logarithm, decimal logarithm, in any base
[abs(x)] or [x.absoluteValue], [max(x, y)], [min(x, y)]
absolute value, maximum, minimum (for the types Int, Long, Float, Double)
[floor(x)], [ceil(x)], [truncate(x)]
lower integer part, upper integer part, truncation toward zero
[x.roundToInt()], [x.roundToLong()]
rounded to the nearest integer, with half values rounded up
[round(x)]
round to the nearest integer, rounding half values up to the nearest even number (actual result)
[x.coerceIn(min, max)]
value mapped to the interval [min, max]
[PI], [E]
the constants π and e

These are high-level functions (or, like [pow] and [roundToInt], extension functions called on the number itself): they are called without a prefix. Thus, we write:

    val y = 4.0
    val x = sqrt(y)

The static methods of the Java class [Math] ([Math.sqrt(y)], [Math.floorMod(-5, 2)]...) can still be used without importing them, since [Math] belongs to the [java.lang] package. The complete description of the [kotlin.math] package can be found in the Kotlin online documentation at kotlinlang.org/api/core/kotlin-stdlib/kotlin.math/.

Real numbers of type [Double] are stored in binary format: most decimal numbers, such as 0.1, are only approximated. This is inconsequential for scientific calculations but unacceptable for monetary amounts. The [BigDecimal] class (in the [java.math] package of JDK), on the other hand, represents a decimal number exactly, with as many digits as necessary. In Java, calculations are performed using these methods: [add], [subtract], [multiply], [divide]... In Kotlin, these methods remain available, but the standard library adds the operators +, -, *, /, %, and the unary - to the class: you write [a + b] instead of [a.add(b)]. It also adds the conversion functions ["0.1".toBigDecimal()] and [3.toBigDecimal()]. A [BigDecimal] is immutable: an operation does not modify the object to which it is applied; instead, it returns a new object.

The program [Basics11.kt] is as follows:

// arithmetic expressions
package basics11

import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import kotlin.math.*

fun main() {
    // integer division and real division
    println("5 / 2 = ${5 / 2}, 5.0 / 2 = ${5.0 / 2}, 5 / 2.0 = ${5 / 2.0}")
    // remainder of the division: the % operator (rem function) and the mod function
    println("5 % 2 = ${5 % 2}, -5 % 2 = ${-5 % 2}, 5.5 % 2 = ${5.5 % 2}")
    println("(-5).rem(2) = ${(-5).rem(2)}, (-5).mod(2) = ${(-5).mod(2)}")
    // real division by zero: no error, infinity or NaN (Not a Number)
    val zero = 0.0
    println("1 / 0.0 = ${1 / zero}, 0 / 0.0 = ${0 / zero}")
    // integer division by zero: an exception
    val integerZero = "0".toInt()
    try {
        println(1 / integerZero)
    } catch (e: ArithmeticException) {
        println("1 / 0 : ${e::class.simpleName} - ${e.message}")
    }

    // a few functions of the kotlin.math package
    val y = 4.0
    val x = sqrt(y)
    println("sqrt(4.0) = $x")
    println("2.0.pow(10) = ${2.0.pow(10)}, 2.0.pow(0.5) = ${2.0.pow(0.5)}")
    println("abs(-3.5) = ${abs(-3.5)}, max(3, 7) = ${max(3, 7)}, (-7).absoluteValue = ${(-7).absoluteValue}")
    println("PI = $PI, cos(PI) = ${cos(PI)}")
    println("exp(1.0) = ${exp(1.0)}, ln(E) = ${ln(E)}, log10(1000.0) = ${log10(1000.0)}")
    println("floor(-2.5) = ${floor(-2.5)}, ceil(-2.5) = ${ceil(-2.5)}, truncate(-2.5) = ${truncate(-2.5)}")
    // roundToInt rounds to the nearest integer, halves upwards; it returns an Int
    println("2.5.roundToInt() = ${2.5.roundToInt()}, (-2.5).roundToInt() = ${(-2.5).roundToInt()}")
    // round rounds halves to the nearest even number; it returns a Double
    println("round(2.5) = ${round(2.5)}, round(3.5) = ${round(3.5)}")
    // coerceIn brings a value back into a range
    println("150.coerceIn(0, 100) = ${150.coerceIn(0, 100)}")
    // the methods of Java's Math class remain usable
    println("Math.sqrt(4.0) = ${Math.sqrt(4.0)}, Math.round(2.5) = ${Math.round(2.5)}")

    // precedence: * / % before + -
    println("2 + 3 * 4 = ${2 + 3 * 4}, (2 + 3) * 4 = ${(2 + 3) * 4}")
    println("10 - 4 - 3 = ${10 - 4 - 3}, \"x\" + 1 + 2 = ${"x" + 1 + 2}")

    // Double reals are binary approximations
    println("0.1 + 0.2 = ${0.1 + 0.2}")
    // BigDecimal computes exactly in base 10, with the + - * / operators
    val a = BigDecimal("0.1")
    val b = "0.2".toBigDecimal()
    println("BigDecimal : 0.1 + 0.2 = ${a + b}")
    val price = BigDecimal("19.99")
    val total = price * 3.toBigDecimal()
    println("3 x 19.99 = $total, with VAT = ${total * BigDecimal("1.2")}")
    // rounding to 2 decimals, halves upwards (commercial rounding)
    println("2.565 rounded = ${BigDecimal("2.565").setScale(2, RoundingMode.HALF_UP)}")
    // the / operator keeps the number of decimals of the dividend
    val three = 3.toBigDecimal()
    println("10 / 3 = ${BigDecimal.TEN / three}, 10.00 / 3 = ${BigDecimal("10.00") / three}")
    // the divide method with a precision
    println("10 / 3 = ${BigDecimal.TEN.divide(three, MathContext.DECIMAL64)}")
}

Results:

> gradlew -q run -Pmain=basics11.Basics11Kt
5 / 2 = 2, 5.0 / 2 = 2.5, 5 / 2.0 = 2.5
5 % 2 = 1, -5 % 2 = -1, 5.5 % 2 = 1.5
(-5).rem(2) = -1, (-5).mod(2) = 1
1 / 0.0 = Infinity, 0 / 0.0 = NaN
1 / 0 : ArithmeticException - / by zero
sqrt(4.0) = 2.0
2.0.pow(10) = 1024.0, 2.0.pow(0.5) = 1.4142135623730951
abs(-3.5) = 3.5, max(3, 7) = 7, (-7).absoluteValue = 7
PI = 3.141592653589793, cos(PI) = -1.0
exp(1.0) = 2.718281828459045, ln(E) = 1.0, log10(1000.0) = 3.0
floor(-2.5) = -3.0, ceil(-2.5) = -2.0, truncate(-2.5) = -2.0
2.5.roundToInt() = 3, (-2.5).roundToInt() = -2
round(2.5) = 2.0, round(3.5) = 4.0
150.coerceIn(0, 100) = 100
Math.sqrt(4.0) = 2.0, Math.round(2.5) = 3
2 + 3 * 4 = 14, (2 + 3) * 4 = 20
10 - 4 - 3 = 3, "x" + 1 + 2 = x12
0.1 + 0.2 = 0.30000000000000004
BigDecimal : 0.1 + 0.2 = 0.3
3 x 19.99 = 59.97, with VAT = 71.964
2.565 rounded = 2.57
10 / 3 = 3, 10.00 / 3 = 3.33
10 / 3 = 3.333333333333333

Comments:

  • line 7: all declarations from the [kotlin.math] package are imported;
  • line 11: division of two integers is integer division. Each operation is written in its own template: unlike with Java concatenation, there is no need to worry about operator precedence relative to the + operator, which constructs the string;
  • line 13: the remainder has the same sign as the dividend; the % operator also applies to real numbers;
  • line 14: [rem] is the function of the % operator; [mod] returns a remainder with the same sign as the divisor, just like [Math.floorMod] in Java. The parentheses in [(-5)] are necessary: [-5.mod(2)] would be interpreted as [-(5.mod(2))], because a function call using the dot operator takes precedence over the unary minus operator;
  • Line 17: Dividing a real number by zero does not cause an error; it returns infinity ([Infinity]) or the special value [NaN] (“Not a Number”) for 0/0. In [1 / zero], the integer 1 is divided by a real number: the operation is real. Kotlin does not perform implicit conversions between numeric types, but arithmetic operators accept operands of different types: the class [Int] has a function [div(other: Double): Double];
  • lines 19–23: however, dividing an integer by zero throws a [ArithmeticException] exception. The divisor is obtained by converting the string "0" so that the compiler does not know its value;
  • lines 27–28: the example above. [sqrt] expects and returns a [Double]: 2.0. Since Kotlin does not implicitly convert a [Int] to a [Double], [sqrt(4)] would be rejected; we write [sqrt(4.0)];
  • line 30: the function [pow] is called on the number to be raised to a power;
  • Line 31: [absoluteValue] is an extension property, which is used without parentheses;
  • Line 34: [floor] rounds down, [ceil] rounds up, and [truncate] rounds to zero. To obtain an integer by truncating a real number, convert it using [toInt()], as we will see later;
  • line 36: [roundToInt] rounds to the nearest integer; a value exactly halfway between two integers is rounded up: 2.5 becomes 3 and -2.5 becomes -2. The result is a [Int]. This is how Java’s [Math.round] behaves;
  • line 38: Note that Kotlin’s [round] function does not behave the same way as Java’s [Math.round]: it rounds a value exactly halfway to the nearest even integer (“banking” rounding): 2.5 becomes 2 and 3.5 becomes 4. The result is a [Double]. This is the behavior of Java’s [Math.rint];
  • line 39: [coerceIn] maps 150 to the interval [0, 100], just like [Math.clamp] (Java 21). The functions [coerceAtLeast] and [coerceAtMost] set only one bound;
  • line 42: the methods of the Java class [Math];
  • line 45: operator precedence, shown below;
  • line 46: "x" + 1 + 2 is evaluated from left to right: "x" + 1 yields the string "x1," to which 2 is concatenated. To perform addition first, one must write "x" + (1 + 2); using a template, one would write ["x${1 + 2\}"];
  • line 49: 0.1 and 0.2 do not have exact binary representations: their sum is not exactly 0.3;
  • lines 51–52: two [BigDecimal] values created from strings. Do not write [BigDecimal(0.1)]: the parameter would be [Double] 0.1, whose exact value is 0.1000000000000000055511151231257827021181583404541015625. The function [0.1.toBigDecimal()], which uses the string "0.1", works, however;
  • line 53: the exact addition, written using the + operator;
  • lines 54–56: [3.toBigDecimal()] converts an integer. A product has as many decimal places as the sum of its two factors: 59.97 × 1.2 = 71.964;
  • line 58: [setScale(2, RoundingMode.HALF_UP)] rounds to two decimal places, with values halfway between two digits rounded away from zero: this is commercial rounding;
  • line 61: a trap. Kotlin’s / operator calls the method [divide(divisor, RoundingMode.HALF_EVEN)]: the quotient retains the number of decimal places of the dividend, rounded to the nearest value. Since 10 has no decimal places, 10/3 equals 3; 10.00/3 equals 3.33;
  • line 63: the Java method [divide], with a precision specified by [MathContext.DECIMAL64] (16 significant digits). Without specifying precision or rounding mode, the [divide] method would call [ArithmeticException]: [Non-terminating decimal expansion; no exact representable decimal result.], because 10/3 has no finite decimal representation. For divisions of [BigDecimal], we therefore explicitly choose the precision or the number of decimal places and the rounding mode: [a.divide(b, 2, RoundingMode.HALF_UP)].

3.4.5.3. Operators in Arithmetic Expression Evaluation

The order of operator precedence when evaluating an arithmetic expression is as follows (from highest to lowest):

[function calls, a.f()], [unary -], [*, /, %], [+, -]

Operators within the same block [ ] have the same precedence. When precedence is equal, evaluation proceeds from left to right: 10 - 4 - 3 evaluates to (10 - 4) - 3 = 3. Parentheses can be used to specify a different order of evaluation.

3.4.5.4. Relational Expressions

The operators are as follows: <, <=, , !=, >, >=; Kotlin adds === and ! (see below). Their precedence:

  • >, >=, <, <=;
  • , !=, =, !==.

The result of a relational expression is the Boolean [false] if the expression is false, and [true] otherwise.

    val x = 5
    val end = x > 4

Comparison of Two Characters

Let C1 and C2 be two characters. They can be compared using the operators <, <=, ==, !=, >, >=. Their Unicode codes—which are numbers—are compared. According to the Unicode order, the following relationships hold:

space < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. < 'z' < .. < 'é'

The Unicode code for a character [c] is given by its property [c.code], which is of type [Int]. In Kotlin, a [Char] is not a number: you cannot write ['a' + 'b'] or assign a character to an integer variable, which Java allows.

Comparing Two Strings

They are compared character by character. The first inequality encountered between two characters results in an inequality of the same sign for the strings. Consider comparing the strings "Cart" and "Cat":

1
2
3
4
5
"Cart"    "Cat"
---------------------
'C'   =   'C'
'a'   =   'a'
'r'   <   't'

This last inequality allows us to conclude that "Cart" < "Cat." Consider comparing the strings "Cat" and "Catkin." They are equal until the string "Cat" is exhausted. In this case, the exhausted string is declared the "smaller" one. We therefore have the relationship "Cat" < "Catkin."

Structural Equality and Referential Equality

In this regard, Kotlin corrects a classic pitfall in Java. In Java, when applied to two strings—or to any two objects—the operators == and != compare references: [s1 == s2] is true if the two variables point to the same object in memory, not if the two strings contain the same characters; to compare the contents, you must write [s1.equals(s2)]. In Kotlin:

  • the == operator compares the contents: [a == b] is translated by the compiler into a call to the method [a.equals(b)], preceded by a test for [null]: if [a] equals [null], [a == b] equals [b === null], without the risk of [NullPointerException]. This is structural equality; != is its negation;
  • the === operator compares references: [a === b] is true if [a] and [b] refer to the same object. This is referential equality; !== is its negation. We rarely need it.

For primitive types ([Int], [Double]...), == simply compares the values. C# behaves the same way as Kotlin for strings: its == operator compares their contents.

The Order of Two Strings

The operators < <= > >= apply to strings, just as they do to any comparable object: in Kotlin, [a < b] is another way of writing [a.compareTo(b) < 0]. The method [compareTo] returns an integer:

  • zero if the two strings are equal;
  • positive if string #1 is greater than string #2;
  • negative if string #1 is less than string #2.

The Kotlin functions [equals] and [compareTo] accept a [ignoreCase] parameter to ignore case: [s1.equals(s2, ignoreCase = true)] (the Java functions [equalsIgnoreCase] and [compareToIgnoreCase]). The syntax [ignoreCase = true] is that of a named argument, which we will encounter again with the functions.

[compareTo] compares Unicode codes character by character: its result is the difference between the codes of the first two different characters, or the difference in lengths if one of the strings is the beginning of the other. This is not lexicographical order: since the code for 'é' (233) is greater than that for 'f' (102), "élan" comes after "face." To sort text intended for users, a [Collator] object is used (from the [java.text] package of JDK), which applies the sorting rules for a specific language. [compareTo] remains the correct method for identifiers, keywords, and file names.

The [Basics12.kt] program is as follows:

// relational expressions and string comparison
package basics12

import java.math.BigDecimal
import java.text.Collator
import java.util.Locale

fun main() {
    val x = 5
    val end = x > 4
    println("x > 4 : $end, x == 5 : ${x == 5}, x != 5 : ${x != 5}")

    // comparison of characters: their Unicode codes are compared
    println("'a' < 'b' : ${'a' < 'b'}, 'Z' < 'a' : ${'Z' < 'a'}, '9' < 'A' : ${'9' < 'A'}")
    println("codes : 'A'=${'A'.code}, 'a'=${'a'.code}, 'é'=${'é'.code}")

    // comparison of strings: == compares the contents, compareTo and < > the order
    val string1 = "cat"
    val string2 = "dog"
    val n = string1.compareTo(string2)
    val equal = string1 == string2
    println("n=$n, equal=$equal, string1 < string2 : ${string1 < string2}")
    println("\"Cat\".compareTo(\"Catkin\") : ${"Cat".compareTo("Catkin")}")

    // == compares the contents, === the references
    val string3 = "CAT".lowercase()
    println("string3=$string3, string1 == string3 : ${string1 == string3}")
    println("string1 === string3 : ${string1 === string3}, string1 === \"cat\" : ${string1 === "cat"}")
    // == accepts the null value
    val absent: String? = System.getProperty("unknown.property")
    println("absent == string1 : ${absent == string1}, absent == null : ${absent == null}")

    // compareTo compares the Unicode codes, character by character
    println("\"élan\".compareTo(\"face\") : ${"élan".compareTo("face")}")
    println("\"Cat\".compareTo(\"cat\") : ${"Cat".compareTo("cat")}")
    // ignoring case (upper / lower case)
    println("compareTo with ignoreCase = true : ${"Cat".compareTo("cat", ignoreCase = true)}")
    println("\"Cat\" == \"cat\" : ${"Cat" == "cat"}")
    println("\"Cat\".equals(\"cat\", ignoreCase = true) : ${"Cat".equals("cat", ignoreCase = true)}")
    // a Collator compares according to the sorting rules of a language
    val collator = Collator.getInstance(Locale.FRENCH)
    println("collator.compare(\"élan\", \"face\") : ${collator.compare("élan", "face")}")
    println("collator.compare(\"Cat\", \"cat\") : ${collator.compare("Cat", "cat")}")

    // == calls equals: for a BigDecimal, 2.0 and 2.00 are different
    val m1 = BigDecimal("2.0")
    val m2 = BigDecimal("2.00")
    println("m1 == m2 : ${m1 == m2}, m1.compareTo(m2) : ${m1.compareTo(m2)}, m1 >= m2 : ${m1 >= m2}")
}

Results:

> gradlew -q run -Pmain=basics12.Basics12Kt
x > 4 : true, x == 5 : true, x != 5 : false
'a' < 'b' : true, 'Z' < 'a' : true, '9' < 'A' : true
codes : 'A'=65, 'a'=97, 'é'=233
n=-1, equal=false, string1 < string2 : true
"Cat".compareTo("Catkin") : -3
string3=cat, string1 == string3 : true
string1 === string3 : false, string1 === "cat" : true
absent == string1 : false, absent == null : true
"élan".compareTo("face") : 131
"Cat".compareTo("cat") : -32
compareTo with ignoreCase = true : 0
"Cat" == "cat" : false
"Cat".equals("cat", ignoreCase = true) : true
collator.compare("élan", "face") : -1
collator.compare("Cat", "cat") : 1
m1 == m2 : false, m1.compareTo(m2) : 0, m1 >= m2 : true

Comments:

  • Line 10: a relational expression; [end] is of type [Boolean];
  • line 14: character comparison;
  • line 15: the property [code] of a character returns its Unicode code (in Java, this is written as [(int) 'A']);
  • line 20: the variable [n] is assigned a negative integer because "cat" < "dog": the difference between the codes for 'c' (99) and 'd' (100);
  • line 21: the variable [equal] has the value [true] if the two strings have the same content, and [false] otherwise;
  • line 22: the < operator applied to two strings;
  • line 23: the string that runs out first is the shorter one; the result is the difference in lengths: 3 - 6. In a string literal, \" denotes a quotation mark;
  • line 26: the function [lowercase()] constructs, at runtime, a new string object that contains "cat";
  • lines 27–28: [string1] and [string3] have the same content but refer to two different objects: == returns [true], === returns [false]. However, [string1 === "cat"] is true: two identical string literals are stored only once in memory;
  • line 30: the Java method [System.getProperty] returns the value of a property of JVM, or [null] if it does not exist; its result is stored in a variable of type [String?];
  • line 31: The == operator accepts a [null] value on the left: it returns [false], whereas the Java call to [absent.equals(string1)] would return [NullPointerException];
  • line 34: 233 - 102 = 131: for [compareTo], "élan" comes after "face";
  • line 35: 'C' (67) - 'c' (99) = -32;
  • lines 37–39: ignoring case, “Cat” and “cat” are equal;
  • lines 41–43: a [Collator] for French: “élan” precedes “face,” and “Cat” follows “cat” (when the letters are the same, the lowercase letter comes before the uppercase one). The [compare] method returns a negative, zero, or positive integer, just like [compareTo];
  • lines 46–48: since == calls [equals], it inherits the characteristics of that method. The behavior of the [BigDecimal] class takes the number of decimal places into account: 2.0 and 2.00 are different for [equals], but equal for [compareTo], and therefore for the operators <<, <=, >>, and >=. To compare two [BigDecimal] amounts, you should therefore use [compareTo(...) == 0], not ==.

3.4.5.5. Boolean Expressions

The operators that can be used are AND (&&), OR (||), and NOT (!). The result of a Boolean expression is a Boolean value. Operator precedence:

  • ! ;
  • && ;
  • ||.
    val x = 3.5
    val valid = x > 2 && x < 4

Relational operators have higher precedence than the && and || operators. These two operators evaluate their right-hand operand only if necessary: in [a && b], b is not evaluated if a is false; in [a || b], b is not evaluated if a is true. This is referred to as “short-circuit evaluation.” You can therefore safely write [text != null && text.length > 0]: on the right-hand side, the compiler knows that [text] is not equal to [null] (smart type conversion). The functions [and] and [or], when applied to two Booleans ([a and b], [a or b]), perform the same calculation but always evaluate both operands; the function [xor] is the exclusive OR. The test to determine whether a value falls within an interval is written more simply using the operator [in]: [x in 2.0..4.0] is true if 2 ≤ x ≤ 4.

3.4.5.6. Bitwise Operations

Kotlin does not have symbolic operators for bitwise operations: Java’s << >> >>> & | ^ ~ are replaced by infix functions—that is, single-argument functions written between their two operands, without a period or parentheses: [i shl 4] is another way of writing [i.shl(4)]. Let [i] and [j] be two integers ([Int] or [Long]):

Operation
In Java
Role
i shl n
i << n
shifts i n bits to the left. The incoming bits are zeros
i shr n
i >> n
shift i n bits to the right. The sign bit is preserved
i ushr n
i >>> n
Unsigned right shift: the incoming bits are zeros, even if i is negative
i and j
i & j
performs a bit-by-bit logical AND of i and j
i or j
i|j
performs the bit-by-bit logical OR of i and j
i xor j
i ^ j
performs the exclusive OR operation on i and j bit by bit
i.inv()
~i
complements i to 1

Shifts are defined only for the types [Int] and [Long] (and for the unsigned types [UInt] and [ULong], for which the shift [shr] always fills with zeros): For a [Short] or a [Byte], the value is first converted to [Int]. The compiler thus rejects [s shl 1] if [s] is a [Short]: [unresolved reference 'shl' on receiver of type 'Short']. These operations are primarily used to manipulate binary data; the explicit names have the advantage of not being confused with the Boolean operators && and ||.

The program [Basics13.kt] is as follows:

// bit manipulation
package basics13

fun main() {
    val i = 100
    val j = -13
    // %08x: the integer in hexadecimal, on 8 digits (32 bits)
    println("i=0x%08x, j=0x%08x".format(i, j))
    println("i shl 4=0x%08x, i shr 4=0x%08x".format(i shl 4, i shr 4))
    println("i and j=0x%08x, i or j=0x%08x".format(i and j, i or j))
    println("i xor j=0x%08x, i.inv()=0x%08x".format(i xor j, i.inv()))
    println("j shl 2=0x%08x, j shr 2=0x%08x".format(j shl 2, j shr 2))
    // the same values in binary (16 low-order bits)
    println("i=${binary(i, 16)}, j=${binary(j, 16)}, i and j=${binary(i and j, 16)}")
    // unsigned right shift ushr: the incoming bits are zeros
    val m = -16
    println("m=${binary(m, 32)}")
    println("m shr 2=${binary(m shr 2, 32)}")
    println("m ushr 2=${binary(m ushr 2, 32)}")
    // an unsigned integer: shr brings in zeros
    val k = 0xF123_0000U
    println("k=${k.toString(16)}, k shr 4=${(k shr 4).toString(16)}, k=$k")
}

// the n low-order bits of value, as a string of 0s and 1s
fun binary(value: Int, n: Int): String = buildString {
    for (rank in n - 1 downTo 0) {
        append((value ushr rank) and 1)
    }
}

Results:

> gradlew -q run -Pmain=basics13.Basics13Kt
i=0x00000064, j=0xfffffff3
i shl 4=0x00000640, i shr 4=0x00000006
i and j=0x00000060, i or j=0xfffffff7
i xor j=0xffffff97, i.inv()=0xffffff9b
j shl 2=0xffffffcc, j shr 2=0xfffffffc
i=0000000001100100, j=1111111111110011, i and j=0000000001100000
m=11111111111111111111111111110000
m shr 2=11111111111111111111111111111100
m ushr 2=00111111111111111111111111111100
k=f1230000, k shr 4=f123000, k=4045602816

Comments:

  • lines 5–6: two integers of type [Int] (32 bits);
  • line 8: the %08x specifier displays an 8-digit hexadecimal (x) integer padded with zeros (08), representing its 32 bits: -13 becomes fffffff3;
  • line 9: shifting 100 (0x64) 4 bits to the left is equivalent to multiplying it by 2⁴ (0x640); shifting it to the right is equivalent to dividing it by 2⁴ (0x6);
  • Lines 10–11: AND, OR, XOR (bitwise exclusive OR), and the one’s complement. Note that the function [inv] is called like a regular method, with a period and parentheses;
  • Line 12: Shifts of a negative number. [shr] preserves the sign bit: -13 shifted 2 bits to the right yields -4 (0xfffffffc);
  • Line 14: The function [binary] (lines 26–28) displays the n least significant bits of an integer. The logical AND of i and j is checked bit by bit;
  • lines 16–19: the [shr] shift preserves the sign bit of m, which is negative; the [ushr] shift fills with zeros;
  • lines 21–22: an unsigned integer [UInt], whose most significant bit is 1: it is equal to 4,045,602,816, whereas a [Int] with the same bits would be negative. Its shift [shr] inserts zeros. The function [toString(16)] writes it in hexadecimal;
  • Line 26: The function [buildString] constructs a string by successive additions: in the following block, the function [append] adds an element to the string currently being constructed. It will be presented in Chapter 5. The loop [for (rank in n - 1 downTo 0)] iterates over [rank] from n-1 to 0 in descending order; the loops are described later;
  • line 28: [(value ushr rank) and 1] sets bit No. [rank] to 0 and then separates it from the others.

3.4.5.7. Combination of Operators

[a = a + b] can be written as [a += b]; [a = a - b] can be written as [a -= b]. The same applies to the *, /, and % operators. Thus, [a = a / 2] can be written as [a /= 2]. Since bitwise operations are functions, they do not have a combined operator: we write [a = a shl 2]. Like simple assignments, compound assignments are statements, not expressions. They do not perform any implicit conversions: if [s] is a [Short], [s += 1] is rejected, as is [s = s + 1], because the result of the addition is a [Int]; we write [s = (s + 1).toShort()]. [Short] and [Byte] are, in fact, rarely used in calculations.

3.4.5.8. Increment and Decrement Operators

The notation [variable++] means [variable = variable + 1] or [variable += 1]. The notation [variable--] means [variable = variable - 1] or [variable -= 1]. They apply to a variable [var]. When used in an expression, these operators have two forms:

  • [i++] (postfix): the value of the expression is the value of i before the increment;
  • [++i] (prefixed): the value of the expression is the value of i after the increment.

Kotlin has retained them, even though assignment is not an expression; however, you should avoid using them within larger expressions, where they impair readability.

3.4.5.9. No ternary operator: the if expression

Java, like C and C#, has a ternary operator: the expression [cond ? expr1 : expr2] evaluates to [expr1] if the condition [cond] is true, and to [expr2] otherwise. Kotlin does not have a ternary operator; the compiler rejects [val z = x > 4 ? x + 1 : x - 1] with the message [syntax error: Unexpected tokens (use ';' to separate expressions on the same line)]. It does not need one, because its statement *[if] is also an expression* that has a value:

val i = if (j > 4) j + 1 else j - 1

The expression [if (cond) expr1 else expr2] is evaluated as follows:

  • The expression [cond] is evaluated. It is a Boolean expression;
  • if it is true, the value of the expression is that of [expr1], and [expr2] is not evaluated;
  • if it is false, the opposite occurs: the value of the expression is that of [expr2], and [expr1] is not evaluated.

The operation above will assign the value j+1 to the variable [i] if j > 4, and j-1 otherwise. This is the same as writing [if (j > 4) i = j + 1 else i = j - 1], but it is more concise, and [i] can be declared along with [val]. When used as an expression, [if] must have a branch [else] so that the expression evaluates to a value in all cases. We’ll find [if] in the “Simple Selection Structure” section, and [when]—which is also an expression—in the “Case Structure” section.

3.4.5.10. Null Operators

Unlike Java, Kotlin has operators dedicated to the value [null]. We encountered them in the “Null Safety” section; here is a summary. Let [a] be a nullable expression:

Operator
Role
[a?.member]
Safe call: evaluates to [a.member] if a is not [null], and to [null] otherwise (the member is then not evaluated: a function is not called). Safe calls are chained: [a?.b?.c] evaluates to [null] as soon as any of the links evaluates to [null]
[a ?: b]
Elvis operator: evaluates to a if a is not equal to [null], and b otherwise (b is then evaluated only if a is equal to [null]). It is used to provide a fallback value
[a!!]
assertion: evaluates to a if a is not equal to [null]; raises a [NullPointerException] otherwise
[a?.let { ... \}]
executes the block, where the value of a is called [it], only if a is not equal to [null]

These are the equivalents of the ?. and ?? operators in C#. Java, which does not have these, uses the ternary operator ([a != null ? a : b]), the method [Objects.requireNonNullElse(a, b)], or the class [Optional] and its method [map] instead. The right-hand side of the Elvis operator can also be a statement that interrupts normal execution: [val n = input ?: return] exits the function if [input] evaluates to [null], and [val n = input ?: throw IllegalArgumentException("missing input")] throws an exception; we’ll revisit this form when discussing exceptions.

The program [Basics14.kt] combines these different operators:

// other operators
package basics14

// returns the value associated with a key, or null if the key is unknown
fun valueOf(key: String): String? = if (key == "title") "Kotlin by example" else null

fun main() {
    // boolean expressions
    val x = 3.5
    val valid = x > 2 && x < 4
    println("valid=$valid, !valid=${!valid}, valid || x > 10 : ${valid || x > 10}")
    println("x in 2.0..4.0 : ${x in 2.0..4.0}, valid xor true : ${valid xor true}")

    // compound operators
    var a = 10
    a += 5
    print("a += 5 : $a")
    a *= 2
    print(", a *= 2 : $a")
    a /= 4
    print(", a /= 4 : $a")
    a %= 4
    print(", a %= 4 : $a")
    // no compound operator for bit operations
    a = a shl 2
    println(", a = a shl 2 : $a")

    // increment and decrement
    var i = 5
    val j = i++    // j receives the old value of i, then i is incremented
    val k = ++i    // i is incremented, then k receives the new value of i
    println("i=$i, j=$j, k=$k")
    i--
    println("i=$i")

    // no ternary operator: if is an expression
    val n = 5
    val p = if (n > 4) n + 1 else n - 1
    val parity = if (n % 2 == 0) "even" else "odd"
    println("p=$p, $n is $parity")

    // replacement value if a reference is null: the Elvis operator ?:
    val input = valueOf("name")
    val name = input ?: "unknown"
    val age: Int? = "twenty".toIntOrNull()
    val displayedAge = age ?: -1
    println("name=$name, displayedAge=$displayedAge")
    // assignment only if the variable is null
    var title = valueOf("subtitle")
    if (title == null) {
        title = "Untitled"
    }
    println("title=$title")
    // access to a member only if the object is not null: the safe call ?.
    var text = valueOf("text")
    println("length=${text?.length}")
    text = valueOf("title")
    println("length=${text?.length ?: -1}")
    // chained safe calls
    println("first word: ${valueOf("title")?.split(" ")?.first()?.uppercase()}")
    println("first word: ${valueOf("author")?.split(" ")?.first()?.uppercase() ?: "none"}")
    // the !! assertion: NullPointerException if the value is null
    println("title!!.length=${valueOf("title")!!.length}")
}

Results:

> gradlew -q run -Pmain=basics14.Basics14Kt
valid=true, !valid=false, valid || x > 10 : true
x in 2.0..4.0 : true, valid xor true : false
a += 5 : 15, a *= 2 : 30, a /= 4 : 7, a %= 4 : 3, a = a shl 2 : 12
i=7, j=5, k=7
i=6
p=6, 5 is odd
name=unknown, displayedAge=-1
title=Untitled
length=null
length=17
first word: KOTLIN
first word: none
title!!.length=17

Comments:

  • line 5: a function that returns a string for the "title" key, and [null] for any other key. It provides the program with values of type [String?];
  • line 10: a Boolean expression;
  • line 12: the same range test, written using the [in] operator; [valid xor true] is the exclusive OR of two Booleans;
  • Lines 15–26: Combined operators: 10 + 5 = 15, 15 × 2 = 30, 30 ÷ 4 = 7 (integer division), 7 % 4 = 3, 3 shl 2 = 12. The variable [a] is declared along with [var], since its value changes;
  • line 30: [j] is assigned 5, then [i] is set to 6;
  • line 31: [i] changes to 7, then [k] is assigned 7;
  • lines 38–39: two [if] expressions, which replace Java’s ternary operators;
  • lines 43–44: [input] is equal to [null]: [name] is assigned “unknown.” Its type is [String], not [String?]: the Elvis operator ensures that [name] is never [null];
  • Line 46: [age], of type [Int?], is equal to [null]: [displayedAge], of type [Int], is assigned -1. There is no unboxing and no risk of an exception;
  • lines 49–51: the assignment occurs only if [title] equals [null]. We could have written this more simply as [val title = valueOf("subtitle") ?: "Untitled"];
  • line 56: [text] equals [null]; [text?.length] equals [null], without the property [length] having been read; a template displays [null];
  • line 58: [text] is "Kotlin by example": its length is 17. The Elvis operator would return -1 for a value of [null];
  • line 60: a chain of safe calls. [split(" ")] splits the string into a list of words (Chapter 5), [first()] returns the first word, and [uppercase()] capitalizes it;
  • line 61: the same sequence of calls applied to a value [null]: no calls are made, the expression evaluates to [null], and the Elvis operator replaces it with "none";
  • line 63: the assertion !!. [valueOf("title")] is of type [String?], but the programmer knows that it does not equal [null] for the “title” key; the assertion allows [length] to be used directly. For the "author" key, it would have returned a [NullPointerException].

3.4.5.11. General Operator Precedence

The following table lists the operator precedence in Kotlin, from highest to lowest:

Operators
Associativity
[x.y] [x?.y] [f(x)] [a[i]] [x++] [x--] [x!!] (postfixed)
LR
[+x] [-x] [!x] [++x] [--x] (prefixed)
RL
[as] [as?] (type change)
LR
[*] [/] [%]
LR
[+] [-]
LR
[..] [..<] (intervals)
LR
infix functions: [shl] [shr] [ushr] [and] [or] [xor] [downTo] [step] [until]...
LR
[?:] (Elvis)
LR
[in] [!in] [is] [!is]
LR
[<] [<=] [>] [>=]
LR
[==] [!=] [===] [!==]
LR
[&&]
LR
[||]
LR
[=] [+=] [-=] [*=] [/=] [%=] (instructions)
 

"LR" indicates that, when operators have equal precedence, left-to-right order of precedence is followed: when an expression contains operators of the same precedence, the operator farthest to the left is evaluated first. "RL" indicates right-to-left order of precedence. Three important points to note are:

  • infix functions have lower precedence than arithmetic operators: [1 shl 2 + 3] equals [1 shl 5], which is 32;
  • they have higher precedence than comparisons: [x and 1 == 0] evaluates to [(x and 1) == 0], which is indeed what is intended (in Java, [x & 1 == 0] is rejected because == has higher precedence than &);
  • the Elvis operator has lower precedence than arithmetic operators: [a ?: 0 + 1] evaluates to [a ?: (0 + 1)]. To add 1 to the result, you must write [(a ?: 0) + 1], as the program [Basics06.kt] did.

When in doubt, don’t hesitate to use parentheses: they also make the expression easier to read.

3.4.5.12. Type conversions

In an expression, it is possible to temporarily change the encoding of a value. This is called changing the data type, or “type casting.” Java does this in two ways:

  • widening conversions—from a [int] to a [long] or a [double], for example—are performed automatically by the compiler;
  • narrowing conversions—which may result in a loss of information, such as from a [double] to a [int]—are specified by the programmer using the notation [(type) value].

Kotlin is stricter: it does not perform any implicit conversions between numeric types, not even widening conversions. Any conversion must be explicitly requested using one of the functions [toByte()], [toShort()], [toInt()], [toLong()], [toFloat()], or [toDouble()], which each numeric type possesses (and [toUInt()], [toULong()]... for unsigned types). The compiler therefore rejects:

1
2
3
4
5
val i = 3
val l: Long = i
error: initializer type mismatch: expected 'Long', actual 'Int'.
val f: Float = i / 4
error: initializer type mismatch: expected 'Float', actual 'Int'.

You must write [val l: Long = i.toLong()]. This strictness prevents conversions from occurring without the programmer’s knowledge, particularly those that lose precision without warning (such as a [long] converted to [float] or [double] in Java). It has two modes:

  • a literal is accepted for any type it fits into: [val l: Long = 1000] and [val b: Byte = 5] are accepted;
  • arithmetic operators and comparisons accept operands of different numeric types: [i + l], where [i] is a [Int], [l] is a [Long], the result is a [Long]; [i * 1.5] is a [Double]. This is not an implicit conversion, but rather the existence, within the [Int] class, of functions [plus(other: Long): Long], [times(other: Double): Double], etc.

Conversions that may lose information have, with the [toXxx()] functions, the same effect as Java type casts: [toInt()] applied to a floating-point number truncates the decimal part (toward zero); when applied to a [Long], it retains only the least significant 32 bits. Kotlin’s type conversion, the [as] operator, is reserved for objects (Chapter 4): [d as Int], where [d] is a [Double], does not convert a floating-point number to an integer: the compiler issues a warning ([this cast can never succeed. Use 'toInt' to perform numeric conversion.]), and runtime would throw an exception ([ClassCastException]).

Characters are not numbers: you convert from a character to its Unicode code using the property [code], and from a code to a character using the constructor [Char(code)]. However, you can add an integer to a character (['A' + 2] equals 'C') and subtract two characters (['C' - 'A'] equals 2).

An overflow in an integer calculation or a conversion between integers is never reported: the result is simply incorrect. The Kotlin compiler does not report this either, even when only constants are involved ([Int.MAX_VALUE + 1]). When this risk exists, you should perform the calculation using a larger type ([Long], or [BigInteger] instead of JDK), or use the methods [addExact], [subtractExact], [multiplyExact], [toIntExact]... from the Java class [Math], which throw a [ArithmeticException] exception if the limit is exceeded. The Kotlin library has no equivalent.

The [Basics15.kt] program is as follows:

// type changes
package basics15

import kotlin.math.roundToInt

fun main() {
    val i = 3
    val j = 4
    val f1 = (i / j).toFloat()
    val f2 = i.toFloat() / j
    println("f1=$f1, f2=$f2")

    // no implicit conversion, even without loss of information
    val l: Long = i.toLong()
    val d: Double = l.toDouble()
    println("l=$l, d=$d")
    // but the operators accept operands of different types
    val sum = i + l
    val product = i * 1.5
    println("i + l = $sum (${sum::class.simpleName}), i * 1.5 = $product (${product::class.simpleName})")

    // conversions with possible loss of information
    val real = 3.99
    val truncated = real.toInt()
    val rounded = real.roundToInt()
    println("3.99.toInt()=$truncated, (-3.99).toInt()=${(-3.99).toInt()}, 3.99.roundToInt()=$rounded")
    val big = 3_000_000_000L
    val overflowed = big.toInt()
    println("3000000000L.toInt()=$overflowed")
    // a real that is too big is brought back to the largest integer value
    println("1e10.toInt()=${1e10.toInt()}, Double.NaN.toInt()=${Double.NaN.toInt()}")

    // characters and integers
    val c = Char(65)
    val code = 'é'.code
    println("Char(65)=$c, code of 'é'=$code, 'A' + 2=${'A' + 2}, 'C' - 'A'=${'C' - 'A'}")
    // a digit and its value
    println("'7'.digitToInt()=${'7'.digitToInt()}, '7'.code=${'7'.code}")

    // overflow: silent
    val max = Int.MAX_VALUE
    val next = max + 1
    println("Int.MAX_VALUE + 1 = $next, as a Long: ${max.toLong() + 1}")
    // the ...Exact methods of Java's Math class report the overflow with an exception
    try {
        val checked = Math.addExact(max, 1)
        println("checked=$checked")
    } catch (e: ArithmeticException) {
        println("Math.addExact(max, 1) : ${e::class.simpleName} - ${e.message}")
    }
    try {
        val conversion = Math.toIntExact(big)
        println("conversion=$conversion")
    } catch (e: ArithmeticException) {
        println("Math.toIntExact(big) : ${e::class.simpleName} - ${e.message}")
    }
}

Results:

> gradlew -q run -Pmain=basics15.Basics15Kt
f1=0.0, f2=0.75
l=3, d=3.0
i + l = 6 (Long), i * 1.5 = 4.5 (Double)
3.99.toInt()=3, (-3.99).toInt()=-3, 3.99.roundToInt()=4
3000000000L.toInt()=-1294967296
1e10.toInt()=2147483647, Double.NaN.toInt()=0
Char(65)=A, code of 'é'=233, 'A' + 2=C, 'C' - 'A'=2
'7'.digitToInt()=7, '7'.code=55
Int.MAX_VALUE + 1 = -2147483648, as a Long: 2147483648
Math.addExact(max, 1) : ArithmeticException - integer overflow
Math.toIntExact(big) : ArithmeticException - integer overflow

Comments:

  • Line 9: [f1] will have the value 0.0. The division 3/4 is an integer division since both operands are of type [Int]; its result, 0, is then converted to [Float] by the function [toFloat()];
  • Line 10: [i.toFloat()] is the value of [i] converted to [Float]. We now have a division between a real number of type [Float] and an integer of type [Int], the result of which is [Float]: The division of the two real numbers is performed, and [f2] has the value 0.75. In the operation [i.toFloat()], [i] is a value encoded exactly as a 4-byte real number, while [i.toFloat()] is the same value encoded as an approximate 4-byte real number. This transcoding occurs only during the calculation: the variable [i] always retains its type [Int];
  • lines 14–15: upcasts, which must be explicitly requested;
  • lines 18–19: operations between different types. The result type is displayed as [::class.simpleName]: [Long] and [Double];
  • line 24: converting a real number to an integer truncates the decimal part toward zero: 3.99 becomes 3 and -3.99 becomes -3;
  • line 25: To round, we use [roundToInt()] from the [kotlin.math] package, imported on line 4, which directly returns a [Int];
  • line 27: 3 billion exceeds the range of [Int]: the literal is of type [Long], with or without the L suffix;
  • line 28: 3 billion does not fit into a [Int]: only the least significant 32 bits are retained, and the result no longer makes sense;
  • line 31: a floating-point number too large for a [Int] is reduced to [Int.MAX_VALUE]; the value [NaN] becomes 0;
  • lines 34–36: conversions between characters and Unicode codes, and character arithmetic;
  • line 38: the function [digitToInt()] returns the value of a digit (7), not to be confused with its Unicode code (55);
  • line 42: the overflow goes unnoticed: the result is the smallest value, [Int]. The same calculation performed using [Long] yields the correct result;
  • line 46: with [Math.addExact], the overflow triggers a [ArithmeticException] exception;
  • Line 52: [Math.toIntExact] converts a [Long] to a [Int] and also throws an exception if the value does not fit into a [Int].

3.5. Program flow control statements

3.5.1. Stop

The function [exitProcess] from the package [kotlin.system] allows you to stop the execution of a program:

fun exitProcess(status: Int): Nothing

[exitProcess] terminates the current process and returns control to the calling process, typically the command interpreter. The value of [status], known as the return code, can be used by the command interpreter. By convention, 0 indicates that the program ran successfully, while any other value signals an error. On Windows, the return code is available in the [%ERRORLEVEL%] variable at the command prompt and in the [$LASTEXITCODE] variable of PowerShell; on Linux and macOS, in the [$?] variable in the shell. Thus, [exitProcess(0)] will terminate the program’s execution with a return code of 0.

[exitProcess] calls the Java method [System.exit], which can also be used directly. However, it has one advantage over that method: its return type is [Nothing]. [Nothing] is the type of expressions that never return a value because they do not terminate normally (program termination, exception thrown, infinite loop). The compiler therefore knows that the statement following a call to [exitProcess] will never be executed, and it takes this into account in its checks, as we will see.

Without a call to [exitProcess], a program terminates with a return code of 0 when its [main] function ends, possibly with a [return] statement. If it is terminated by an exception that no one has caught, it displays that exception and terminates with a return code of 1. As in Java, the function [main] cannot return an integer: [exitProcess] is the only way to specify the return code.

The program [Basics16.kt] receives its return code as an argument:

// stopping the program with an exit code
package basics16

import kotlin.system.exitProcess

fun main(args: Array<String>) {
    println("Start of the program")
    // the exit code is passed as an argument (see the section on the program arguments)
    // without an argument or with a non-numeric argument, we get null
    val code = args.firstOrNull()?.toIntOrNull()
    if (code == null) {
        System.err.println("Syntax: java -jar basics16.jar exit_code")
        exitProcess(1)
    }
    // here, the compiler knows that [code] is not null: exitProcess never returns
    println("Stopping with exit code $code")
    exitProcess(code)
    println("This line is never executed")
}

To view the return code, the program must be the process launched by the shell. With [gradlew -q run -Pmain=basics16.Basics16Kt --args="3"], the launched process is Gradle: a non-zero return code causes the [run] task to fail, Gradle displays its own error message, and it is Gradle’s return code that we receive. So we build an executable archive using the [kotlinc] compiler, as in Chapter 2 ([kotlinc Basics16.kt -include-runtime -d basics16.jar]), and run it with [java -jar].

Results in a bash terminal:

> java -jar basics16.jar 3
Start of the program
Stopping with exit code 3
> echo $?
3
> java -jar basics16.jar
Start of the program
Syntax: java -jar basics16.jar exit_code
> echo $?
1
> java -jar basics16.jar three
Start of the program
Syntax: java -jar basics16.jar exit_code
> echo $?
1

Comments:

  • line 6: the function [main] receives the command-line arguments in the array [args]. The program’s arguments are listed below;
  • line 10: [args.firstOrNull()] returns the first argument, or [null] if there are none; the operator [?.] calls [toIntOrNull()] only if it is not [null], and [toIntOrNull()] returns [null] if the string is not an integer. Thus, both the absence of an argument and a non-numeric argument result in [null], which is of type [Int?], without any exception being thrown. The corresponding Java program was supposed to catch the [NumberFormatException] exception from [Integer.parseInt];
  • line 13: the program terminates with a return code of 1. The error message is written to the error output [System.err];
  • Line 16: The compiler knows that we only reach this point if [code] is not [null], since the [if] block ends with a call to a function of type [Nothing]. The variable [code], declared as type [Int?], is therefore treated as a [Int] starting from this line: this is known as a “smart cast.” Without it, the call to [exitProcess(code)] on the next line—which expects a [Int]—would be rejected. In Java, the compiler was unaware that [System.exit] never returns control, so a redundant [return] statement had to be added after it;
  • line 17: termination with the return code passed as an argument. The following line is not executed. The Kotlin 2.4 compiler accepts it without a warning; IntelliJ IDEA flags it as “unreachable code”;
  • Results: The command [echo $?] displays the return code of the last executed program: 3, then 1 in both error cases.

3.5.2. Simple decision structure

1
2
3
4
5
if (condition) {
    actions_condition_true
} else {
    actions_condition_false
}

Notes:

  • the condition is enclosed in parentheses. It must be of type [Boolean]: like Java, and unlike the C language, Kotlin rejects [if (n)] when [n] is an integer;
  • actions are not terminated by a semicolon: in Kotlin, the end of the line terminates the statement. The semicolon is only used to write multiple statements on a single line;
  • curly braces are only required if there is more than one statement; however, they are often used even for a single statement, for readability and to avoid errors when adding a statement. As in Java, the convention is to place the opening curly brace at the end of the line;
  • the clause [else] may be omitted;
  • there is no [then] keyword.

The algorithmic equivalent of this structure is the if...then...else structure:

1
2
3
4
if condition
    then actions_condition_true
    else actions_condition_false
endif

Example:

if (x > 0) { nx = nx + 1; sx = sx + x } else dx = dx - x

The main difference from Java is that Kotlin’s [if] is also an expression: it returns the value of the executed branch. We can therefore write [val max = if (a > b) a else b]. This is why Kotlin does not have a ternary operator [condition ? value1 : value2]: the [if] serves as its equivalent. When used as an expression, [if] must have a [else] clause; otherwise, it would have no value when the condition is false.

Choice structures can be nested. This sometimes raises the problem illustrated by the program [Basics17.kt]:

// simple choice structure if
package basics17

fun main() {
    val n = 5
    // an else always refers to the nearest if
    if (n > 1)
        if (n > 6)
            println(">6")
        else
            println("<=6")

    // with braces, the else is attached to the first if
    val n2 = 0
    if (n2 > 1) {
        if (n2 > 6) {
            println(">6")
        }
    } else {
        println("<=1")
    }

    // a sequence of choices: else if
    val grade = 13
    val honours: String
    if (grade < 10) {
        honours = "failed"
    } else if (grade < 12) {
        honours = "pass"
    } else if (grade < 14) {
        honours = "fairly good"
    } else {
        honours = "good"
    }
    println("grade=$grade, honours=$honours")

    // if is an expression: it returns the value of the executed branch
    val grade2 = 8
    val honours2 = if (grade2 < 10) "failed" else if (grade2 < 12) "pass" else "at least fairly good"
    println("grade=$grade2, honours=$honours2")
    // the equivalent of Java's ternary operator (n > 0 ? "positive" : "negative or zero")
    println("$n is ${if (n > 0) "positive" else "negative or zero"}")
    // a branch can be a block: its value is that of its last expression
    val max = if (n > n2) {
        println("the largest is n")
        n
    } else {
        println("the largest is n2")
        n2
    }
    println("max=$max")
}

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=basics17.Basics17Kt
<=6
<=1
grade=13, honours=fairly good
grade=8, honours=failed
5 is positive
the largest is n
max=5

Comments:

  • Lines 7–11: Which [if] does the [else] on line 10 refer to? The rule is that a [else] always refers to the nearest [if]: [if (n > 6)], line 8. The code indentation, which has no effect on the compiler, was chosen to illustrate this;
  • lines 15–20: here, we wanted to place a [else] next to [if (n2 > 1)], and not a [else] next to [if (n2 > 6)]. Because of the previous rule, we are required to place curly braces around the [if (n2 > 1) { ... }];
  • lines 26–33: a sequence of mutually exclusive choices is written using successive [else if] statements. The conditions are tested in order; the first one that evaluates to true determines the action to be executed. The variable [honours] is declared as [val] without a value on line 25: the compiler verifies that it is assigned a value—only once—in each branch before it is used on line 35. In the next section, we will see a more concise way to write this sequence of choices;
  • Line 39: [if] is used as an expression. Its value—the selected string—is assigned to [honours2]. This avoids declaring the variable without a value;
  • line 42: [if] replaces the Java ternary operator, here inside a string template [${...}];
  • lines 44–49: When a branch is a block, its value is that of the last expression in the block: [n] for the first branch, [n2] for the second. The preceding statements are executed normally.

3.5.3. Case Structure

3.5.3.1. The when statement

Kotlin has no [switch] statement. It replaces it with the [when] statement, which is simpler and more powerful. Its syntax is as follows:

1
2
3
4
5
6
when (expression) {
    v1 -> actions1
    v2, v3 -> actions2
    ...
    else -> actions_otherwise
}

Notes:

  • The control expression is enclosed in parentheses. It can be of any type: an integer, a character, a string, an enum value, as well as [Long], [Double], [Boolean], or any object. Values are compared using the [==] operator, that is, using the [equals] method;
  • each clause associates one or more values, separated by commas, with an action. The action, placed after the arrow [->], is a single statement or a block enclosed in curly braces;
  • the values vi are not necessarily constants: they can be any expressions, evaluated in the order of the clauses;
  • only the action of the first clause for which a value is valid is executed: there is no need to write [break], nor is there a chaining from one clause to the next. The classic error from Java’s historical [switch] syntax—forgetting the [break]—is therefore impossible. This is how the arrow syntax introduced in Java 14 works, which is the only form used in Kotlin;
  • the [else] clause—the equivalent of Java’s [default]—may be omitted when [when] is used as a statement.

Algorithmic example:

depending on the value of choice
    case 0
        end of the module
    case 1
        execute module M1
    case 2
        execute module M2
    otherwise
        error <- true
endcase

3.5.3.2. The when expression and its various clauses

Very often, a case structure is used solely to select a value. Like [if], [when] can be used as an expression for this purpose:

1
2
3
4
5
6
result = when (expression) {
    v1 -> value1
    v2, v3 -> value2
    ...
    else -> value_otherwise
}

The expression [when] must be exhaustive: it must specify a value for all possible values of the control expression, which generally requires a [else] clause. Without it, the compiler rejects the program:

error: 'when' expression must be exhaustive. Add an 'else' branch.

The clause [else] is unnecessary only when the compiler can verify on its own that all values are handled: for an expression of type [Boolean] ([true] and [false]), an enumeration (all its constants; see the section on enumerations below), or a sealed class (Chapter 4). When a clause requires multiple statements, a block is written, whose value is that of its last expression. There is no equivalent to the Java statement [yield].

Instead of a value, a clause may contain:

  • a membership test [in range]: [in 1..10 -> "small"] is evaluated if the expression is between 1 and 10. We write [!in] for non-membership;
  • A test of type [is Type]: [is String -> ...] is selected if the object being examined is of type [String]. In the action, the object is then automatically treated as a [String] (intelligent type conversion): there is no need to introduce a new variable, as in Java’s [case String s]. [!is] checks that an object is not of a given type;
  • a guard: since Kotlin 2.2, a clause can be supplemented with a condition introduced by [if]: [is Int if o > 0 -> ...] is only evaluated for a positive integer. This is equivalent to the [when] guards in Java 21.

Finally, [when] can be used without an argument. In this case, each clause is any Boolean condition, and the first true condition is selected. This is a more readable way of writing a sequence of [if ... else if] clauses.

The clauses are evaluated in order: the first one that satisfies the condition is selected. Unlike Java’s [switch], Kotlin’s [when] handles both primitive types ([Int], [Double]...) as well as objects: there’s no need to go through a [Integer] to classify an integer with guards.

The [Basics18.kt] program is as follows:

// case structure: when, statement and expression
package basics18

fun main() {
    val choice = 2
    var error = false
    when (choice) {
        0 -> return
        1 -> m1()
        2 -> m2()
        else -> error = true
    }
    println("error=$error")

    // several values for the same case, expression of type String
    val day = "saturday"
    when (day) {
        "saturday", "sunday" -> println("$day : weekend")
        else -> println("$day : weekday")
    }

    // when is also an expression: it returns a value
    val dayType = when (day) {
        "saturday", "sunday" -> "weekend"
        "monday", "tuesday", "wednesday", "thursday", "friday" -> "weekday"
        else -> "unknown day"
    }
    println("$day : $dayType")
    // the value of a block is that of its last expression
    val nbHours = when (day) {
        "saturday" -> 4
        "sunday" -> 0
        else -> {
            val hours = 7
            hours + 1
        }
    }
    println("$day : $nbHours opening hour(s)")

    // when with ranges
    println("-5 : ${classify(-5)}")
    println("0 : ${classify(0)}")
    println("7 : ${classify(7)}")
    println("42 : ${classify(42)}")
    println("1000 : ${classify(1000)}")
    // when without an argument: a sequence of conditions
    val grade = 13
    val honours = when {
        grade < 10 -> "failed"
        grade < 12 -> "pass"
        grade < 14 -> "fairly good"
        else -> "good"
    }
    println("grade=$grade, honours=$honours")
    // matching on the type of the object
    val objects = arrayOf<Any?>(12, -3, "kotlin", 2.5, null)
    for (o in objects) {
        println("$o : ${describe(o)}")
    }
}

// functions used above
fun m1() {
    println("M1")
}

fun m2() {
    println("M2")
}

fun classify(n: Int): String = when (n) {
    in Int.MIN_VALUE..<0 -> "negative"
    0 -> "zero"
    in 1..10 -> "small"
    in 11..99 -> "medium"
    else -> "large"
}

fun describe(o: Any?): String = when (o) {
    null -> "no object"
    is Int if o > 0 -> "positive integer"
    is Int -> "negative or zero integer"
    is String -> "string of ${o.length} characters"
    else -> "object of type ${o::class.simpleName}"
}

Results:

> gradlew -q run -Pmain=basics18.Basics18Kt
M2
error=false
saturday : weekend
saturday : weekend
saturday : 4 opening hour(s)
-5 : negative
0 : zero
7 : small
42 : medium
1000 : large
grade=13, honours=fairly good
12 : positive integer
-3 : negative or zero integer
kotlin : string of 6 characters
2.5 : object of type Double
null : no object

Comments:

  • Lines 7–11: the Kotlin implementation of the algorithm above. A clause may contain an assignment when [when] is used as an instruction;
  • line 8: [return] terminates the function [main], and thus the program;
  • line 18: two values for the same actions, separated by a comma;
  • lines 23–26: a [when] expression on a string. The [else] clause makes it exhaustive;
  • lines 30–35: a clause whose action is a block: the value of the block’s last expression, [hours + 1] (line 35), is the value of the expression [when];
  • lines 71–76: the function [classify] sorts an integer using intervals. [Int.MIN_VALUE..<0] is the interval of integers ranging from the smallest [Int] to 0 excluded, that is, negative integers; [1..10] contains the integers from 1 to 10 inclusive. The constant 0 is included among the intervals. The clause [else] handles the remaining values, in this case integers greater than or equal to 100. The body of the function is an expression, written after the sign [=]: we will return to these functions with expression bodies later;
  • lines 48–52: a [when] with no arguments, which rewrites the sequence of [else if] from the program [Basics17.kt]: the first true condition assigns its value to the expression;
  • line 56: an array of objects of different types. The type of its elements, [Any?] (any object, or [null]), is specified explicitly: without it, the compiler would infer a complex type from the values and reject the program (“type argument for reified type parameter 'T' was inferred to the intersection of ['Comparable<*>?' & 'Serializable?']”);
  • Lines 79–84: The function [describe] selects based on the object type. On line 80, the value [null] is just another value for [when]. In line 81, the guard [if o > 0] can compare [o] to 0 because, after the test for [is Int], [o] is treated as a [Int]. Similarly, on line 83, the property [length] of the string can be used without conversion. The clause without a guard, [is Int] (line 82), must be placed after the clause with a guard, which it would otherwise hide. On line 84, [o::class.simpleName] provides the simple name of the object’s class: [Double];
  • lines 63–68: two functions called by the first one, [when].

The following table summarizes the correspondences between the two languages:

Java
Kotlin
[switch (x) { case 1: ... break; default: ... }]
[when (x) { 1 -> ... ; else -> ... }]
[case "saturday", "sunday" -> ...]
["saturday", "sunday" -> ...]
[default -> ...]
[else -> ...]
[yield value;]
last expression in the block
[case Integer i when i > 0 -> ...]
[is Int if x > 0 -> ...]
[case null -> ...]
[null -> ...]
(no equivalent)
[in 1..10 -> ...]
chain of [if ... else if]
[when { condition -> ... }]

3.5.4. Repetition Structures

3.5.4.1. Known number of repetitions

Structure for

Kotlin does not have the “three-argument” loop found in C and Java ([for (i = id; i <= if; i = i + ip)]). Its [for] loop iterates over the elements of a range, an array, or a collection; it is the equivalent of Java’s for-each loop. To iterate over an integer, you iterate over a range:

1
2
3
for (i in id..if step ip) {
    actions
}

Notes:

  • [id..if] is the range of integers from [id] to [if], inclusive. [step ip] sets the step size of the sequence; it is optional and defaults to 1;
  • [id..<if], which can also be written as [id until if], is the interval from [id] to [if], excluding the endpoints. It is convenient for iterating through the indices of an array of [n] elements: [for (i in 0..<n)];
  • [if downTo id] is the descending sequence from [if] to [id];
  • the loop variable [i] is declared by [for], without [val] or [var]: it exists only within the loop, and cannot be modified within the loop body;
  • curly braces are only necessary if there is more than one action.

The algorithmic equivalent is the “for” structure:

1
2
3
for i varying from id to if in steps of ip
    actions
endfor

which can be translated into a "while" structure:

1
2
3
4
5
i <- id
while i <= if
    actions
    i <- i + ip
endwhile

Intervals are not a syntax specific to the [for] loop: they are objects ([IntRange], [IntProgression], [CharRange]...) that can be assigned to a variable, passed to a function, evaluated using the [in] operator ([if (x in 1..10)], as in [when]), or manipulated using functions from the standard library ([(1..10).sum()]). Character ranges also exist: ['a'..'z'].

for loop on an array or collection

The syntax is as follows:

1
2
3
for (variable in collection) {
    instructions
}

Notes:

  • [collection] is an array or a collection of iterable objects (in fact, any object that has a [iterator()] function). We will cover collections ([List], [Set], [Map]...) in Chapter 5;
  • the type of *[variable]* is inferred from the type of the collection’s elements: it is not explicitly specified;
  • [variable] is a local variable of the loop that will successively take on all the values in the collection. It is read-only, as if it were declared as [val]: it cannot be assigned a different value within the loop;
  • To iterate through the indices of an array, use its property [indices]: [for (i in array.indices)]; to obtain both the index and the element, use the function [withIndex()]: [for ((i, element) in array.withIndex())].

The repeat function

When you want to execute a block of instructions exactly [n] times, the [repeat] function from the standard library is the simplest:

1
2
3
repeat(n) {
    instructions
}

Within the block, the name [it] denotes the number of the current iteration, ranging from 0 to n-1. This block is actually a lambda—an anonymous function passed as a parameter to the function [repeat]: we’ll cover lambdas in Chapter 7.

3.5.4.2. Unknown number of iterations

While loop

1
2
3
while (condition) {
    actions
}

The loop continues as long as the condition is true. The loop may never be executed. The notes are the same as for [for]. The corresponding algorithmic structure is the while structure:

1
2
3
while condition
    actions
endwhile

"Do While" Loop

The syntax is as follows:

1
2
3
do {
    instructions
} while (condition)

The loop continues until the condition evaluates to false. Here, the loop is executed at least once. A unique feature of Kotlin: a variable declared within the loop body is visible in the condition of the [while], which allows you to write [do { val line = readln() } while (line != "end")]. The corresponding algorithmic structure is the "repeat...until" structure:

1
2
3
repeat
    actions
until not condition

No structure for "general"

The Java loop [for (start_instructions; condition; end_of_loop_instructions)] does not exist in Kotlin. When a range is not sufficient, we write the equivalent loop [while]:

1
2
3
4
5
start_instructions
while (condition) {
    instructions
    end_of_loop_instructions
}

3.5.4.3. Loop control statements

Statement
Role
[break]
Exits the loop [for], [while], [do ... while]
[continue]
proceeds to the next iteration of the loops [for], [while], [do ... while]
[break@label]
Exits the loop preceded by the label [label@], even if the statement is located within a nested loop
[continue@label]
advances to the next iteration of the loop preceded by the label [label@]

A label is an identifier followed by the character [@], placed before the loop: [search@ for (...)]. It is referenced, without a space, after [break] or [continue]: [break@search]. In Java, the label was followed by a colon ([search:]).

3.5.4.4. Examples

The program [Basics19.kt] is as follows:

// repetition structures
package basics19

fun main() {
    // four ways to compute the sum of the first 10 integers
    val n = 10
    var sum = 0
    for (i in 1..n) {
        sum += i
    }
    println("for : sum=$sum")

    var i = 1
    sum = 0
    while (i <= n) {
        sum += i
        i++
    }
    println("while : sum=$sum")

    i = 1
    sum = 0
    do {
        sum += i++
    } while (i <= n)
    println("do while : sum=$sum")

    // the standard library can compute the sum of the elements of a range
    println("sum : sum=${(1..n).sum()}")

    // ranges and progressions
    var product = 1
    for (k in 1..5) {
        product *= k
    }
    println("5! = $product")
    for (k in 0..<5) print("$k ")    // 5 excluded (also written 0 until 5)
    println()
    for (k in 10 downTo 0 step 2) print("$k ")
    println()
    for (c in 'a'..'e') print(c)
    println()
    // repeat: repeat a block n times; it is the number of the repetition (0, 1, ...)
    repeat(3) {
        print("[round $it] ")
    }
    println()

    // iterating over the elements of an array or a collection
    val friends = arrayOf("paul", "hélène", "jacques", "sylvie")
    for (name in friends) {
        println(name)
    }
    // through the indices
    for (j in friends.indices) {
        print("${friends[j].length} ")
    }
    println()
    // elements and indices at the same time
    for ((j, name) in friends.withIndex()) {
        println("friends[$j]=$name")
    }

    // break and continue
    for (k in 1..10) {
        // even numbers are skipped
        if (k % 2 == 0) {
            continue
        }
        // we stop after 7
        if (k > 7) {
            break
        }
        print("$k ")
    }
    println()

    // infinite loop exited with break: the first multiple of 7 greater than 50
    var multiple = 50
    while (true) {
        multiple++
        if (multiple % 7 == 0) {
            break
        }
    }
    println("first multiple of 7 greater than 50: $multiple")

    // break with a label: we exit both loops at once
    val grid = arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, -5, 6), intArrayOf(7, 8, 9))
    search@ for (row in grid.indices) {
        for (column in grid[row].indices) {
            if (grid[row][column] < 0) {
                println("first negative at [$row][$column]")
                break@search
            }
        }
    }
}

Results:

> gradlew -q run -Pmain=basics19.Basics19Kt
for : sum=55
while : sum=55
do while : sum=55
sum : sum=55
5! = 120
0 1 2 3 4
10 8 6 4 2 0
abcde
[round 0] [round 1] [round 2]
paul
hélène
jacques
sylvie
4 6 7 6
friends[0]=paul
friends[1]=hélène
friends[2]=jacques
friends[3]=sylvie
1 3 5 7
first multiple of 7 greater than 50: 56
first negative at [1][1]

Comments:

  • lines 6–29: four ways to calculate the sum of the first 10 integers;
  • lines 8–11: the loop [for] iterates over the range [1..n], including 10. The loop variable [i] exists only within the loop: the variable [i] declared on line 13 is a different variable;
  • lines 15–17: the equivalent loop [while]: the initialization, condition check, and increment of [i] are written explicitly. This is the Kotlin translation of a general Java loop [for];
  • Line 24: [i++] returns the value of [i] before it is incremented: [i] is added to [sum], and then [i] is incremented. The operators [++] and [--] exist in both Kotlin and Java;
  • line 29: the range [1..n] is an object; its function [sum()] returns the sum of its elements. This is the most idiomatic way to write this in Kotlin. The parentheses around the range are necessary: the function call takes precedence over the operator [..];
  • lines 33–36: the calculation of 5! = 1 × 2 × 3 × 4 × 5;
  • line 37: an interval whose upper bound is excluded: [k] ranges from 0 to 4. When the loop body consists of a single statement, it can be written on the same line as the [for];
  • line 39: a descending sequence, from 10 to 0, with a step size of 2;
  • line 41: a range of characters;
  • lines 44–45: the block is executed 3 times; [it] is successively set to 0, 1, and 2;
  • lines 51–52: the loop [for] iterates over the elements of the array [friends];
  • lines 55–56: [friends.indices] is the range of array indices, which is [0..3] in this case. The length of each name is displayed;
  • lines 60–61: [friends.withIndex()] returns (index, element) tuples. The syntax [(j, name)] breaks them down into two variables: this is a destructuring assignment, which we’ll revisit at the end of this chapter;
  • lines 65–74: [continue] moves to the next iteration for even numbers; [break] stops the loop as soon as [k] exceeds 7;
  • lines 80–86: an infinite loop [while (true)], which is exited via [break]. This form is convenient when the termination condition is calculated in the middle of the loop. We’ll use it for keyboard input;
  • Lines 89–94: Finding the first negative number in a two-dimensional array, an array of arrays of integers ([IntArray]). The label [search@] (line 90) names the outer loop; the statement [break@search] (line 94) exits both loops at once. A simple [break] would exit only the inner loop. Kotlin does not have a [goto] statement: labels serve this purpose in this specific case.

3.6. Exception Handling

Many functions in Kotlin and Java are capable of throwing exceptions, i.e., errors. When a function is capable of throwing an exception, the programmer should handle it to make programs more error-tolerant: you should always avoid a sudden termination of an application.

3.6.1. The try/catch/finally Structure

Exception handling follows this pattern:

1
2
3
4
5
6
try {
    code likely to throw an exception
} catch (e: Exception) {
    handle the exception e
}
next statement

If the code in the [try] block does not throw an exception, execution proceeds to the next statement; otherwise, execution proceeds to the body of the [catch] block, then to the next statement. Note the following points:

  • [e] is an object of type [Exception] or a derived type. As with any Kotlin declaration, the name precedes the type: [e: Exception]. There are many types of exceptions: [ArrayIndexOutOfBoundsException], [NumberFormatException], [ArithmeticException], [IllegalArgumentException], [IllegalStateException], [NullPointerException], [IOException], etc. These are Java exception classes that Kotlin uses directly (the most common ones are also available in the [kotlin] package, under the same name, without [import]). By writing [catch (e: Exception)], you indicate that you want to handle all types of exceptions. If the code in the [try] clause is likely to generate multiple types of exceptions, you may want to be more specific by handling the exception with multiple [catch] clauses:
1
2
3
4
5
6
7
8
try {
    code likely to throw the exceptions
} catch (e1: ArrayIndexOutOfBoundsException) {
    handle the exception e1
} catch (e2: NumberFormatException) {
    handle the exception e2
}
next statement

[catch] clauses are evaluated in order; the first one whose type matches the exception is executed. A clause that handles a specific exception type must therefore precede one that handles a more general type. Note: Unlike the Java compiler, the Kotlin compiler does not reject a clause that could never be reached; it is up to the programmer to ensure this (we’ll come back to this);

  • Kotlin does not have Java’s multiple [catch] clause ([catch (IOException | NumberFormatException e)]). You can write multiple clauses, or a single clause for a more general type in which you test the exception type, for example with a [when];
  • You can add a [finally] clause to the [try / catch] clauses:
1
2
3
4
5
6
7
8
try {
    code likely to throw an exception
} catch (e: Exception) {
    handle the exception e
} finally {
    code executed after try or catch
}
next statement

Whether an exception occurs or not, the code in the [finally] clause is always executed. Typically, this is where you close a file; we’ll see that there’s a simpler way to do this, using the [use] function;

  • in the [catch] clause, you may not want to use the exception object. In that case, you can name it _: [catch (_: NumberFormatException)], where the _ character denotes an anonymous variable that will not be used;
  • An exception has a property named [message] (the equivalent of the [getMessage()] method in Java) that contains the message detailing the error that occurred. It is of type [String?]: some exceptions do not have a message. To display it, write:
1
2
3
4
} catch (ex: Exception) {
    System.err.println("The following error occurred: ${ex.message}")
    ...
}
  • The [toString()] method for an exception returns the full name of its class followed by its message; its [printStackTrace()] method additionally displays the call stack—that is, the list of functions that were executing at the time of the error—along with the source file name and line number. This is the most useful information for the developer, but it is not intended for the user. In this course, we will most often display the name of the exception class—[ex.javaClass.name] or [ex.javaClass.simpleName]—and its message. [ex.javaClass] is the Java class of the object, the one returned by the [getClass()] method in Java;
  • Finally, and this is an important difference from Java, [try] is an expression: its value is that of the last expression in the [try] block if no exception occurs, and that of the last expression in the [catch] block if an exception does occur. The clause [finally] does not affect this value. Therefore, we can write [val n = try { text.toInt() } catch (_: NumberFormatException) { 0 }].

3.6.2. A First Example

The program [Basics20.kt] demonstrates an exception generated by using a nonexistent array element:

// an exception caused by an incorrect array index
package basics20

fun main() {
    val tab = intArrayOf(0, 1, 2, 3)
    // displaying the array with its indices
    for (i in tab.indices) {
        println("tab[$i]=${tab[i]}")
    }
    // displaying the elements of the array
    for (elmt in tab) {
        println(elmt)
    }
    // generating an exception
    try {
        tab[100] = 6
    } catch (e: Exception) {
        System.err.println("The following error occurred: ${e.javaClass.name} - ${e.message}")
        return
    } finally {
        println("finally ...")
    }
    println("This line is not executed")
}

Results:

> gradlew -q run -Pmain=basics20.Basics20Kt
tab[0]=0
tab[1]=1
tab[2]=2
tab[3]=3
0
1
2
3
The following error occurred: java.lang.ArrayIndexOutOfBoundsException - Index 100 out of bounds for length 4
finally ...

Comments:

  • line 16: this statement throws an exception because the array [tab] does not have an element at index 100. The compiler does not check indices, even constant ones: the error is detected only at runtime by the Java Virtual Machine;
  • line 17: the [catch (e: Exception)] clause catches all exceptions;
  • line 18: The exception is of type [java.lang.ArrayIndexOutOfBoundsException], and its message indicates the incorrect index and the size of the array (penultimate line of the results);
  • line 19: the function [main] is exited, and thus the program;
  • lines 20–21: the [finally] clause was executed (last line of the results), even though the [catch] clause contained a [return] statement. Note that the [finally] clause is always executed;
  • line 23: because of [return], this line is not executed.

3.6.3. A Controlled Input

Here is another example, [Basics21.kt], where we handle the exception caused by converting a string that does not represent an integer to an integer:

// entering an age with handling of the conversion exception
package basics21

fun main() {
    // we ask for the name
    print("Name: ")
    val name = readlnOrNull() ?: ""
    // we ask for the age
    var age = 0
    var ageOK = false
    while (!ageOK) {
        // question, reading-checking the answer
        print("age: ")
        try {
            age = readln().toInt()
            ageOK = age >= 1
        } catch (_: NumberFormatException) {
            // ageOK stays false
        }
        if (!ageOK) {
            println("Incorrect age, try again...")
        }
    }
    // final display
    println("Your name is $name and you are $age year(s) old")

    // the same input with a try used as an expression
    var age2: Int
    while (true) {
        print("age: ")
        // the value of the try is that of the try block, or that of the catch block on exception
        age2 = try {
            readln().toInt()
        } catch (_: NumberFormatException) {
            0
        }
        if (age2 >= 1) break
        println("Incorrect age, try again...")
    }
    println("Your name is $name and you are $age2 year(s) old")
}

Results (the entries, typed on the keyboard, have been inserted into the display):

1
2
3
4
5
6
> gradlew -q run -Pmain=basics21.Basics21Kt
Name: dupont
age: 23
Your name is dupont and you are 23 year(s) old
age: 23
Your name is dupont and you are 23 year(s) old
> gradlew -q run -Pmain=basics21.Basics21Kt
Name: durand
age: x
Incorrect age, try again...
age: -4
Incorrect age, try again...
age: 99999999999
Incorrect age, try again...
age: 12
Your name is durand and you are 12 year(s) old
age: abc
Incorrect age, try again...
age: 7
Your name is durand and you are 7 year(s) old

Comments:

  • line 7: if the input stream is closed, [readlnOrNull] yields [null]: the [?:] operator replaces it with an empty string;
  • Lines 11–21: the age input loop;
  • line 15: the line typed on the keyboard is converted to an integer by the function [toInt]. This function throws a [NumberFormatException] exception if the conversion is not possible. That is why the operation was placed inside a [try / catch]. The function [readln], on the other hand, throws an exception if the input stream is closed: we do not catch this exception, as doing so would cause the program to terminate;
  • line 16: if we reach this line, it means the conversion was successful. However, we verify that the resulting integer is indeed greater than or equal to 1;
  • line 17: if a [NumberFormatException] exception is thrown, we branch to [catch], where nothing is done. Thus, the Boolean variable [ageOK], initialized to [false], remains at [false]. The exception variable, which is unused, is the anonymous variable _. Only the expected exception is caught: a [catch (_: Exception)] clause would also mask errors that were not anticipated;
  • lines 20–21: An error message is displayed if the age is incorrect;
  • lines 28–40: the same input, written using a [try] as an expression. If the conversion succeeds, the value of [try] is the resulting integer; otherwise, it is the value of the [catch] block, 0, which will be rejected by the test on line 37. The infinite loop [while (true)], which is exited via [break], avoids the Boolean variable [ageOK]. The compiler knows that the loop is only exited after a value has been assigned to [age2]; therefore, it allows its use on line 40, even though it was declared without a value.

A number that is too large for a [Int]—such as 99999999999—also triggers a [NumberFormatException]: it is therefore handled like other invalid inputs (second execution).

Using an exception to validate an input is common practice in Java, but Kotlin offers a better solution: the function [toIntOrNull()], introduced at the beginning of this chapter, returns [null] instead of throwing an exception. We would then write [val age = readln().toIntOrNull()] followed by [if (age != null && age >= 1)], without [try]. This is what we’ll do in the example application that follows. Exceptions should be reserved for exceptional situations.

3.6.4. Multiple catch clauses and exception throws

The program itself can throw an exception using the statement [throw]:

throw IllegalArgumentException("error message")

There is no [new] operator in Kotlin: an object is created simply by calling its class constructor. Execution of the current function stops, and the exception is propagated up through the calling functions until a [catch] clause handles it; if none does, the program terminates, displaying the exception and the call stack. In a [catch] clause, the [throw e] statement rethrows the current exception, for example after logging it.

In Kotlin, [throw] is an expression of type [Nothing] (like [exitProcess]). It can therefore be used wherever a value is expected, for example after the [?:] operator: [val name = input ?: throw IllegalArgumentException("missing name")].

Kotlin, like Java, does not provide a filter for [catch] clauses: to handle an exception only in certain cases, you test the condition in the clause and rethrow the exception if necessary.

For the most common checks, the Kotlin standard library provides functions that throw the exception for us:

Function
Role
[require(condition) { message }]
checks an argument: throws a [IllegalArgumentException] with the message calculated by the block if the condition is false
[requireNotNull(value) { message }]
throws an [IllegalArgumentException] if [value] is equal to [null]; otherwise, it returns the value as a non-nullable type. This is equivalent to Java’s [Objects.requireNonNull], which in turn throws a [NullPointerException]
[check(condition) { message }]
checks a condition: throws a [IllegalStateException] if the condition is false
[checkNotNull(value) { message }]
throws an [IllegalStateException] if [value] equals [null], and otherwise returns the value
[error(message)]
always throws an [IllegalStateException]; of type [Nothing], it is used to signal a case that should not occur

The message is generated by a block (a lambda), which is executed only if the check fails: a string is not calculated unnecessarily when everything is fine.

The program [Basics22.kt] illustrates these possibilities:

// several catch clauses, throwing exceptions
package basics22

fun main() {
    val inputs = arrayOf("12", "abc", "", "99999999999", "-3", "150")
    for (input in inputs) {
        try {
            val age = readAge(input)
            println("[$input] : age=$age")
        } catch (e: NumberFormatException) {
            // no filter on a catch clause: we test inside the clause
            if (input.isEmpty()) {
                println("[$input] : empty input")
            } else {
                println("[$input] : ${e.javaClass.simpleName} - ${e.message}")
            }
        } catch (e: IllegalArgumentException) {
            println("[$input] : ${e.javaClass.simpleName} - ${e.message}")
        }
    }

    // require, requireNotNull, check, error: checks that throw exceptions
    val names = arrayOf("dupont", null)
    for (name in names) {
        try {
            val valid = requireNotNull(name) { "the name is mandatory" }
            println("name=$valid")
        } catch (e: IllegalArgumentException) {
            println("${e.javaClass.simpleName} - ${e.message}")
        }
    }
    for (input in arrayOf("45", "200")) {
        try {
            println("age=${readAge2(input)}")
        } catch (e: IllegalArgumentException) {
            println("${e.javaClass.simpleName} - ${e.message}")
        }
    }
    for (state in arrayOf("open", "closed", "broken")) {
        try {
            println("$state : ${process(state)}")
        } catch (e: IllegalStateException) {
            println("$state : ${e.javaClass.simpleName} - ${e.message}")
        }
    }
}

// a function that throws exceptions
fun readAge(text: String): Int {
    // toInt throws NumberFormatException
    val age = text.toInt()
    // we throw an exception ourselves if the age is out of bounds
    if (age < 0 || age > 130) {
        throw IllegalArgumentException("age out of bounds [0,130] : $age")
    }
    return age
}

// the same check with require
fun readAge2(text: String): Int {
    val age = text.toInt()
    require(age in 0..130) { "age out of bounds [0,130] : $age" }
    return age
}

// check verifies a state, error reports an impossible case
fun process(state: String): String {
    check(state != "closed") { "operation impossible on a closed resource" }
    return when (state) {
        "open" -> "processing done"
        else -> error("unknown state: $state")
    }
}

Results:

> gradlew -q run -Pmain=basics22.Basics22Kt
[12] : age=12
[abc] : NumberFormatException - For input string: "abc"
[] : empty input
[99999999999] : NumberFormatException - For input string: "99999999999"
[-3] : IllegalArgumentException - age out of bounds [0,130] : -3
[150] : IllegalArgumentException - age out of bounds [0,130] : 150
name=dupont
IllegalArgumentException - the name is mandatory
age=45
IllegalArgumentException - age out of bounds [0,130] : 200
open : processing done
closed : IllegalStateException - operation impossible on a closed resource
broken : IllegalStateException - unknown state: broken

Comments:

  • line 5: simulated entries, valid or invalid;
  • line 8: the [readAge] function (lines 49–56) can trigger several types of exceptions;
  • line 10: the entries "abc", "", and "99999999999" trigger a [NumberFormatException];
  • lines 12–15: in the absence of a filter, the empty input is distinguished within the [catch] clause;
  • line 17: the exception thrown by [readAge] itself;
  • Line 26: [requireNotNull] throws [IllegalArgumentException] if [name] equals [null]; its message is the one calculated by the block (line 9 of the results). Otherwise, it returns [name] with the type [String]: the variable [valid] is not nullable;
  • line 54: the function [readAge] throws an exception if the age is out of bounds;
  • line 62: the same check, written using [require]. The condition uses the operator [in] and a range;
  • line 68: [check] checks that the object is in a state that allows the operation;
  • line 71: [error] throws an [IllegalStateException]. Because its type is [Nothing], it can terminate a branch of a [when] expression of type [String].

The order of the [catch] clauses in lines 10 and 17 matters: the class [NumberFormatException] derives from the class [IllegalArgumentException] (a [NumberFormatException] is a special case of an incorrect argument). If we reverse the two clauses, the [IllegalArgumentException] clause also intercepts the [NumberFormatException] clauses, and the [NumberFormatException] clause is never executed again. The Java compiler rejects this clause, which has become inaccessible ([error: exception NumberFormatException has already been caught]); the Kotlin 2.4 compiler, however, accepts it without any errors or warnings. The empty entry would then be displayed as [[] : NumberFormatException - For input string: ""], and the error would go unnoticed.

3.6.5. No Checked Exceptions in Kotlin

This is one of the most noticeable differences between Java and Kotlin. Exceptions are objects whose classes form a hierarchy. These are Java classes: the class [kotlin.Throwable] is simply another name for [java.lang.Throwable], and the same applies to the classes [Exception], [RuntimeException], [IllegalArgumentException]...:

Image

  • the root class is [Throwable];
  • the class [Error] and its derived classes indicate serious virtual machine issues ([OutOfMemoryError], [StackOverflowError]...), which a program would not normally attempt to intercept;
  • the class [RuntimeException] and its derivatives ([NullPointerException], [ArithmeticException], [ArrayIndexOutOfBoundsException], [IllegalArgumentException], [NumberFormatException], [IllegalStateException]...) generally indicate programming errors or incorrect data;
  • the other classes derived from [Exception] ([IOException], [InterruptedException], [ParseException], [SQLException]...) report incidents caused by the program’s environment (missing file, network outage, database down) that a properly written program must anticipate.

In Java, exceptions in this latter category are called “checked exceptions”: the Java compiler verifies that a method capable of throwing one—either directly or by calling another method—either catches it using a [catch] clause or declares it in its signature using the [throws] keyword:

String readFile(String name) throws IOException { ... }

The responsibility is then passed on to the calling methods, and so on. Exceptions derived from [RuntimeException] and [Error] are called “unchecked”: they can be caught, but the compiler does not require it.

Kotlin does not have checked exceptions. As in C#, all exceptions are unchecked: the compiler never requires an exception to be caught or declared, and the keyword [throws] does not exist. A [IOException] thrown by a Java method is, to Kotlin, just another exception. This choice by the language designers is based on experience gained with Java:

  • in large programs, the requirement to declare exceptions propagates from method to method, and method signatures become bloated with [throws] clauses that provide little information;
  • not knowing what to do with a checked exception, many programmers catch it with an empty [catch] block just to silence the compiler: the error disappears silently, which is the worst possible outcome;
  • Checked exceptions do not play well with lambdas (Chapter 7): a lambda passed to a library function cannot throw a checked exception without some contortions;
  • Finally, many recent libraries in the Java ecosystem (Spring, Hibernate, which we’ll use later) already use only unchecked exceptions. In the Java course, we often wrapped a checked exception in an unchecked exception: in Kotlin, this is unnecessary.

The trade-off is that the compiler no longer reminds us that a function might fail: it’s up to the programmer to know which exceptions a function might throw (the documentation specifies this) and to catch them where they can be handled.

When a Kotlin function needs to be called from Java code, the [@Throws(IOException::class)] annotation generates the [throws IOException] clause in the bytecode that the Java compiler expects. Without it, a Java program that calls the function and catches the [IOException] exception is rejected by the Java compiler, which assumes the exception cannot occur ([error: exception IOException is never thrown in body of corresponding try statement]). The annotation has no effect on Kotlin callers.

The [Basics23.kt] program covers the various cases of the corresponding Java program:

// no checked exceptions in Kotlin
package basics23

import java.io.IOException
import java.io.UncheckedIOException
import java.nio.file.NoSuchFileException
import kotlin.io.path.Path
import kotlin.io.path.readText

fun main() {
    // 1 - readFile can throw an IOException: we catch it because we want to
    try {
        val content = readFile("missing.txt")
        println(content)
    } catch (e: IOException) {
        println("readFile : ${e.javaClass.name} - ${e.message}")
    }
    // 2 - Thread.sleep can throw an InterruptedException: no try is required
    pause(100)
    println("wait of 100 ms finished")
    // 3 - an ArithmeticException, like any exception, can be caught
    try {
        println("divide(1, 0) = ${divide(1, 0)}")
    } catch (e: ArithmeticException) {
        println("divide : ${e.javaClass.name} - ${e.message}")
    }
    // 4 - an exception wrapped in another one, which adds information
    try {
        println(readConfiguration("absent.properties"))
    } catch (e: UncheckedIOException) {
        println("${e.message} ; cause : ${e.cause?.javaClass?.simpleName}")
    }
    // 5 - the place of a few exceptions in the class hierarchy
    println(ancestors(NoSuchFileException("x")))
    println(ancestors(NumberFormatException()))
    println(ancestors(OutOfMemoryError()))
}

// readText throws an IOException: nothing to declare for Kotlin,
// @Throws declares it for callers written in Java
@Throws(IOException::class)
fun readFile(name: String): String = Path(name).readText()

// Thread.sleep throws an InterruptedException: nothing to declare
fun pause(milliseconds: Long) {
    Thread.sleep(milliseconds)
}

// an integer division can throw an ArithmeticException
fun divide(a: Int, b: Int): Int = a / b

// the IOException is caught then rethrown, wrapped in a more explicit exception
fun readConfiguration(name: String): String {
    try {
        return Path(name).readText()
    } catch (e: IOException) {
        throw UncheckedIOException("unreadable configuration: $name", e)
    }
}

// the chain of parent classes of the class of an object
fun ancestors(o: Any): String {
    val chain = StringBuilder()
    var cls: Class<*>? = o.javaClass
    while (cls != null) {
        if (chain.isNotEmpty()) {
            chain.append(" -> ")
        }
        chain.append(cls.simpleName)
        cls = cls.superclass
    }
    return chain.toString()
}

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=basics23.Basics23Kt
readFile : java.nio.file.NoSuchFileException - missing.txt
wait of 100 ms finished
divide : java.lang.ArithmeticException - / by zero
unreadable configuration: absent.properties ; cause : NoSuchFileException
NoSuchFileException -> FileSystemException -> IOException -> Exception -> Throwable -> Object
NumberFormatException -> IllegalArgumentException -> RuntimeException -> Exception -> Throwable -> Object
OutOfMemoryError -> VirtualMachineError -> Error -> Throwable -> Object

Comments:

  • lines 41–42: The extension function [readText()] from the package [kotlin.io.path] (Chapter 5) reads the entire contents of a file, specified by a path [Path]; it can throw an [IOException]. The [readFile] function does not intercept it and has nothing to declare for Kotlin. The [@Throws] annotation is there for a potential Java caller;
  • lines 12–16: the function [main] intercepts [IOException] because it chooses to, not because the compiler requires it. Since the file does not exist, the exception thrown is a *[NoSuchFileException]*, a class derived from *[IOException]*, whose message is the file name;
  • lines 45–46: [Thread.sleep] suspends the program for the specified duration in milliseconds; it may throw a [InterruptedException], which we’ll cover in Chapter 8. In Java, this is a checked exception; in Kotlin, the function [pause] requires no declaration;
  • line 19: the call to [pause] is not nested within a [try / catch]. The corresponding Java program was required to do so;
  • line 50: an integer division may throw a [ArithmeticException];
  • lines 22–25: the caller catches the exception because it chooses to;
  • lines 53–57: In Java, wrapping a *[IOException] inside a *[UncheckedIOException] allowed it to be omitted from the declaration. In Kotlin, this reason no longer applies, but the technique remains useful for another reason: the new exception carries a more explicit message for the caller (“unreadable configuration”), and the original exception is passed to it as the cause;
  • line 31: the property [cause] (the Java method [getCause()]) returns the original exception. It is of type [Throwable?]: hence the operator [?.];
  • Lines 62–72: The [ancestors] function traverses the parent class chain of an object (property [superclass], the Java method [getSuperclass()]) all the way up to the class [Object], the root of all Java classes. The variable [cls] is of type [Class<*>?] (a class of any type, or [null]), because [superclass] yields [null] for the class [Object];
  • Lines 34–36: The results (the last three lines) reflect the hierarchy described above. In Kotlin, the root of all classes is [Any], which corresponds to the Java class [Object].

If we remove the annotation [@Throws] and even the [try / catch] from the function [main], the program is still accepted by the Kotlin compiler. The uncaught exception would then terminate the program, which would display the exception and the call stack and exit with a return code of 1. This is acceptable for a small test program, but not for a real application. In this course, we catch an exception where we know how to handle it.

3.6.6. Resource Release: The use Function

A resource (an open file, a network connection, a database connection) must be closed after use, even if an error occurs while it is being used. It can be closed within a [finally] clause, which requires several lines of code that can be tricky to write. Java 7 introduced the **try with resources ([try (Type resource = ...) { ... }]*) for this purpose. Kotlin does not have this syntax; instead, it uses a simple function from its standard library, the *[use] function:

1
2
3
resource.use { r ->
    use of the resource r
}

The [use] function applies to any resource whose type implements the Java interface [AutoCloseable] (or [Closeable], which derives from it), an interface that contains only the method [close()]; this is the case for all Java file, network, and database classes. It executes the block passed to it (a lambda, Chapter 7), whose parameter [r] refers to the resource, then closes the resource (by calling its method [close()]), whether the block terminates normally or with an exception. It returns the value of the block. If the parameter name is omitted, the resource is referred to in the block as [it].

[use] is an extension function: a function written in Kotlin, outside the [AutoCloseable] interface, but called as if it were a method of that interface. We’ll learn how to write such functions in Chapter 4. Multiple resources are handled using nested [use] blocks: the innermost one is closed first. The [use] can be placed inside a [try] along with [catch] and [finally] clauses, which are executed after the resources have been closed.

We mentioned earlier that Kotlin does not have Java’s multi-catch feature ([catch (NumberFormatException | ArithmeticException e)]). To handle multiple exception types in the same way, we catch a common type and then sort the exceptions using a [when].

The program [Basics24.kt] is as follows:

// the use function replaces Java's try-with-resources
package basics24

import java.io.StringReader

fun main() {
    // 1 - a resource is closed automatically when leaving the use block
    openResource("R1").use { r ->
        println("using ${r.name}")
    }
    // 2 - several resources: nested use calls. They are closed in the reverse order
    // of their opening, even if an exception occurs, and before the catch and finally clauses
    try {
        openResource("R2").use { r2 ->
            openResource("R3").use { r3 ->
                println("using ${r2.name} and ${r3.name}")
                throw IllegalStateException("incident during use")
            }
        }
    } catch (e: IllegalStateException) {
        println("catch : ${e.message}")
    } finally {
        println("finally")
    }
    // 3 - a BufferedReader is a resource
    StringReader("line 1\nline 2").buffered().use { reader ->
        while (true) {
            // readLine returns null at the end of the text
            val line = reader.readLine() ?: break
            println("read: $line")
        }
    }
    // 4 - use returns the value of its block
    val nbLines = StringReader("a\nb\nc").buffered().use { reader -> reader.readLines().size }
    println("number of lines: $nbLines")
    // 5 - the same handling for several exception types
    val operations = arrayOf(arrayOf("10", "2"), arrayOf("10", "zero"), arrayOf("10", "0"))
    for (operation in operations) {
        try {
            val a = operation[0].toInt()
            val b = operation[1].toInt()
            println("$a / $b = ${a / b}")
        } catch (e: RuntimeException) {
            when (e) {
                is NumberFormatException, is ArithmeticException ->
                    println("${operation.contentToString()} : ${e.javaClass.simpleName} - ${e.message}")
                // the other exceptions are rethrown
                else -> throw e
            }
        }
    }
}

// a resource: an object that implements the AutoCloseable interface (chapter 4)
data class Resource(val name: String) : AutoCloseable {
    override fun close() {
        println("closing $name")
    }
}

fun openResource(name: String): Resource {
    println("opening $name")
    return Resource(name)
}

Results:

> gradlew -q run -Pmain=basics24.Basics24Kt
opening R1
using R1
closing R1
opening R2
opening R3
using R2 and R3
closing R3
closing R2
catch : incident during use
finally
read: line 1
read: line 2
number of lines: 3
10 / 2 = 5
[10, zero] : NumberFormatException - For input string: "zero"
[10, 0] : ArithmeticException - / by zero

Comments:

  • Lines 55–57: a demonstration resource. This is a data class (Chapter 4), a very simple class in which each object has a name; it implements the [AutoCloseable] interface by redefining ([override]) the [close()] method, which displays a message;
  • lines 61–63: the function [openResource] displays a message and creates a resource;
  • lines 8–9: the resource [r] is automatically closed at the end of the block (line 4 of the results);
  • Lines 14–17: Two resources, with two nested [use] instances. An exception is thrown while they are being used (line 17). The results (lines 5–11) show that R3 is closed before R2, and that both are closed before the [catch] and [finally] clauses are executed;
  • line 26: a [BufferedReader] that reads a string here as if it were a file. The extension function [buffered()] creates a [BufferedReader] from the [StringReader], without writing [BufferedReader(StringReader(...))]. We will read actual files in Chapter 5;
  • line 29: [readLine()] returns the next line, or [null] at the end of the text. In Kotlin, an assignment is not an expression: you cannot write [while ((line = reader.readLine()) != null)], as you can in Java. The operator [?:] followed by [break] (an expression of type [Nothing], such as [throw]) exits the loop when [readLine()] evaluates to [null]. The variable [line] is then of type [String], which is non-nullable. In Chapter 5, we will look at functions that read all lines without a loop ([readLines], [forEachLine], [useLines]);
  • line 34: [use] returns the value of its block—in this case, the number of lines read by [readLines()]—after closing the reader;
  • lines 43–48: Replacing the multi-catch block: the common parent class [RuntimeException] is intercepted, then a [when] applies the same handling to the two expected types (a [when] clause can list multiple [is] tests, separated by commas). The other exceptions are rethrown; unexpected errors are not suppressed;
  • line 46: [operation.contentToString()] returns the contents of the array in the form “[10, zero],” similar to [Arrays.toString] in Java. The property [e.javaClass.simpleName] provides the name of the exception class (lines 16 and 17 of the results).

3.7. Sample Application - V1

We aim to write a program to calculate a taxpayer’s tax. We’ll consider the simplified case of a taxpayer who has only a single salary to report. The figures are taken from the 2004 tax schedule for 2003 income and are used as-is: this is a programming exercise, not a calculation of current taxes. This application will serve as our guiding thread: we will revisit it in the following chapters, incorporating classes, files, a database, coroutines, and eventually network functionality.

  • We calculate the number of shares for the employee: shares = nbChildren/2 + 1 if the employee is unmarried, nbChildren/2 + 2 if married, where nbChildren is the number of children;
  • if he has at least three children, he receives an additional half-share;
  • his taxable income R is calculated as R = 0.72 * S, where S is his annual salary;
  • his family quotient is calculated as QF = R / shares;
  • his tax I is calculated using the following table:
threshold
coeffR
coeffN
4962
0
0
8382
0.068
291.09
14,753
0.191
1,322.92
23,888
0.283
2,668.39
38,868
0.374
4,846.98
47,932
0.426
6,883.66
0
0.481
9,505.54

Each row has 3 fields. To calculate tax I, find the first row where QF is less than or equal to the limit. For example, if QF = 5,000, the row found will be:

8382    0.068    291.09

Tax I is then equal to 0.068 * R - 291.09 * shares, rounded to the nearest euro. If QF is such that the condition QF <= limit is never satisfied, the coefficients in the last row are used: I = 0.481 * R - 9505.54 * shares. The limit 0 in the last row is therefore never used.

The calculations involve amounts and decimal coefficients: we perform them using the Java class [BigDecimal], which Kotlin uses directly—and even more conveniently than Java: the Kotlin standard library defines the operators [+], [-], [*], and the comparison operators [<], [>]... apply to any comparable object. The corresponding Kotlin program [Tax1.kt] is as follows:

// tax calculation - version 1
// 2004 tax scale (2003 income): this is an exercise, not the current tax scale
package tax1

import java.math.BigDecimal
import java.math.MathContext

fun main() {
    // data arrays needed for the tax calculation
    val limits = decimals("4962", "8382", "14753", "23888", "38868", "47932", "0")
    val coeffR = decimals("0", "0.068", "0.191", "0.283", "0.374", "0.426", "0.481")
    val coeffN = decimals("0", "291.09", "1322.92", "2668.39", "4846.98", "6883.66", "9505.54")

    // marital status
    val married = readMaritalStatus()
    // number of children
    val nbChildren = readPositiveInteger("Number of children: ")
    // annual salary
    val salary = readPositiveInteger("Annual salary: ")

    // number of shares
    var shares = nbChildren.toBigDecimal().divide(BigDecimal.TWO) + (if (married) 2 else 1).toBigDecimal()
    if (nbChildren >= 3) {
        shares += BigDecimal("0.5")
    }

    // taxable income
    val income = BigDecimal("0.72") * salary.toBigDecimal()

    // family quotient (the division may not be exact: a precision is set)
    val quotient = income.divide(shares, MathContext.DECIMAL128)

    // search for the tax bracket: the first one such that quotient <= limit,
    // the last one if none fits
    var i = 0
    while (i < limits.size - 1 && quotient > limits[i]) {
        i++
    }

    // the tax (truncated to the euro)
    val tax = (coeffR[i] * income - coeffN[i] * shares).toInt()

    // displaying the result
    println("Tax to pay: $tax euros")
}

// displays a prompt, reads a line and returns it without leading and trailing spaces
// (empty string if the input stream is exhausted)
fun read(prompt: String): String {
    print(prompt)
    return (readlnOrNull() ?: "").trim()
}

// asks for the marital status until a correct answer is obtained
fun readMaritalStatus(): Boolean {
    while (true) {
        when (read("Are you married (Y/N)? ").lowercase()) {
            "y" -> return true
            "n" -> return false
            else -> System.err.println("Incorrect answer. Try again")
        }
    }
}

// asks for a positive or zero integer until a correct answer is obtained
fun readPositiveInteger(prompt: String): Int {
    while (true) {
        // toIntOrNull returns null if the input is not an integer: no exception to handle
        val number = read(prompt).toIntOrNull()
        if (number != null && number >= 0) {
            return number
        }
        System.err.println("Incorrect answer. Try again")
    }
}

// converts strings into exact BigDecimal numbers
fun decimals(vararg values: String): Array<BigDecimal> =
    Array(values.size) { i -> BigDecimal(values[i]) }

Comments:

  • Lines 10–12: The three arrays in [BigDecimal] are constructed by the function [decimals] (lines 78–79) from character strings: each value, such as 0.068, is therefore exact. With [Double], 0.068 would be only a binary approximation. The function [decimals] accepts any number of parameters ([vararg values: String]), which it receives in an array; this notation is explained in the section “Passing Parameters to a Function.” The result array is constructed by [Array(size) { i -> value }], which calculates each element based on its index;
  • line 15: the entry of marital status is handled by the function [readMaritalStatus] (lines 55–60). The Java program performed all entries in its method [main], using variables declared without initial values; In Kotlin, each entry is a function that returns the entered value, and the variables [married], [nbChildren], and [salary] can be [val];
  • Line 49: The function [read] displays the prompt with [print] (without a line break) and reads the response; if the input stream is closed, the value [null] returned by [readlnOrNull] is replaced with an empty string. The function [trim()] removes the leading and trailing spaces from the read string;
  • lines 56–60: a loop [while (true)] that exits via [return] when the answer is correct. The [when] operates on the answer converted to lowercase by [lowercase()]: this allows both Y and y to be accepted. In Kotlin, strings are compared using the [==] operator, which compares their contents (this is what [when] does); the Java method [equals] is not needed;
  • line 60: error messages are written to the [System.err] stream;
  • Lines 66–73: Input of a positive or zero integer, used for the number of children and for the salary: the Java program was repeating the same loop twice. [toIntOrNull()] returns [null] if the input is not an integer: there is neither a [try] nor an exception to handle. Line 70, after the [number != null] test, the compiler knows that [number] is not [null]: it accepts the comparison [number >= 0] and the instruction [return number] from a function that returns a [Int] (smart type casting);
  • Line 22: The number of shares is calculated as [BigDecimal]. You cannot simply write [nbChildren / 2]: if nbChildren is 3, you would get 3/2, an integer division that would result in 1 rather than 1.5. The extension function [toBigDecimal()] converts the integer, [divide(BigDecimal.TWO)] divides it exactly by two, and the operator [+] adds 2 or 1, as determined by an expression [if]. Note: The Kotlin operator [/] applied to [BigDecimal] preserves the number of decimal places in the dividend and rounds the result: [3.toBigDecimal() / 2.toBigDecimal()] is 2, not 1.5. For a division involving [BigDecimal], the [divide] method is always used;
  • line 24: a [BigDecimal] is immutable: the operator [+=] does not modify the object; it creates a new object [shares + BigDecimal("0.5")] and assigns it to the variable, hence the declaration [var] of [shares]. In Java, you would have had to write [shares = shares.add(...)], and forgetting the assignment was a classic pitfall;
  • line 28: taxable income, calculated using the operator [*];
  • line 31: the family quotient. Dividing income by 1.5 or 3.5 shares generally does not result in a finite decimal representation: we set a precision, here [MathContext.DECIMAL128] (34 significant digits), which is more than sufficient;
  • lines 35–37: determining the tax bracket. The search stops at the first upper limit that is greater than or equal to the family quotient, and no later than the last tax bracket, using the condition [i < limits.size - 1]. In Kotlin, the operator [>] applies to [BigDecimal]: [quotient > limits[i]] is translated by the compiler into [quotient.compareTo(limits[i]) > 0], which is how we would have written it in Java. However, we do not compare two [BigDecimal] expressions with [==], which calls [equals] and takes the number of decimal places into account: 2.0 and 2.00 are not “equal” for [equals];
  • Line 41: The tax is truncated to the nearest euro by the function [toInt()], which discards the decimal part.

Here are some examples of the output (the entries, typed on the keyboard, have been included in the display):

1
2
3
4
5
> gradlew -q run -Pmain=tax1.Tax1Kt
Are you married (Y/N)? y
Number of children: 2
Annual salary: 60000
Tax to pay: 4282 euros
> gradlew -q run -Pmain=tax1.Tax1Kt
Are you married (Y/N)? yes
Incorrect answer. Try again
Are you married (Y/N)? y
Number of children: three
Incorrect answer. Try again
Number of children: 3
Annual salary: 60000 euros
Incorrect answer. Try again
Annual salary: 60000
Tax to pay: 2959 euros
1
2
3
4
5
> gradlew -q run -Pmain=tax1.Tax1Kt
Are you married (Y/N)? n
Number of children: 0
Annual salary: 60000
Tax to pay: 11519 euros

For an annual salary of 60,000 euros, a married taxpayer with 2 children pays 4,282 euros, with 3 children 2,959 euros, and a single person with no children 11,519 euros: these are the results from the Java version. In the second run, the input “60000 euros” is rejected: [toIntOrNull] accepts only strings that represent an exact integer (leading and trailing spaces were removed by [trim()]).

3.8. Main Program Arguments

The main function [main] can accept an array of strings [Array<String>] as a parameter. This array contains the command-line arguments used to launch the application. Thus, if we launch program P with the command:

java -jar P.jar arg0 arg1 ... argn

and if the function [main] is declared as follows:

fun main(args: Array<String>) {

we will have args[0]="arg0", args[1]="arg1", and so on. When the program does not need its arguments, we simply write [fun main()], as we have done so far. With Gradle, arguments intended for the program are passed to the task [run] via the option [--args], in the form of a single string enclosed in quotes; Gradle’s own options (such as [-q] or [-P]) are placed next to it:

gradlew -q run -Pmain=package.ProgKt --args="a0 a1 a2"

For IntelliJ and IDEA, the arguments are entered in the program’s run configuration (in the “Program arguments” field).

Here is an example, [Basics25.kt]:

1
2
3
4
5
6
7
8
9
// the command-line arguments
package basics25

fun main(args: Array<String>) {
    println("There are ${args.size} argument(s)")
    for (i in args.indices) {
        println("args[$i]=${args[i]}")
    }
}

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=basics25.Basics25Kt --args="a0 a1 a2 a3"
There are 4 argument(s)
args[0]=a0
args[1]=a1
args[2]=a2
args[3]=a3

An argument that contains spaces must be enclosed in quotation marks; within the string [--args], single quotes are used:

1
2
3
4
> gradlew -q run -Pmain=basics25.Basics25Kt --args="'jean dupont' 23"
There are 2 argument(s)
args[0]=jean dupont
args[1]=23

Without an argument, the array [args] is empty; it is never equal to [null]:

> gradlew -q run -Pmain=basics25.Basics25Kt
There are 0 argument(s)

Comments:

  • line 4: the program’s entry point. The compiler generates a class [Basics25Kt] containing a method [public static void main(String[] args)]: this is exactly the standard Java entry point;
  • line 5: the size of an array is specified by its [size] property (the [length] field in Java);
  • lines 6–7: iterating through the array indices.

In Java, the standard [main] method is a static method of a class. Kotlin does not have static methods; however, if you want the entry point to be a class with a specific name (for example, for a tool that requires it), you place the function [main] inside an object, declared with the keyword [object] (Chapter 4), and annotate it with [@JvmStatic]. This is what the program [Basics26.kt] does:

// the command-line arguments with a main function placed in an object
package basics26

object Basics26 {
    @JvmStatic
    fun main(args: Array<String>) {
        println("There are ${args.size} argument(s)")
        for ((i, arg) in args.withIndex()) {
            println("args[$i]=$arg")
        }
    }
}

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=basics26.Basics26 --args="a0 a1 a2 a3"
There are 4 argument(s)
args[0]=a0
args[1]=a1
args[2]=a2
args[3]=a3

Comments:

  • line 4: a [Basics26] object. The compiler generates a [Basics26] class, with a single object;
  • Lines 5–6: The annotation [@JvmStatic] instructs the compiler to make [main] a static method of the class [Basics26], which is what the Java Virtual Machine expects. The class to be executed is therefore [basics26.Basics26], not [basics26.Basics26Kt];
  • lines 8–9: Iterating through the array using the function [withIndex()], which returns both the index and the element.

This form is rare in Kotlin: a high-level function [main], outside of any class, is the norm. Finally, note that, as in Java, the function [main] does not return a value: the program’s exit code is set with [exitProcess].

3.9. Enumerations

An enumeration is a data type whose value domain consists of a set of named constants. Consider a program that needs to handle exam grades. There are five possible grades: Pass, Fairly Good, Good, Very Good, and Excellent. We can then define an enumeration for these five constants:

enum class Honours { PASS, FAIRLY_GOOD, GOOD, VERY_GOOD, EXCELLENT }

Kotlin writes [enum class]*** where Java writes [enum]*: [enum] is not a reserved keyword, but a modifier** for a class declaration. By convention, constant names are written in uppercase. A variable can be declared to take its values from the enumeration:

    val myHonours = Honours.PASS

You can compare a variable to the different possible values in the enumeration using the == operator:

1
2
3
    if (myHonours == Honours.PASS) {
        println("Could do better")
    }

As in Java, and unlike other languages where an enum is merely a set of named integers, a Kotlin enum is a class, compiled into a class derived from [java.lang.Enum], whose only objects are the declared constants. Each constant is a unique object, created once and for all. Each constant knows its name (property [name], or [toString()]) and its ordinal number in the declaration (property [ordinal], starting at 0). Constants are comparable in this order: you can write [m1 < m2]. Every enumeration also has:

  • [Honours.entries]: the (non-modifiable) list of constants in the [Honours] enumeration, in the order in which they were declared. It is a better alternative to the [Honours.values()] function—inherited from Java and still available—which creates a new array on each call;
  • [Honours.valueOf(name)]: returns the constant named [name], and throws a [IllegalArgumentException] if no constant has that name.

An enumeration cannot be converted to an integer, nor can an integer be converted to an enumeration: we use [ordinal] and [entries].

The program [Basics27.kt] illustrates what has just been described:

// enumerations
package basics27

enum class Honours { PASS, FAIRLY_GOOD, GOOD, VERY_GOOD, EXCELLENT }

fun main() {
    // a variable that takes its values from the Honours enumeration
    val myHonours = Honours.PASS
    // displaying the value of the variable
    println("honours=$myHonours")
    // test with a value of the enumeration
    if (myHonours == Honours.PASS) {
        println("Could do better")
    }
    // list of honours: name and ordinal number
    for (m in Honours.entries) {
        println("${m.name} = ${m.ordinal}")
    }
    // ordinal number --> enumeration
    val m3 = Honours.entries[3]
    println("Honours.entries[3] = $m3")
    // string --> enumeration
    val m2 = Honours.valueOf("GOOD")
    println("m2=$m2, m2 after PASS : ${m2 > Honours.PASS}")
    try {
        Honours.valueOf("Good")
    } catch (e: IllegalArgumentException) {
        println("valueOf(\"Good\") : ${e.message}")
    }
    // without an exception: null if the string is not a constant name
    val m4 = Honours.entries.find { it.name == "Good" }
    println("m4=$m4")
    // an enumeration in a when expression: no else needed if all cases are handled
    val assessment = when (myHonours) {
        Honours.PASS, Honours.FAIRLY_GOOD -> "correct"
        Honours.GOOD, Honours.VERY_GOOD -> "very correct"
        Honours.EXCELLENT -> "remarkable"
    }
    println("assessment : $assessment")
    println("type : ${Honours::class.java.name}")
    println("parent class : ${Honours::class.java.superclass.name}")
}

Results:

> gradlew -q run -Pmain=basics27.Basics27Kt
honours=PASS
Could do better
PASS = 0
FAIRLY_GOOD = 1
GOOD = 2
VERY_GOOD = 3
EXCELLENT = 4
Honours.entries[3] = VERY_GOOD
m2=GOOD, m2 after PASS : true
valueOf("Good") : No enum constant basics27.Honours.Good
m4=null
assessment : correct
type : basics27.Honours
parent class : java.lang.Enum

Comments:

  • line 4: the enumeration declaration. It goes at the top of the file, next to the functions; alternatively, like a class (Chapter 4), it can be declared in its own file, [Honours.kt];
  • line 8: a variable of type [Honours], with the type inferred from the assigned value;
  • line 10: an enumeration constant is listed by name;
  • lines 16–17: the list of constants, their names, and their index numbers;
  • line 20: the conversion of an index number to a constant: [entries] is a list, which is indexed like an array;
  • line 23: converting a string to an enumeration constant;
  • line 24: constants are compared based on their order of declaration: GOOD comes after PASS. The operator [>] is translated by the compiler into a call to the method [compareTo], which would have to be written in Java;
  • lines 25–28: [valueOf] is case-sensitive: “Good” is not a constant name. The exception message provides the full name of the enumeration, [basics27.Honours];
  • line 31: to avoid the exception, you can search for the constant in [entries] using the function [find], which returns the first element that satisfies the condition written between curly braces (a lambda, Chapter 7; where [it] denotes the element being examined), or [null] if there are none;
  • lines 34–37: enumerations lend themselves well to [when] expressions. Since all constants are processed, the expression is exhaustive without a [else] clause. If a constant is added to the enumeration later, the compiler will reject the [when] expression, which has become incomplete, by naming the omitted constant. For example, without line 37: [error: 'when' expression must be exhaustive. Add the 'EXCELLENT' branch or an 'else' branch.]. This is a good reason not to include a [else] clause within a [when] for an enumeration. Unlike Java’s [switch], constants are prefixed with the enumeration name; a [import basics27.Honours.*] statement would allow their names to be written on their own;
  • line 40: the full name of the enum is [basics27.Honours]: it is declared at the file level, in the [basics27] package. [Honours::class.java] is the Java class for the enum;
  • line 41: The parent class of any enumeration is [java.lang.Enum].

3.9.1. Enhanced Enumerations

Since an enumeration is a class, it can have properties, a constructor, and methods. Kotlin makes this particularly concise: properties are declared using [val] within the parentheses following the enum’s name (the primary constructor, Chapter 4), and each constant is created by calling this constructor with the values written in parentheses after its name. This is a widely used feature that eliminates the need to manage separate arrays of values associated with the constants.

To represent a set of options, such as file access permissions (read, write, execute), people used to combine powers of 2 with the [or] operator (bitwise OR). Instead, we use an enum and the Java class [EnumSet], a set of constants from an enum, which is just as efficient as a bitwise combination (it is one, internally) but much more readable. The [EnumMap] class is similarly a dictionary whose keys are the constants of an enumeration. Kotlin uses them directly.

The program [Basics28.kt] is as follows:

// enriched enumerations: properties, constructor, methods; EnumSet and EnumMap
package basics28

import java.util.EnumMap
import java.util.EnumSet

// each honour has a minimum grade and a label
enum class Honour(val minimumGrade: Int, val label: String) {
    PASS(10, "Pass"),
    FAIRLY_GOOD(12, "Fairly good"),
    GOOD(14, "Good"),
    VERY_GOOD(16, "Very good"),
    EXCELLENT(18, "Excellent");

    // a method: the next honour (null for the last one)
    fun next(): Honour? = entries.getOrNull(ordinal + 1)

    // a function of the enumeration itself: the honour matching a grade (null if grade < 10)
    companion object {
        fun forGrade(grade: Double): Honour? = entries.lastOrNull { grade >= it.minimumGrade }
    }
}

// access permissions: a set of values will be represented by an EnumSet
enum class Permission { READ, WRITE, EXECUTE }

fun main() {
    for (m in Honour.entries) {
        println("$m : \"${m.label}\" from ${m.minimumGrade}, next : ${m.next()}")
    }
    val grades = doubleArrayOf(8.5, 12.0, 15.5, 19.0)
    for (grade in grades) {
        val label = Honour.forGrade(grade)?.label ?: "no honours"
        println("grade $grade : $label")
    }

    // EnumSet: a set of values of an enumeration (replaces bit combinations)
    val permissions = EnumSet.of(Permission.READ, Permission.WRITE)
    println("permissions=$permissions, read : ${Permission.READ in permissions}, execute : ${Permission.EXECUTE in permissions}")
    permissions.add(Permission.EXECUTE)
    permissions.remove(Permission.WRITE)
    println("permissions=$permissions")
    println("all=${EnumSet.allOf(Permission::class.java)}, none=${EnumSet.noneOf(Permission::class.java)}" +
            ", complement of [READ]=${EnumSet.complementOf(EnumSet.of(Permission.READ))}")
    println("from FAIRLY_GOOD to VERY_GOOD : ${EnumSet.range(Honour.FAIRLY_GOOD, Honour.VERY_GOOD)}")

    // EnumMap: a dictionary whose keys are the values of an enumeration (chapter 5)
    val headcounts = EnumMap<Honour, Int>(Honour::class.java)
    headcounts[Honour.GOOD] = 7
    headcounts[Honour.PASS] = 12
    println("headcounts=$headcounts")
}

Results:

> gradlew -q run -Pmain=basics28.Basics28Kt
PASS : "Pass" from 10, next : FAIRLY_GOOD
FAIRLY_GOOD : "Fairly good" from 12, next : GOOD
GOOD : "Good" from 14, next : VERY_GOOD
VERY_GOOD : "Very good" from 16, next : EXCELLENT
EXCELLENT : "Excellent" from 18, next : null
grade 8.5 : no honours
grade 12.0 : Fairly good
grade 15.5 : Good
grade 19.0 : Excellent
permissions=[READ, WRITE], read : true, execute : false
permissions=[READ, EXECUTE]
all=[READ, WRITE, EXECUTE], none=[], complement of [READ]=[WRITE, EXECUTE]
from FAIRLY_GOOD to VERY_GOOD : [FAIRLY_GOOD, GOOD, VERY_GOOD]
headcounts={PASS=12, GOOD=7}

Comments:

  • Line 8: The enumeration and its primary constructor. The keywords [val] make [minimumGrade] and [label] read-only properties of each constant. The Java program would have had to declare two fields named [private final], write the constructor that initializes them, and then write two methods, [getMinimumGrade] and [getLabel], to read them: Kotlin generates all of this. The constructor of an enum is private: you cannot create additional constants;
  • lines 9–13: the enumeration’s constants, each with its two parameters. The semicolon after the last constant is required when members follow it: this is one of the very few semicolons required in Kotlin;
  • line 16: a method that applies to a constant ([m.next()]). It uses the constant’s property [ordinal] and the list [entries]; [getOrNull] returns the element at the given index, or [null] if the index is out of bounds: the last constant has no successor. The return type [Honour?] indicates this;
  • lines 19–20: a function that is called on the enumeration itself ([Honour.forGrade(grade)]), similar to a static method in Java. Kotlin does not have static methods: they are placed in the class’s companion object ([companion object]), introduced in Chapter 4. Since the constants are declared in ascending order by score, the last one whose minimum score is met is the correct one: [lastOrNull] returns the last element in the list that satisfies the condition, or [null] if there are none. The result is of type [Honour?], or [null] if the score is less than 10: where Java used a [Optional], Kotlin uses a nullable type;
  • line 25: a simple enumeration for access rights;
  • lines 28–29: the list of entries with their properties: we write [m.label], not [m.getLabel()];
  • line 31: an array of real numbers [DoubleArray]. The scores are written as 12.0 and 19.0, not 12 and 19: Kotlin does not implicitly convert a [Int] to a [Double];
  • line 33: the operator [?.] reads the property [label] only if [Honour.forGrade(grade)] is not [null]; the operator [?:] returns a text otherwise. This is equivalent to Java’s [map(Honour::getLabel).orElse("no honours")];
  • line 38: a set of two rights;
  • line 39: the operator [in] checks whether a constant belongs to the set, just as one would check a bit. It is translated by the compiler into a call to the method [contains];
  • lines 40–41: constants are added to and removed from the set;
  • lines 43–44: the set of all constants, the empty set, and the complement of a set. [Permission::class.java] is the Java class for the enumeration, which these Java methods call ([Permission.class] in Java);
  • line 45: the constants between two constants, in the order in which they were declared;
  • lines 48–50: a [EnumMap] associates a set of elements with certain entries. The notation [headcounts[key] = value] replaces the call [headcounts.put(key, value)]. The keys of a [EnumMap] are always traversed in the order in which the constants are declared: PASS appears before GOOD, even though it was added later. Dictionaries are discussed in Chapter 5.

3.10. Passing Parameters to a Function

In Kotlin, a function can be declared at the top level of a file, outside of any class, like our [main] functions: these are called top-level functions. A function declared within a class is a method (Chapter 4). Here, we’ll focus on how parameters are passed to a function. Consider the following function:

1
2
3
fun changeInt(a: Int) {
    println("Formal parameter a=$a")
}

In the function definition, on line 1, [a] is referred to as a formal parameter. It is included solely for the purposes of the function definition [changeInt]. It could just as easily have been named [b]. Its type is required: type inference does not apply to parameters. Let’s now consider an instance of this function:

1
2
3
    val age = 20
    changeInt(age)
    println("Actual parameter age=$age")

Here, in the statement on line 2, [age] is the actual parameter (also called an argument) that will pass its value to the formal parameter [a]. We are interested in how a formal parameter retrieves the value of an actual parameter.

3.10.1. Pass-by-value and immutable parameters

Like Java, Kotlin has only one mode of parameter passing: pass-by-value. The value of the actual parameter is copied into the corresponding formal parameter. These are two distinct entities.

Kotlin goes a step further than Java: a formal parameter is always a [val]. It cannot be assigned a new value within the function; it’s as if, in Java, all parameters were declared as [final]. The question “Can the function modify the actual parameter by modifying its formal parameter?” therefore does not even arise. If the function needs a variable that it modifies, it declares a local variable initialized with the parameter’s value.

When the parameter is an array or an object, the value passed is the reference to the object: the formal parameter and the actual parameter then refer to the same object. The function can therefore modify this object, if it is mutable, and the caller will see the change; however, it cannot cause the caller’s variable to refer to a different object.

Kotlin does not support pass-by-reference, in which the formal parameter and the actual parameter would be one and the same variable (such as the parameters [ref] and [out] in C#): a function cannot modify a local variable of its caller, nor can it “return” results to the caller via its parameters. Instead, we use:

  • the function’s return value, which the caller assigns to its variable;
  • a data class or a tuple ([Pair], [Triple]) when the function needs to return multiple values;
  • an array or a mutable object passed as a parameter, into which the function places its results.

The program [Basics29.kt] illustrates these different cases:

// parameter passing: always by value, and parameters are vals
package basics29

import java.math.BigDecimal

fun main() {
    // 1 - passing by value: the actual parameter is not modified
    val age = 20
    changeInt(age)
    println("Actual parameter age=$age")

    // 2 - to modify a variable of the caller: the function returns the new value
    var age2 = 20
    age2 = changeInt2(age2)
    println("Actual parameter age2=$age2")

    // 3 - to return several values: a data class
    val c1 = convert("123")
    val c2 = convert("12x")
    println("c1=$c1, c2=$c2")
    if (c1.succeeded) {
        println("value of c1 : ${c1.value}")
    }

    // 4 - a swap of values: the values are stored in an array
    val xy = intArrayOf(1, 2)
    swap(xy)
    println("after swap : x=${xy[0]}, y=${xy[1]}")
    // swapping two local variables, without a function or a temporary variable
    var x = 1
    var y = 2
    x = y.also { y = x }
    println("after swap : x=$x, y=$y")

    // 5 - an array passed by value: it is its reference that is copied
    val array = intArrayOf(1, 2, 3)
    modifyArray(array)
    println("array=${array.contentToString()}")

    // 6 - a modifiable (mutable) object: the function modifies the referenced object
    val message = StringBuilder("Hello")
    complete(message)
    println("message=$message")

    // 7 - a parameter cannot receive a new value in the function
    val price = BigDecimal("100")
    println("InclTax($price)=${inclTax(price)}")
}

// the result of a conversion: success and value obtained
data class Conversion(val succeeded: Boolean, val value: Int)

fun changeInt(a: Int) {
    // a = 30 would be rejected: a parameter is a val. We work on a local copy
    var copy = a
    copy += 10
    println("Formal parameter a=$a, local copy=$copy")
}

fun changeInt2(a2: Int): Int {
    println("Formal parameter a2=$a2")
    return a2 + 10
}

fun convert(text: String): Conversion =
    try {
        Conversion(true, text.toInt())
    } catch (_: NumberFormatException) {
        Conversion(false, 0)
    }

fun swap(t: IntArray) {
    val temp = t[0]
    t[0] = t[1]
    t[1] = temp
}

fun modifyArray(t: IntArray) {
    // we modify the referenced array: the caller sees the modification
    t[0] = 100
    // t = intArrayOf(7, 8, 9) would be rejected: t cannot be made to point to another array
    println("in modifyArray : t=${t.contentToString()}")
}

fun complete(text: StringBuilder) {
    text.append(" everyone")
}

fun inclTax(amount: BigDecimal): BigDecimal {
    // amount = BigDecimal.ZERO would be rejected: a parameter is a val
    return amount * BigDecimal("1.2")
}

Results:

> gradlew -q run -Pmain=basics29.Basics29Kt
Formal parameter a=20, local copy=30
Actual parameter age=20
Formal parameter a2=20
Actual parameter age2=30
c1=Conversion(succeeded=true, value=123), c2=Conversion(succeeded=false, value=0)
value of c1 : 123
after swap : x=2, y=1
after swap : x=2, y=1
in modifyArray : t=[100, 2, 3]
array=[100, 2, 3]
message=Hello everyone
InclTax(100)=120.0

Comments:

  • Lines 8–10: The value 20 of the actual parameter [age] was copied into the formal parameter [a] (line 53). The function cannot modify [a]: it creates a local copy, [copy] (line 55), which it then modifies. The actual parameter remains unchanged;
  • lines 13–15: the function [changeInt2] (lines 60–62) returns a new value, which the caller assigns to its own variable [age2], declared as [var] for this purpose;
  • line 51: a data class ([data class]) declares, in a single line, a class whose objects consist of two immutable values, [succeeded] and [value]. The compiler adds, among other things, a method named [toString()], which displays its components, as well as the methods [equals()] and [hashCode()]. This is the Kotlin equivalent of Java records; data classes are covered in Chapter 4. Its components are properties: we write [c1.succeeded], not [c1.succeeded()] as with a record;
  • Lines 18–22: The function [convert] (lines 65–69) returns two pieces of information—whether the conversion was successful and the resulting value—combined into a single [Conversion] object. Its body is a [try] expression, which evaluates to one or the other [Conversion] object. Note that in Kotlin, one would typically write a function of type [Int?] that returns [null] in case of failure: this is exactly what [toIntOrNull()] does;
  • lines 26–28: swapping two values. A function of type [swap(a: Int, b: Int)] would be of no use to the caller; therefore, the two values are stored in an array, which the function [swap] (lines 72–75) modifies;
  • lines 30–32: To swap two local variables, Kotlin provides an idiom: [x = y.also { y = x }]. The function [also] executes the block, which assigns the value of [x] to [y], then returns the value of [y] before the block is executed, which is assigned to [x]. Functions such as [also] are covered in Chapter 7;
  • lines 36–38: the array reference is passed by value. The function [modifyArray] therefore operates on the same array as the caller: the modification made by [t[0]] (line 80) is visible to the caller. However, it is impossible to have [t] point to a different array: [t] is a [val];
  • Lines 41–43: An object [StringBuilder] is a mutable string: the function [complete] (lines 85–86) appends text to it, and the caller sees the result. With a [String], this would have been impossible: [String] objects are immutable;
  • lines 46–47: the function [inclTax] (lines 89–91) calculates a price that includes all taxes. The product of two [BigDecimal] objects has as many decimal places as the sum of its two factors: 100 × 1.2 = 120.0, displayed as 120.0.

If the comment on line 90 is replaced with the [amount = BigDecimal.ZERO] statement, the compiler rejects the program:

1
2
3
error: 'val' cannot be reassigned.
    amount = BigDecimal.ZERO
    ^^^^^^

3.10.2. Default values, named arguments, overloading, variable number of parameters

Java has neither optional parameters nor named arguments and must simulate them through method overloading: multiple versions of the same method, with the short versions calling the full version using default values. Kotlin, like C#, has both:

  • a parameter can have a default value: [fun greet(name: String, greeting: String = "Hello")]. The caller can then omit the corresponding actual parameter: [greet("Pierre")];
  • when calling the method, an argument can be named: [greet("Pierre", upperCase = true)]. This allows you to omit a parameter that has a default value while passing the subsequent ones, to pass arguments in any order, and, most importantly, to make the call readable: [upperCase = true] is clearer than a [true] isolated in a list of arguments.

Default values and named arguments render most overloading—and Java “builders”—unnecessary. Overloading does, however, exist in Kotlin: multiple functions can have the same name if their parameter lists differ in the number or type of parameters; the compiler selects the appropriate one based on the actual parameters. This is used when the processing differs depending on the type of the parameters. To allow a Kotlin function with default parameters to be called from Java with fewer arguments, the [@JvmOverloads] annotation instructs the compiler to generate the corresponding overloads.

Finally, a function can have a variable number of parameters: the formal parameter is preceded by the keyword [vararg], [vararg numbers: Int]. The caller can pass a list of values—which may be empty—that the compiler organizes into an array; within the function, the parameter is an array ([IntArray] for [Int], [Array<String>] for [String]...). Unlike in Java, you cannot directly pass an array in place of the list of values: you must “spread” it using the spread operator ([*]), [sum(*values)], which also allows you to mix arrays and individual values.

The program [Basics30.kt] is as follows:

// default values, named arguments, overloading, variable number of parameters
package basics30

fun main() {
    // parameters with a default value: a single function is enough
    println(greet("Pierre"))
    println(greet("Pierre", "Good evening"))
    // a named argument: we pass upperCase without passing greeting
    println(greet("Pierre", upperCase = true))
    // all arguments named, in any order
    println(greet(upperCase = true, greeting = "Hi", name = "Paul"))

    // overloading: several functions with the same name, chosen according to the parameter type
    describe(12)
    describe(12.5)
    describe("twelve")

    // vararg: variable number of parameters
    println("sum()=${sum()}")
    println("sum(1, 2, 3)=${sum(1, 2, 3)}")
    val values = intArrayOf(10, 20, 30)
    // the * operator spreads the elements of the array
    println("sum(*values)=${sum(*values)}")
    println("sum(1, *values, 100)=${sum(1, *values, 100)}")
    // Kotlin and JDK functions with a variable number of parameters
    println(listOf(1, 2, 3, 4))
    println("%s, %s and %s".format("one", "two", "three"))
    println(maxOf(3, 9, 4, 7))
    println(setOf("a", "b", "a", "c"))
}

// a function two of whose parameters have a default value
fun greet(name: String, greeting: String = "Hello", upperCase: Boolean = false): String {
    val message = "$greeting $name"
    return if (upperCase) message.uppercase() else message
}

// three functions with the same name that differ by the type of their parameter
fun describe(n: Int) = println("$n is an integer")

fun describe(x: Double) = println("$x is a real")

fun describe(s: String) = println("\"$s\" is a string of ${s.length} characters")

fun sum(vararg numbers: Int): Int {
    // numbers is an array of integers (IntArray)
    var sum = 0
    for (number in numbers) {
        sum += number
    }
    return sum
}

Results:

> gradlew -q run -Pmain=basics30.Basics30Kt
Hello Pierre
Good evening Pierre
HELLO PIERRE
HI PAUL
12 is an integer
12.5 is a real
"twelve" is a string of 6 characters
sum()=0
sum(1, 2, 3)=6
sum(*values)=60
sum(1, *values, 100)=161
[1, 2, 3, 4]
one, two and three
9
[a, b, c]

Comments:

  • Lines 33–35: a single function, [greet], in which two parameters have default values. The Java program required three overloaded methods. The body of the function returns the value of an expression, [if];
  • Line 6: [greeting] and [upperCase] take their default values, "Hello" and [false];
  • line 7: [greeting] receives “Good evening,” and [upperCase] receives its default value;
  • line 9: thanks to the named argument, we pass [upperCase] without passing [greeting], which retains its default value. In Java, the greeting also had to be passed;
  • line 11: all arguments are named, in an order different from that of the declaration;
  • lines 39–43: three [describe] functions that differ in the type of their parameter. These are expression-bodied functions: the body, reduced to a single expression, is written immediately after the [=] sign, without curly braces or [return]. The return type is then inferred from the expression ([Unit] here; the type of [println]: the function does not return a useful value);
  • lines 14–16: The compiler selects the function based on the type of the actual parameter. The character ["] is written as [\"] in a string (line 43);
  • lines 45–51: [sum] accepts any number of integers, received in the array [numbers] of type [IntArray];
  • lines 19–24: [sum] is called without parameters, with three integers, with the elements of an array spread out by the operator [*], and then with a combination of the two forms;
  • lines 26–29: Many Kotlin functions have a variable number of parameters: [listOf], [setOf], [arrayOf], [format], [maxOf]... The extension function [format] applies to the format string, not to the class [String] like Java’s static method [String.format]. Kotlin does not have static methods: [String.join(" - ", "a", "b", "c")] does not exist in Kotlin (you would write [listOf("a", "b", "c").joinToString(" - ")], Chapter 5). The set created by [setOf] removes the duplicate "a".

3.11. Local functions and tuples: Kotlin’s solutions

3.11.1. Local functions, lambdas, and local classes

Unlike Java, Kotlin allows you to define a local function—that is, a function written inside another function and usable only within that scope. This is useful for breaking up a long function without cluttering the entire file with visible functions. A local function has access to the local variables of the function that contains it and can modify them.

Inside a function, you also have access to:

  • Lambdas, which are small anonymous functions that can be stored in a variable: [val cube = { x: Int -> x * x * x }]. The variable then has a functional type, in this case [(Int) -> Int], and the lambda is called like a function: [cube(3)]. Lambdas are covered in Chapter 7, “Functional Types, Lambdas, and Events.” Unlike in Java, a Kotlin lambda can modify the local variables [var] surrounding it;
  • local classes, declared within the function and usable only within it.

3.11.2. Pairs, Triples, Destructuring, and Data Types

Other languages have tuples, which group multiple values together without the need to declare a type. Kotlin does not have a specific syntax for them, but its standard library provides two generic data classes, [Pair<A, B>] and [Triple<A, B, C>], whose components are called [first], [second], and [third]. A pair is created using [Pair(1, "one")] or, more readably, with the infix function [to]: [1 to "one"].

The components of a pair, a triplet, and any data class can be extracted in a single statement using a destructuring declaration: [val (number, name) = pair] declares two variables, initialized with the first and second components. An unnecessary component is replaced by _: [val (_, age, _) = triple]. Destructuring is also used in a loop, such as [for] (as we saw with [withIndex()]), and in the parameters of a lambda. It is based on the position of the components, not their names: [val (name, number) = pair] would place the number in [name].

To give a tuple named components, you declare a data class: [data class Couple(val number: Int, val name: String)]. This is equivalent to a Java record, but more flexible (a data class can have components like [var], be destructured, etc.). Tuples are convenient for local use when the relationship between the two values is obvious; as soon as a public function returns multiple values, a data class—whose components have names—makes the code more readable.

The program [Basics31.kt] is as follows:

// local functions, lambdas, local classes; Pair, Triple, destructuring, data classes
package basics31

fun main() {
    // a local function: declared in main, it can only be used there
    fun square(x: Int) = x * x
    println("square(7)=${square(7)}")
    // a lambda stored in a variable (chapter 7)
    val cube = { x: Int -> x * x * x }
    println("cube(3)=${cube(3)}")

    // a local function or a lambda can read and modify the local variables around it
    var counter = 0
    fun increment() {
        counter++
    }
    val increment2 = { counter += 10 }
    increment()
    increment()
    increment2()
    println("counter=$counter")

    // a local class: declared in the function, it can only be used there
    class Accumulator {
        var total = 0
            private set

        fun add(value: Int) {
            total += value
        }
    }
    val acc = Accumulator()
    acc.add(5)
    acc.add(8)
    println("total=${acc.total}")

    // Pair and Triple: tuples with two and three elements
    val pair = Pair(1, "one")
    val pair2 = 2 to "two"
    println("pair=$pair, pair.first=${pair.first}, pair.second=${pair.second}, pair2=$pair2")
    val triple = Triple("Dupont", 45, true)
    println("triple=$triple, triple.third=${triple.third}")
    // destructuring declaration: a tuple split into several variables
    val (number, name) = pair2
    println("number=$number, name=$name")
    val (_, age, _) = triple
    println("age=$age")
    // destructuring in a for loop
    for ((n, label) in listOf(1 to "one", 2 to "two", 3 to "three")) {
        print("$n:$label ")
    }
    println()

    // a data class: a tuple whose components have a name
    data class Couple(val number: Int, val name: String)
    val t1 = Couple(1, "one")
    println("t1=$t1, t1.number=${t1.number}, t1.name=${t1.name}")
    // type test and destructuring
    val obj: Any = Couple(3, "three")
    if (obj is Couple) {
        val (num, label) = obj
        println("number=$num, name=$label")
    }

    // functions that return several values
    val (quotient, remainder) = divide(17, 5)
    println("17 = 5 * $quotient + $remainder")
    val d = divide(23, 4)
    println("d=$d, d.quotient=${d.quotient}")
    val (min, max) = minMax(intArrayOf(12, -4, 35, 7))
    println("min=$min, max=$max")

    // swapping two variables with a pair
    var a = 1
    var b = 2
    val (newA, newB) = b to a
    a = newA
    b = newB
    println("a=$a, b=$b")
}

// a data class instead of a record
data class IntegerDivision(val quotient: Int, val remainder: Int)

fun divide(a: Int, b: Int) = IntegerDivision(a / b, a % b)

// a pair, when the meaning of the two values is obvious
fun minMax(values: IntArray): Pair<Int, Int> {
    var min = values[0]
    var max = values[0]
    for (v in values) {
        if (v < min) min = v
        if (v > max) max = v
    }
    return min to max
}

Results:

> gradlew -q run -Pmain=basics31.Basics31Kt
square(7)=49
cube(3)=27
counter=12
total=13
pair=(1, one), pair.first=1, pair.second=one, pair2=(2, two)
triple=(Dupont, 45, true), triple.third=true
number=2, name=two
age=45
1:one 2:two 3:three
t1=Couple(number=1, name=one), t1.number=1, t1.name=one
number=3, name=three
17 = 5 * 3 + 2
d=IntegerDivision(quotient=5, remainder=3), d.quotient=5
min=-4, max=35
a=2, b=1

Comments:

  • line 6: a local function with an expression body, declared in the function [main] and called on line 7;
  • line 9: a lambda. [{ x: Int -> x * x * x }] reads “the function that associates x * x * x with an integer x.” The type of the variables [cube] and [(Int) -> Int] is inferred by the compiler;
  • line 10: the lambda is called as a function. In Java, you had to know the method of the functional interface ([applyAsInt]);
  • lines 13–21: the local function [increment] and the lambda [increment2] both modify the local variable [counter] of the function [main]: 1 + 1 + 10 = 12. In Java, a lambda could not modify a local variable, so it was necessary to use a single-element array;
  • lines 24–29: a local class *[Accumulator]*, with a property *[total] and a method *[add]. [private set] makes the property writable only within the class: it can be read from [main] (line 35), but not modified. Classes are covered in Chapter 4;
  • lines 32–34: an object of type [Accumulator] is created by calling its constructor—without the [new] operator—and then its method is called;
  • lines 38–40: two pairs, created using the [Pair] builder and the [to] function. The [toString()] method for one pair displays it in parentheses;
  • lines 41–42: a triplet of three different types;
  • line 44: the pair [pair2] is split into two variables, [number] and [name];
  • line 46: only the second component of the triplet is retained;
  • lines 49–50: unpacking each pair from a list within the loop [for];
  • line 55: a local data class;
  • line 57: the [toString()] method of a data class displays the class name and its components; the [number] and [name] properties return the components;
  • lines 59–62: after the [obj is Couple] check, [obj] is treated as a [Couple] (smart type casting) and can be destructured. Kotlin does not have an equivalent to Java 21's record patterns ([obj instanceof Couple(int number, String name)]), but the type check followed by a destructuring assignment produces the same result;
  • lines 66–71: The functions [divide] and [minMax] (lines 85–95) each return two values: the first in a [IntegerDivision] data class (line 83), and the second as a pair. The caller can destruct the result (lines 66 and 70) or store it in a variable and use the component names (lines 68–69);
  • lines 74–78: swapping two variables with a pair. Kotlin does not allow assigning to existing variables in a single statement via destructuring ([(a, b) = b to a] is rejected): we destruct into two new variables, which we then assign. The [a = b.also { b = a }] idiom in the [Basics29.kt] program is shorter;
  • lines 92–93: a [if] statement whose single action is written on the same line, without curly braces.

In a larger program, we define classes, which group together data and the functions that manipulate it: this is the subject of the next chapter.