Skip to content

4. Classes, Interfaces, Data Classes

4.1. Objects by Example

4.1.1. Overview

We will now explore object-oriented programming through examples. An object is an entity that contains data defining its state (referred to as fields, attributes, properties, etc.) and functions (referred to as methods). An object is created based on a template called a class. For example, a class named [C1] declares two properties—[var p1: Type1] and [var p2: Type2]—and two methods—[fun m3(...): Type3 { ... }] and [fun m4(...): Type4 { ... }], all enclosed within the curly braces of the declaration [class C1 { ... }].

From the class [C1], we can create many objects such as [o1], [o2], … All of them will have the properties [p1], [p2], … and the methods [m3], [m4], … However, they will have different values for their [pi] properties, thus each having its own state. If [o1] is an object of type [C1], [o1.p1] refers to the property [p1] of [o1], and [o1.m3(...)] refers to thecall to the [m3] method of [o1].

Let’s consider an initial object model: the class [Person].

4.1.2. A program consisting of several files

In the examples from the previous chapter, a program fit into a single source file. From now on, a program can consist of multiple source files: for example, one class per file, plus the main program. Unlike Java, Kotlin does not require any connection between a file’s name and the classes it contains: a file can contain multiple classes and high-level functions. The convention—which we will follow—is nevertheless to place an important class in a file that bears its name: the class *[Person] is in the file *[Person.kt].

The programs in this chapter are grouped together in the Gradle project [04-classes], structured like the one in the previous chapter ([03-basics]): each program is a package within the [src/main/kotlin] directory. The multi-file program we are studying is the [persons] package:

1
2
3
4
04-classes/src/main/kotlin/
  persons/
    Person.kt       the Person class
    Main.kt         the main program

Both files begin with the line [package persons]: they belong to the same package and can be seen without an import statement. The file [Main.kt] contains the high-level function [main]; the compiler places it in a JVM class named after the file, [MainKt], whose full name is [persons.MainKt]. This is the name provided to Gradle to run the program:

> gradlew -q run -Pmain=persons.MainKt
[Jean, Dupont, 30]

Gradle compiles all the files in the project, then executes the [main] method of the specified class. In IntelliJ IDEA, simply click the green triangle displayed in the margin, next to the [main] function. The compiler writes a [.class] file (containing the intermediate code, the “bytecode,” of JVM) for each class: in this case, [persons/Person.class] and [persons/MainKt.class], in the project’s [build] folder.

4.1.3. Definition of the Person class

The definition of the [Person] class in the source file [Person.kt] will be as follows:

package persons

class Person {
    // private properties
    private var firstName = ""
    private var lastName = ""
    private var age = 0

    // method
    fun initialize(p: String, n: String, age: Int) {
        this.firstName = p
        this.lastName = n
        this.age = age
    }

    // method
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}
  • line 1: the class belongs to the [persons] package. Packages are discussed later in this chapter;
  • line 3: The class is not preceded by any visibility keyword; it is public and can be used by all other classes. In Kotlin, [public] is the default visibility;
  • Lines 5–7: the class’s three properties. The keyword [var] indicates that they are mutable (the keyword [val] would make them read-only). Kotlin requires that a property be initialized (here with the empty string and 0): unlike Java, there are no implicit default values ([null], 0, [false]). The types of the properties, [String] and [Int], are inferred from these initial values, just as with a local variable;
  • lines 10–13: a method that sets the object’s state;
  • lines 17–18: a method that displays the object’s state.

Here we have the definition of a class, and thus a data type. When we create variables of this type, we will refer to them as objects or class instances. A class is therefore a template from which objects are constructed. By convention, a class name begins with a capital letter ([Person]), while a property or method name begins with a lowercase letter ([firstName], [initialize]).

Kotlin uses the term “properties” rather than “fields”: we’ll see a little later that a property is a field accompanied by its getter and setter methods, similar to properties in C#. A private property, as shown here, is simply a field. The members of a class can be data (properties) or methods (functions). These members can be accompanied by one of the following keywords:

Keyword
Access
[private]
A private member is accessible only by the class’s own methods
[protected]
A protected member is accessible via the methods of the class and those of its derived classes (see the concept of inheritance below). Unlike in Java, it is not accessible from other classes in the package
[internal]
An internal member is accessible by any method in the same module, that is, the set of files compiled together (a Gradle project or subproject, a module of IntelliJ IDEA)
[public]
A public member is accessible by any method, whether or not it is defined within the class. This is the default access level when no keyword is specified

In general, a class’s data is declared private, while its methods are public. This means that the user of an object (the programmer):

  • will not have direct access to the object’s private data;
  • can call the object's public methods, including those that provide access to its private data.

The order in which members are declared is arbitrary. Note that the default access level is public, unlike in Java, where the default is package-private: in Kotlin, you must explicitly write [private] to make a member private. Kotlin does not have “package” access; [internal] access, at the module level, serves this purpose.

The keywords [private], [internal], and [public] also apply to top-level declarations (classes, functions, and properties defined outside of any class). A top-level declaration [private] is visible only within its own file.

4.1.4. The initialize method

What is the role of the method [initialize]? Because [lastName], [firstName], and [age] are private members of the [Person] class, a statement like [p1.firstName = "Jean"], written in the main program, is invalid. The compiler rejects it:

1
2
3
Main.kt:5:8: error: cannot access 'var firstName: String': it is private in 'persons.Person'.
    p1.firstName = "Jean"
       ^^^^^^^^^

The message indicates the file, line, and column of the error ([5:8]), then highlights it with circumflex accents. In IntelliJ IDEA, the error is underlined in red as soon as it is typed, and Gradle displays the same message, preceded by [e:] and the full file path. The message is in English: the Kotlin compiler does not provide French translations for its messages. We need to initialize an object of type [Person] via a public method. This is the purpose of the [initialize] method. We’ll write [p1.initialize("Jean", "Dupont", 30)]. This syntax is valid because [initialize] is publicly accessible.

4.1.5. Creating an object: no new operator

However, something is still missing. The object must be created. In the program [Main.kt], the expression [Person()] creates an object of type [Person]: the class is “called” like a function. Kotlin does not have a new operator, unlike Java and C#. The created object is not yet initialized: its properties have their default values, which are the empty string for [lastName] and [firstName], and 0 for [age].

The declaration [val p1 = Person()] declares [p1] as a reference to this object. If we declare the variable without initializing it, [val p1: Person]***, and then write ***[p1.initialize("Jean", "Dupont", 30)]***, we are calling the method [initialize] on an object that does not exist, and the compiler reports the error: the variable [p1]* has not been assigned a value.

1
2
3
Main.kt:5:5: error: variable 'p1' must be initialized.
    p1.initialize("Jean", "Dupont", 30)
    ^^

In Java, you can write [Person p1 = null;] to indicate that the variable does not yet reference any object; the call [p1.initialize(...)] is accepted by the compiler and causes a [NullPointerException] exception at runtime. As we saw in the previous chapter, Kotlin distinguishes between types that allow the value [null] and those that do not. The type [Person] does not allow it:

1
2
3
Main.kt:4:22: error: null cannot be a value of a non-null type 'Person'.
    val p1: Person = null
                     ^^^^

The type *[Person?]* does accept it, but the compiler then rejects a direct method call:

1
2
3
Main.kt:5:7: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'Person?'.
    p1.initialize("Jean", "Dupont", 30)
      ^

You must write [p1?.initialize(...)], which calls the method only if [p1] is not [null], or [p1!!.initialize(...)], which tells the compiler that [p1] is not [null]. In the latter case, if the assertion is false, the Java exception is thrown at runtime:

1
2
3
4
> gradlew -q run -Pmain=persons.MainKt
Exception in thread "main" java.lang.NullPointerException
    at persons.MainKt.main(Main.kt:5)
    at persons.MainKt.main(Main.kt)

Most of the errors that Java detects at runtime are thus detected by the Kotlin compiler. To make [p1] reference an object, we write [val p1 = Person()]. Now that [p1] refers to an object, the initialization statement for that object, [p1.initialize("Jean", "Dupont", 30)], is valid.

4.1.6. The this keyword

Let’s look at the code for the [initialize] method:

1
2
3
4
5
    fun initialize(p: String, n: String, age: Int) {
        this.firstName = p
        this.lastName = n
        this.age = age
    }

The statement [this.firstName = p] means that the property [firstName] of the current object ([this]) is assigned the value [p]. The keyword [this] refers to the current object: the one on which the method is executed. How do we know this? Let’s look at how the object referenced by [p1] is initialized in the calling program: [p1.initialize("Jean", "Dupont", 30)]. It is the method [initialize] of the object [p1] that is called. When, within this method, the object [this] is referenced, it actually refers to the object [p1].

When a method of an object references a property [a] of that object, the notation [this.a] is implied: the method [initialize] could have written [firstName = p] and [lastName = n]. You must use [this] explicitly when there is an identifier conflict. This is the case with the statement [this.age = age] (line 4), where [age] refers both to a property of the current object and to the parameter [age] received by the method. Within the method, the name [age] alone refers to the parameter; the property must be referred to as [this.age]. Note that a function parameter is read-only: the statement [age = 0] written within the method would be rejected by the compiler.

4.1.7. A Test Program

Here is a short test program. It is written in the source file [Main.kt]:

1
2
3
4
5
6
7
8
9
package persons

// the main program
fun main() {
    // a Person object
    val p1 = Person()
    p1.initialize("Jean", "Dupont", 30)
    p1.identify()
}
  • line 4: the function [main] is the program’s entry point. As in the previous chapter, it is a high-level function, written outside of any class;
  • line 6: creation of a [Person] object referenced by [p1]. Since the [Person] class belongs to the same package, it is recognized without needing to be imported. The type of [p1], [Person], is inferred by the compiler;
  • line 7: initialization of the object;
  • line 8: display of its state.

Results:

> gradlew -q run -Pmain=persons.MainKt
[Jean, Dupont, 30]

4.1.8. Multiple Classes in a Single File

For a short example, a package containing multiple files is unnecessarily large. As in the previous chapter, we will therefore write a single file—[Classes01.kt], for example—which contains the function [main], possibly other functions, and the classes the program needs. Each file is placed in a package named after it ([classes01]), which allows each program in the project to have its own [Person] class: the class [Person] in the file [Classes01.kt] has the full name [classes01.Person]; the class in the file [Classes02.kt] has the full name [classes02.Person], and so on. It is executed using the command [gradlew -q run -Pmain=classes01.Classes01Kt].

It is important to understand how the compiler processes such a file. The classes in the file are ordinary, high-level classes, exactly as if they were written in their own file; the high-level functions, including [main], are grouped into a class named after the file, [Classes01Kt]. Compiling the file demonstrates this:

1
2
3
4
> kotlinc src/main/kotlin/classes01/Classes01.kt -d classes
> ls classes/classes01
Classes01Kt.class
Person.class

This has a consequence that distinguishes Kotlin from Java. In Java, the classes in a compact source file are nested within an implicit class and share their private members with the [main] method in the file. In Kotlin, there is no enclosing class: a member [private] of a class remains inaccessible to the function [main], even if it is written in the same file. The short examples in this chapter will therefore show the same compilation errors as the program [persons].

4.1.9. Another way to initialize: overloading

Let’s continue with the class [Person] and add a second method, [initialize], to it. The program [Classes01.kt] is as follows:

// overloading of the initialize method
package classes01

fun main() {
    val p1 = Person()
    p1.initialize("Jean", "Dupont", 30)
    p1.identify()
    val p2 = Person()
    p2.initialize(p1)
    p2.identify()
}

// the Person class is declared after the main function
class Person {
    // private properties
    private var firstName = ""
    private var lastName = ""
    private var age = 0

    // initialization from three values
    fun initialize(p: String, n: String, age: Int) {
        firstName = p
        lastName = n
        this.age = age
    }

    // initialization from another person
    fun initialize(p: Person) {
        firstName = p.firstName
        lastName = p.lastName
        age = p.age
    }

    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}
  • line 2: the program package;
  • lines 4–10: the main program;
  • line 14: the class [Person] is declared after the function [main]. In Kotlin, the order of declarations in a file does not matter;
  • lines 21–24: the [initialize] method from the previous paragraph;
  • lines 28–31: a second method, [initialize].

We now have two methods named [initialize]: this is valid as long as they accept different parameters (in number or type). The method [initialize] is said to be overloaded. The compiler chooses which method to call based on the actual parameters: [p1.initialize("Jean", "Dupont", 30)] (line 6) calls the first one, and [p2.initialize(p1)] (line 9) calls the second one. The return type alone is not sufficient to distinguish between two methods: two methods with the same name and the same parameters that differ only in their return type would be rejected. We’ll see that in Kotlin, default parameter values often eliminate the need to write multiple versions of the same method.

The parameter of the second method is a reference ([p]) to a person. The properties of the person ([p]) are then assigned to the current object ([this]). Note that the method [initialize] has direct access to the properties of the object [p] even though they are private (lines 29–31). This is always true: a method of a [C] class has access to the private members of all objects of the same [C] class.

Results:

1
2
3
> gradlew -q run -Pmain=classes01.Classes01Kt
[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.10. Constructors of the Person class

A constructor is a function called when an object is created. It is generally used to initialize the object. It can accept parameters but does not return a value. If a class [C] has a constructor that accepts n parameters, creating an object of that class is written as [val obj = C(arg1, arg2, ... argn)].

When a class [C] has one or more constructors, one of these constructors must be used to create an object of that class. If a class [C] does not declare any constructors, it has a default constructor with no parameters that does nothing. The object’s properties then retain their initial values. This is what happened in the previous programs, where we wrote [Person()].

Kotlin distinguishes between two types of constructors:

  • the primary constructor, at most one per class, declared in the class header immediately after the class name: [class Person(firstName: String, lastName: String, age: Int)];
  • secondary constructors, declared in the body of the class using the keyword [constructor]. When a class has a primary constructor, every secondary constructor must call it, either directly or through another secondary constructor.

The program [Classes02.kt] provides three constructors for the class [Person]:

// constructors of the Person class
package classes02

fun main() {
    val p1 = Person("Jean", "Dupont", 30)
    p1.identify()
    val p2 = Person(p1)
    p2.identify()
    val p3 = Person()
    p3.identify()
}

// the primary constructor is declared in the class header:
// its parameters preceded by val (or var) are also properties of the class
class Person(private val firstName: String, private val lastName: String, private val age: Int) {
    // secondary copy constructor: it calls the primary constructor
    constructor(p: Person) : this(p.firstName, p.lastName, p.age)

    // secondary constructor without parameters: it also calls the primary constructor
    constructor() : this("?", "?", 0)

    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}
  • Line 15: the primary constructor. Its three parameters are preceded by [private val]: this is a shorthand notation that declares both a constructor parameter and a private, read-only property with the same name, initialized with the parameter’s value. The class has no other properties: these are declared in the header;
  • Line 17: the secondary copy constructor. The notation [: this(p.firstName, p.lastName, p.age)] calls the constructor of the same class that accepts three parameters: [String], [String], and [Int]—in this case, the primary constructor. This is referred to as chained constructors. As a result, the initialization code is written only once. This constructor has no body: it has nothing else to do;
  • line 20: the secondary constructor with no parameters, also chained to the primary constructor. Since the class has declared a primary constructor with parameters, it no longer has a default constructor: if we want to be able to write [Person()], we must declare it;
  • Lines 4–10: The three constructors are used.

The abbreviated notation in line 15 is equivalent to the following, longer notation, where the parameters of the primary constructor—excluding *[val]*—are simply parameters used to initialize properties declared in the body of the class:

1
2
3
4
5
6
7
class Person(firstName: String, lastName: String, age: Int) {
    // properties initialized with the parameters of the primary constructor
    private val firstName: String = firstName
    private val lastName: String = lastName
    private val age: Int = age
    ...
}

This is much shorter than the Java version, where you must declare the fields and then initialize them one by one in the constructor ([this.firstName = firstName;]…). The primary constructor has no body: the statements to be executed when the object is constructed are placed in [init] blocks, which we’ll cover a little later. We’ll also see that, thanks to default parameter values, Kotlin often doesn’t need secondary constructors: [class Person(..., private val age: Int = 0)] allows you to write both [Person("Jean", "Dupont", 30)] and [Person("Jean", "Dupont")].

Results:

1
2
3
4
> gradlew -q run -Pmain=classes02.Classes02Kt
[Jean, Dupont, 30]
[Jean, Dupont, 30]
[?, ?, 0]

4.1.11. Object References

We use the [Person] class from the previous program, omitting its parameterless constructor but including the [initialize(String, String, Int)] method from the beginning of the chapter. Its properties are therefore declared using [var] rather than [val]. The test program becomes the following [Classes03.kt]:

// object references
package classes03

fun main() {
    // p1
    val p1 = Person("Jean", "Dupont", 30)
    print("p1="); p1.identify()
    // p2 references the same object as p1
    val p2 = p1
    print("p2="); p2.identify()
    // p3 references an object that is a copy of the object referenced by p1
    val p3 = Person(p1)
    print("p3="); p3.identify()
    // we change the state of the object referenced by p1
    p1.initialize("Micheline", "Benoît", 67)
    print("p1="); p1.identify()
    // since p2 = p1, the object referenced by p2 has changed state
    print("p2="); p2.identify()
    // p3 does not reference the same object as p1: the object referenced by p3 has not changed
    print("p3="); p3.identify()
    // are p1 and p2 the same reference? and p1 and p3?
    println("p1 === p2 : ${p1 === p2}, p1 === p3 : ${p1 === p3}")
}

class Person(private var firstName: String, private var lastName: String, private var age: Int) {
    // secondary copy constructor
    constructor(p: Person) : this(p.firstName, p.lastName, p.age)

    // initialization
    fun initialize(p: String, n: String, age: Int) {
        firstName = p
        lastName = n
        this.age = age
    }

    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}

The function [print] (lines 7, 10, …) writes its parameter without starting a new line. The two statements on these lines are separated by a semicolon, which is only necessary in Kotlin to write multiple statements on a single line. Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=classes03.Classes03Kt
p1=[Jean, Dupont, 30]
p2=[Jean, Dupont, 30]
p3=[Jean, Dupont, 30]
p1=[Micheline, Benoît, 67]
p2=[Micheline, Benoît, 67]
p3=[Jean, Dupont, 30]
p1 === p2 : true, p1 === p3 : false

When the variable [p1] is declared by [val p1 = Person("Jean", "Dupont", 30)] (line 6), [p1] references the object [Person("Jean", "Dupont", 30)] but is not the object itself. In C, this would be referred to as a pointer—that is, the address of the created object. If [p1] were declared as [var p1: Person? = ...] and we then wrote [p1 = null], it wouldn’t be the object [Person("Jean", "Dupont", 30)] that would be modified, but rather the reference [p1] that would change its value. The object would be “lost” if it were not referenced by any other variable: the memory it occupies would later be reclaimed by the Java virtual machine’s garbage collector, which executes Kotlin programs just like Java programs.

When we write [val p2 = p1] (line 9), we initialize the reference [p2]: it “points” to the same object (it refers to the same object) as the reference [p1]. Thus, if you modify the object “pointed to” (or referenced) by [p1], you also modify the one referenced by [p2] (lines 4–5 of the results). Note that [p1] and [p2] are declared alongside [val]: the keyword prohibits changing the reference contained in the variable, not modifying the referenced object, which is what the call to [p1.initialize(...)] does (line 15).

When you enter [val p3 = Person(p1)] (line 12), a new object, [Person], is created. This new object will be referenced by [p3]. If you modify the object referenced by [p1], it does not affect the object referenced by [p3] (line 6 of the results).

Finally, on line 22, the operator [===], applied to two object references, indicates whether they refer to the same object: [p1] and [p2] refer to the same object, while [p1] and [p3] refer to two different objects, although they have the same content (line 7 of the results). This is the [] operator in Java. In Kotlin, the [] operator compares the contents of the objects by calling their [equals] method: we’ll see later how to define it. For a class that does not define [equals], as in this case, [p1 == p3] still compares the references and evaluates to [false].

4.1.12. Passing Object Reference Parameters

In the previous chapter, we saw that Kotlin, like Java, passes function parameters by value: the function receives a copy of the actual parameter’s value. Let’s see what this means when the parameter is an object reference. To do this, we’ll use the class [StringBuilder] (the class of JDK, which Kotlin uses directly). A [String] object cannot be modified: an operation like [s.uppercase()] returns a new [String] object. A [StringBuilder] object, on the other hand, is a mutable string: its [append] method appends characters to the string without creating a new object.

In Java, the example assigned a new object to the parameter [sbf1] of the called function. This is not possible in Kotlin: a function parameter is read-only, just like a variable [val]. The compiler rejects the statement [sbf1 = StringBuilder("test1*****")]:

1
2
3
Classes04.kt:5:5: error: 'val' cannot be reassigned.
    sbf1 = StringBuilder("test1*****")
    ^^^^

The program [Classes04.kt] therefore copies the parameter [sbf1] into a modifiable local variable:

// passing parameters of object reference type
package classes04

fun main() {
    val sb0 = StringBuilder("test0")
    val sb1 = StringBuilder("test1")
    println("In calling function before the call: sb0=$sb0, sb1=$sb1")
    val sb2 = changeStringBuilder(sb0, sb1)
    println("In calling function after the call: sb0=$sb0, sb1=$sb1, sb2=$sb2")
}

// the called function
fun changeStringBuilder(sbf0: StringBuilder, sbf1: StringBuilder): StringBuilder {
    println("Start of called function: sbf0=$sbf0, sbf1=$sbf1")
    // we modify the object referenced by sbf0
    sbf0.append("*****")
    // a parameter is read-only: sbf1 = StringBuilder(...) would be rejected.
    // The local variable sbl first references the same object as sbf1, then a new object
    var sbl = sbf1
    sbl = StringBuilder("test1*****")
    // a new object is returned to the caller
    val sbf2 = StringBuilder("test2*****")
    println("End of called function: sbf0=$sbf0, sbf1=$sbf1, sbl=$sbl, sbf2=$sbf2")
    return sbf2
}
  • Lines 5–6: two references, [sb0] and [sb1], to objects of type [StringBuilder];
  • line 8: these are passed to the function [changeStringBuilder], whose result—a third reference—is assigned to [sb2];
  • lines 13–24: a high-level function, which has the formal parameters [sbf0] and [sbf1]. When the function starts, [sbf0] and [sb0] are two distinct references that point to the same object: the reference [sb0] was copied into [sbf0] (references are passed by value). The same applies to [sbf1] and [sb1];
  • lines 19–20: the local variable [sbl] first receives the reference [sbf1], then the reference to a new object. This is what the Java program did with the parameter [sbf1] itself.

Results:

1
2
3
4
5
> gradlew -q run -Pmain=classes04.Classes04Kt
In calling function before the call: sb0=test0, sb1=test1
Start of called function: sbf0=test0, sbf1=test1
End of called function: sbf0=test0*****, sbf1=test1, sbl=test1*****, sbf2=test2*****
In calling function after the call: sb0=test0*****, sb1=test1, sb2=test2*****

Explanations:

  • [sb0] and [sbf0] are two distinct references to the same object. This object was modified via [sbf0] (line 16). This modification is visible via [sb0] (line 4 of the results);
  • [sb1], [sbf1], and, initially, [sbl] are three distinct references to the same object. [sbl] is assigned a new value and now points to a new object (line 20). This does not change the value of [sbf1] or that of [sb1], which continue to point to the original, unmodified object (lines 3 and 4 of the results);
  • a Kotlin function cannot, therefore, change the value of a variable belonging to the caller. To pass a new object to the caller, it returns it as a result (lines 22 and 24). When there are multiple results, they are grouped into an object: a pair ([Pair]), as seen in the previous chapter, or a data class (see below).

Remember that a function can modify the object referenced by a parameter, but not the caller’s variable that contains the reference. Like Java, and unlike C#, Kotlin does not pass parameters by reference; it goes a step further than Java by prohibiting any assignment to a parameter, which prevents confusion between the parameter and the caller’s variable.

4.1.13. Temporary Objects

In an expression, you can explicitly call an object’s constructor: the object is created, but there is no variable to access it later (to modify it, for example). This temporary object is created for the purpose of evaluating the expression and is then discarded. The memory space it occupied will later be automatically reclaimed by the garbage collector, whose role is to reclaim memory space occupied by objects that are no longer referenced by the program’s data.

Consider the following program, [Classes05.kt], where the constructors of the [Person] class display a message. This is an opportunity to introduce the initialization block [init]:

// temporary objects
package classes05

fun main() {
    Person(Person("Jean", "Dupont", 30)).identify()
}

class Person(private val firstName: String, private val lastName: String, private val age: Int) {
    // the init block is executed by the primary constructor
    init {
        println("Constructor Person(String, String, Int)")
    }

    // the secondary constructor first calls the primary constructor, then executes its body
    constructor(p: Person) : this(p.firstName, p.lastName, p.age) {
        println("Constructor Person(Person)")
    }

    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}
  • line 5: the object [Person("Jean", "Dupont", 30)] is constructed and then passed to the copy constructor, which constructs a second object; the method [identify] is then called on that object. Neither of the two objects is referenced by a variable;
  • lines 9–11: Since the primary constructor has no body, the statements to be executed when the object is constructed are placed in a [init] block. A class can have multiple [init] blocks: these are executed, along with the initializations of the properties declared in the class body, in the order in which they are written. They are part of the primary constructor and have access to its parameters;
  • lines 15–16: A secondary constructor may have a body. It is executed after the constructor called by [: this(...)]—in this case, the primary constructor with its [init] blocks.

Results:

1
2
3
4
5
> gradlew -q run -Pmain=classes05.Classes05Kt
Constructor Person(String, String, Int)
Constructor Person(String, String, Int)
Constructor Person(Person)
[Jean, Dupont, 30]

The results show the sequential construction of the two temporary objects. The first is constructed by the primary constructor (line 1 of the results). The second is constructed by the secondary constructor, which begins by calling the primary constructor—hence the execution of the [init] block (line 2)—and then executes its own body (line 3). In Java, the copy constructor in the example was not chained to the primary constructor and displayed only its own message; in Kotlin, as long as a class has a primary constructor, every object passes through it: the [init] blocks are always executed.

4.1.14. Properties: Getter and Setter Methods

In Java, the convention is to declare private fields and provide access to their values through public methods: getters, such as [getAge()], and setters, such as [setAge(int)]. These methods are written by hand (or generated by the development environment), and the user of the class writes [p.setAge(p.getAge() + 1)]. Kotlin, like C#, has properties: a public property is data that the user reads and writes using dot notation, like a field, but which is actually read and written through a getter and a setter. The compiler generates them. The program [Classes06.kt] demonstrates this:

// properties instead of getters and setters
package classes06

fun main() {
    val p = Person("Jean", "Michelin", 34)
    // reading the properties: implicit call of their getter
    println("p=(${p.firstName},${p.lastName},${p.age})")
    // writing a property: implicit call of its setter
    p.age = 56
    println("p=(${p.firstName},${p.lastName},${p.age})")
}

// three public read-write properties
class Person(var firstName: String, var lastName: String, var age: Int)
  • line 14: the class *[Person]* fits on a single line. The three parameters of the primary constructor are preceded by *[var]*: these are three public properties (default visibility), which are both readable and writable. Since the class has no body, the curly braces are omitted;
  • line 7: reading the properties [p.firstName], [p.lastName], and [p.age]. Each read operation is a call to the property’s accessor;
  • Line 9: Writing the property [age], that is, calling its setter.

Results:

1
2
3
> gradlew -q run -Pmain=classes06.Classes06Kt
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

The [javap] tool from JDK, which displays the contents of a [.class] file, shows what the Kotlin compiler generated for the [Person] class (the [-p] option also displays private members):

> javap -p -cp build/classes/kotlin/main classes06.Person
Compiled from "Classes06.kt"
public final class classes06.Person {
  private java.lang.String firstName;
  private java.lang.String lastName;
  private int age;
  public classes06.Person(java.lang.String, java.lang.String, int);
  public final java.lang.String getFirstName();
  public final void setFirstName(java.lang.String);
  public final java.lang.String getLastName();
  public final void setLastName(java.lang.String);
  public final int getAge();
  public final void setAge(int);
}

Each property resulted in a private field (lines 3–5), a public getter, and a public setter (lines 7–12), which follow the naming convention established in the early days of Java, namely “JavaBeans”: the getter for the property [age] is named [getAge], and its setter is [setAge]; a boolean property whose name begins with [is]—such as [isActive]—has a getter with the same name, [isActive], and a setter named [setActive]. This is important: many Java libraries (such as Jackson for JSON and Hibernate for databases, which we’ll use later) rely on this convention to locate an object’s data; they can find the data for Kotlin objects without any difficulty. Conversely, a Kotlin program that uses a Java class writes [p.age] to call [p.getAge()], and [p.age = 56] to call [p.setAge(56)]. We also note that the class and its methods are [final]: we’ll see with inheritance that in Kotlin, classes and methods are final by default.

The notation [p.age = 56] resembles the assignment of a public field. The difference is significant: because it uses a setter, we can—without changing anything in the code written by the class’s users—later assign a setter to the property that controls the values it receives. This is the subject of the following section. In Kotlin, therefore, we never write accessors or setters in the Java style: we declare properties.

4.1.15. Setters That Validate Received Values

The advantage of setters over public fields is the ability to control the values assigned to the fields: a setter can validate the received value and throw an exception if it is invalid. Kotlin allows you to provide a property with a custom getter ([get]) and a custom setter ([set]). Consider the program [Classes07.kt]:

// setters that check the validity of the values received
package classes07

fun main() {
    val p = Person("Jean", "Michelin", 34)
    println("p=(${p.firstName},${p.lastName},${p.age})")
    p.age = 56
    println("p=(${p.firstName},${p.lastName},${p.age})")
    try {
        p.age = -4
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    // constructor without parameters then assignment of the properties
    val p2 = Person()
    p2.age = 7
    p2.firstName = "Arthur"
    p2.lastName = "Martin"
    println("p2=(${p2.firstName},${p2.lastName},${p2.age})")
    try {
        val p3 = Person()
        p3.firstName = "Paul"
        p3.lastName = " "
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    // the constructor also checks the values received
    try {
        val p4 = Person("", "Durand", 20)
        println("p4=(${p4.firstName},${p4.lastName},${p4.age})")
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
}

// the class has a primary constructor without parameters
class Person() {
    // properties with a custom setter (set)
    var firstName = ""
        set(value) {
            // valid first name?
            require(value.isNotBlank()) { "invalid first name ($value)" }
            // field designates the backing field that stores the value of the property
            field = value
        }

    var lastName = ""
        set(value) {
            // valid last name?
            require(value.isNotBlank()) { "invalid last name ($value)" }
            field = value
        }

    var age = 0
        set(value) {
            // valid age?
            require(value >= 0) { "invalid age ($value)" }
            field = value
        }

    // secondary constructor: it goes through the setters to benefit from their checks
    constructor(firstName: String, lastName: String, age: Int) : this() {
        this.firstName = firstName
        this.lastName = lastName
        this.age = age
    }
}

The three properties have a custom setter that validates the received value:

  • lines 39–44: the property [firstName]. Its declaration is followed, on the subsequent lines, by the definition of its setter, [set(value) { ... }]. The parameter [value] is the value assigned to the property (the name [value] is conventional, as in C#); its type is that of the property. The property has no custom accessor: it retains the generated accessor, which returns the stored value;
  • Line 42: The function [require(condition) { message }], discussed in the previous chapter, throws a [IllegalArgumentException] exception—the exception designed to signal an invalid parameter—if the condition is false. The message, calculated by the lambda expression within curly braces, is constructed only in this case. The function [isNotBlank] returns [true] if the string contains at least one character other than a space. There is no need to test the value [null] as in Java: the property is of type [String], not [String?], and the compiler prohibits assigning [null] to it;
  • Line 44: In a getter or setter, the keyword [field] refers to the field that stores the property’s value (known as the “backing field”). Do not write [firstName = value]: this would be a call to the setter itself, which would call itself indefinitely;
  • lines 47–58: The properties [lastName] and [age] are constructed using the same pattern;
  • line 37: the class has a primary constructor with no parameters. The initial values of the properties ([""] and 0) are assigned directly to the fields, without going through the setters;
  • Lines 62–65: A secondary constructor that initializes the object using the setters. This allows it to take advantage of the checks performed by the setters, which are written only once. Here, [this.firstName = firstName] is the call to the setter for the property [firstName] with the value of the parameter [firstName].

Why not write [class Person(firstName: String, lastName: String, age: Int)] with properties initialized by [var age = age]? Because initializing a property at its declaration directly affects the field without calling the setter: an age of -4 would be accepted without an error. To verify the values received by the primary constructor, we write the checks in a [init] block, or, as shown here, we pass the values through the setters.

In Java, setters called by the constructor had to be declared as [final]: otherwise, a derived class could have redefined them, and the parent class’s constructor would have called the new version even before the derived class’s constructor was executed. In Kotlin, classes and their members are final by default: this precaution is unnecessary.

Results:

1
2
3
4
5
6
7
> gradlew -q run -Pmain=classes07.Classes07Kt
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
invalid age (-4)
p2=(Arthur,Martin,7)
invalid last name ( )
invalid first name ()

Line 3 of the results shows that the assignment [p.age = -4] (line 10) was rejected by the setter: [ex.message] is the exception message. Lines 15–18: An object is created using the parameterless constructor and then initialized by its setters. Lines 21–23: The name containing a space is rejected (line 5 of the results). Lines 29–30: The three-parameter constructor rejects an empty first name (line 6 of the results).

Java does not have object initializers—a syntax that would allow multiple properties to be set within the object creation expression itself. Neither does Kotlin, but it offers two equivalent solutions: named and default-valued constructor parameters, and the [apply] function. We’ll cover them in the next two paragraphs.

4.1.16. Read-Only Properties and Optional Parameters

A property declared with [val] must be assigned a value exactly once—at the time of its declaration, in the primary constructor, or within a [init] block—and cannot be modified thereafter. The compiler enforces this. This is equivalent to the [final] field in Java: the property has only a getter, no setter. The [Classes08.kt] program illustrates several cases, as well as default parameters and named arguments:

// read-only properties, private setter, computed property,
// mandatory and optional parameters, named arguments
package classes08

fun main() {
    val p = Person("Jean", "Dupont", 30, "Angers")
    println(p.identity)
    // the first name is read / write
    p.firstName = "Jacques"
    // the age can only be modified inside the class
    p.growOlder()
    println(p.identity)
    // the following lines are rejected by the compiler
    // p.lastName = "Durand" // lastName is a val property
    // p.age = 40            // the setter of age is private
    // p.identity = "?"      // identity is a val (computed) property

    // a mandatory parameter and an optional parameter
    val c1 = Contact("jean.dupont@example.com")
    println("Contact : ${c1.email}, ${c1.phone}")
    // named arguments, in any order
    val c2 = Contact(phone = "02 41 00 00 00", email = "paul.martin@example.com")
    println("Contact : ${c2.email}, ${c2.phone}")
    // Contact(null) is rejected by the compiler; an empty email is rejected at runtime
    try {
        val c3 = Contact(" ")
        println("Contact : ${c3.email}")
    } catch (ex: IllegalArgumentException) {
        println("Error: ${ex.message}")
    }
}

class Person(var firstName: String, val lastName: String, age: Int, val city: String) {
    // the age can be read by everyone, but its setter is private
    var age = age
        private set

    // a computed property: it has no backing field, its getter computes its value
    val identity: String
        get() = "[$firstName, $lastName, $age, $city]"

    // the age is only modified by the methods of the class
    fun growOlder() {
        age++
    }
}

// the email is mandatory, the phone optional: its parameter has a default value
class Contact(val email: String, val phone: String = "unknown") {
    // check executed when the object is constructed
    init {
        require(email.isNotBlank()) { "the email is mandatory" }
    }
}
  • line 33: the first name is a [var] property, which is read-write; the last name and city are [val] properties, which are read-only. The constructor assigns a value to them, which cannot be changed afterward. The parameter [age], without [val] or [var], is simply a constructor parameter;
  • lines 35–36: The property [age] is initialized with the parameter [age]. It is readable by everyone, but its setter is private ([private set]): it can only be modified by the class’s methods, such as [growOlder] (lines 43–44). The setter retains its default behavior: it does not require a body;
  • lines 39–40: a computed property. It has a custom accessor, [get() = ...], which calculates its value each time it is read based on the other properties; it has no field to store a value. To the user of the class, nothing distinguishes it from another property: [p.identity] (line 7). This is equivalent to the [getIdentity()] method in the Java version;
  • lines 14–16: three statements that the compiler rejects, for the reasons given in the comments. If we uncomment them, the compiler reports the following (we have also uncommented the [Contact(null)] statement mentioned on line 24):
Classes08.kt:14:7: error: 'val' cannot be reassigned.
    p.lastName = "Durand" // lastName is a val property
      ^^^^^^^^
Classes08.kt:15:7: error: cannot access 'age': it is private in 'classes08.Person'.
    p.age = 40            // the setter of age is private
      ^^^
Classes08.kt:16:7: error: 'val' cannot be reassigned.
    p.identity = "?"      // identity is a val (computed) property
      ^^^^^^^^
Classes08.kt:24:13: error: null cannot be a value of a non-null type 'String'.
    Contact(null)
            ^^^^
  • line 49: the class [Contact] has a required property, email, and an optional property, phone. The optional nature of the phone property is expressed by a default value for the constructor’s parameter [phone]: [val phone: String = "unknown"]. If the argument is omitted, as in line 19, the parameter takes this value. In Java, which does not have optional parameters, a second constructor—chained to the first—was required; in Kotlin, a single constructor suffices. Default values are used in the same way for the parameters of any function;
  • line 22: the arguments are named: [phone = "02 41 00 00 00"]. They can then be provided in any order, and the call is more readable when multiple parameters have the same type. Named arguments also allow you to omit only one optional parameter among several: with [f(a: Int = 0, b: Int = 0)], you can write [f(b = 5)];
  • Lines 51–52: The mandatory nature of the email is first expressed by the parameter type: [String] does not accept the value [null], and the call [Contact(null)] is rejected at compile time (last error above). In Java, this had to be checked at runtime ([Objects.requireNonNull]). The next step is to reject an empty email address: this is the role of the block [init], which is executed when the object is constructed. On line 26, an email address consisting of a single space is rejected.

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=classes08.Classes08Kt
[Jean, Dupont, 30, Angers]
[Jacques, Dupont, 31, Angers]
Contact : jean.dupont@example.com, unknown
Contact : paul.martin@example.com, 02 41 00 00 00
Error: the email is mandatory

Note how short the declarations are: the [Person] class, which took up about fifty lines in Java with its getters, setters, and validation, takes up only about fifteen lines in Kotlin.

4.1.17. The apply and also functions instead of fluid mutators

In Java, when an object has many optional fields, the number of constructors increases and setter calls become longer. A common technique is to have each setter return the current object ([this]): the calls can then be chained together—this is referred to as “chained” methods: [new Person().setFirstName("jean").setLastName("Dupont").setAge(30)]. In Kotlin, default parameters and named arguments make this technique much less useful. And when you want to set multiple properties of an object immediately after its creation, the standard library provides “scope functions” that can be used with any object, including [apply] and [also]. The program [Classes09.kt] illustrates these and, in passing, introduces the properties [lateinit]:

// instead of "fluent" setters: the scope functions apply and also
// a lateinit property
package classes09

fun main() {
    // apply executes the block with the object as current object (this), then returns the object
    val p = Person().apply {
        firstName = "  jean "
        lastName = "Dupont"
        age = 30
    }
    println(p.identity)
    try {
        p.lastName = ""
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    try {
        p.age = -4
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    println(p.identity)
    // also executes the block with the object as parameter (it), then returns the object
    val p2 = Person().apply { firstName = "Arthur"; lastName = "Martin"; age = 7 }
        .also { println("Creation of ${it.identity}") }
    // reading a lateinit property that has not yet received a value
    try {
        println(p2.city)
    } catch (ex: UninitializedPropertyAccessException) {
        println(ex.message)
    }
    p2.city = "Angers"
    println("${p2.identity} lives in ${p2.city}")
}

class Person {
    // properties checked by their setter
    var firstName = ""
        set(value) {
            field = validate(value, "first name")
        }

    var lastName = ""
        set(value) {
            field = validate(value, "last name")
        }

    var age = 0
        set(value) {
            require(value >= 0) { "invalid age ($value)" }
            field = value
        }

    // a property without an initial value: it will receive its value later
    lateinit var city: String

    // identity: the type of the property is inferred from its getter
    val identity get() = "[$firstName, $lastName, $age]"
}

// checking a mandatory string: it is returned without its leading and trailing spaces
// (private top-level function: it is only visible in this file)
private fun validate(value: String, label: String): String {
    require(value.isNotBlank()) { "invalid $label ($value)" }
    return value.trim()
}
  • lines 7–10: [obj.apply { ... }] executes the block between curly braces with [obj] as the current object: within the block, [firstName = " jean "] refers to [this.firstName = " jean "], where [this] is the new person. Then [apply] returns the object itself, which is assigned to [p]. The assignment acts as an object initializer; each assignment goes through the property setter and thus its validations;
  • lines 39–46: the setters for the first name and last name assign the value returned by the function [validate] to the field;
  • lines 64–66: The validations for the first name and last name, which are identical, are factored into the function [validate]. It does not use any properties of a person: we have made it a high-level function, declared as [private], and therefore visible only in the file [Classes09.kt]. In Java, this would have had to be implemented as a private static method of the class. The function [trim] removes the leading and trailing spaces from the string;
  • line 59: the type of the computed property [identity], [String], is inferred from its accessor expression;
  • Lines 25–26: [obj.also { ... }] executes the block by passing it the object as a parameter, referred to as [it], and then returns the object. It is used for a secondary action—such as a progress display, as shown here—without interrupting the call chain. Here, [apply] returns the person, [also] displays it and then returns it in turn, and it is assigned to [p2]. Line 25: The three assignments in the [apply] block are written on a single line, separated by semicolons;
  • Line 56: A property of a type other than [null] must, in principle, be assigned a value upon creation. The keyword [lateinit] (“late initialization”) waives this requirement: the property [city] will be assigned its value later (line 33). If it is accessed before then, a [UninitializedPropertyAccessException] exception is thrown (lines 29–31). [lateinit] applies only to a property [var] of an object type that is not [null] ([String], but not [Int] or [String?]) and without custom accessors or mutators. It is used when the value is provided after the object is constructed, for example by a dependency injection framework such as Spring (Chapter 6) or by an initialization method in a test. Within the class, the expression [::city.isInitialized] indicates whether the property has been assigned a value.

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=classes09.Classes09Kt
[jean, Dupont, 30]
invalid last name ()
invalid age (-4)
[jean, Dupont, 30]
Creation of [Arthur, Martin, 7]
lateinit property city has not been initialized
[Arthur, Martin, 7] lives in Angers

The first name "jean," which was entered with spaces around it (line 8), was stored without them (line 1 of the results). Invalid modification attempts were rejected, and the person was not changed (lines 2–4 of the results). Line 6 of the results is the message for exception [UninitializedPropertyAccessException].

There are three other scope functions: [let], seen in the previous chapter with the values [null], [run], and [with]. They differ in how the block refers to the object ([this] or [it]) and in what they return (the object or the result of the block):

Function
Object in the block
Result
Typical Use
[apply]
[this]
The object
Configure a newly created object
[also]
[it]
The object
Follow-up action (monitoring, verification) in a sequence
[let]
[it]
The result of the block
Process a non-[null] value: [x?.let { ... }]
[run]
[this]
The result of the block
Calculate a value from an object
[with]
[this]
The result of the block
[with(obj) { ... }]: Multiple calls on the same object

To build complex objects, Java often uses the “Builder” design pattern: an auxiliary class, equipped with fluent methods, collects values and then constructs the final object. In Kotlin, a constructor with named parameters and default values replaces this pattern in most cases.

4.1.18. Class members: the companion object

Suppose we want to count the number of [Person] objects created in an application. We could manage a counter ourselves, but we risk overlooking temporary objects that are created here and there. It seems safer to include a statement in the constructor of the [Person] class that increments a counter. This counter cannot be an ordinary property, since each object has its own properties: it is a property of the class itself, not of a particular instance of that class.

Java declares such a member using the keyword [static]. Kotlin does not have a [static] keyword: class members are declared in a companion object ([companion object { ... }]), a single object attached to the class and declared within the class body. Its members are used with the class name: [Person.nbPersons], [Person.unknown()]. Here is an example, [Classes10.kt]:

// class members: the companion object; a singleton object (object)
package classes10

fun main() {
    val p1 = Person("Jean", "Dupont", 30)
    val p2 = Person(p1)
    Person(p1)
    Log.write("Number of persons created: ${Person.nbPersons}")
    // a class method
    val p3 = Person.unknown()
    p3.identify()
    p2.identify()
    Log.write("Number of persons created: ${Person.nbPersons}")
}

class Person(private val firstName: String, private val lastName: String, private val age: Int) {
    // secondary copy constructor
    constructor(p: Person) : this(p.firstName, p.lastName, p.age)

    // the init block is part of the primary constructor: every construction increments the counter
    init {
        nbPersons++
    }

    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }

    // the companion object: its members are those of the class itself
    companion object {
        // class property: readable by all, modifiable only by the Person class
        var nbPersons = 0L
            private set

        // class method
        fun unknown() = Person("?", "?", 0)
    }
}

// a singleton object: the Log class and its single instance are declared together
object Log {
    // the state of the object: the number of the last message written
    private var number = 0

    fun write(message: String) {
        number++
        println("[$number] $message")
    }
}
  • lines 31–37: the companion object for the class [Person];
  • lines 33–34: the class property [nbPersons], initialized to 0 (of type [Long] due to the suffix [L]). It is readable by everyone ([Person.nbPersons], line 8), but its setter is private: only the class [Person] can modify it. There is no need to define, as in Java, a private field and a static accessor method [getNbPersons()];
  • line 37: a class method. It is called using the class name: [Person.unknown()] (line 10). A method of the companion object does not have a current [Person] object: it cannot directly use a person’s properties. A method that creates and returns an object of its class, such as this one, is called a “factory”;
  • lines 21–22: the block [init] increments the counter. It is part of the primary constructor, and the copy constructor—which is chained to the primary constructor—therefore also executes it.

The last element of the program is a singleton object, [Log], declared using the keyword [object] (lines 42–48):

  • A declaration such as [object Log { ... }] declares both a class and its single instance, which is created automatically when it is first used. There is no constructor: no other [Log] object can be created. The object is referenced by its name: [Log.write(...)];
  • an object has a state—in this case, the number of the last message written—which is preserved from one call to the next;
  • A companion object is a special case of a singleton object: one that is attached to a class.

Results:

1
2
3
4
5
> gradlew -q run -Pmain=classes10.Classes10Kt
[1] Number of persons created: 3
[?, ?, 0]
[Jean, Dupont, 30]
[2] Number of persons created: 4

The program correctly counted the temporary object created on line 7. The log numbered its two messages. Compilation produces a class for the companion object, [Person$Companion], alongside the classes [Person], [Log], and [Classes10Kt]; a method of the companion object is thus, for JVM, an instance method of the [Person.Companion] object. To allow a Java program to call it as a static method ([Person.unknown()] rather than [Person.Companion.unknown()]), it is annotated with [@JvmStatic].

From the beginning, we’ve been using functions that, in Java, would be static methods: [println], [readln]… In Kotlin, these are top-level functions, declared outside of any class. This is the Kotlin way of writing what Java calls a utility class, such as the class [Math]: a file containing high-level functions (the standard library provides [kotlin.math.sqrt] and [kotlin.math.PI]), or, if you want to group functions under a single name, a [object]. The companion object is reserved for items that are truly specific to the class: factories, constants, and counters.

4.1.19. An array of people

An object is a piece of data like any other, and as such, multiple objects can be grouped into an array [Classes11.kt]:

// an array of persons
package classes11

fun main() {
    // an array of 3 references, all initialized to null
    val friends = arrayOfNulls<Person>(3)
    friends[0] = Person("Jean", "Dupont", 30)
    friends[1] = Person("Sylvie", "Vartan", 52)
    friends[2] = Person("Neil", "Armstrong", 66)
    // display: the elements are of type Person?
    for (friend in friends) {
        friend?.identify()
    }
}

class Person(private val firstName: String, private val lastName: String, private val age: Int) {
    // identity of the person
    fun identify() {
        println("[$firstName, $lastName, $age]")
    }
}
  • Line 6: creates an array of 3 elements. The function [arrayOfNulls<Person>(3)] initializes these elements with the value [null], meaning they do not reference any objects: the array type is therefore [Array<Person?>], an array of references that can hold the value [null]. Colloquially, we refer to this as an “array of objects,” even though it is merely an array of object references. The creation of the array—which is itself an object—does not create any objects of the same type as its elements; this must be done subsequently;
  • lines 7–9: creation of the 3 objects of type [Person];
  • lines 11–12: display the contents of the array [friends]. The variable [friend] is of type [Person?]: the compiler requires the safe call [friend?.identify()], which does nothing if [friend] is equal to [null].

Results:

1
2
3
4
> gradlew -q run -Pmain=classes11.Classes11Kt
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

The element type [Person?] indicates that an array created empty in Java contains [null]. This is avoided in Kotlin by creating the array with its elements, as shown in the following paragraph.

4.1.20. Type Inference and Array Initialization

The program [Classes12.kt] demonstrates the syntax that simplifies the creation of objects and arrays:

// type inference and array initialization
package classes12

fun main() {
    // the type of the variable is inferred from the expression that initializes it
    val p1 = Person("Jean", "Dupont", 30)
    val p2 = Person()
    p2.firstName = "Arthur"
    p2.lastName = "Martin"
    p2.age = 7
    // an array created and initialized by a list of values
    val friends = arrayOf(
        p1,
        p2,
        Person("Sylvie", "Vartan", 52),
        Person("Neil", "Armstrong", 66),
    )
    for (friend in friends) {
        println(friend.identity)
    }
    // an array created on the fly, passed as a parameter
    display(arrayOf(Person("Pauline", "Leduc", 12), p1))
    // an array whose elements are computed by a lambda from their index
    val children = Array(3) { i -> Person("Child${i + 1}", "Martin", i + 2) }
    display(children)
}

// a function that receives an array of persons
fun display(persons: Array<Person>) {
    for (p in persons) {
        println("Display : ${p.identity}")
    }
}

// all properties have a default value: Person() is possible
class Person(var firstName: String = "", var lastName: String = "", var age: Int = 0) {
    // identity
    val identity get() = "[$firstName, $lastName, $age]"
}
  • Line 6: The type of the variable, [Person], is inferred from the expression that initializes it. The variable is nonetheless typed: [p1] can only refer to a person. This has been the standard notation in Kotlin since the beginning of the course; the Java equivalent is the keyword [var]. Kotlin goes a step further than Java: type inference also applies to properties ([identity], line 38) and to the return type of functions whose body is an expression;
  • line 36: all properties have a default value. The primary constructor can therefore be called without arguments, [Person()] (line 7), and the person is then assigned its properties (lines 8–10). There is no need to declare a parameterless constructor;
  • Lines 12–16: An array created and populated by the function [arrayOf(...)], which receives the values of the elements. The size of the array is the number of values; its type, [Array<Person>], is derived from the type of the values: the elements cannot be of type [null]. A trailing comma (line 16) is allowed in an argument list, as in any parameter list: it makes it easier to add an element;
  • line 18: the loop variable [friend] is of type [Person]: no safe call this time;
  • line 22: an array created on the fly, passed as a parameter. This is called an anonymous array: it is not referenced by any variable in the function [main]. Unlike in Java, there is no difference in syntax depending on whether the array is created in a declaration or elsewhere;
  • line 24: the constructor [Array(size) { i -> ... }] creates an array in which each element is calculated by the lambda based on its index [i];
  • lines 29–31: a high-level function that takes an array of people.

Results:

> gradlew -q run -Pmain=classes12.Classes12Kt
[Jean, Dupont, 30]
[Arthur, Martin, 7]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]
Display : [Pauline, Leduc, 12]
Display : [Jean, Dupont, 30]
Display : [Child1, Martin, 2]
Display : [Child2, Martin, 3]
Display : [Child3, Martin, 4]

We will make little use of arrays of objects: the next chapter introduces lists ([List], [MutableList]), which are more flexible ([listOf(p1, p2)] is written as [arrayOf(p1, p2)]).

4.2. Inheritance by Example

4.2.1. Overview

Here we discuss the concept of inheritance. The purpose of inheritance is to “customize” an existing class so that it meets our needs. Suppose we want to create a class [Teacher]: a teacher is a specific type of person. They have attributes that other people do not have: the subject they teach, for example. But they also have the attributes common to all people: first name, last name, and age. A teacher is therefore a full member of the class [Person] but has additional attributes. Rather than creating a class [Teacher] from scratch, we prefer to build upon the existing class [Person] and adapt it to the specific characteristics of teachers. This is made possible by the concept of inheritance.

To indicate that the class [Teacher] inherits from the class [Person], we write [class Teacher(...) : Person(...)]: the name of the parent class follows the colon, as in C#, whereas Java writes [extends Person].

[Person] is called the parent class (or base class, or superclass), and [Teacher] is called the derived class (or child class, or subclass). A [Teacher] object has all the characteristics of a [Person] object: it has the same properties and methods. These properties and methods of the parent class are not repeated in the definition of the child class; we simply specify the properties and methods added by the child class. A Kotlin class can inherit from only one parent class.

An important difference from Java: in Kotlin, a class is final by default, meaning it cannot be inherited from. To allow derivation, the parent class must be declared [open] (“open”). This is a deliberate choice by the language designers: a class must be designed to be derived from, and the person writing it must state this explicitly.

The examples in this paragraph are two multi-file programs, located in the packages [inheritance1] and [inheritance2]. In the [inheritance1] package, the [Person] class is defined as follows in [Person.kt]:

package inheritance1

// open: the Person class can have derived classes
open class Person(firstName: String, lastName: String, age: Int) {
    // properties checked by their setter
    var firstName = ""
        set(value) {
            field = validate(value, "first name")
        }

    var lastName = ""
        set(value) {
            field = validate(value, "last name")
        }

    var age = 0
        set(value) {
            require(value >= 0) { "invalid age ($value)" }
            field = value
        }

    // identity of the person
    val identity get() = "[$firstName, $lastName, $age]"

    // construction: initialization through the setters, counting, trace
    init {
        this.firstName = firstName
        this.lastName = lastName
        this.age = age
        nbPersons++
        println("Constructor Person(String, String, Int)")
    }

    companion object {
        // number of persons created
        var nbPersons = 0L
            private set
    }
}

// checking a mandatory string
private fun validate(value: String, label: String): String {
    require(value.isNotBlank()) { "invalid $label ($value)" }
    return value
}
  • line 4: the class is open ([open]): subclasses can be derived from it. The primary constructor has three parameters, which are not properties (no [val] or [var]);
  • lines 6–19: three properties whose setters verify the received value (see program [Classes07.kt]). The checks for the first name and last name, which are identical, are factored into the private function [validate] (lines 42–44), as in the program [Classes09.kt];
  • line 23: the method [identify] from the first examples has been replaced by the calculated property [identity], which returns the person’s identity instead of displaying it;
  • Lines 26–31: The primary constructor’s block [init] initializes the object using property setters, increments the person counter, and displays a follow-up message. It is placed after the property declarations: property initializations and the [init] blocks are executed in the order in which they are written, and a property must have been initialized (here with [""] or 0) before it can be used;
  • lines 34–37: the person counter from the section on the companion object.

We create a class [Teacher] that inherits from the class [Person] [Teacher.kt]:

package inheritance1

// the Teacher class derives from the Person class:
// its primary constructor passes three of its parameters to the constructor of the parent class
class Teacher(firstName: String, lastName: String, age: Int, var section: Int) :
    Person(firstName, lastName, age) {

    // trace
    init {
        println("Constructor Teacher(String, String, Int, Int)")
    }
}

The class [Teacher] adds to the properties and methods of the class [Person]:

  • lines 5–6: The class [Teacher] derives from the class [Person]. The header is written on two lines to fit on the page;
  • line 5: a property [section], declared in the primary constructor ([var section: Int]), which is the number of the section to which the teacher belongs within the faculty (roughly one section per subject). The property is read-write: it replaces the getter and setter of the Java version;
  • Lines 9–10: a [init] block that displays a progress message.

4.2.2. Creating a Teacher Object

A child class does not inherit constructors from its parent class. It must define its own constructors. The header for the [Teacher] class is as follows:

class Teacher(firstName: String, lastName: String, age: Int, var section: Int) :
    Person(firstName, lastName, age) {

The primary constructor receives four parameters ([firstName], [lastName], [age], [section]) and passes three ([firstName], [lastName], [age]) to its parent class—in this case, the class [Person]—using the syntax [: Person(firstName, lastName, age)] (line 2). We know that this class has a constructor, [Person(String, String, Int)], which will allow us to create a person using the passed parameters. Once the construction of the [Person] portion is complete (initializations and [init] blocks of the [Person] class), the construction of the [Teacher] object continues with the initialization of its [section] property and the execution of its own [init] blocks. The notation [: Person(firstName, lastName, age)] corresponds to the [super(firstName, lastName, age);] statement in the Java constructor; if the derived class does not have a primary constructor, each of its secondary constructors calls the parent class’s constructor via [constructor(...) : super(...)].

In summary, the constructor of a derived class:

  • passes to its parent class the parameters that the parent class needs to construct itself;
  • uses the remaining parameters to initialize its own properties.

It might have been preferable to directly initialize the properties [firstName], [lastName], and [age] directly in the [Teacher] class, using statements like [this.firstName = firstName]. This is possible here because these properties are public and mutable, but it would be a poor solution, and it would be impossible if they were private: the private members of the [Person] class are accessible only to methods of the [Person] class. All other objects, including objects of derived classes as in this case, must access them through public (or protected, such as [protected]) members. Most importantly, the [Person] part of the object must in any case be instantiated by a constructor of the [Person] class. If you write [: Person()] without arguments, the compiler reports that the parameters for the parent class’s constructor are missing:

1
2
3
4
5
6
7
8
9
Teacher.kt:6:5: error: no value passed for parameter 'firstName'.
    Person() {
    ^^^^^^
Teacher.kt:6:5: error: no value passed for parameter 'lastName'.
    Person() {
    ^^^^^^
Teacher.kt:6:5: error: no value passed for parameter 'age'.
    Person() {
    ^^^^^^

Using the parent class’s constructor is therefore the correct solution, and it is the standard approach: when creating a child object, you first call the parent object’s constructor and then complete the initializations specific to the child object ([section] in our example).

Finally, if you omit the keyword [open] before the class [Person], the derivation is rejected:

1
2
3
Teacher.kt:6:5: error: this type is final, so it cannot be extended.
    Person(firstName, lastName, age) {
    ^^^^^^

4.2.3. Inherited Methods

The class [Teacher] does not define a property named [identity]. The expression [e.identity], where [e] is a teacher, therefore refers to the property [identity] inherited from the class [Person]: it is public and, by inheritance, becomes a public property of the class [Teacher]. The same applies to methods. The test program [Main.kt] from the package [inheritance1] is as follows:

package inheritance1

fun main() {
    // a teacher
    val e = Teacher("Lucile", "Dumas", 56, 61)
    // the identity property inherited from the Person class
    println(e.identity)
    // the section property specific to the Teacher class
    println("section=${e.section}")
    // a person
    val p = Person("Jean", "Dupont", 30)
    // the display function receives a Person or an object of a derived class
    display(e)
    display(p)
    // the toString method inherited from the Any class
    println(e.toString())
    println(p.toString())
    println("Number of persons created: ${Person.nbPersons}")
}

// displays the identity of p
fun display(p: Person) {
    println(p.identity)
}
  • line 5: creation of a teacher;
  • line 7: display of the teacher’s identity, obtained via the [identity] property inherited from the [Person] class;
  • line 9: the property [section], specific to the class [Teacher];
  • lines 13–14: call to the function [display] (lines 22–23) with a teacher and then a person. We’ll return to this in the next paragraph;
  • lines 16–17: call to the [toString] method on both objects. We’ll also come back to this.

Results:

> gradlew -q run -Pmain=inheritance1.MainKt
Constructor Person(String, String, Int)
Constructor Teacher(String, String, Int, Int)
[Lucile, Dumas, 56]
section=61
Constructor Person(String, String, Int)
[Lucile, Dumas, 56]
[Jean, Dupont, 30]
inheritance1.Teacher@2f0e140b
inheritance1.Person@7440e464
Number of persons created: 2

We can see that:

  • an object [Person] (line 1 of the results) was constructed before the object [Teacher] (line 2);
  • [e.identity] provided the identity of the teacher’s [Person] part, without its section (line 3);
  • the person counter also counts the teacher (line 10): a teacher is a person.

To obtain a teacher’s complete identity, the class [Teacher] must define its own property [identity]. This is what the package [inheritance2] does.

4.2.4. Polymorphism

Consider a class hierarchy: C0 ← C1 ← C2 ← … ← Cn, where Ci ← Cj indicates that class Cj is derived from class Ci. This implies that class Cj has all the characteristics of class Ci plus additional ones. Let Oi be objects of type Ci. It is valid to write [Oi = Oj] with j > i. Indeed, by inheritance, class Cj has all the characteristics of class Ci plus additional ones. Therefore, an object Oj of type Cj contains an object of type Ci. The operation [Oi = Oj] causes the variable Oi, of type Ci, to reference the object Oj: only the members defined in class Ci can be accessed through it.

The fact that a variable Oi of class Ci can actually reference not only an object of class Ci but any object derived from class Ci is called polymorphism: the ability of a variable to reference different types of objects.

Let’s take an example and consider the function [display] from the previous program:

1
2
3
fun display(p: Person) {
    println(p.identity)
}

We can pass it either a *[Person] or a *[Teacher]: In the latter case, the formal parameter [p], which is of type [Person], receives a value of type [Teacher]. Since the type [Teacher] is derived from the type [Person], this is valid. The expression [p.identity] used here the property [identity] of the class [Person], the only one that exists in the program: for the teacher (line 6 of the results), we only obtain the identity of its “Person” part .” Let’s see what happens when the class [Teacher] has its own property [identity].

4.2.5. Method Redefinition and Dynamic Binding

In package [inheritance2], class [Person] is the same as the one in package [inheritance1], with two changes:

1
2
3
4
5
    // identity of the person: open, it can be overridden in a derived class
    open val identity get() = "[$firstName, $lastName, $age]"

    // overriding the toString method of the Any class
    override fun toString() = identity
  • Line 2: The property [identity] is declared as [open]: it can be overridden in a derived class. In Kotlin, methods and properties are final by default, just like classes: only the members [open] of a class [open] can be overridden;
  • lines 4–5: an override of the [toString] method, discussed in the following paragraph.

In the [Teacher] class, the [identity] property is redefined as follows:

package inheritance2

// a Kotlin class is final by default: the Teacher class cannot be derived from
class Teacher(firstName: String, lastName: String, age: Int, var section: Int) :
    Person(firstName, lastName, age) {

    // trace
    init {
        println("Constructor Teacher(String, String, Int, Int)")
    }

    // the identity property overrides that of the parent class
    override val identity get() = "Teacher[${super.identity},$section]"
}
  • Line 13: The property [identity] overrides the property of the parent class; it has the same name and the same type. It uses the property [identity] from its parent class, designated as [super.identity], to retrieve the “Person” portion, then supplements it with the section specific to the class [Teacher]. A method is redefined in the same way: same name, same parameters, same return type, and the keyword [override];
  • Line 13: The keyword [override] is required. It serves the same purpose as the [@Override] annotation in Java, which is optional: if the property did not redefine anything (e.g., a typo in the name or a different type), the compiler would report the error instead of silently creating a new member. Conversely, a redefinition without *[override]* is rejected:
1
2
3
Teacher.kt:13:9: error: 'identity' hides member of supertype 'Person' and needs an 'override' modifier.
    val identity get() = "Teacher[${super.identity},$section]"
        ^^^^^^^^
  • and if the [identity] property of the [Person] class is not declared as [open], the redefinition is rejected:
1
2
3
Teacher.kt:13:5: error: 'identity' in 'Person' is final and cannot be overridden.
    override val identity get() = "Teacher[${super.identity},$section]"
    ^^^^^^^^
  • Line 3: The class [Teacher] is not declared as [open]: it is final, as we’ll explain a little later.

Consider a [Teacher] object [E]. This object contains a [Person] object:

Image

The property [identity] is defined in both the class [Teacher] and its parent class [Person]. The version used by [p.identity] is determined at runtime, based on the actual type of the object referenced by [p], rather than at compile time based on the declared type of the variable. This is known as dynamic binding. To execute [o.m()], the virtual machine searches for a method named [m] in the following order:

  • in the class of the [o] object;
  • in its parent class, if it has one;
  • in the parent class of its parent class, if it exists;
  • and so on.

The same applies to a property, where reading the property involves calling its accessor, which is a method. Inheritance therefore allows you to redefine members in the child class that have the same signature as those in the parent class. This is what allows you to adapt the child class to its own needs. The program [Main.kt] from the package [inheritance2] is as follows:

package inheritance2

fun main() {
    // a teacher
    val e = Teacher("Lucile", "Dumas", 56, 61)
    display(e)
    // a person
    val p = Person("Jean", "Dupont", 30)
    display(p)
    // seen as a Person, the teacher remains a teacher
    val pe: Person = e
    println(pe.identity)
    println((e as Person).identity)
    // println(Any?) displays the result of the toString method of the object
    println(e)
    println(p)
    // an array of persons of different types
    val persons = arrayOf(p, e)
    for (person in persons) {
        println("${person::class.simpleName} : $person")
    }
}

// displays the identity of p
fun display(p: Person) {
    println(p.identity)
}

Results:

> gradlew -q run -Pmain=inheritance2.MainKt
Constructor Person(String, String, Int)
Constructor Teacher(String, String, Int, Int)
Teacher[[Lucile, Dumas, 56],61]
Constructor Person(String, String, Int)
[Jean, Dupont, 30]
Teacher[[Lucile, Dumas, 56],61]
Teacher[[Lucile, Dumas, 56],61]
Teacher[[Lucile, Dumas, 56],61]
[Jean, Dupont, 30]
Person : [Jean, Dupont, 30]
Teacher : Teacher[[Lucile, Dumas, 56],61]

This time, in row 3 of the results, the [display] function correctly displayed the teacher's full identity. Lines 11–13 of the program show that, when viewed through a variable of type [Person] or converted to [Person] by the operator [as] (the equivalent of the Java operator [(Person)]), the teacher remains a teacher: it is still the property [identity] of the class [Teacher] that is used (lines 6–7 of the results). The conversion changes the type of the reference, not that of the object.

In this regard, Kotlin is closer to C# than to Java. In C#, a method can only be overridden if it is declared virtual ([virtual]), and the override is indicated by [override]; in Kotlin, [open] serves the same purpose as [virtual]. In Java, on the other hand, all methods are overridable by default, except those declared as [final]. In all cases, once the method is overridden, the binding is dynamic.

4.2.6. The Any class and the toString method

The [Any] class is the “parent” class of all Kotlin classes. Thus, when you write [class Person], you are implicitly writing [class Person : Any()]. It corresponds to the Java class [java.lang.Object]: at runtime, for JVM, it is actually [Object].

The class *[Any] defines three methods, all of which are *[open], and from which all objects inherit:

  • [toString(): String]: returns a textual representation of the object. By default, this is the object’s full class name, followed by the character [@] and a hash code written in hexadecimal;
  • [equals(other: Any?): Boolean]: indicates whether the object is “equal” to the object [other]. By default, it is only equal to itself: the method compares references, just like the [===] operator. This is the method called by the [==] operator;
  • [hashCode(): Int]: returns a hash code for the object, used by dictionaries and sets (next chapter). The methods [equals] and [hashCode] are discussed later.

The [getClass()] method of the Java class [Object] is not a method of [Any]: In Kotlin, the expression [obj::class] returns the Kotlin description of the object’s actual class (of type [KClass]), whose property [simpleName] is the class name; [obj::class.java], or [obj.javaClass], returns the Java description ([Class]).

It was the method [toString] of the class [Any] that was used by the calls [e.toString()] and [p.toString()] from the package [inheritance1]: [inheritance1.Teacher@2f0e140b] and [inheritance1.Person@7440e464] (lines 8–9 of the results; the numbers vary from one execution to another). Although we did not redefine the [toString] method in the [Person] and [Teacher] classes, the [toString] method in the [Any] class was able to display the object’s actual class name.

In the [inheritance2] package, the [Person] class redefines the [toString] method: [override fun toString() = identity]. The keyword [override] is also required here. Let’s return to the program [Main.kt] in the package [inheritance2]:

1
2
3
4
5
6
7
8
    // println(Any?) displays the result of the toString method of the object
    println(e)
    println(p)
    // an array of persons of different types
    val persons = arrayOf(p, e)
    for (person in persons) {
        println("${person::class.simpleName} : $person")
    }

On line 2, the function [println(message: Any?)] accepts a parameter of type [Any?]; therefore, since [Any] is the superclass of all classes, any object—in this case, a [Teacher]—can be passed. It prints the result of the object’s [toString] method to the console. Since the binding is dynamic, the [toString] method of the actual object is used—in this case, the one inherited from the [Person] class. This method returns [identity], a redefined property: for the teacher [e], it is therefore the property [identity] of the class [Teacher] that is used, as shown in line 8 of the results. It is therefore unnecessary to redefine [toString] in the class [Teacher].

The same mechanism applies to string templates: in ["... : $person"] (line 7), the object [person] is converted to a string by its method [toString]. The loop on line 6 iterates over an array of references to [Person], which actually point to objects of different types: [person::class.simpleName] returns the actual class name of each object (lines 10–11 of the results).

4.2.7. Final, open, and sealed classes

The class [Teacher] in the package [inheritance2] is declared as [class Teacher(...) : Person(...)], without the keyword [open]: it is final. The declaration [class TeacherResearcher(...) : Teacher(...)] would be rejected by the compiler with the message already encountered: [this type is final, so it cannot be extended]. In Java, it’s the opposite: a class is open by default, and you make it final using the keyword [final]. Kotlin’s approach prevents accidental subclassing of a class that was not intended to be subclassed. The same applies to members: a method or property can only be overridden if it is declared as [open]. A member that overrides another ([override]) is itself overridable in subsequent derived classes, unless it is declared as [final override].

Between a class open to all derivations and a final class, Kotlin offers—as Java has since version 17—an intermediate option: the sealed class ([sealed]), for which all direct derived classes are known at compile time. The program [Classes13.kt] illustrates this:

// final classes, open classes and sealed classes
package classes13

fun main() {
    val persons = arrayOf(
        Teacher("Lucile", "Dumas", 61),
        Student("Paul", "Martin", "computer science"),
        Adjunct("Marc", "Petit"),
        Consultant("Lise", "Morel", "Airbus"),
    )
    for (p in persons) {
        println("${p::class.simpleName} : ${p.identity}")
    }
    // when on a sealed class: the compiler knows that all cases are handled
    for (p in persons) {
        println("${p.firstName} is ${description(p)}")
    }
    // the allowed direct subclasses of the sealed class Person
    println("Person is sealed: ${Person::class.java.isSealed}")
    for (c in Person::class.java.permittedSubclasses) {
        println("allowed subclass: ${c.simpleName}")
    }
}

// a when expression without an else branch
fun description(p: Person): String = when (p) {
    is Teacher -> "a teacher in section ${p.section}"
    is Student -> "a student in ${p.major}"
    is Adjunct -> "an adjunct"
}

// a sealed class: its direct derived classes are all known at compile time
sealed class Person(val firstName: String, val lastName: String) {
    open val identity get() = "[$firstName, $lastName]"
}

// a derived class is final by default
class Teacher(firstName: String, lastName: String, val section: Int) : Person(firstName, lastName) {
    override val identity get() = "${super.identity}, section $section"
}

class Student(firstName: String, lastName: String, val major: String) : Person(firstName, lastName) {
    override val identity get() = "${super.identity}, major $major"
}

// open: the hierarchy is open again from this class
open class Adjunct(firstName: String, lastName: String) : Person(firstName, lastName)

// a class derived from Adjunct: allowed, since Adjunct is open
class Consultant(firstName: String, lastName: String, val company: String) : Adjunct(firstName, lastName) {
    override val identity get() = "${super.identity}, company $company"
}
  • line 33: the class [Person] is sealed. Its direct subclasses must be declared in the same package and module as it (in any file within that package). Unlike in Java, there is no list of allowed classes ([permits]): it includes all classes in the package that are its direct subclasses, in this case [Teacher], [Student], and [Adjunct]. A sealed class is implicitly abstract (see below: you cannot create objects of this class, only of its derived classes) and its constructor is protected: it is intended only for derived classes;
  • line 34: the property [identity] is open: derived classes can redefine it;
  • Lines 38–43: The classes [Teacher] and [Student] are final by default. In Java, a class derived from a sealed class must be declared as [final], [sealed], or [non-sealed]; in Kotlin, the default rule suffices;
  • Line 47: The class [Adjunct] is open ([open]): it reopens the hierarchy, allowing any class to derive from it. This is equivalent to Java’s [non-sealed]. A third option is to declare it as [sealed] itself. The class has no body: there is nothing to add to the [Person] class;
  • line 50: the class [Consultant] derives from [Adjunct], which is allowed;
  • lines 5–9: The array type, [Array<Person>], is inferred by the compiler: it is the most specific common type among the four objects;
  • lines 26–29: the expression [when], seen in the previous chapter, tests the type of the person here using the operator [is]. In each branch, the compiler knows that [p] is of the type being tested: [p.section] is accepted in the [is Teacher] branch without explicit conversion. This is “smart casting,” which is explained in more detail at the end of the chapter. Most importantly, the expression [when] has no branch [else]: since the class [Person] is sealed, the compiler knows that every person is a [Teacher], a [Student], or a [Adjunct], and that all three cases are handled. A consultant is an adjunct: they are handled by the third branch;
  • lines 19–21: the Java description of the class ([Person::class.java]) indicates whether it is sealed ([isSealed]) and lists its permitted direct subclasses ([permittedSubclasses]). For a sealed class, the Kotlin compiler produces the same information as the Java compiler: for JVM, the class [Person] is a sealed Java class.

Results:

> gradlew -q run -Pmain=classes13.Classes13Kt
Teacher : [Lucile, Dumas], section 61
Student : [Paul, Martin], major computer science
Adjunct : [Marc, Petit]
Consultant : [Lise, Morel], company Airbus
Lucile is a teacher in section 61
Paul is a student in computer science
Marc is an adjunct
Lise is an adjunct
Person is sealed: true
allowed subclass: Adjunct
allowed subclass: Student
allowed subclass: Teacher

If you write a class [Artist] derived from [Person] in another package—in this case, the [others] package—the compiler reports:

1
2
3
Artist.kt:6:53: error: a class can only extend a sealed class or interface declared in the same package.
class Artist(firstName: String, lastName: String) : Person(firstName, lastName)
                                                    ^^^^^^

And if we remove the branch [is Adjunct] from the expression [when], the compiler reports that not all cases are being handled:

1
2
3
Classes13.kt:26:38: error: 'when' expression must be exhaustive. Add the 'is Adjunct' branch or an 'else' branch.
fun description(p: Person): String = when (p) {
                                     ^^^^

What is the purpose of a sealed class? It allows the author of a class hierarchy to maintain control over it: the author knows that any [Person] is a [Teacher], a [Student], or a [Adjunct]. The compiler knows this as well and takes advantage of it in the expression [when]: if a fourth class derived from [Person] is added later, all [when] expressions that do not handle it are flagged during compilation. A branch like [else], on the other hand, would have masked this omission. This mechanism also applies to interfaces ([sealed interface]), discussed later, and is a very common way in Kotlin to model a closed set of cases: the possible outcomes of an operation (success, failure), the states of a screen, messages exchanged between two programs…

4.3. Operator overloading

Some languages, such as C# or C++, allow you to redefine the meaning of operators ([+], [==], [<]…) for a class. Java does not allow this: you must write methods, such as [a.add(b)] instead of [a + b], [s1.equals(s2)] instead of [s1 == s2], [a.compareTo(b) < 0] instead of [a < b]. Kotlin allows this in a simple and controlled way: each operator corresponds to a method with a fixed name, and all you need to do is declare a method with that name, preceded by the keyword [operator], for the operator to be usable with objects of that class. The expression [a + b] is thus translated by the compiler into [a.plus(b)]. You cannot invent new operators or change their precedence. The main conventions are as follows:

Expression
Translated to
Note
[a + b], [a - b], [a * b], [a / b], [a % b]
[a.plus(b)], [a.minus(b)], [a.times(b)], [a.div(b)], [a.rem(b)]
arithmetic operators
[-a], [+a], [!a]
[a.unaryMinus()], [a.unaryPlus()], [a.not()]
unary operators
[a++], [a--]
[a.inc()], [a.dec()]
The method returns the new value
[a += b]
[a.plusAssign(b)], or otherwise [a = a.plus(b)]
the same applies to [-=], [*=]…
[a == b], [a != b]
[a?.equals(b) ?: (b === null)]
[equals] is inherited from [Any]: it is redefined as [override]
[a < b], [a <= b], [a > b], [a >= b]
[a.compareTo(b) < 0]…
[compareTo] returns a negative, zero, or positive integer
[b in a], [b !in a]
[a.contains(b)], [!a.contains(b)]
membership
[a[i]], [a[i] = v]
[a.get(i)], [a.set(i, v)]
indexers (next paragraph)
[a(x)]
[a.invoke(x)]
an object “called” like a function
[a..b]
[a.rangeTo(b)]
intervals
[for (x in a)]
[a.iterator()]
iteration

The operators [===] and [!==], which compare references, cannot be redefined. Classes in the Kotlin standard library use these conventions, and—thanks to extension functions (discussed later in this chapter)—Kotlin also applies them to classes such as JDK and [BigDecimal]. In a Java class, a method with the expected name and signature ([compareTo], [get]…) can also be used as an operator without the [operator] keyword.

4.3.1. An example

The [lists] package contains a class named [ListOfPersons], a list of people, whose methods define the operators [+], [+=], [in], iteration, and indexers. The classes [Person] and [Teacher] in this package are simplified versions of those in the [inheritance2] package: the properties are read-only ([val]), there are no longer any validations, progress messages, or counters. The [Person] class gains a comparison operator, [Person.kt]:

package lists

open class Person(val firstName: String, val lastName: String, val age: Int) {
    // identity of the person
    open val identity get() = "[$firstName, $lastName, $age]"

    override fun toString() = identity

    // the comparison operators <, <=, >, >=: persons are compared by age
    operator fun compareTo(other: Person) = age.compareTo(other.age)
}
  • Line 10: The method [compareTo], declared as [operator], defines the four operators [<], [<=], [>], and [>=] between two people: [p1 < p2] is translated to [p1.compareTo(p2) < 0]. The method must return a negative integer if the current object precedes the object [other], zero if they are equivalent, and a positive integer otherwise. Here, the people are compared by age: the [compareTo] method for integers does the job.

The people are stored in a mutable list of type [MutableList<Person>] (lists are covered in the next chapter). Rather than having the [ListOfPersons] class inherit from a list class, we place the list in a private property of the class. This is referred to as using composition rather than inheritance: the class *[ListOfPersons]* thus exposes only the operations it has chosen. From the list, we use only the following elements:

  • the method [l.add(p)], which adds the person [p] to the list [l], and the method [l.addAll(l2)], which adds all elements from the list [l2] to it;
  • the property [l.size], which returns the number of elements in the list [l], and the property [l.indices], the range [0..size-1] of its indices;
  • the notation [l[i]], which refers to element number i of the list [l], for both reading and writing: Kotlin lists have indexers;
  • the method [l.iterator()], which returns an iterator over the elements of the list, and the method [l.joinToString(...)], which constructs a string from its elements.

The definition of the [ListOfPersons] class is as follows: [ListOfPersons.kt]:

package lists

class ListOfPersons {
    // the persons of the list
    private val persons = mutableListOf<Person>()

    // number of persons in the list
    val size get() = persons.size

    // the + operator: returns a new list containing the persons
    // of the current list and the person p
    operator fun plus(p: Person): ListOfPersons {
        val result = ListOfPersons()
        result.persons.addAll(persons)
        result.persons.add(p)
        return result
    }

    // the += operator: adds p to the current list
    operator fun plusAssign(p: Person) {
        persons.add(p)
    }

    // the in operator: does the list contain a person with the given last name?
    operator fun contains(lastName: String) = indexOf(lastName) != -1

    // the iteration operator: allows writing for (p in list)
    operator fun iterator(): Iterator<Person> = persons.iterator()

    // the indexers: list[i] for reading and writing
    operator fun get(i: Int) = persons[i]

    operator fun set(i: Int, p: Person) {
        persons[i] = p
    }

    // a second read indexer: list[lastName] returns the person with that last name, or null
    operator fun get(lastName: String): Person? {
        val i = indexOf(lastName)
        return if (i != -1) persons[i] else null
    }

    // position of the person with the given last name, -1 if there is none
    fun indexOf(lastName: String): Int {
        for (i in persons.indices) {
            if (persons[i].lastName == lastName) {
                return i
            }
        }
        return -1
    }

    // toString: returns (person1,person2,...)
    override fun toString() = persons.joinToString(",", "(", ")")
}
  • line 5: the list of people. The function [mutableListOf<Person>()] creates an empty, mutable list of elements of type [Person]. The keyword [val] indicates that the property will always refer to the same list; the list’s contents may change. No import is necessary: lists are part of the standard Kotlin library and are imported automatically;
  • Line 8: the number of people, as a calculated property;
  • Lines 12–16: The method [plus] defines the operator [+] for the operation [l + p], where [l] is of type [ListOfPersons] and [p] is of type [Person] or a derivative thereof. It creates a new list (line 13), copies the people from the current list into it (line 14), and then adds the person [p] (line 15). As with the [+] operator for numbers or strings, the operands are not modified. Since the list [result] is of the same class, the method has access to its private property [result.persons]. The returned list is of type [ListOfPersons], which allows operations to be chained as in [l + p1 + p2];
  • lines 20–21: The method [plusAssign] defines the operator [+=]: it modifies the current list by adding the person [p] to it and returns nothing;
  • line 25: the method [contains] defines the operator [in]: ["leduc" in l] is translated to [l.contains("leduc")]. The parameter here is a name; it is the method [indexOf], discussed in the following paragraph, that searches for the person;
  • line 28: the method [iterator] allows the list to be traversed using a loop [for (p in l)]. It makes the list iterator private, of type [Iterator<Person>], which allows only read access to the elements;
  • line 54: We redefine the method [toString] to display a list of people in the form (person1, person2, ...), where personi is itself the result of the person’s [toString] method. The [joinToString(separator, prefix, postfix)] method for lists constructs a string from successive elements—converted to strings by their [toString] method—separating them with the specified separator (here, a comma) and enclosing them with a prefix and a suffix (here, parentheses).

Note that the [ListOfPersons] class has no constructor. It therefore uses the default constructor, which takes no parameters and does nothing other than initialize the [persons] property with the value specified in its declaration. Lines 30–50 are discussed in the following section.

The test program [Main.kt] begins as follows:

package lists

import java.math.BigDecimal

fun main() {
    // --- the operators of strings and BigDecimal numbers
    val s1 = "jean"
    val s2 = StringBuilder("je").append("an").toString()
    println("s1 == s2 : ${s1 == s2}, s1 === s2 : ${s1 === s2}")
    println("s1 < \"paul\" : ${s1 < "paul"}, s1.compareTo(\"paul\") : ${s1.compareTo("paul")}")
    val a = BigDecimal("0.10")
    val b = BigDecimal("0.20")
    println("a + b = ${a + b}, a * b = ${a * b}, -a = ${-a}, a < b : ${a < b}")

    // --- the compareTo operator of the Person class
    val jean = Person("jean", "martin", 10)
    val pauline = Person("pauline", "leduc", 12)
    println("jean < pauline : ${jean < pauline}, jean >= pauline : ${jean >= pauline}")

    // --- the + operator
    // a list of persons
    var l = ListOfPersons()
    // adding persons
    l = l + jean + pauline
    // display
    println("l=$l")
    // l + p returns a new list: l is not modified
    val l2 = l + Person("louis", "durand", 8)
    println("l=$l")
    println("l2=$l2")

    // --- the += operator
    val l3 = l
    // += modifies the list referenced by l3 (and by l), without creating a new list
    l3 += Teacher("camille", "germain", 27, 60)
    println("l=$l")
    // l and l3 still reference the same object
    println("l3 === l : ${l3 === l}")

    // --- the in and !in operators, iteration
    println("\"leduc\" in l : ${"leduc" in l}, \"xx\" !in l : ${"xx" !in l}")
    for (p in l) {
        print("${p.firstName} ")
    }
    println("(${l.size} persons)")
  • Lines 7–10: two strings with the same content but distinct: the string [s2] is constructed at runtime. The operator [] compares the contents (it calls [equals]*), while the operator *[=] compares the references. In Kotlin, therefore, two strings are compared using [==], which is a classic error in Java. The [<] operator compares two strings based on the order of their character codes: it calls the [compareTo] method, which returns a negative number here because “jean” comes before “paul”;
  • Lines 11–13: The class [BigDecimal] derived from JDK (exact decimal numbers, which we will use for the amounts) contains only methods: [add], [multiply], [negate], [compareTo]. The Kotlin standard library adds the unary operators [+], [*], [-]…: you write [a + b] instead of [a.add(b)], and [-a] instead of [a.negate()]. The comparison operators, on the other hand, directly use the method [compareTo] from [BigDecimal]. The import on line 3 is necessary: [BigDecimal] is a class of JDK;
  • lines 16–18: the comparison operators defined by the [compareTo] method of the [Person] class;
  • line 22: creation of an empty list of people [l]. The variable is declared as [var]: its value will change;
  • line 24: Adds two people using the operator [+], which returns a new list each time;
  • line 28: [l + p] creates a new list, [l2]; the list [l] remains unchanged;
  • line 35: a teacher is added using the operator [+=], i.e., the method [plusAssign];
  • line 41: the operators [in] and [!in];
  • Lines 42–45: Iterating through the list using a [for] loop, via the [iterator] method and the [size] property;
  • line 26 and following: a string template containing an object uses the redefined method [toString] of the class [ListOfPersons].

Results (beginning):

> gradlew -q run -Pmain=lists.MainKt
s1 == s2 : true, s1 === s2 : false
s1 < "paul" : true, s1.compareTo("paul") : -6
a + b = 0.30, a * b = 0.0200, -a = -0.10, a < b : true
jean < pauline : true, jean >= pauline : false
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12])
l2=([jean, martin, 10],[pauline, leduc, 12],[louis, durand, 8])
l=([jean, martin, 10],[pauline, leduc, 12],Teacher[[camille, germain, 27],60])
l3 === l : true
"leduc" in l : true, "xx" !in l : true
jean pauline camille (3 persons)

Why are there two operators, when [l = l + p] produces the same output as [l += p]? The operator [+] creates a new list, copies all the people from [l] into it, and then [l] refers to this new list, while the old one is discarded to the garbage collection. The operator [+=], defined by [plusAssign], adds the person to the existing list without copying. Line 33 stores a second reference, [l3], to the list [l]; after the [l3 += ...] operation, both references still point to the same object (line 9 of the results), and the added person appears in [l] (line 8), which proves that the list was not recreated. For a large list, or within a loop, the difference in performance is significant. The choice between the two styles is reflected in the library: the immutable objects [String] and [BigDecimal] return new objects; the mutable lists and [StringBuilder] are modified.

Why was it written as [l3 += ...] and not [l += ...]? Because [l] is a variable of [var]: for that variable, [l += p] could just as easily mean [l.plusAssign(p)] as it could mean [l = l.plus(p)], and the compiler refuses to choose:

1
2
3
4
5
Main.kt:35:7: error: ambiguity between assign operator candidates:
fun plus(p: Person): ListOfPersons
fun plusAssign(p: Person): Unit
    l += Teacher("camille", "germain", 27, 60)
      ^^

With a variable like [val]—or [l3]—only the first translation is possible. In practice, a class defines one or the other: [plus] for an immutable class, [plusAssign] for a mutable class. Kotlin lists follow this rule: a [List] (read-only) has the [+] operator, a [MutableList] has the [+=] operator; for a [MutableList] referenced by a variable [var], the compiler reports the same ambiguity.

Operator overloading should not be overused: it only makes sense if the operator retains its usual meaning (addition, comparison, membership, etc.). In other cases, a method with an explicit name is preferable.

4.4. Indexers: the get and set operators

Here, we continue to use the class [ListOfPersons]. If *[l]* is a [ListOfPersons] object, we want to be able to refer to person number i in the list *[l] for both reading and writing, using the array notation ***[l[i]]***. C# calls this an indexer. Java does not allow this: the [[i]] notation is reserved for arrays in Java, and methods are written as [get(int i)] and [set(int i, value)]. In Kotlin, these two methods, declared as [operator], define the [l[i]] notation: [l[i]] is translated to [l.get(i)], and [l[i] = p] is translated to [l.set(i, p)]*. This is how Kotlin lists and dictionaries are used as arrays.

    // the indexers: list[i] for reading and writing
    operator fun get(i: Int) = persons[i]

    operator fun set(i: Int, p: Person) {
        persons[i] = p
    }

    // a second read indexer: list[lastName] returns the person with that last name, or null
    operator fun get(lastName: String): Person? {
        val i = indexOf(lastName)
        return if (i != -1) persons[i] else null
    }

    // position of the person with the given last name, -1 if there is none
    fun indexOf(lastName: String): Int {
        for (i in persons.indices) {
            if (persons[i].lastName == lastName) {
                return i
            }
        }
        return -1
    }
  • line 2: the method [get] returns person #i from the private list [persons], which is itself identified by its index;
  • lines 4–5: The method [set] assigns the person [p] to it;
  • lines 9–11: a second, read-only indexer (there is no method [set(String, Person)]). The index is not necessarily an integer: [l["martin"]] returns the person named “martin,” or [null] if that person is not in the list. The result type, [Person?], indicates this. The two methods [get] are overloads: the compiler chooses based on the index’s type. An indexer can also have multiple parameters: [m[i, j]] is translated to [m.get(i, j)];
  • lines 15–21: we also want to be able to obtain the position in the list of the person with a given name, or -1 if that person is not in the list. The name [indexOf] is that of the equivalent method for lists and strings. The loop iterates through the indices of the list ([persons.indices]); the names are compared using the operator [==] (line 17).

The test program continues as follows:

    // --- the indexers: the get and set operators
    // changing element no. 1
    l[1] = Person("franck", "gallon", 5)
    // displaying element no. 1
    println("l[1]=${l[1]}")
    // displaying the list l
    println("l=$l")
    // searching for persons by their last name, with indexOf
    val names = arrayOf("martin", "germain", "xx")
    for (name in names) {
        val iname = l.indexOf(name)
        if (iname != -1) {
            println("Person($name)=${l[iname]}")
        } else {
            println("Person($name) does not exist")
        }
    }
    // the same search with the indexer l[name]
    for (name in names) {
        println("l[\"$name\"]=${l[name] ?: "does not exist"}")
    }
}
  • line 3: call to the method [set];
  • line 5: call to method [get];
  • Line 11: Call to the [indexOf] method;
  • line 13: call to the [get] method with an integer index;
  • line 20: call to the [get] method with a name. The [?:] operator replaces the result of [null] with a message.

Results (end):

1
2
3
4
5
6
7
8
9
...
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Teacher[[camille, germain, 27],60])
Person(martin)=[jean, martin, 10]
Person(germain)=Teacher[[camille, germain, 27],60]
Person(xx) does not exist
l["martin"]=[jean, martin, 10]
l["germain"]=Teacher[[camille, germain, 27],60]
l["xx"]=does not exist

4.5. Value and Reference

4.5.1. Basic Types and Classes

Other languages, such as C or C#, offer structures in addition to classes: types whose variables directly contain data, rather than a reference to an object. Java does not have them: it distinguishes between the eight primitive types ([int], [double], [boolean]…), whose variables contain the value itself, and reference types (classes, arrays…), whose variables contain a reference to an object. Kotlin also has no structures, and, from the programmer’s perspective, it doesn’t even have primitive types: everything is an object; [Int], [Double], and [Boolean] are classes whose methods ([10.toString()], [age.coerceAtLeast(0)]) are called. But as we saw in the previous chapter, the compiler translates the base types [Int], [Long], [Double], [Boolean], [Char]… into the primitive types of the JVM ([int], [long]…) whenever possible: a variable [Int] then contains the value itself. The program [Classes14.kt] highlights the difference with a class:

// basic types and classes: value copy, reference copy
package classes14

fun main() {
    // two variables of type Int
    val age1 = 10
    var age2 = age1
    println("age1=$age1, age2=$age2")
    // age2 is modified
    age2 = 30
    // checking age1 and age2
    println("age1=$age1, age2=$age2")

    // an object op1
    val op1 = CPerson()
    op1.name = "paul"
    op1.age = 10
    println("op1=CPerson(${op1.name},${op1.age})")
    // an object op2
    val op2 = op1
    println("op2=CPerson(${op2.name},${op2.age})")
    // op2 is modified
    op2.name = "nicole"
    op2.age = 30
    // checking op1 and op2
    println("op1=CPerson(${op1.name},${op1.age})")
    println("op2=CPerson(${op2.name},${op2.age})")

    // an explicit copy of the object
    val op3 = CPerson()
    op3.name = op1.name
    op3.age = op1.age
    op3.age = 50
    println("op1=CPerson(${op1.name},${op1.age})")
    println("op3=CPerson(${op3.name},${op3.age})")
}

// class CPerson: two modifiable public properties
class CPerson {
    var name = ""
    var age = 0
}
  • lines 39–41: a class with two public, mutable properties: [name], [age].

Results:

1
2
3
4
5
6
7
8
9
> gradlew -q run -Pmain=classes14.Classes14Kt
age1=10, age2=10
age1=10, age2=30
op1=CPerson(paul,10)
op2=CPerson(paul,10)
op1=CPerson(nicole,30)
op2=CPerson(nicole,30)
op1=CPerson(nicole,30)
op3=CPerson(nicole,50)

The declaration [val age1 = 10] (line 6) reserves a memory location that contains the integer 10: the value of [age1] is the integer itself. In the case of the class, the object ([name], [age]) is created explicitly by calling the constructor [CPerson()] (line 15). The object is placed in a memory area managed by the garbage collector, the heap, and the value of [op1] is the address (the reference) of this object.

When [var age2 = age1] is written in the program (line 7), the value of [age1]—the integer 10—is copied into [age2]: this is a value copy. Modifying [age2] does not modify [age1] (line 2 of the results). Now consider the instruction [val op2 = op1] (line 20). The value of [op1] is copied into [op2], but since this value is the object’s address, the object itself is not duplicated: [op1] and [op2] refer to the same object. If the object pointed to by [op2] is modified, the object pointed to by [op1] is also modified since they are the same (lines 5–6 of the results). To obtain a second object, you must create it and copy the properties of the first one into it (lines 30–32): modifying the copy does not modify the original (lines 7–8 of the results).

Image

From these explanations, we can conclude that:

  • the value of a variable of a basic type ([Int], [Double]…) is the data itself;
  • The value of a variable of a class type is the address of the object it points to;
  • Assignment and parameter passing always copy the value of the variable: the data for a basic type, the reference for an object.

The compiler cannot always use a primitive type from JVM. A variable of type [Int?] must be able to hold a value of type [null], which a [int] cannot do, and an element of a [List<Int>] list must be an object (JVM generics only accept reference types; see below): in these cases, the integer is placed inside a [java.lang.Integer] object, which Java calls “autoboxing.” The Kotlin programmer does not need to worry about this, with one exception: they must not compare two numbers using the [===] operator, which would compare the references of the [Integer] objects. The compiler issues a warning ([identity equality for arguments of types 'Int?' and 'Int?' is prohibited]) for this. Numbers are compared using [==], which always compares the values.

To represent a simple composite data type (a point, a color, a date, an amount) that is treated as a value, immutable classes are used: their objects cannot be modified after creation. Sharing a reference to an immutable object is safe: no one can modify it, and the object then behaves like a value. Two tools round out the system: the method [equals], called by the operator [==], which compares two objects by their content, and the data classes ([data class]), which declare an immutable class equipped with this method in a single line. This is the subject of the following two paragraphs.

4.5.2. Equality of Two Objects: equals and hashCode

As we have seen, the [===] operator, when applied to two references, indicates whether they refer to the same object. The [] operator calls the [equals] method, and the one inherited from [Any]* does the same thing as *[=]. For two distinct objects with the same content to be considered equal, a class must redefine the method [equals] and, necessarily along with it, the method [hashCode]. The program [Classes15.kt] demonstrates this:

// equality of two objects: equals and hashCode
package classes15

import java.util.Objects

fun main() {
    val p1 = Person("paul", 10)
    val p2 = Person("paul", 10)
    val p3 = Person("nicole", 30)
    // === compares references, == compares values (equals method)
    println("p1 === p2 : ${p1 === p2}, p1 == p2 : ${p1 == p2}, p1 == p3 : ${p1 == p3}")
    // two equal objects have the same hash code
    println("p1.hashCode() == p2.hashCode() : ${p1.hashCode() == p2.hashCode()}")
    // a set (Set) does not contain two equal objects
    val persons = hashSetOf(p1, p2, p3)
    println("size of the set: ${persons.size}")
    println("the set contains (paul,10): ${Person("paul", 10) in persons}")
    // the == operator accepts the null value
    val unknown: Person? = persons.find { it.name == "pierre" }
    println("unknown == p1 : ${unknown == p1}, unknown == null : ${unknown == null}")
}

class Person(val name: String, val age: Int) {
    // two persons are equal if they have the same name and age
    override fun equals(other: Any?): Boolean {
        // same object?
        if (this === other) {
            return true
        }
        // is other a Person (and not null)?
        if (other !is Person) {
            return false
        }
        // comparison of the properties: other is now known to be a Person
        return age == other.age && name == other.name
    }

    // hash code consistent with equals
    override fun hashCode() = Objects.hash(name, age)

    override fun toString() = "Person($name,$age)"
}
  • lines 25–35: The method [equals] overrides that of the class [Any]. Its parameter is of type [Any?]: a person can be compared to any object, and even to [null]. The method follows a standard pattern:
    • lines 27–28: an object is equal to itself;
    • lines 31–32: the expression [other !is Person] is true if [other] does not reference an object of type [Person] (or a derived class), in particular if it is equal to [null]. [false] is then returned;
    • Line 35: the properties are compared. After the test on line 31, the compiler knows that [other] is a [Person]: [other.age] can be used without conversion. This is the “smart cast” we’ve already encountered with the expression [when]. The names are compared with [==], which calls the [equals] method for strings;
  • line 39: the method [hashCode] returns an integer calculated from the same properties, in this case by the method [Objects.hash] of JDK (hence the import on line 4). The rule is mandatory: two objects that are equal in the sense of [equals] must have the same hash code. Dictionaries and sets (next chapter) sort objects based on this code and would no longer function if the rule were not followed;
  • line 11: [===] compares references, while [==] compares contents. In Java, we would write [p1.equals(p2)]; in Kotlin, we write [p1 == p2], which is more readable;
  • Lines 15–17: A set [HashSet], created by the function [hashSetOf], never contains two equal objects: adding [p2], which is equal to [p1], has no effect, and the search for a new person ([paul, 10]) using the operator [in] is successful;
  • lines 19–20: The [find] function of the collections returns the first element that satisfies the condition given by the lambda, or [null] if there is none (lambdas are covered in Chapter 7). The variable [unknown] is therefore equal to [null]. The operator [==] accepts an operand [null]: [a == b] is translated to [a?.equals(b) ?: (b === null)]. There is no [NullPointerException] exception to worry about, unlike the Java call [a.equals(b)] when [a] is equal to [null].

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=classes15.Classes15Kt
p1 === p2 : false, p1 == p2 : true, p1 == p3 : false
p1.hashCode() == p2.hashCode() : true
size of the set: 2
the set contains (paul,10): true
unknown == p1 : false, unknown == null : true

Writing [equals] and [hashCode] by hand is tedious and prone to errors; IntelliJ and IDEA can generate them (via the “Code / Generate…” menu). Data classes, on the other hand, provide them automatically.

4.5.3. Immutable Objects

The program [Classes16.kt] offers three ways to define immutable data:

// immutable objects: an immutable class, a data class, a value class
package classes16

fun main() {
    val a = Point(1, 2)
    // a.x = 5   // rejected: x is a val property
    val b = a.move(3, 4)
    println("a=$a, b=$b")

    // a data class: toString, equals, hashCode, copy... are generated
    val red = Color(255, 0, 0)
    val red2 = Color(255, 0, 0)
    println("red=$red, r=${red.r}")
    println("red == red2 : ${red == red2}, red === red2 : ${red === red2}")
    // a modified copy: the copy method and named arguments
    val pink = red.copy(g = 128, b = 128)
    println("red=$red, pink=$pink")
    // destructuring an object of a data class
    val (r, g, blue) = pink
    println("r=$r, g=$g, b=$blue")

    // a value class: a wrapped and checked Int
    val age = Age(30)
    println("age=$age, age.value + 1 = ${age.value + 1}")
    try {
        val age2 = Age(-4)
        println("age2=$age2")
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
}

// an immutable class: val properties, no setter; it is final by default
class Point(val x: Int, val y: Int) {
    // we do not modify the point: we return a new one
    fun move(dx: Int, dy: Int) = Point(x + dx, y + dy)

    override fun toString() = "($x,$y)"
}

// a data class: an immutable class declared in one line
data class Color(val r: Int, val g: Int, val b: Int)

// a value class: at runtime, an Age object is most often a simple int
@JvmInline
value class Age(val value: Int) {
    init {
        require(value >= 0) { "invalid age ($value)" }
    }
}
  • lines 34–38: a manually written immutable class. Its properties are read-only ([val]); it is final—as all Kotlin classes are by default—so that no derived class can add mutable state to it. To “move” a point, the method [move] returns a new point (line 36). The statement [a.x = 5] (line 6) would be rejected by the compiler: ['val' cannot be reassigned];
  • line 42: a data class ([data class]). The header [data class Color(val r: Int, val g: Int, val b: Int)] declares a class with three properties, a three-parameter constructor, and the methods [toString], [equals], and [hashCode], all of which use the properties of the primary constructor, as well as the methods [copy] and [component1], [component2], and [component3], presented below. This is the Kotlin equivalent of the Java record, which appeared long before it; since the properties are declared with [val], the class is immutable;
  • line 13: a property is accessed by its name, [red.r];
  • line 14: two objects with the same content are equal in the sense of [==], even though they are distinct;
  • line 16: Since a data class is immutable, it cannot be modified; instead, a copy is created in which certain properties have been changed. This is the role of the generated method [copy], where each parameter’s default value is the current value of the property: with named arguments, only the properties that change are specified. In Java, you have to write “wither” methods ([withG(int g)]…) by hand to achieve the same result;
  • Line 19: destructuring, as discussed in the previous chapter using pairs: the three variables receive the values of the three properties, in the order specified by the constructor. This relies on the generated methods [component1], [component2], and [component3]. The variable names can be chosen freely: we wrote [blue] because [b] is already in use;
  • Lines 45–48: a value class ([value class]), annotated as [@JvmInline]. It has a single property—in this case, an integer—which it “wraps” to give it its own type and validation rules: a [Age] is not just any [Int]; a function that expects a *[Age] will not accept a weight or a number of children, and a negative age is rejected by the *[init] block (line 26). The advantage of the value class is that, at runtime, the compiler usually replaces it with the wrapped value: a variable of type [Age] is simply a [int] of the JVM, with no object created on the heap. This provides the safety of a distinct type without its cost. A value class is immutable; it has generated [toString], [equals], and [hashCode], just like a data class.

Results:

1
2
3
4
5
6
7
8
> gradlew -q run -Pmain=classes16.Classes16Kt
a=(1,2), b=(4,6)
red=Color(r=255, g=0, b=0), r=255
red == red2 : true, red === red2 : false
red=Color(r=255, g=0, b=0), pink=Color(r=255, g=128, b=128)
r=255, g=128, b=128
age=Age(value=30), age.value + 1 = 31
invalid age (-4)

The [toString] generated for a data class displays the class name and its properties (line 2 of the results); the same applies to the value class (line 6). Note here that a data class is the most concise way to declare a small, immutable piece of data: this is the type we will use for the tax brackets in the example application in this chapter. Data classes are discussed in detail later.

4.6. Interfaces

4.6.1. A First Example

An interface is a set of method declarations (and, in Kotlin, property declarations) that forms a contract. A class that chooses to implement an interface commits to providing an implementation for all the abstract members defined in the interface. The compiler verifies this implementation.

For example, here is the (simplified) definition of the [Iterator<T>] interface from the Kotlin standard library, which allows you to iterate over the elements of a collection: [interface Iterator<out T> { operator fun next(): T; operator fun hasNext(): Boolean }]. The methods of the interface are defined only by their signatures: they have no body and are implicitly public and abstract (the keyword [out] will be explained in the section on generics; the keyword [operator] indicates that the loop [for] can use these methods). It is the classes that implement the interface that provide the code for its methods. A class named [C] declares that it implements the interface by listing it after the colon, just like a parent class but without parentheses (an interface does not have a constructor): [class C : Iterator<String>], and it must then define the two methods [override fun hasNext(): Boolean { ... }] and [override fun next(): String { ... }]. Whereas Java distinguishes between [extends] (inheritance) and [implements] (implementation), Kotlin, like C#, uses the same colon; the keyword [override] is required to implement an interface member, just as it is to override a method of the parent class.

A common convention (though not universal—neither JDK nor the Kotlin library follows it) is to start an interface name with the letter I. The following examples make up the [statistics] package (directory [src/main/kotlin/statistics]). Consider the following interface [IStats.kt]:

package statistics

// the statistics of a series of values
interface IStats {
    // average of the series: an abstract property
    val average: Double

    // standard deviation of the series
    val standardDeviation: Double
}

The interface [IStats] has:

  • a property [average]: the mean of a series of values;
  • a property [standardDeviation]: the standard deviation of this series.

Note that it is not specified anywhere which series of values is being referred to. It could be the average grades of a class, the monthly average sales of a particular product, the average temperature in a given location… This is the principle behind interfaces: they assume the existence of members in the object but not that of specific data.

Where the Java interface declared two methods, [double average()] and [double standardDeviation()], the Kotlin interface declares two abstract properties, [val average: Double] and [val standardDeviation: Double]. An interface property says nothing about how the value is obtained: an implementation class can store it in a field, calculate it each time it is read via an accessor *[get]*, or calculate it on the first read and then store it. This is demonstrated by the following two classes. An interface property [val] promises only read access; a property [var] would additionally promise write access.

One possible implementation of the [IStats] interface could be a class used to store the grades of students in a class for a given subject. A student is characterized by the following [Student] data class: [Student.kt]:

1
2
3
4
package statistics

// a student
data class Student(val firstName: String, val lastName: String)

The student is identified by their first and last name. The data class, introduced above (in the section “Immutable Objects”) and discussed in detail later, can be summarized in a single line: it replaces the record in the Java version. A grade is represented by the following data class: [Grade] [Grade.kt]:

1
2
3
4
5
6
package statistics

// the grade of a student
data class Grade(val student: Student, val value: Double) {
    override fun toString() = "[${student.firstName},${student.lastName},$value]"
}

The grade is defined by the student being graded and the grade itself. The class redefines the [toString] method generated by the compiler to display a grade in the form [firstName,lastName,value]. Note the form [fun f() = expression]: a function whose body consists of a single expression can be written using the symbol [=], without curly braces or [return], in which case the result type is inferred from the expression.

The grades of all students in a given subject are compiled in the following class [ArrayOfGrades]:

package statistics

import kotlin.math.pow
import kotlin.math.sqrt

// grades stored in an array, fixed at construction
class ArrayOfGrades(val subject: String, val grades: Array<Grade>) : IStats {
    // implementation of the properties of the IStats interface:
    // the average and the standard deviation are computed once and for all, at construction
    override val average: Double =
        if (grades.isNotEmpty()) grades.sumOf { it.value } / grades.size else -1.0

    override val standardDeviation: Double =
        if (grades.isNotEmpty()) sqrt(grades.sumOf { (it.value - average).pow(2) } / grades.size) else -1.0

    override fun toString() = "subject=$subject, grades=${grades.contentToString()}"
}
  • Line 7: The class [ArrayOfGrades] implements the interface [IStats], listed after the colon. It must therefore implement the properties [average] and [standardDeviation]. Its primary constructor declares two read-only properties: the subject in which the object stores the grades and the array of grades;
  • lines 10–14: the implementation of the two interface properties, preceded by the keyword [override]. We assume here that the grades passed to the constructor will not change thereafter. Therefore, the mean and standard deviation of these grades are calculated immediately and stored in the property fields: the property initializers are executed when the object is constructed, in the order in which they are written, which allows the standard deviation calculation to use the mean that has already been calculated. In Java, it would have taken four fields ([final]) and a constructor to achieve the same result;
  • line 11: [if] is a Kotlin expression: it replaces the Java operator [condition ? value1 : value2] and sets the mean of an empty array to -1. The [sumOf] extension function for arrays and collections sums the values that the [{ it.value }] lambda calculates for each element (lambdas are covered in Chapter 7; [it] refers to the current element there): it replaces the cumulative loop [for] from the Java version;
  • line 14: [x.pow(2)] calculates x², and [sqrt] calculates the square root. These are functions from the [kotlin.math] package, imported on lines 3–4; they replace the static methods [Math.pow] and [Math.sqrt] from Java (which remain usable);
  • line 16: the method [toString] uses the array extension function [contentToString], which returns the array elements in square brackets, separated by commas, with each element converted to a string by its method [toString], here that of the [Grade] class. This is equivalent to the static [Arrays.toString] method in Java. The [toString] of a Kotlin array, like that of a Java array, displays only its type and a hash code.

In the previous class, grades were stored in an array. It was not possible to add a new grade after the [ArrayOfGrades] object was constructed. We now propose a second implementation of the [IStats] interface, called [ListOfGrades], where this time the grades are stored in a list, with the ability to add grades after the initial construction of the [ListOfGrades] and [ListOfGrades.kt] objects:

package statistics

import kotlin.math.pow
import kotlin.math.sqrt

// grades stored in a list, to which grades can be added
class ListOfGrades(val subject: String, grades: List<Grade>) : IStats {
    // the private mutable list: a copy of the list received
    private val _grades = grades.toMutableList()

    // the public read-only view of this list
    val grades: List<Grade>
        get() = _grades

    // cached average and standard deviation (null: not yet computed)
    private var averageCache: Double? = null
    private var standardDeviationCache: Double? = null

    // adding a grade
    fun add(grade: Grade) {
        _grades.add(grade)
        // the average and the standard deviation will have to be recomputed
        averageCache = null
        standardDeviationCache = null
    }

    // average of the grades: computed on the first request
    override val average: Double
        get() = averageCache ?: computeAverage().also { averageCache = it }

    // standard deviation of the grades: computed on the first request
    override val standardDeviation: Double
        get() = standardDeviationCache ?: computeStandardDeviation().also { standardDeviationCache = it }

    private fun computeAverage(): Double =
        if (_grades.isEmpty()) -1.0 else _grades.sumOf { it.value } / _grades.size

    private fun computeStandardDeviation(): Double {
        if (_grades.isEmpty()) return -1.0
        val m = average
        return sqrt(_grades.sumOf { (it.value - m).pow(2) } / _grades.size)
    }

    override fun toString() = "subject=$subject, grades=$_grades"
}
  • Line 7: The class [ListOfGrades] implements the interface [IStats]. The constructor parameter [grades] is not preceded by [val]: it is a simple parameter used only to initialize the property on line 9;
  • Line 9: The grades are now placed in a mutable list ([MutableList<Grade>]), a copy of the list received by the extension function [toMutableList]. Kotlin distinguishes between read-only collections ([List]) and mutable collections ([MutableList]); the copy ensures that the caller cannot modify the list stored by the object behind our backs (see Chapter 5);
  • lines 12–13: The list is exposed as type [List<Grade>], which is read-only: external code can read the grades but cannot add new ones without calling the [add] method. This pair of a private property [_grades] and a public property [grades] of a more restricted type is a common Kotlin idiom known as a “backing property”;
  • lines 20–24: We can now add a grade to those already stored. The mean and standard deviation are no longer calculated at construction time but on demand, within the accessors of the interface’s properties: [average] (lines 28–29) and [standardDeviation] (lines 32–33). Once calculated, these values are stored so they are not recalculated with every request: they are said to be cached. Adding a grade invalidates the cache;
  • lines 16–17: the cache. Its type, [Double?], is a nullable type (Chapter 3): the value [null] signifies “not yet calculated.” Whereas Java used the wrapper class *[Double] to access the value *[null], Kotlin simply adds a question mark to the type;
  • Line 29: The accessor reads as follows: the cached value if it is not [null] (Elvis operator [?:]), otherwise the result of ***[computeAverage()]***, which the function *[also] stores in the cache before returning it. [x.also { ... }] executes the block with [it = x] and then returns [x]: it is used to graft a secondary action (in this case, caching) onto an expression. The compiler knows that the result of the expression is never [null]: it is of type [Double]*, as required by the interface;
  • lines 35–41: the two calculations, written using [sumOf]. Note, on line 39, the “early exit” style: the function returns -1 immediately if the list is empty;
  • line 44: the [toString] for a list displays its elements in square brackets, without the need for a special function.

A test program might look like the following [Main.kt]:

package statistics

fun main() {
    // a few students and their English grades
    val students = arrayOf(Student("Paul", "Martin"), Student("Maxime", "Germain"), Student("Berthine", "Samin"))
    val grades = arrayOf(Grade(students[0], 14.0), Grade(students[1], 16.0), Grade(students[2], 18.0))
    // which are recorded in an ArrayOfGrades object
    val english = ArrayOfGrades("English", grades)
    // display of average and standard deviation
    displayStats(english)
    // the same grades in a ListOfGrades object
    val french = ListOfGrades("French", grades.toList())
    displayStats(french)
    // we add a grade
    french.add(Grade(Student("Jérôme", "Jaric"), 10.0))
    displayStats(french)
    // a variable can have an interface as its type
    var stats: IStats = english
    println("English average: ${stats.average}")
    stats = french
    println("French average: ${stats.average}")
}

// display of the average and standard deviation of an object implementing IStats
fun displayStats(values: IStats) {
    println("$values, Average=${values.average}, Standard deviation=${values.standardDeviation}")
}
  • line 5: creates an array of students using the [arrayOf] function;
  • line 6: creation of an array of grades. The grades are written as [14.0], not [14]: Kotlin does not implicitly convert a [Int] to a [Double], and the constructor for [Grade] expects a [Double];
  • line 8: a [ArrayOfGrades] object whose mean and standard deviation are displayed on line 10;
  • line 12: an object [ListOfGrades], whose mean and standard deviation are displayed on line 13. The extension function [toList] converts the array into a list; the class [ListOfGrades] creates a modifiable copy of it. In Java, you would have to write [new ArrayList<>(List.of(grades))];
  • line 15: add a new grade, then refresh the display;
  • Lines 25–26: The function [displayStats] takes a type [IStats] as a parameter, which is an interface type. This means that the actual parameter can be any object that implements the interface [IStats]. When using a data object of an interface type, only the interface members implemented by that object are used. The rest are ignored. This property is similar to the polymorphism we saw with classes. If a set of classes Ci that are not related to each other through inheritance (and therefore cannot use inheritance-based polymorphism) has a set of methods with the same signature, it may be useful to group these methods into an interface I that all the relevant classes would implement. Instances of these classes Ci can then be used as actual parameters of functions that accept a formal parameter of type I—that is, functions that use only the members of the Ci objects defined in the interface I, and not the specific properties and methods of the various Ci classes;
  • lines 18–21: A variable can be of the type of an interface. It can then refer to any object of a class that implements the interface. The compiler allows access only to the members of the interface (and to those of the class *[Any]*, such as [toString]*). The type must be specified here; without it, the compiler would have inferred the type [ArrayOfGrades]* from the initial value, and the assignment on line 20 would have been rejected.

The package contains two high-level functions, [main] and [displayStats], defined in the file [Main.kt]: the class JVM that contains them is therefore named [statistics.MainKt].

Results:

1
2
3
4
5
6
> gradlew -q run -Pmain=statistics.MainKt
subject=English, grades=[[Paul,Martin,14.0], [Maxime,Germain,16.0], [Berthine,Samin,18.0]], Average=16.0, Standard deviation=1.632993161855452
subject=French, grades=[[Paul,Martin,14.0], [Maxime,Germain,16.0], [Berthine,Samin,18.0]], Average=16.0, Standard deviation=1.632993161855452
subject=French, grades=[[Paul,Martin,14.0], [Maxime,Germain,16.0], [Berthine,Samin,18.0], [Jérôme,Jaric,10.0]], Average=14.5, Standard deviation=2.958039891549808
English average: 16.0
French average: 14.5

In a string template, a number [Double] is converted to a string by the [Double.toString] method of JVM, which always writes the number with a decimal point and at least one decimal place (16.0), regardless of the workstation’s language. For a French-style display, use ["%.2f".format(x)], as in the following example.

Finally, note that interfaces can be implemented multiple times; that is, you can write [class DerivedClass : BaseClass(), I1, I2, ..., In], where the Ij are interfaces: a class inherits from only one class (whose constructor is called, hence the parentheses), but can implement as many interfaces as it wishes. An interface can itself inherit from one or more other interfaces: [interface I3 : I1, I2].

4.6.2. Interface methods with a body

A Kotlin interface can provide a default implementation for some of its methods: simply give them a body, without any specific keyword (Java requires the keyword [default]). A class that implements the interface can then use this implementation or provide its own. This makes it possible, for example, to add a method to an already published interface without requiring all classes that implement it to be modified. An interface property can similarly have a default accessor [get]. An interface can also contain private functions. However, Kotlin does not have static members ([static]), neither in interfaces nor in classes: their role is fulfilled by the companion object ([companion object]), discussed above, which an interface can have just as a class can. The program [Classes17.kt] provides an example:

package classes17

import kotlin.math.PI

// interfaces: methods with a default body, "static" members (companion object),
// private functions, implementation of several interfaces
fun main() {
    val shapes = arrayOf(Square(2.0), Circle(1.0), IShape.unitSquare())
    for (shape in shapes) {
        println(shape.description())
    }
    // the default method is inherited by the Square class
    val square = Square(3.0)
    println(square.description())
    // a class that implements two interfaces
    val redSquare = ColoredSquare(1.5, "red")
    println(redSquare.description())
    println("name=${redSquare.name}, color=${redSquare.color}")
}

interface IShape {
    // abstract method: to be implemented
    fun area(): Double

    // method with a default implementation
    fun description(): String = "${this::class.simpleName} of area ${formatNumber(area())}"

    // property with a default getter (an interface has no backing field)
    val name: String
        get() = "shape"

    // private function: usable only by the methods of the interface
    private fun formatNumber(value: Double) = "%.2f".format(value)

    // the companion object of the interface: its members are called with the interface name
    companion object {
        fun unitSquare(): IShape = Square(1.0)
    }
}

// Square uses the default implementation of description
open class Square(private val side: Double) : IShape {
    override fun area() = side * side
}

// Circle provides its own implementation of description
class Circle(private val radius: Double) : IShape {
    override fun area() = PI * radius * radius

    override fun description() = "Circle of radius %s and area %.2f".format(radius, area())
}

// a second interface, which also has a default description method
interface IColored {
    // abstract property: to be implemented
    val color: String

    fun description(): String = "$color object"
}

// a class derived from Square that also implements IColored:
// it inherits two description methods and must therefore override it
class ColoredSquare(side: Double, override val color: String) : Square(side), IColored {
    override fun description() = "${super<Square>.description()}, ${super<IColored>.description()}"

    override val name = "colored square"
}
  • lines 21–37: the interface [IShape]:
    • line 23: an abstract method [area], to be implemented;
    • line 26: a method [description] with a default implementation. This implementation may use the other methods of the interface ([area], [formatNumber]). The expression [this::class.simpleName] returns the simple name of the object’s class ([Square]): [this::class] is the Kotlin class ([KClass]) of the object, equivalent to [getClass()] in Java;
    • lines 29–30: a property with a default accessor. An interface has no state: it cannot store a value, and therefore an interface property never has a field. It is either abstract or computed by an accessor;
    • line 33: a private function, accessible only by the interface’s methods. ["%.2f".format(value)] formats the number with two decimal places, following the conventions of the system’s locale—in this case, the French decimal comma. The string extension function [format] is the Kotlin equivalent of [String.format];
    • lines 36–37: the companion object for the interface. Its method [unitSquare] is called using the interface’s name: [IShape.unitSquare()] (line 8), just like the static method in the Java version. It is a factory;
  • Lines 42–43: The class [Square] only implements [area]; it uses the [description] method by default. It is declared as [open] because a class inherits from it later on. In Java, the class [Square] would have had to be declared as [static] because the interface creates squares; in Kotlin, classes declared in a file are all top-level, so this issue does not arise;
  • Lines 47–50: The class [Circle] provides its own method [description]. The format [%s] inserts the radius converted to a string by [Double.toString] (“1.0”), while the format [%.2f] inserts the area with two decimal places. The constant [PI] comes from the [kotlin.math] package;
  • line 14: the default method is inherited by the class [Square] just like any other method: it can be called on a variable of type [Square];
  • lines 54–58: a second interface, with an abstract property [color] and a default method [description], with the same signature as that of [IShape];
  • lines 63–66: the class [ColoredSquare] derives from [Square] (which implements [IShape]) and implements [IColored]. It therefore inherits two [description] methods by default. The compiler then requires the class to redefine the method; otherwise, it rejects the class (see below). The new method can call either of the inherited versions using the syntax [super<Type>.method()], where [Type] is the parent class or interface whose version is desired: on line 64, the method combines both. Java writes [I1.super.method()];
  • line 63: the abstract property [color] of the interface [IColored] is implemented directly in the primary constructor by [override val color: String];
  • line 66: the property [name] of the interface [IShape], which had a default accessor, is redefined as a property with a field: the class itself can store a value.

Results:

1
2
3
4
5
6
7
> gradlew -q run -Pmain=classes17.Classes17Kt
Square of area 4,00
Circle of radius 1.0 and area 3,14
Square of area 1,00
Square of area 9,00
ColoredSquare of area 2,25, red object
name=colored square, color=red

Here are the errors the compiler generates when a class [C] implements two interfaces, [I1] and [I2], each of which has a method [fun f() = ...] without redefining it, and when an interface attempts to initialize a property ([val counter: Int = 0]):

Error17.kt:9:1: error: class 'C' must override 'f' because it inherits multiple interface methods for it.
Error17.kt:12:24: error: property initializers in interfaces are prohibited.

Unlike an abstract class, an interface therefore has no state: it cannot declare stored properties. Constants, which in Java are fields [static final] of the interface, are placed in Kotlin within its companion object ([const val]).

Finally, for readers coming from Java, note that a Kotlin interface method with a body is compiled (since Kotlin 2.2) into a method [default] of the generated Java interface: a Java class can therefore implement a Kotlin interface without rewriting these methods.

4.7. Abstract Classes

An abstract class is a class that cannot be instantiated. You must create derived classes, which can then be instantiated. Abstract classes can be used to factor out code across a lineage of classes. Let’s examine the following example, which forms the package [users] [User.kt]:

package users

// an abstract class: it cannot be instantiated
abstract class User protected constructor(
    private val login: String,
    private val password: String,
) {
    // the role of the user, obtained by the abstract identify method
    // unidentified user: exception
    private val role: String = identify() ?: throw UnknownUserException("[$login,$password]")

    override fun toString() = "User[$login,$password,$role]"

    // identification: returns the role of the user or null if not recognized
    protected abstract fun identify(): String?
}
  • Line 4: The class is declared abstract using the keyword [abstract]. An abstract class is implicitly open to inheritance: there is no need to add [open];
  • Lines 4–6: The primary constructor of the [User] class. This class stores information about a web application’s users. The application has various types of users who are authenticated via a username and password (lines 5–6). These two pieces of information are verified against a directory (LDAP) for some users, against a database for others, and so on. To make the primary constructor visible, you must explicitly include the keyword [constructor] after the class name: [protected constructor(...)]. The constructor is [protected]: it is intended for the constructors of derived classes. The comma following the last parameter (line 6) is allowed: it makes it easier to add a parameter later;
  • line 10: the information is verified by a method named [identify]. Because the identification method is not known at this level, it is declared abstract (line 15) using the keyword [abstract]. An abstract method has no body: derived classes will provide one. A class that has an abstract method must itself be declared abstract. The method [identify] returns a string specifying the user’s role (basically, what they are authorized to do), or [null] if the user is not recognized: its type is therefore [String?];
  • Line 10: If the user is not recognized, an exception is thrown. The Elvis operator [?:] evaluates the expression to the role returned by [identify] if it is not [null], and otherwise executes the expression on the right, in this case, [throw UnknownUserException(...)]. In Kotlin, [throw] is an expression (of type [Nothing]), which allows for this compact syntax. The property [role] is of type [String] and is non-nullable: the compiler knows that it cannot be [null]. No [throws] clause is necessary: Kotlin does not have checked exceptions (Chapter 3);
  • line 12: the method *[toString],* which identifies an instance of the class.

We assume here that the developer wants to control the construction of instances of the class [User] and its derived classes, perhaps because they want to ensure that an exception of a certain type is thrown if the user is not recognized. Derived classes will be able to rely on this constructor. To do so, they must provide the method [identify]. Note that the initialization of the property [role], which is part of the object’s construction, calls a method redefined by the derived classes even though those classes have not yet performed their own initialization: the method *[identify] must therefore not use properties of the derived class, which have not yet been initialized (a non-nullable property could even be set to *[null] there!). This is a practice that should be handled with caution. The Kotlin compiler does not flag this; it is IntelliJ IDEA that highlights it through a code inspection (“Leaking 'this' in constructor of non-final class”). In this case, it is intentional and safe, because the methods of the derived classes do not use any properties.

The [UnknownUserException] class is as follows:

1
2
3
4
package users

// an application-specific exception
class UnknownUserException(message: String) : Exception(message)
  • Line 4: It derives from the [Exception] class (an alias for [java.lang.Exception]) and has a single constructor that takes an error message as a parameter, which is immediately passed to the parent class’s constructor. One line is all it takes. In Java, a class derived from [Exception] is a checked exception: the compiler verifies that any method capable of throwing it declares it using a [throws] clause. Kotlin does not perform this check: all exceptions in Kotlin behave like Java’s unchecked exceptions. If a Kotlin method is to be called from Java code that wants to catch the exception, it is marked for Java using the [@Throws(UnknownUserException::class)] annotation. The Kotlin compiler also does not require a version number [serialVersionUID].

We now derive the [Administrator] class from the parent class [User]:

package users

class Administrator(login: String, password: String) : User(login, password) {
    // identification
    override fun identify(): String {
        // LDAP identification
        // ...
        return "admin"
    }
}
  • Line 3: The primary constructor simply passes the parameters it receives to its parent class: [: User(login, password)]. The parameters [login] and [password] are not preceded by [val]: these are simple parameters, with the corresponding properties declared in the parent class;
  • Lines 5–8: The [identify] method of the [Administrator] class. Suppose an administrator is identified by a directory named LDAP. This method implements the abstract method [identify] from its parent class. It retains the [protected] access level of the method it overrides: in Kotlin, an overridden method inherits the visibility of the overridden method by default (it can extend it, but not restrict it). Its return type is *[String]*, not *[String?]: an overridden method can return a more specific type than the method it overrides, and a *[String] is a special case of *[String?]*.

The classes [Observer] and [Unknown] also derive from the class [User]. They differ from the [Administrator] class only in their [identify] method. The method in the [Observer] class assumes that an observer is identified by a database and returns the “observer” role:

1
2
3
4
5
6
    // identification
    override fun identify(): String {
        // identification through a database
        // ...
        return "observer"
    }

The method for class [Unknown] returns the reference [null] to indicate that the user was not recognized; its result type is therefore [String?]:

1
2
3
4
5
6
    // identification
    override fun identify(): String? {
        // user not known
        // ...
        return null
    }

Ultimately, the objects [Administrator], [Observer], and [Unknown] are initialized by the same code, that of the parent class [User]. This class uses the [identify] method provided by these classes. A test program might look like the following [Main.kt]:

package users

fun main() {
    try {
        println(Observer("observer", "pwd1"))
        println(Administrator("admin", "pwd2"))
        println(Unknown("xx", "yy"))
    } catch (e: UnknownUserException) {
        println("Unknown user: ${e.message}")
    }
    // val u = User("x", "y")   // rejected: the User class is abstract
}

Note that on lines 5 and 6, the [User.toString()] method is used by the [println] function. On line 7, the constructor throws a [UnknownUserException] exception, which is caught on line 8: the object is not created, and nothing is displayed by [println]. The [try] block is not required by the compiler, as it was in Java: without it, the exception would terminate the program. Line 11, if uncommented, is rejected by the compiler, which gives two reasons:

Main.kt:11:13: error: cannot access 'constructor(login: String, password: String): User': it is protected in 'users.User'.
Main.kt:11:13: error: cannot create an instance of an abstract class.

You cannot call a protected constructor outside of derived classes, and, more importantly, you cannot create an object of an abstract class.

Results:

1
2
3
4
> gradlew -q run -Pmain=users.MainKt
User[observer,pwd1,observer]
User[admin,pwd2,admin]
Unknown user: [xx,yy]

An abstract class is similar to an interface: both declare members that other classes must implement. However, an abstract class can have state (stored properties), constructors, and code common to its derived classes; in contrast, a class can derive from only one abstract class, whereas it can implement multiple interfaces. The two are often combined: an interface defines the contract, while an abstract class that implements it factors out the code common to multiple implementations. This is what the example application in this chapter will do. Finally, recall that the sealed class ([sealed class]), seen earlier, is an abstract class whose derived classes are all known to the compiler: we will use it later with the expression [when].

4.8. Generic Classes, Interfaces, and Functions

4.8.1. Generic Classes and Functions

Suppose we want to write a class [IntPair] that stores two integers and can swap them: two properties, [var first: Int] and [var second: Int], and a method, [swap()], that swaps their values using a temporary variable, [val temp]. If we then wanted a pair of strings, followed by a pair of people, we would write two more classes, [StringPair] and [PersonPair], identical to the previous one except for the type: [String] or [Person] instead of [Int].

Generic classes, interfaces, and functions address the need for code that differs only in the type of certain data. Kotlin has had them since its first release; they are based on the generics introduced in JVM with Java 5 (2004), with which they share the strengths and certain limitations (type erasure, discussed later), along with some notable improvements (declarative variance, reified types). It should be noted that Kotlin, like Java, does not support passing parameters by reference: a function cannot swap the values of two variables from the caller, but it can swap two elements of an object, a pair, or an array.

The program [Classes18.kt] contains a generic class, a generic function, and a second generic class:

package classes18

// generic classes and functions
fun main() {
    // --- a generic class Pair<T>
    // Int
    val pi = Pair<Int>(1, 2)
    pi.swap()
    println("pi=$pi")
    // String: the type T is inferred from the actual parameters
    val ps = Pair("s1", "s2")
    ps.swap()
    println("ps=$ps")
    // Person
    val p1 = Person("jean", "clu", 20)
    val p2 = Person("pauline", "dard", 55)
    val pp = Pair(p1, p2)
    pp.swap()
    println("pp=$pp")
    // the first element is of type Person: no conversion
    val firstOne: Person = pp.first
    println("first element: $firstOne")

    // --- a generic function swap<T>
    val integers = arrayOf(1, 2, 3)
    swap<Int>(integers, 0, 2)
    println("integers=${integers.contentToString()}")
    // the compiler infers T from the type of the actual parameters
    val persons = arrayOf(p1, p2)
    swap(persons, 0, 1)
    println("persons=${persons.contentToString()}")

    // --- a generic class with a state: a stack of elements of type T
    val stack = MyStack<Person>()
    stack.push(p1)
    stack.push(p2)
    println("The stack contains ${stack.size} persons")
    val top = stack.pop()
    println("Person popped: $top, ${stack.size} left")
    // stack.push("x")   // rejected: the stack only accepts Person objects
}

// a generic class: a pair of two elements of type T
class Pair<T>(first: T, second: T) {
    // two properties readable from outside, modifiable only by the class
    var first = first
        private set
    var second = second
        private set

    // we swap the two elements
    fun swap() {
        val temp = first
        first = second
        second = temp
    }

    override fun toString() = "($first,$second)"
}

// a top-level generic function:
// it swaps elements no. i and j of an array of elements of type T
fun <T> swap(array: Array<T>, i: Int, j: Int) {
    val temp = array[i]
    array[i] = array[j]
    array[j] = temp
}

// a stack of elements of type T
class MyStack<T> {
    // the elements of the stack
    private val elements = mutableListOf<T>()

    // number of elements
    val size: Int
        get() = elements.size

    // adding an element at the top of the stack
    fun push(element: T) {
        elements.add(element)
    }

    // removing the element at the top of the stack
    fun pop(): T {
        check(elements.isNotEmpty()) { "empty stack" }
        return elements.removeLast()
    }
}

// a person
data class Person(val firstName: String, val lastName: String, val age: Int) {
    override fun toString() = "[$firstName, $lastName, $age]"
}
  • lines 44–58: The class [Pair] is parameterized by a type denoted as [T], enclosed in angle brackets after the class name. You can name it whatever you want; by convention, it is an uppercase letter ([T] for type, [E] for element, [K] and [V] for key and value). This type, [T], is then used in the class just like any other type: for the constructor parameters, the two properties, and the temporary variable [temp] (line 53), from which the compiler infers the type [T]. The class [Pair] is said to be a generic class;
  • lines 46–49: two properties initialized by the constructor parameters, which have the same names. They are declared as [var] because the method [swap] modifies them, but their accessor [set] is private ([private set]): external code can read [pp.first], but cannot assign a value to it. This is equivalent to the private fields and their accessors [getFirst] and [getSecond] in the Java version;
  • Lines 52–55: Swapping the two elements. Fans of Kotlin idioms sometimes write this on a single line: [first = second.also { second = first }] ([also] returns the value of [second] after executing the assignment in the block). The version using a temporary variable is more readable;
  • Line 7: When using a generic class parameterized by types T1, T2, …, these types can be specified within angle brackets. Here, [Pair<Int>] is a pair of integers. Unlike Java, which requires [Pair<Integer>] and rejects [Pair<int>], Kotlin has only one type, [Int]: the compiler uses the wrapper class *[java.lang.Integer]* for JVM wherever an object is required, as shown here;
  • lines 11 and 17: most of the time, the type isn’t specified; the compiler infers it from the actual constructor parameters, here [String] and then [Person]. This is the equivalent of Java’s “diamond” operator [new Pair<>(...)], but more concise;
  • line 21: the property [first] of a [Pair<Person>] is of type [Person], without conversion;
  • lines 63–66: a high-level generic function. The parameter of type [<T>] is declared between the keyword [fun] and the function name. In Java, a class named [Generic2] was required to contain the static method; in Kotlin, a function can exist outside of any class. It swaps elements i and j of an array of elements of type [T]. The type [Array<T>] is a Kotlin array of objects (the equivalent of [T[]] in Java);
  • line 26: the function [swap] is called, specifying the type [T] between [<>]. In fact, the compiler is able to infer [T] from the type of the actual parameters. Therefore, the syntax in line 30 is valid, and this is the one used in practice. Note that [arrayOf(1, 2, 3)] creates a [Array<Int>] (an array of [Integer] objects), to which the function is applied; a [IntArray] array (created by [intArrayOf]), which is an array of primitive integers [int[]], would not be accepted: as in Java, a type parameter cannot refer to a primitive type of JVM;
  • Lines 70–86: A generic class with a state: a stack of elements of type [T]. Elements are pushed onto the top of the stack and popped from the top. The elements are stored in a mutable generic list of type [MutableList<T>] (line 72), created by the generic function [mutableListOf<T>()], which is itself parameterized by the stack’s type, [T]. The number of elements is a calculated property [size]. The function [removeLast] retrieves and returns the last element of the list. An empty stack causes a [IllegalStateException] exception, the exception thrown when an operation is requested from an object that is not in the expected state: this is precisely the exception thrown by the Kotlin function [check(condition) { message }] when the condition is false (line 85);
  • line 34: a stack of people. Here, the type must be specified, since there is no way for the compiler to infer it. The compiler guarantees that it will contain only people: the statement on line 40, when uncommented, is rejected ([argument type mismatch: actual type is 'String', but 'Person' was expected]). The method [pop] returns an object of type [Person], without conversion;
  • lines 91–92: the person is a data class for which the method [toString] has been redefined.

Results:

1
2
3
4
5
6
7
8
9
> gradlew -q run -Pmain=classes18.Classes18Kt
pi=(2,1)
ps=(s2,s1)
pp=([pauline, dard, 55],[jean, clu, 20])
first element: [pauline, dard, 55]
integers=[3, 2, 1]
persons=[[pauline, dard, 55], [jean, clu, 20]]
The stack contains 2 persons
Person popped: [pauline, dard, 55], 1 left

4.8.2. Generic parameter bounds

In the class [Pair<T>], the compiler knows nothing about the type [T]: it allows only the methods of class [Any] ([toString], [equals], [hashCode]), and even then, only through the [?.] operator: by default, [T] can indeed be a nullable type! The implicit bound of a generic parameter is [Any?]. To use other methods, you must constrain the generic parameter by specifying, after the colon, the type from which it must be derived:

Constraint
Meaning
[<T : SomeClass>]
T must be the specified class or a derived class
[<T : Interface>]
T must implement the specified interface
[<T> ... where T : I1, T : I2]
Multiple constraints: they are listed in a [where] clause, placed before the body of the function or class
[<T : Comparable<T>>]
a bound that uses T itself: T must be comparable to T
[<T : Any>]
T must be a non-nullable type

The syntax is the same as for inheritance: Java writes [<T extends SomeClass>] and [<T extends SomeClass & I1 & I2>], while C# writes [where T : SomeClass]. As in Java, there is no constraint requiring it to have a parameterless constructor: you cannot write [T()] in a generic class (see type erasure below). The program [Classes19.kt] uses a constraint:

package classes19

import kotlin.random.Random

// the bounds of generic parameters
fun main() {
    // --- T can be any type, including Int and a nullable type
    val pi = Pair(1, 2)
    println("pi=$pi")
    val pn = Pair<String?>("a", null)
    println("pn=$pn")

    // --- the bound T : IValue
    // ordering pairs of objects of type RandomValue
    repeat(5) {
        val pair = Pair(RandomValue(), RandomValue())
        print("Before --> $pair")
        order(pair)
        println("   After --> $pair")
    }
    // ordering a pair of objects of type StringValue
    val strings = Pair(StringValue("xxxxxxxxxxxxxx"), StringValue("xx"))
    println("Before --> $strings")
    order(strings)
    println("After --> $strings")
    // order(pi)   // error: Int does not implement IValue
}

// a pair of two elements of type T
class Pair<T>(first: T, second: T) {
    var first = first
        private set
    var second = second
        private set

    fun swap() {
        val temp = first
        first = second
        second = temp
    }

    override fun toString() = "($first,$second)"
}

// the type T must implement IValue
fun <T : IValue> order(pair: Pair<T>) {
    // if the 1st element is greater than the 2nd, the elements are swapped
    if (pair.first.value > pair.second.value) {
        pair.swap()
    }
}

// an object having an integer value
interface IValue {
    val value: Int
}

// an object having a random value
class RandomValue : IValue {
    // value of the object
    override val value = generator.nextInt(100)

    override fun toString() = value.toString()

    companion object {
        // random number generator shared by all instances
        // (initialized with a fixed seed so that the results are reproducible)
        private val generator = Random(2026)
    }
}

// an object whose value is the length of a string
class StringValue(private val s: String) : IValue {
    override val value: Int
        get() = s.length

    override fun toString() = s
}
  • line 8: a pair of integers. Unlike the Java version, there is no error to report here: [Pair<Int>] is allowed, and the compiler uses the class [Integer] in the generated code;
  • line 10: since the implicit bound is [Any?], the type [T] can be a nullable type: the pair [pn] contains a string and the value [null]. To prevent this, we would have declared [class Pair<T : Any>];
  • Lines 46–49: The generic function [order] requires that the type [T] implement the interface [IValue] (lines 54–55). This interface has a property [value], used on line 48, which returns the value of the object of type [T]. Without the bound, the compiler would reject the expression [pair.first.value]: any [T] type does not have a [value] property. The two elements of the pair are swapped only if the value of the first is greater than that of the second: the function [order] sorts the two elements in ascending order by value;
  • lines 59–68: The class [RandomValue] implements the interface [IValue]. The value of an instance is a random number between 0 and 99 (line 61), provided by a random number generator in the class [kotlin.random.Random] shared by all instances: it is stored in the class’s companion object (line 68), which serves the same purpose as the static field in the Java version. This generator is initialized with a fixed “seed”: it therefore always produces the same sequence of numbers, making the results reproducible. This is not the same algorithm as that of the [java.util.Random] class: the resulting sequence is therefore not the same as that of the Java version. If reproducibility is not required, the default generator [Random.Default] (for example, [Random.nextInt(100)]) is used; or, for security purposes, the class [java.security.SecureRandom];
  • lines 73–77: The class [StringValue] also implements the interface [IValue]: the value of an instance is the length of the character string passed to the constructor, calculated by the property accessor;
  • lines 15–19: Pairs of objects of type [RandomValue] are sorted. The function [repeat(5) { ... }] executes the block five times;
  • lines 22–25: A pair of objects of type [StringValue] is sorted;
  • line 26: if this line is uncommented, the compiler reports that the type [Int] is invalid.

Results:

> gradlew -q run -Pmain=classes19.Classes19Kt
pi=(1,2)
pn=(a,null)
Before --> (67,8)   After --> (8,67)
Before --> (73,41)   After --> (41,73)
Before --> (28,53)   After --> (28,53)
Before --> (26,93)   After --> (26,93)
Before --> (31,39)   After --> (31,39)
Before --> (xxxxxxxxxxxxxx,xx)
After --> (xx,xxxxxxxxxxxxxx)

Here are the errors encountered during compilation once line 26 is uncommented:

Classes19.kt:26:5: error: cannot infer type for type parameter 'T'. Specify it explicitly.
Classes19.kt:26:11: error: argument type mismatch: actual type is 'Pair<Int>', but 'Pair<uninferred T (of fun <T : IValue> order)>' was expected.

The compiler attempted to infer the type [T]: the pair constraint requires [T] = [Int], and the bound constraint requires that [T] implements [IValue]; since the two constraints are incompatible, it cannot find a type [T] (“uninferred T”). If you specify the type yourself—[order<Int>(pi)]—the message is more straightforward: [type argument is not within its bounds: type parameter 'T (of fun <T : IValue> order)' must be subtype of 'IValue', but actual: 'Int'].

4.8.3. Generic Interfaces: Comparable<T> and Comparator<T>

To illustrate the concept of a generic interface, we’ll sort a list of people according to different criteria. For this purpose, Kotlin provides functions for mutable lists (and arrays) that sort the collection in place, and functions for all collections that return a new, sorted list without modifying the original. We’ll use several of them:

  • [list.sort()] sorts the list according to the natural order of its elements: These elements must implement the [Comparable<T>] interface, whose sole method, [operator fun compareTo(other: T): Int], compares the current object to the [other] object. This is the Java interface [java.lang.Comparable], as seen from Kotlin;
  • [list.sortWith(comparator)] sorts the list according to the order defined by a comparator object, which implements the [Comparator<T>] interface (an alias for [java.util.Comparator]), whose method [fun compare(a: T, b: T): Int] compares two objects;
  • [list.sortedWith(comparator)], [list.sortedBy { ... }], and [list.sortedByDescending { ... }] return a new sorted list.

The methods [compareTo] and [compare] follow the same rule: they return a positive number if x > y, 0 if x == y, and a negative number if x < y. It is up to the developer to define the meaning of the operators <, ==, and >. For example, if [p1] and [p2] are two [Person] objects, we can say that p1 > p2 if the name of [p1] comes after the name of [p2] in alphabetical order. This will result in an ascending sort based on the people’s names. If we want to sort by age, we’ll say that p1 > p2 if the age of [p1] is greater than the age of [p2]. To sort in descending order, simply reverse the signs of the results.

We now know enough to sort a list of people. The program [Classes20.kt] is as follows:

package classes20

import java.text.Collator
import java.util.Locale

// the generic interfaces Comparable<T> and Comparator<T>: sorting a list of persons
fun main() {
    val persons = mutableListOf(
        Person("claude", "pollon", 25),
        Person("valentine", "germain", 35),
        Person("paul", "germain", 32),
    )
    // display
    display("List to sort", persons)
    // sort according to the natural order of persons (Comparable): the age
    persons.sort()
    display("List after sorting by age", persons)
    // sort by last name and first name (Comparator)
    persons.sortWith(CompareNames())
    display("List after sorting by last and first name", persons)
    // sort by decreasing age, with an anonymous object
    persons.sortWith(object : Comparator<Person> {
        override fun compare(p1: Person, p2: Person) = p2.age.compareTo(p1.age)
    })
    display("List after sorting by decreasing age", persons)
    // the > operator uses the compareTo method of Comparable
    println("${persons[0]} > ${persons[1]} : ${persons[0] > persons[1]}")
    // comparators built by compareBy and thenBy; sortedWith returns a new list
    val collator = Collator.getInstance(Locale.FRENCH)
    val byNames = compareBy<Person, String>(collator) { it.lastName }.thenBy(collator) { it.firstName }
    display("New list sorted by last and first name", persons.sortedWith(byNames))
    // a sort on a single property
    display("New list sorted by first name", persons.sortedBy { it.firstName })
    // the original list has not changed
    display("Original list", persons)
}

// display of a list of persons
fun display(text: String, persons: List<Person>) {
    println(text + "-".repeat(50 - text.length))
    for (p in persons) {
        println(p)
    }
}

// a person, comparable to another person
class Person(val firstName: String, val lastName: String, val age: Int) : Comparable<Person> {
    // the natural order of persons: the order of ages
    override fun compareTo(other: Person) = age.compareTo(other.age)

    override fun toString() = "[$firstName, $lastName, $age]"
}

// comparison of the last and first names of two persons
class CompareNames : Comparator<Person> {
    // string comparison according to the rules of the French language
    private val collator = Collator.getInstance(Locale.FRENCH)

    override fun compare(p1: Person, p2: Person): Int {
        // we compare the last names
        val i = collator.compare(p1.lastName, p2.lastName)
        if (i != 0) {
            return i
        }
        // equal last names: we compare the first names
        return collator.compare(p1.firstName, p2.firstName)
    }
}
  • lines 8–11: the editable list of people. The Java version sorted an array; the same functions [sort] and [sortWith] exist for Kotlin arrays;
  • Lines 47–51: The class [Person] implements the generic interface [Comparable<Person>]: its objects are comparable to other people. Its method [compareTo] (line 49) defines the natural order of people, in this case, the order of ages. The [a.compareTo(b)] method for integers returns a negative, zero, or positive number depending on whether [a] is less than, equal to, or greater than [b]: this is exactly what the [compareTo] method expects (Java writes [Integer.compare(a, b)]). The parameter is called [other] in the interface; we keep this name when redefining it, otherwise the compiler—which allows named arguments—would issue a warning;
  • line 16: sorting in natural order;
  • line 19: sorting the list of people by last name and first name. The parameter of the function [sortWith] is an instance of the class [CompareNames], which implements the generic interface [Comparator<Person>];
  • lines 55–66: the class [CompareNames]. Its method [compare] implements the method [compare(a: T, b: T)] of the interface [Comparator<T>], where [T] = [Person]. Strings are not compared using the [compareTo] method, which follows the order of character codes (uppercase letters before lowercase, accented letters after "z"), but rather by a [java.text.Collator] object (line 57) that applies the sorting rules for the French language. Kotlin directly uses a class from JDK here;
  • lines 22–24: sorting by descending age. This time, the comparator is an anonymous object: the expression [object : Comparator<Person> { ... }] declares an unnamed class that implements the [Comparator<Person>] interface and immediately creates an instance of that class. This is equivalent to Java’s anonymous class ([new Comparator<Person>() { ... }]). The body of the object provides the method [compare]. An anonymous object avoids having to declare a class that is used only once;
  • Line 27: The method [compareTo] of the interface [Comparable] is declared as [operator]: the operators [<], [>], [<=], and [>=] can therefore be applied to people ([p1 > p2] is translated to [p1.compareTo(p2) > 0]), as we saw earlier with operator overloading;
  • Lines 29–30: The Kotlin library creates comparators: [compareBy(comparator) { it.lastName }] returns a comparator for people that compares their names (the value calculated by the lambda) using the specified comparator, in this case [Collator]; [thenBy(comparator) { it.firstName }] adds a second criterion, used in case of a tie on the first criterion. Both types of [compareBy] (the element and the key) are specified between angle brackets, as the compiler cannot infer them from the lambda alone. Without a specific comparator, we simply write [compareBy<Person> { it.lastName }.thenBy { it.firstName }]: the keys are then compared in their natural order. The comparator [byNames] is a better replacement for the class [CompareNames];
  • line 31: the function [sortedWith] returns a new list sorted by the comparator; the list [persons] remains unchanged;
  • line 33: The function [sortedBy] sorts according to the key calculated by the lambda expression—in this case, the first name—in its natural order ([sortedByDescending] would sort in descending order);
  • line 35: the original list has not been modified by the functions [sortedWith] and [sortedBy];
  • line 40: the function [repeat] repeats a string: the title line is padded with dashes up to 50 characters.

Results:

> gradlew -q run -Pmain=classes20.Classes20Kt
List to sort--------------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
List after sorting by age-------------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]
List after sorting by last and first name---------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
List after sorting by decreasing age--------------
[valentine, germain, 35]
[paul, germain, 32]
[claude, pollon, 25]
[valentine, germain, 35] > [paul, germain, 32] : true
New list sorted by last and first name------------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
New list sorted by first name---------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]
Original list-------------------------------------
[valentine, germain, 35]
[paul, germain, 32]
[claude, pollon, 25]

In this case, the Java version featured three types of classes declared inside another class. Kotlin has the same types, but with different names: a class declared inside another class is nested by default (the equivalent of the Java class [static class]: it is not bound to any object of the enclosing class); an inner class, associated with an object of the enclosing class, is declared using the keyword [inner] ([inner class]); finally, the expression [object : ...] creates an anonymous object. We’ll see in Chapter 7 that we can make it even shorter than an anonymous object for an interface that has only one abstract method, such as [Comparator]: a lambda, [persons.sortWith { p1, p2 -> p2.age.compareTo(p1.age) }], or, as we just did, a comparator built from a lambda: [persons.sortWith(compareByDescending { it.age })].

4.8.4. Variance, projections, and type erasure

A [MutableList<Int>] is not a [MutableList<Number>], even though [Int] derives from [Number]: if it were, one could add a [Double] to a list of integers via a variable of type [MutableList<Number>]. The type [MutableList<T>] is said to be invariant: a function that accepts a [MutableList<Number>] rejects a [MutableList<Int>]. In contrast, a read-only list of type [List<Int>] is a [List<Number>]: since nothing can be added to it, the previous risk does not exist, and every integer read from the list is indeed a number. The type [List<T>] is said to be covariant.

This is a significant difference from Java. In Java, all generic interfaces are invariant, and you have to write wildcards for each use: [List<? extends Number>] for a list you’re reading from, [List<? super Integer>] for a list you’re populating. Kotlin allows you to specify the variance once and for all, at the declaration of the class or interface: this is declaration variance. The interface [List] is declared as [interface List<out E>]: the keyword [out] means that the type [E] appears only as an “output” (the result of methods and properties of type [val]), which makes the type covariant. Similarly, the interface [Comparable] is declared as [interface Comparable<in T>]: the type [T] appears there only as an “input” (parameter of [compareTo]), which makes the type contravariant: a [Comparable<Number>] is a [Comparable<Int>] (anyone who can compare any number can compare an integer). The compiler verifies that the class adheres to its declaration.

For invariant types like [MutableList], Kotlin also offers type projections, which are equivalent to Java’s wildcards:

Kotlin
Java
Meaning
[MutableList<out Number>]
[List<? extends Number>]
A list of elements of an unknown type derived from [Number]: you can read [Number] from it, but you cannot add elements to it
[MutableList<in Int>]
[List<? super Integer>]
a list of elements of an unknown type from which [Int] is derived ([Int], [Number], [Any]): [Int] can be added to this list
[MutableList<*>]
[List<?>]
a list of elements of an unknown type (star projection): you can read [Any?]

The program [Classes21.kt] illustrates variance, projections, and a specific feature of the generics in JVM—type erasure—as well as Kotlin’s partial solution to this issue: reified type parameters:

package classes21

// variance (out, in), projections, star projection, type erasure and reified types
fun main() {
    val integers: List<Int> = listOf(1, 2, 3)
    val reals: List<Double> = listOf(1.5, 2.25)
    // List<out E> is covariant: a List<Int> is a List<Number>
    println("Sum of integers: ${sum(integers)}")
    println("Sum of reals: ${sum(reals)}")
    // MutableList<in Int>: a mutable list that can receive Ints
    val numbers = mutableListOf<Number>()
    addIntegers(numbers, 3)
    numbers.add(2.5)
    println("numbers=$numbers, sum=${sum(numbers)}")
    // MutableList<out Number>: a mutable list whose elements are only read
    val mutableIntegers = mutableListOf(10, 20)
    println("Sum of a mutable list: ${mutableSum(mutableIntegers)}")
    // a recursive bound: T must be comparable to itself
    println("max : ${max(listOf("paul", "jean", "valentine"))}, ${max(integers)}")
    // declaration-site variance in our own classes
    val box: Box<Number> = Box(5)
    println("box : ${box.content}")
    // the star projection: a list of elements of an unknown type
    displaySize(listOf("a", "b"))
    displaySize(mutableIntegers)
    // type erasure: at runtime, a list is a list
    val l1 = ArrayList<String>()
    val l2 = ArrayList<Int>()
    println("same class: ${l1.javaClass == l2.javaClass} (${l1.javaClass.name})")
    // the reified type parameters of inline functions
    val mixed = listOf(1, "two", 3.0, "four", 5)
    println("strings : ${countOf<String>(mixed)}, integers : ${countOf<Int>(mixed)}")
    println("the strings : ${mixed.filterIsInstance<String>()}")
    println("type name : ${typeName<Double>()}")
}

// sum of the elements of a list of numbers (List is covariant)
fun sum(values: List<Number>): Double {
    var sum = 0.0
    for (value in values) {
        sum += value.toDouble()
    }
    return sum
}

// sum of the elements of a mutable list of numbers (MutableList is invariant)
fun mutableSum(values: MutableList<out Number>): Double {
    // values.add(1)   // rejected: the exact type of the elements is unknown
    return values.sumOf { it.toDouble() }
}

// adding the integers 1, 2, ..., n to a list able to receive Ints
fun addIntegers(list: MutableList<in Int>, n: Int) {
    for (i in 1..n) {
        list.add(i)
    }
}

// the largest element of a non-empty list
fun <T : Comparable<T>> max(elements: List<T>): T {
    var max = elements.first()
    for (element in elements) {
        if (element > max) {
            max = element
        }
    }
    return max
}

// a covariant class: T only appears in output (method result, val property)
class Box<out T>(val content: T)

// a list whose element type is unknown
fun displaySize(list: List<*>) {
    println("list of ${list.size} elements, first element: ${list.firstOrNull()}")
}

// the type T is known at runtime thanks to reified (inline function)
inline fun <reified T> countOf(elements: List<Any>) = elements.count { it is T }

inline fun <reified T : Any> typeName() = T::class.simpleName
  • lines 38–43: The function [sum] accepts a [List<Number>]; therefore, because [List] is covariant, it accepts a list of elements of any type derived from [Number]: [Int], [Double], [BigDecimal]… It reads the elements as [Number] and uses their [toDouble] method. Kotlin, like Java, does not support generic arithmetic: you cannot write [a + b] for two objects of type [T] or [Number], hence the use of [Double]. Where Java would have to write ***[List<? extends Number>], the simple type ***[List<Number>] is sufficient;
  • lines 8–9: the same function adds a list of [Int] to a list of [Double]. [listOf(...)] creates a read-only list;
  • Lines 47–49: An editable list, on the other hand, is invariant. To accept a [MutableList<Int>] (line 17), the function declares a projection [MutableList<out Number>]. It can read elements such as [Number], but adding an element (line 48) is rejected: the compiler does not know whether it is a list of [Int] or [Double] ([receiver type 'MutableList<out Number>' contains out projection which prohibits the use of 'fun add(element: E): Boolean']);
  • lines 53–55: the function [addIntegers] accepts any mutable list capable of holding [Int], in this case a [MutableList<Number>] (line 11). We follow the rule “producer: [out], consumer: [in]”: a list whose elements are read is written as [out], and a list that is filled is written as [in]. Kotlin’s keywords directly express what Java conveys using [extends] and [super];
  • lines 60–67: The generic function [max] returns the largest element in a list. The bound [T : Comparable<T>] requires that the elements be comparable to one another; it applies to strings as well as integers (line 19). In Java, you must write [T extends Comparable<? super T>] to also accept a type whose parent class is comparable; in Kotlin, since the [Comparable] interface is contravariant ([in T]), [T : Comparable<T>] is sufficient. The function [first] returns the first element of the list, and the operator [>] calls [compareTo]. (The Kotlin library already provides the function [maxOf] and, for collections, [max()].);
  • Line 71: our own covariant class. The type *[T] appears there only as an output, as the type of a property *[val]*. Line 21: A ***[Box<Int>] is therefore accepted where a ***[Box<Number>] is expected. If we add a method [fun replace(newValue: T)] to the class, where [T] appears as an input, the compiler rejects the class: [type parameter 'T' is declared as 'out' but occurs in 'in' position in type 'T (of class Box<out T>)']*;
  • Lines 74–75: The function [displaySize] accepts a list of elements of any type: [List<*>]. It cannot make any assumptions about its elements, which it reads as [Any?]. The function [firstOrNull] returns the first element, or [null] if the list is empty;
  • lines 27–29: at runtime, a ***[ArrayList<String>] and a ***[ArrayList<Int>] are two objects of the same class *[java.util.ArrayList] (*[javaClass] returns the Java class of an object). The compiler checks the generic types and then erases them: the compiled code only recognizes [ArrayList] and [Object], and conversions are inserted where necessary. This “type erasure,” adopted by Java in 2004 to ensure that generic code remains compatible with existing code, also applies to Kotlin, which generates code for the same JVM. This explains several limitations: you cannot write [T()] or test [o is List<String>] ([cannot check for instance of erased type 'List<String>']); however, you can test [o is List<*>];
  • Lines 79–81: Kotlin partially bypasses type erasure for the [inline] functions. The code for a [inline] function is copied by the compiler at every point where it is called (we’ll return to this in Chapter 7): at that point, the type [T] is known. If the type parameter is declared as [reified] (“reified”), the function can then use it as a true type: to test [it is T] (line 79), or to obtain its class [T::class] (line 81). Without [reified], the test [it is T] is rejected: [cannot check for instance of erased type 'T (of fun <T> countOf)'];
  • lines 31–34: use of reified functions. The type is specified between angle brackets: [countOf<String>(mixed)] counts the strings in the list. The Kotlin library makes extensive use of this mechanism, for example in the function [filterIsInstance<String>()], which returns the elements of a collection that are strings as type [List<String>].

Results:

> gradlew -q run -Pmain=classes21.Classes21Kt
Sum of integers: 6.0
Sum of reals: 3.75
numbers=[1, 2, 3, 2.5], sum=8.5
Sum of a mutable list: 30.0
max : valentine, 3
box : 5
list of 2 elements, first element: a
list of 2 elements, first element: 10
same class: true (java.util.ArrayList)
strings : 2, integers : 2
the strings : [two, four]
type name : Double

Unlike other languages such as C#, where interfaces can declare abstract static members to implement generic computations, Kotlin does not allow a type [T] to be required to have a “class” method: the members of an interface’s companion object are called using the interface’s name and are not inherited by the classes that implement it.

4.9. Packages

To print a line to the screen, we use the function [println(...)]. If we look at its definition in the Kotlin library documentation (kotlinlang.org/api), we find that it is part of the [kotlin.io] package. This means that its full name is [kotlin.io.println] and that we could write [kotlin.io.println(...)]. A Kotlin package contains classes, as well as functions, properties, and top-level objects. Several packages are automatically imported into every Kotlin file: [kotlin.*] ([Any], [String], [Int]…), [kotlin.io.*] ([println], [readln]), [kotlin.collections.*] ([List], [listOf]…), [kotlin.text.*] ([StringBuilder], [Regex]…), [kotlin.ranges.*], [kotlin.sequences.*], [kotlin.comparisons.*] ([compareBy]…), [kotlin.annotation.*], and, on JVM, [java.lang.*], and [kotlin.jvm.*]. For the others, a [import] clause is used, placed at the beginning of the file, after the package declaration:

  • [import java.text.Collator] imports the class [Collator] from the package [java.text] (without a trailing semicolon, as with all Kotlin statements);
  • [import java.util.*] imports all public declarations from the [java.util] package on demand;
  • [import kotlin.math.sqrt] imports a top-level function, which can then be used without its package: [sqrt(2.0)]. Kotlin does not require Java’s [import static] clause: classes, functions, properties, and even static members of a Java class ([import java.lang.Math.PI]) are imported in the same way;
  • [import namespaces.e2.Person as PersonE2] imports a declaration under a different name—an alias—that is valid only within that file.

When the compiler encounters a simple name, it looks for it in the current package and among the imported declarations. A [import] clause adds nothing to the program: it simply avoids having to write out the full names.

When you create a class, you place it in a package. The purpose of packages is to prevent name conflicts between classes, for example, when they are distributed. Consider two companies, E1 and E2, distributing classes packaged in the archives [e1.jar] and [e2.jar], respectively. Suppose a customer C purchases these two sets of classes, in which both companies have defined a class named [Person]. If company E1 creates its classes in a package named [e1] and company E2 creates its classes in a package named [e2], the two classes [Person] will then be named [e1.Person] and [e2.Person]. The client can import one of them and refer to the other by its full name, or give it an alias. The package resolves the ambiguity. By convention, package names are in lowercase and begin with the reverse domain name of the organization that publishes them ([com.mysql.cj.jdbc], [org.hibernate], [kotlinx.coroutines]…); for our examples, we will use short names.

To place a file’s declarations in a package, you write the declaration [package name] at the top of the file. This is what we’ve been doing since the beginning of this course: each program is in a package with its own name. Unlike Java, Kotlin does not require a one-to-one correspondence between packages and folders, but it does recommend it, and IntelliJ IDEA indicates a file that is not properly organized: The source files for the [namespaces.e1] package are located in a folder named [namespaces/e1], starting from the source root ([src/main/kotlin]). The [namespaces] package and its two subpackages illustrate these concepts:

1
2
3
4
5
6
src/main/kotlin/namespaces/
  Main.kt               the main program (namespaces package)
  e1/
    Person.kt           the namespaces.e1.Person class
  e2/
    Person.kt           the namespaces.e2.Person class

The first class [Person] [e1/Person.kt]:

1
2
3
4
5
6
7
8
9
// the declarations of this file belong to the namespaces.e1 package
package namespaces.e1

class Person(private val firstName: String, private val lastName: String) {
    override fun toString() = "e1.Person[$firstName, $lastName]"
}

// a top-level function also belongs to a package
fun greet(person: Person) = "Hello $person"
  • Line 2: the package declaration. All declarations in the file belong to this package: the class [Person] and the top-level function [greet] (line 9), whose full name is [namespaces.e1.greet]. In JVM, this function is placed in a class named [namespaces.e1.PersonKt], which is named after the file.

The second one, [e2/Person.kt]:

// the declarations of this file belong to the namespaces.e2 package
package namespaces.e2

class Person(val name: String) {
    override fun toString() = "e2.Person[$name]"
}

// a constant visible in the whole module, and only in the module
internal const val VERSION = "e2-1.0"

// a function visible in this file only
private fun tool() = "private tool"
  • line 4: a class named [Person], which shares the same name as the previous one but is in a different package;
  • line 9: a visibility constant [internal]: it is accessible only to declarations within the same module. In Kotlin, a module is a set of files compiled together: a Gradle project (or subproject), a module IntelliJ, IDEA… Here, it is the project [04-classes];
  • Line 12: a private top-level function: it is accessible only to declarations within its own file. An attempt to import it from [Main.kt] is rejected: [cannot access 'fun tool(): String': it is private in file].

Both classes are public, which is Kotlin’s default visibility (a Java class without the [public] keyword is accessible only to classes in its own package). Let’s summarize the four visibility levels of a top-level declaration:

Visibility
Accessible from
[public] (default)
everywhere
[internal]
the same module
[private]
the same file
[protected]
(only applies to members of a class: the class and its derived classes)

Kotlin does not have an equivalent to Java’s “package” visibility—the default visibility of Java declarations without a modifier: in Kotlin, a package is merely a namespace, and it is the module that defines what a library exposes to its users. The main program [Main.kt] is as follows:

package namespaces

// importing a class from a package
import namespaces.e1.Person
// importing a top-level function
import namespaces.e1.greet
// importing a class under another name
import namespaces.e2.Person as PersonE2
// importing a constant internal to the module
import namespaces.e2.VERSION
// importing a function of the Kotlin library
import kotlin.math.sqrt

fun main() {
    // Person designates namespaces.e1.Person, which has been imported
    val p1 = Person("Jean", "Dupont")
    // the full name of a class removes any ambiguity
    val p2 = namespaces.e2.Person("Durand")
    // the alias PersonE2 designates the class namespaces.e2.Person
    val p3 = PersonE2("Martin")
    println("$p1 $p2 $p3")
    // a top-level function of the namespaces.e1 package
    println(greet(p1))
    // full name and package of a class
    println("${Person::class.qualifiedName}, ${PersonE2::class.qualifiedName}")
    println("package of the Person class: ${Person::class.java.packageName}")
    // the JPMS module that contains a class
    println("e1.Person is in a named module: ${Person::class.java.module.isNamed}")
    println("${String::class.java.name} is in the module ${String::class.java.module.name}")
    // StringBuilder is known without an import
    val sb = StringBuilder("abc")
    println(sb.append("def"))
    // sqrt is known thanks to the import
    println("sqrt(2)=${sqrt(2.0)}")
    // an internal declaration is accessible in the whole module
    println("version=$VERSION")
}
  • line 4: import of the [namespaces.e1.Person] class;
  • line 6: import of the top-level function [greet], just as one imports a class;
  • Line 8: Importing the class [namespaces.e2.Person] under the name [PersonE2]. In this file, the name [PersonE2] refers to this class. Java does not support aliases: you cannot give a class a different name in Java. (Kotlin also allows you to declare a type alias for the entire package, such as [typealias PersonE2 = namespaces.e2.Person]: we’ve seen examples of this, where [Comparator] and [Exception] are aliases for Java types);
  • line 10: import of the constant [internal], which is allowed since [Main.kt] is in the same module;
  • line 12: import of the function [sqrt] from the package [kotlin.math];
  • line 16: [Person] refers to the class [namespaces.e1.Person], which was imported;
  • line 18: the full name of the class [namespaces.e2.Person] removes any ambiguity;
  • line 20: the alias yields the same result, but is more readable;
  • line 23: call to the imported function;
  • lines 25–26: the property [qualifiedName] of the Kotlin description of a class ([Person::class]) returns its full name, prefixed with its package. The property [java] returns the Java description of the class ([java.lang.Class]), whose method [getPackageName]—viewed in Kotlin as a property [packageName]—returns the package name;
  • lines 28–29: the [getModule] method of the Java class returns the JPMS module that contains the class (see below);
  • lines 31–32: the class [StringBuilder] is recognized without an import (it is an alias, defined in the package [kotlin.text], for the class [java.lang.StringBuilder]);
  • line 34: use of the imported function;
  • line 36: use of the constant [internal].

Results:

> gradlew -q run -Pmain=namespaces.MainKt
e1.Person[Jean, Dupont] e2.Person[Durand] e2.Person[Martin]
Hello e1.Person[Jean, Dupont]
namespaces.e1.Person, namespaces.e2.Person
package of the Person class: namespaces.e1
e1.Person is in a named module: false
java.lang.String is in the module java.base
abcdef
sqrt(2)=1.4142135623730951
version=e2-1.0

If we replace the imports of [Person] with the two on-demand imports [import namespaces.e1.*] and [import namespaces.e2.*], without the alias, the name [Person] becomes ambiguous. The two imports cancel each other out, and the compiler can no longer find the class:

Main.kt:7:14: error: unresolved reference 'Person'.

Note that an import with an alias masks the original name in the file: With the clause [import namespaces.e2.Person as PersonE2], the name [Person] can now only refer to the class in the package [e1], even if both packages are imported on demand.

4.9.1. Modules

The term module has two meanings for a Kotlin developer. We’ve just seen the first: the Kotlin module, a set of files compiled together, which defines the scope [internal]. The second meaning is from Java: since Java 9, packages have been grouped into modules (the “Java Platform Module System,” JPMS). JDK is divided into modules: [java.base] contains the core packages ([java.lang], [java.util], [java.math], [java.io]…), [java.sql] provides database access, [java.net.http] is the client, HTTP, and so on. A module named JPMS declares, in a file named [module-info.java], its name, the modules it depends on ([requires java.sql;]), and the packages it makes available to other modules ([exports tax.business;]). A Kotlin project can declare one (the file [module-info.java], written in Java, is placed alongside the Kotlin source files and must then depend on the module [kotlin.stdlib]). Classes in an application that does not declare a JPMS module—such as ours—belong to the unnamed module (line 5 of the output), which has access to all modules. This is the case for most applications, and for all those in this course: the JPMS modules are primarily of interest to library authors, and the JDK module itself.

4.10. Data Classes

Many classes are used solely to carry data: a person, a grade, a tax bracket… For these, we always write the same code: read-only properties, a [toString] method that displays them, and the [equals] and [hashCode] methods. Kotlin’s primary constructor already saves you from having to write the fields, the constructor, and the accessors; the keyword [data], placed before [class], also instructs the compiler to generate the rest. Data classes have existed since the first version of Kotlin (2016); Java caught up with them, with its records, in 2021. We encountered them earlier (in the “Immutable Objects” section) and several times since then; the program [Classes22.kt] presents them more fully:

package classes22

// data classes (data class)
fun main() {
    val r1 = Person("Jean", "Dupont", 30)
    val r2 = Person("Jean", "Dupont", 30)
    // toString generated by the compiler
    println(r1)
    // the properties are those declared by the primary constructor
    println("firstName=${r1.firstName}, identity=${r1.identity()}")
    // equality by value: r1 and r2 are two different but equal objects
    println("r1 == r2 : ${r1 == r2}, r1 === r2 : ${r1 === r2}, equal hashCodes : ${r1.hashCode() == r2.hashCode()}")
    // val properties are read-only
    // r1.age = 31   // rejected: age is a val property
    // a modified copy, with the copy method and a named argument
    val r3 = r1.copy(age = 31)
    println(r3)
    println("r1 == r3 : ${r1 == r3}")
    // destructuring into variables, thanks to the functions component1(), component2()...
    val (firstName, lastName, age) = r1
    println("firstName=$firstName, lastName=$lastName, age=$age")
    println("component2()=${r1.component2()}")

    // the same thing with an ordinary class
    val c1 = PersonC("Jean", "Dupont", 30)
    val c2 = PersonC("Jean", "Dupont", 30)
    println(c1)
    println("c1 == c2 : ${c1 == c2}")

    // two data classes that implement the same interface
    val e = Teacher("Lucile", "Dumas", 56, 61)
    println(e)
    println(e.identity())
    // equality takes the type into account: a Person is not equal to a Teacher
    val pe = Person("Lucile", "Dumas", 56)
    println("pe == e : ${pe == e}")
    // the common type: the HasIdentity interface
    val identities = listOf(pe, e)
    for (i in identities) {
        println(i.identity())
    }
}

// an interface implemented by the data classes
interface HasIdentity {
    fun identity(): String
}

// a data class: firstName, lastName, age are its properties
data class Person(val firstName: String, val lastName: String, val age: Int) : HasIdentity {
    // methods can be added
    override fun identity() = "[$firstName, $lastName, $age]"
}

// a data class cannot be derived from (it is final)
data class Teacher(val firstName: String, val lastName: String, val age: Int, val section: Int) : HasIdentity {
    override fun identity() = "Teacher[[$firstName, $lastName, $age],$section]"
}

// an equivalent ordinary class... or almost
class PersonC(val firstName: String, val lastName: String, val age: Int)
  • lines 50–52: the data class [Person]. The declaration [data class Person(val firstName: String, val lastName: String, val age: Int)] generates, from the properties declared in the primary constructor:
    • three read-only properties [firstName], [lastName], [age] (which a regular class also does), accessed by their names: [r1.firstName] (line 10);
    • a method [toString] that displays the class name and its properties (line 1 of the results);
    • the methods [equals] and [hashCode], which compare property values rather than references;
    • a method [copy], which creates a copy of the object whose properties can be modified;
    • the methods [component1], [component2], and [component3], which return the properties in the order in which they were declared and allow for destructuring;
  • the primary constructor of a data class must have at least one parameter, and all of its parameters must be properties ([val] or [var]). Properties of type [var] can be declared: the data class is then mutable. This is allowed but not recommended, because an object whose [hashCode] changes is lost in a set or dictionary that contains it; data classes are reserved for immutable data;
  • line 52: A data class, like a class, can have additional methods and implement interfaces (line 50);
  • Line 12: [r1] and [r2] are two distinct objects ([===], where the references are equal, results in [false]) but they are equal in the sense of [==], which calls [equals], because their properties have the same values. Their hash codes are therefore equal. For an ordinary class, [equals] compares the references (line 28);
  • line 14: when uncommented, this line is rejected: ['val' cannot be reassigned];
  • line 16: an immutable data class cannot be modified; a modified copy is created. The generated method [copy] has one parameter per property, whose default value is the property’s value in the copied object; with a named argument, only the properties that change are specified. This is the [with] expression that was missing in Java, where you have to manually write “wither” methods ([withAge(31)]);
  • Line 20: destructuring: the properties of *[r1] are extracted into the variables *[firstName]*, *[lastName], and *[age]. The compiler translates this declaration into [val firstName = r1.component1()], [val lastName = r1.component2()], etc.: the mapping is based on position and not on the name (line 22, the [component2] method is called directly). A property can be skipped using the character [_]: [val (firstName, _, age) = r1]. This is equivalent to the Java 21 record pattern ([r1 instanceof Person(String firstName, String lastName, int age)]*); we’ll revisit destructuring with pattern filtering;
  • lines 56–57: A data class is final; it cannot be subclassed (the keyword [open] is prohibited here). Unlike a Java record, however, it can itself be derived from an open, abstract, or sealed class. To give the classes [Person] and [Teacher] a common type, we have them implement the same interface, in this case [HasIdentity] (lines 45–46). The class [Teacher] therefore inherits the properties of the person;
  • line 36: the equality of data classes takes into account the actual type of the objects: a [Person] is never equal to a [Teacher], even if their shared properties are equal;
  • lines 38–40: The common interface allows both classes to be handled polymorphically. The type of the list, [List<HasIdentity>], is inferred by the compiler: it is the most specific common type of the two elements;
  • line 61: an ordinary class that resembles the class [Person]. Thanks to the primary constructor, it also fits on a single line… but equality by value, [toString], [copy], and destructuring are not provided. In Java, you also had to write the fields, the constructor, and the getters and setters.

Results:

> gradlew -q run -Pmain=classes22.Classes22Kt
Person(firstName=Jean, lastName=Dupont, age=30)
firstName=Jean, identity=[Jean, Dupont, 30]
r1 == r2 : true, r1 === r2 : false, equal hashCodes : true
Person(firstName=Jean, lastName=Dupont, age=31)
r1 == r3 : false
firstName=Jean, lastName=Dupont, age=30
component2()=Dupont
classes22.PersonC@4554617c
c1 == c2 : false
Teacher(firstName=Lucile, lastName=Dumas, age=56, section=61)
Teacher[[Lucile, Dumas, 56],61]
pe == e : false
[Lucile, Dumas, 56]
Teacher[[Lucile, Dumas, 56],61]

Note that the method [toString] of the class [PersonC] is the same as that of the class [Any]: the full class name followed by a hash code, which changes from one execution to the next (line 8 of the results), and that the equality of the two [PersonC] objects is false (line 9). The format of the [toString] for a data class, [Person(firstName=Jean, lastName=Dupont, age=30)], differs slightly from that of a Java record, [Person[firstName=Jean, lastName=Dupont, age=30]].

Let’s summarize the differences between a regular class and a data class:

 
Class
Data Class
Equality: [==] ([equals])
same object (unless overridden)
same values for the primary constructor’s properties, same type
[toString]
class name and hash code
class name and properties
Properties, constructor
declared by the primary constructor
declared by the primary constructor (at least one property, only properties)
state
editable or not
immutable with [val] (recommended); other properties may be declared in the body
modified copy
to be written
[copy(prop = value)] generated
destructuring
to be written ([operator fun componentN])
generated
inheritance
of a class, of multiple interfaces; open with [open]
of a class, of multiple interfaces; always final

A data class is used for data defined by its value (a person retrieved from a database, a message exchanged over the network, a tax bracket), while an ordinary class is used for an object with an identity and behavior (a calculation service, a connection, a mutable list). We will see in Chapter 11 that Hibernate entities, which have an identity, are not data classes. Note: A data class with properties of type [val] is “superficially” immutable: if a property references a mutable object—a [MutableList], for example—the contents of that list remain mutable. In this case, we use read-only properties of type [List], initialized with a copy ([list.toList()]). For readers coming from Java: a data class is compiled into a regular, final Java class (not a record); the [@JvmRecord] annotation allows it to be compiled into a true Java record if necessary.

4.11. Data Class Constructors

The primary constructor of a data class simply copies its parameters into the properties. However, you may want to validate or normalize these parameters. Java provides the compact constructor for records for this purpose, which can modify the parameters before storing them. Kotlin does not have an exact equivalent: the parameters of a data class are its properties, stored as-is. On the other hand, we have the tools available to any class: the [init] block for validation, secondary constructors, and the companion object’s factories for normalization. The program [Classes23.kt] illustrates these:

package classes23

import kotlin.math.sqrt

// constructors of data classes: validation, secondary constructor, factory
fun main() {
    val p = Person.create("  Jean ", "Dupont", 30)
    println(p.identity())
    // a data class is immutable: growOlder returns a new person
    val p2 = p.growOlder()
    println("${p.identity()} -> ${p2.identity()}")
    // a secondary constructor
    val unknown = Person("Martin")
    println(unknown.identity())
    // the checks of the factory and of the init block
    try {
        Person.create("Paul", " ", 20)
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    try {
        Person.create("Paul", "Martin", -4)
    } catch (ex: IllegalArgumentException) {
        println(ex.message)
    }
    // a data class with a computed property and a companion object
    val pt = Point(3.0, 4.0)
    println("pt=$pt, distance to the origin: ${pt.distance}, origin=${Point.ORIGIN}")
}

// the primary constructor is private: a person is created with the create factory
// or with the secondary constructor; copy is private like it (@ConsistentCopyVisibility)
@ConsistentCopyVisibility
data class Person private constructor(val firstName: String, val lastName: String, val age: Int) {
    // the init block checks the object, whatever the way it is constructed
    init {
        require(age >= 0) { "invalid age ($age)" }
    }

    // a secondary constructor must call the primary constructor
    constructor(lastName: String) : this("?", validate(lastName, "last name"), 0)

    // a method that returns a new person
    fun growOlder() = copy(age = age + 1)

    fun identity() = "[$firstName, $lastName, $age]"

    companion object {
        // the factory: it checks and normalizes the parameters before construction
        fun create(firstName: String, lastName: String, age: Int) =
            Person(validate(firstName, "first name"), validate(lastName, "last name"), age)

        // a private validation function
        private fun validate(value: String, label: String): String {
            require(value.isNotBlank()) { "invalid $label ($value)" }
            return value.trim()
        }
    }
}

data class Point(val x: Double, val y: Double) {
    // a property declared in the body of the class: it is not part
    // of the properties used by toString, equals, hashCode, copy
    val distance: Double
        get() = sqrt(x * x + y * y)

    companion object {
        val ORIGIN = Point(0.0, 0.0)
    }
}
  • lines 36–37: The block [init] executes on every construction, after the primary constructor’s properties have been initialized. It validates the object: a negative age is rejected. The function [require(condition) { message }] throws a [IllegalArgumentException] exception with the message calculated by the lambda if the condition is false: this is the exception intended for an invalid parameter. All constructors of a class go through the primary constructor, and therefore through the [init] block: there is no way to create a [Person] with a negative age, not even with [copy], which also calls the primary constructor;
  • Line 34: The block [init] cannot modify the [val] properties of the primary constructor, which are already initialized. To normalize the parameters (in this case, removing the spaces around the first and last names), we make the primary constructor private ([private constructor]) and provide a factory method instead: the [create] function of the companion object (lines 50–51). It validates and normalizes the parameters before calling the primary constructor, which it is authorized to call since it is part of the class. It is called using the class name: [Person.create(" Jean ", "Dupont", 30)] (line 7);
  • line 33: the generated method [copy] calls the primary constructor; if it were public, it would allow the factory method ([p.copy(firstName = " x ")]) to be bypassed. The annotation [@ConsistentCopyVisibility] instructs the compiler to give it the visibility of the primary constructor, which is private here. Without it, Kotlin 2.4 issues the warning [non-public primary constructor is exposed via the generated 'copy()' method of the 'data' class], specifying that the visibility of the generated method [copy] will change in a future version (it will then have the same visibility as the constructor);
  • line 41: a secondary constructor, which takes only the name. Like any secondary constructor of a class with a primary constructor, it must call the primary constructor with [: this(...)]; to calculate the parameters, it may call functions of the companion object, even private ones ([validate]). A secondary constructor can be public when the primary constructor is private;
  • line 44: a data class is immutable: aging means creating a new instance, which is done by the method [copy] (private, but usable within the class);
  • lines 54–56: a private validation function in the companion object. [isNotBlank()] returns true if the string contains anything other than spaces; [trim()] removes leading and trailing spaces from the string. The error message is constructed using a string template within the lambda passed to [require]: it is calculated only if an error occurs;
  • Lines 61–68: A data class may have, in addition to the properties of its primary constructor, properties declared within its body—in this case, the computed property [distance]. These do not contribute to [toString], [equals], [hashCode], or [copy]. It may also have a companion object, in this case with the property [ORIGIN], which is equivalent to the static field in the Java version. (Unlike a record, a data class can even have properties stored within its body, but this is of little use for immutable data.)

Results:

1
2
3
4
5
6
7
> gradlew -q run -Pmain=classes23.Classes23Kt
[Jean, Dupont, 30]
[Jean, Dupont, 30] -> [Jean, Dupont, 31]
[?, Martin, 0]
invalid last name ( )
invalid age (-4)
pt=Point(x=3.0, y=4.0), distance to the origin: 5.0, origin=Point(x=0.0, y=0.0)

The first name, provided with spaces on either side (line 7), was stored without them (lines 1 and 2 of the results). The [init] block is the idiomatic way to ensure that an object is always valid; a factory, with a private constructor, also allows the data to be normalized.

4.12. Extension Functions

How do you add a method to a class that cannot be modified, such as the [String] class? Java doesn’t provide a built-in solution: you write static methods, grouped into a utility class ([Arrays], [Collections], [Objects], [Math]…), and we call them using their class name, passing the object as a parameter: [Tools.initials(p)]. Kotlin, like C# with its extension methods, offers extension functions: functions declared outside the class that are used as methods of the extended class: [p.initials()]. We’ve been using them since the beginning of this course: [toInt], [sumOf], [contentToString], [toMutableList], [repeat], [also]… are extension functions from the Kotlin library, which enhance the classes in JDK ([String], arrays, collections) without modifying them. The program [Classes24.kt] shows how to write your own:

package classes24

import java.time.DayOfWeek
import java.time.LocalDate

// extension functions and properties
fun main() {
    val p = Person("jean", "dupont", 30)
    // extension functions are called like methods of the object
    println(p.initials())
    println("jean".firstUpper())
    println("${p.fullName} is an adult: ${p.isAdult}")
    println("\"the weather is fine\" has ${"the weather is fine".nbWords()} words")
    // a function of the companion object of the class itself
    val unknown = Person.unknown()
    println("${unknown.fullName} is an adult: ${unknown.isAdult}")
    // an extension of a Java class
    val date = LocalDate.of(2026, 10, 4)
    println("$date is a weekend day: ${date.isWeekend}")
    // an extension of a nullable type: callable on null
    val absent: Person? = null
    println("${p.description()} / ${absent.description()}")
    // the resolution of extensions is static: the declared type is what counts
    val shape: Shape = Square()
    println("shape.name()=${shape.name()}, Square().name()=${Square().name()}")
    // ...whereas the call of an overridden method is polymorphic
    println("shape.category()=${shape.category()}")
}

// a person
data class Person(val firstName: String, val lastName: String, val age: Int) {
    companion object {
        // factory: an unknown person
        fun unknown() = Person("?", "?", 0)
    }
}

// extension functions of the Person class
fun Person.initials() = "${firstName[0].uppercaseChar()}.${lastName[0].uppercaseChar()}."

// extension properties (computed: an extension cannot store a value)
val Person.isAdult: Boolean
    get() = age >= 18

val Person.fullName: String
    get() = "${firstName.firstUpper()} ${lastName.uppercase()}"

// extension functions of the String class
fun String.firstUpper() = if (isEmpty()) this else this[0].uppercaseChar() + substring(1)

fun String.nbWords() = if (isBlank()) 0 else trim().split(Regex("\\s+")).size

// an extension property of a JDK class
val LocalDate.isWeekend: Boolean
    get() = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY

// an extension function of a nullable type
fun Person?.description() = if (this == null) "absent person" else "person $firstName"

// static resolution of extensions
open class Shape {
    open fun category() = "shape"
}

class Square : Shape() {
    override fun category() = "square"
}

fun Shape.name() = "a shape"

fun Square.name() = "a square"
  • line 39: an extension function for the [Person] class. The function name is prefixed by the extended type, known as the receiver type ([Person.]). In the body of the function, [this] refers to the object on which the function is called—the receiver—and its members are accessible without a prefix, just as in a method: [firstName], [lastName]. [firstName[0]] returns the first character of the string, and [uppercaseChar()] capitalizes it;
  • lines 42–46: two extension properties. They are declared like functions, with a receiver type, and must have an accessor [get]: an extension does not add anything to the object, so it cannot store a value (an extension property has no field and cannot have an initializer). They are read like properties: [p.isAdult], [p.fullName] (line 12);
  • lines 49–51: two extensions of the [String] class, used as methods of any string: ["jean".firstUpper()]. In [firstUpper], [this] is the string itself. The function [nbWords] splits the string using the function [split], to which a regular expression ([Regex]) is passed: ["\\s+"] refers to a sequence of one or more space characters (regular expressions are covered in Chapter 5);
  • lines 54–55: an extension property of a Java class, JDK, [java.time.LocalDate]. An extension applies to any type, including a Java class or a final class. Note that the [getDayOfWeek] method of the Java class is treated by Kotlin as a [dayOfWeek] property;
  • Line 58: a nullable type extension, [Person?]. It can be called on a reference to [null] without the [?.] operator (line 22): this is what checks whether [this] is equal to [null]. After the check, the compiler knows that [this] is not [null] in the [else] branch and allows access to [firstName] (this is smart type casting, which the following paragraph explains in more detail). The function [toString()] from the Kotlin library, which can be called on a null reference, is declared as follows: [fun Any?.toString(): String];
  • lines 8–13: extension functions and properties are called as members: [p.initials()], not [Tools.initials(p)];
  • lines 32–34: when the type can be modified, its methods are naturally placed there: the factory [unknown] is a function of the companion object of the [Person] class (line 15);
  • lines 61–71: a class [Shape] and its derived class [Square], each equipped with a redefined method [category] and an extension function [name]. On line 25, the variable [shape], declared as type [Shape], references a [Square]: however, the call to [shape.name()] actually executes the extension of [Shape]. Extension resolution is static: the compiler selects the function based on the variable’s declared type rather than on the actual type of the object at runtime. On line 27, the method [category] is polymorphic: the method of the class [Square] is the one that is executed.

Results:

> gradlew -q run -Pmain=classes24.Classes24Kt
J.D.
Jean
Jean DUPONT is an adult: true
"the weather is fine" has 4 words
? ? is an adult: false
2026-10-04 is a weekend day: true
person jean / absent person
shape.name()=a shape, Square().name()=a square
shape.category()=square

This behavior is explained by the way the compiler translates an extension function: into a static method, whose first parameter is the receiver. The function [fun Person.initials()] in the file [Classes24.kt] becomes the Java method [public static final String initials(Person $this$initials)] of the class [classes24.Classes24Kt] (the extension property *[isAdult] likewise becomes a static method ***[isAdult(Person)]), and the call ***[p.initials()] becomes ***[Classes24Kt.initials(p)]**. In other words, Kotlin writes the Java utility class for us and makes its usage more readable. A Java program that uses a Kotlin library calls its extensions in this way. This leads to several rules:

  • an extension has access only to the receiver’s public members: it cannot see its private or protected members;
  • an extension cannot be overridden in a derived class: it is not polymorphic;
  • if the class has a member with the same signature, that member is always called, and the compiler warns that the extension is unnecessary ([this extension is shadowed by a member: 'fun g(): Int' defined in 'C']);
  • To be used in another package, an extension must be imported, just like a top-level function ([import tools.initials]).

Extension functions are one of Kotlin’s most distinctive features. They are used to enrich types over which we have no control, but also to separate, within our own classes, the core of the class (its members) from utility functions that only access it from the outside. In Chapter 7, we’ll look at lambdas with receivers, which are based on the same principle ([buildString { append(...) }]).

4.13. Pattern matching on objects

The expression [when] is not limited to simple values (numbers, strings, enumerations): it also applies to objects. You can check an object’s type, add a condition, and use the object in the correct type without explicit conversion—all in a single expression. Kotlin does not, strictly speaking, have “patterns” like Java 21: it achieves essentially the same result using the [is] operator, smart casting, and destructuring. The program [Classes25.kt] uses a sealed interface and three data classes that implement it:

package classes25

// pattern matching on objects: is, smart cast, when, destructuring
fun main() {
    val objects: List<Any?> = listOf(
        Individual("Jean", "Dupont", 30),
        Individual("Lise", "Morel", 12),
        Teacher("Lucile", "Dumas", 56, 61),
        Teacher("Marc", "Petit", 44, 27),
        Student("Paul", "Martin", 19, "computer science"),
        "a string",
        42,
        null,
    )

    // 1 - the is type test and the smart cast
    for (o in objects) {
        if (o is Person) {
            // here o is a Person
            println("${o.firstName} is of type ${o::class.simpleName}")
        }
    }

    // 2 - the type test followed by a condition
    for (o in objects) {
        if (o is Person && o.age >= 18) {
            println("${o.firstName} is an adult")
        }
    }

    // 3 - when on the type of the object, with guards and destructuring
    for (o in objects) {
        println(describe(o))
    }

    // 4 - an exhaustive when on a sealed interface
    for (o in objects) {
        if (o is Person) {
            println("${o.firstName} : ${category(o)}")
        }
    }

    // 5 - the explicit casts as and as?
    val first = objects[0] as Person
    val string = objects[0] as? String
    println("first=${first.firstName}, string=$string")
    try {
        val s = objects[0] as String
        println(s)
    } catch (ex: ClassCastException) {
        println("ClassCastException : ${ex.message}")
    }
}

fun describe(o: Any?): String = when (o) {
    is Teacher if o.section == 27 -> "${o.firstName} teaches computer science"
    is Teacher -> "${o.firstName} teaches in section ${o.section}"
    is Student -> {
        val (firstName, _, _, major) = o
        "$firstName studies $major"
    }
    is Person if o.age < 18 -> "${o.firstName} is a minor"
    is Person -> "${o.firstName} is ${o.age} years old"
    is String -> "a string of ${o.length} characters"
    is Int if o > 0 -> "the positive integer $o"
    null -> "the null reference"
    else -> "an unknown object"
}

// no else: the compiler knows that the three cases cover all persons
fun category(p: Person) = when (p) {
    is Individual -> "individual"
    is Teacher -> "teacher"
    is Student -> "student"
}

// a sealed interface and the three data classes that implement it
sealed interface Person {
    val firstName: String
    val lastName: String
    val age: Int
}

data class Individual(override val firstName: String, override val lastName: String, override val age: Int) : Person

data class Teacher(
    override val firstName: String,
    override val lastName: String,
    override val age: Int,
    val section: Int,
) : Person

data class Student(
    override val firstName: String,
    override val lastName: String,
    override val age: Int,
    val major: String,
) : Person
  • Lines 78–97: A sealed interface [Person] ([sealed interface]), implemented by three data classes: [Individual], [Teacher], and [Student]. Like a sealed class (see the section “Final, Open, and Sealed Classes”), a sealed interface can only be implemented by classes in the same package and the same module: the compiler therefore knows all of its implementations without needing to list them explicitly, as is required by the Java clause [permits]. The interface declares the abstract properties [firstName], [lastName], and [age]; the data classes implement them using properties of their primary constructor, prefixed with [override];
  • lines 5–13: a list of objects of various types, including an integer and the reference [null]. Its type is [List<Any?>]: [Any?] is Kotlin’s most general type, which accepts any object and the value [null];
  • line 18: the expression [o is Person] is true if [o] refers to an object of type [Person] (it is false if [o] is equal to [null]). In the following block, the compiler knows that [o] is a [Person]: it allows [o.firstName] without conversion. This is smart type casting: the type of a variable is tested, and the variable itself changes type where the test is guaranteed to be true. Java 16 achieves the same result by declaring a new variable ([o instanceof Person p]); Kotlin doesn’t need to do this. Smart type conversion applies to local variables [val], to variables [var] that haven’t changed between the test and the use, and to properties [val] in the same module without custom accessors: the compiler must be certain that the value could not have changed between the test and the use;
  • line 26: smart type conversion also applies in the remainder of the condition, after [&&];
  • lines 55–67: the expression [when] with a subject evaluates the branches in order and returns the value associated with the first one that matches:
    • line 56: a teacher from section 27. In a branch [is Type], the subject [o] is intelligently type-cast; the clause [if] adds a condition, called a guard (Kotlin 2.2), which is the equivalent of [when] in Java patterns;
    • line 57: any other teacher. The order is important: if placed before the previous one, this case would have made it inaccessible. The Kotlin compiler does not flag this (Java would reject it): the branch would simply never be chosen;
    • lines 58–60: a student, whose properties are extracted via destructuring: [val (firstName, _, _, major) = o] calls [component1()] and [component4()] from the data class ([_] ignores a property). This is equivalent to Java’s record pattern ([case Student(var firstName, _, _, var major)]), in two steps: the type check, followed by destructuring. A branch may contain a block; its value is that of the last expression in the block;
    • lines 62–63: a minor, then any other person;
    • line 64: a string, whose property [length] can be used;
    • line 65: a positive integer. The type [Int] is checked, not [Integer] as in Java;
    • line 66: the case [null], a simple value, compared to the subject by [==]. Without this case, the null reference would fall into the [else] branch: a [when] on a null reference never throws an exception;
    • line 67: the branch [else] covers everything else. Without it, the compiler would report that the expression [when] does not handle all cases: an expression [when], which returns a value, must be exhaustive;
  • Lines 71–74: a [when] on a [Person], without a [else]. However, it is exhaustive: since the interface is sealed, the compiler knows that a person is a [Individual], a [Teacher], or a [Student];
  • lines 44–51: explicit type conversions. [o as Person] converts [o] to [Person] and throws a [ClassCastException] exception if the object is not a person (line 48); this is the equivalent of [(Person) o] in Java. [o as? String] is the “safe” type conversion: it returns [null] instead of throwing an exception, which works well with the Elvis operator ([o as? String ?: "default"]). With smart type conversion, this is rarely needed.

Results:

> gradlew -q run -Pmain=classes25.Classes25Kt
Jean is of type Individual
Lise is of type Individual
Lucile is of type Teacher
Marc is of type Teacher
Paul is of type Student
Jean is an adult
Lucile is an adult
Marc is an adult
Paul is an adult
Jean is 30 years old
Lise is a minor
Lucile teaches in section 61
Marc teaches computer science
Paul studies computer science
a string of 8 characters
the positive integer 42
the null reference
Jean : individual
Lise : individual
Lucile : teacher
Marc : teacher
Paul : student
first=Jean, string=null
ClassCastException : class classes25.Individual cannot be cast to class java.lang.String (classes25.Individual is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')

If you remove the case [Student] from the function [category], the compiler flags it:

Classes25.kt:71:27: error: 'when' expression must be exhaustive. Add the 'is Student' branch or an 'else' branch.

This is the great benefit of combining sealed types with the expression [when]: if a fourth class is added to the hierarchy, the compiler flags all instances of [when] as needing to be completed. Note also that the [is] test corresponds to the types that implement the interface: the type-checking loop (lines 16–20) has identified the three types of people. The [when] test on types is a useful replacement for cascading [if (o is ...)] tests; however, it should not replace polymorphism: when behavior depends on the object’s type and you have control over the class hierarchy, a method redefined in each class is often the object-oriented solution. Filtering using [when] is well-suited for data hierarchies (sealed data classes) on which you want to write processing logic external to that data.

4.14. Handling the null value

In Java, any reference-type variable can be null, and using a null reference causes the NullPointerException, the most common runtime error. We saw in Chapter 3 how Kotlin handles this: the compiler distinguishes between types that can be [null] ([String?]) and those that cannot ([String]), and only allows access to the members of a nullable reference after a check, or through the operators [?.], [?:], and [!!]. Where Java uses the [Optional<T>] class, the [Objects.requireNonNull] and [Objects.requireNonNullElse] methods, or annotations such as those in the JSpecify library ([@Nullable], [@NullMarked]), Kotlin relies on the type system itself. Here, we explore these concepts in more detail in the context of classes: nullable properties, late-initialized properties, and the boundary with Java code. The program [Classes26.kt] uses the example from the Java version:

package classes26

// handling the null value in classes: nullable types, ?., ?:, ?.let, requireNotNull, !!,
// lateinit, platform types
fun main() {
    val jean = Person("Jean", "Dupont")
    jean.middleName = "Marie"
    val persons = listOf(jean, Person("Paul", "Martin"))
    for (p in persons) {
        // a default value when the reference is null: the Elvis operator
        val middle = p.middleName ?: "(none)"
        // the safe call ?.: null if the reference is null
        val length = p.middleName?.length
        println("${p.firstName} : middle name=$middle, length=$length")
    }

    // a function that may find nothing: it returns a Person?
    var found = find(persons, "Durand")
    println("Durand found: ${found != null}")
    println(found?.firstName ?: "Durand: person not found")
    // the reference is tested before being used: smart cast
    found = find(persons, "Martin")
    if (found != null) {
        println("Martin : ${found.firstName}")
    }
    // ?.let: the block is executed only if the reference is not null
    find(persons, "Dupont")?.let { it.middleName = "Louise" }
    find(persons, "Durand")?.let { it.middleName = "Louise" }
    println(jean.middleName)
    // requireNotNull: we assert that the reference is not null, otherwise an exception is thrown
    val martin = requireNotNull(find(persons, "Martin")) { "Martin cannot be found" }
    println(martin.firstName)
    try {
        requireNotNull(find(persons, "Durand")) { "Durand cannot be found" }
    } catch (ex: IllegalArgumentException) {
        println("Error: ${ex.message}")
    }
    // the !! operator: a NullPointerException if the reference is null
    try {
        val lise = Person("Lise", "Morel")
        println(lise.middleName!!.length)
    } catch (ex: NullPointerException) {
        println("Error: $ex")
    }

    // a lateinit property: initialized after construction
    val paul = persons[1]
    println("Paul's email initialized: ${paul.hasEmail}")
    try {
        println(paul.email)
    } catch (ex: UninitializedPropertyAccessException) {
        println("Error: ${ex.message}")
    }
    paul.email = "paul.martin@example.com"
    println("Paul's email: ${paul.email}, initialized: ${paul.hasEmail}")

    // platform types: a Java method can return null
    val variable = System.getenv("NONEXISTENT_VARIABLE")
    println("variable=$variable, length=${variable?.length}")
    try {
        val value: String = System.getenv("NONEXISTENT_VARIABLE")
        println(value.length)
    } catch (ex: NullPointerException) {
        println("Error: ${ex.message}")
    }
}

// search for a person by last name: null if not found
fun find(persons: List<Person>, lastName: String): Person? = persons.firstOrNull { it.lastName == lastName }

class Person(val firstName: String, val lastName: String) {
    // optional property: null means "no middle name"
    var middleName: String? = null

    // mandatory property, but initialized after construction
    lateinit var email: String

    // has the lateinit property been initialized?
    val hasEmail: Boolean
        get() = this::email.isInitialized
}
  • Lines 71–80: The class [Person] has two required properties, of type [String], initialized by the primary constructor, and one optional property, [middleName], of type [String?]: [null] means “no middle name.” The constructor does not need to validate its parameters as the Java version did with [Objects.requireNonNull]: the compiler rejects a call to [Person(null, "Morel")] ([null cannot be a value of a non-null type 'String']). The error is detected even before execution;
  • line 11: the Elvis operator [?:] returns the left-hand value if it is not equal to [null], and the right-hand value otherwise. This is equivalent to [Objects.requireNonNullElse(value, defaultValue)];
  • line 13: the safe call [?.] returns [null] if the reference is [null], and otherwise the length of the string: [length] is of type [Int?]. Direct access to [p.middleName.length] is rejected by the compiler: [only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'];
  • line 69: the function [find] returns the person being searched for, or [null] if it cannot find them: its return type, [Person?], warns the caller that the result may be missing, and the compiler requires the caller to handle this case in order to use the person. This is the role played by [Optional<Person>] in Java, without any additional objects. The extension function [firstOrNull] returns the first element in the list that satisfies the condition, or [null];
  • line 19: the presence of a value is tested by comparing it to [null] (the equivalent of [isPresent()]);
  • line 20: the combination [?.] followed by [?:] converts any person present into their first name and provides a default value. This is equivalent to [found.map(Person::getFirstName).orElse(...)];
  • Lines 23–24: After the [found != null] test, intelligent type conversion changes [found] to [Person] within the block: [found.firstName] is accessed directly. Smart type conversion works here even though [found] is a variable of type [var], because it is not modified between the test and its use;
  • Lines 27–28: [x?.let { ... }] executes the block only if [x] is not equal to [null]; within the block, [it] refers to the value, which is of a non-nullable type. This is equivalent to [ifPresent]. Dupont exists and is assigned a new middle name; Durand does not exist, and the statement does nothing;
  • Line 31: [requireNotNull(value) { message }] returns the value if it is not [null], as a non-nullable type, and otherwise throws a [IllegalArgumentException] exception with the specified message (lines 34–36). It is used when the absence of a value constitutes an error: it is the equivalent of [orElseThrow]. Its counterpart, [checkNotNull], throws a [IllegalStateException] exception;
  • Lines 40–41: The operator [!!] checks whether the reference is equal to [null] and throws the exception [NullPointerException] if it is. Here, the exception has no message: we prefer [requireNotNull] or [checkNotNull], which do provide one. The operator [!!] should be reserved for cases where you are certain… and where the compiler cannot know this;
  • Line 76: a [lateinit] property (“initialized late”). A non-nullable property must normally be initialized at construction. But some values are only known afterward: a dependency injected by Spring (Chapter 6), an object prepared by a test initialization method, etc. Rather than declaring the property as nullable—and having to write [?.] or [!!] each time it is used—we declare it as [lateinit var]: it is of type [String] and is used as such. A property named [lateinit] must be a [var] of a non-nullable, non-primitive type ([lateinit var n: Int] is prohibited);
  • lines 50–51: Reading a [lateinit] property before it is initialized throws the [UninitializedPropertyAccessException] exception, whose message names the property; this is clearer than a [NullPointerException] exception occurring later;
  • lines 79–80: The expression [this::email.isInitialized] indicates whether the property has been initialized. [this::email] is a property reference (Chapter 7); this test is permitted only within the class that declares the property;
  • lines 58–62: the boundary with Java. The Java method [System.getenv(name)] returns the value of an environment variable, or [null] if it does not exist; however, nothing in its Java signature indicates this to the Kotlin compiler. Its return value has a platform type, denoted as [String!] in compiler messages and in IntelliJ IDEA: a type for which Kotlin does not know whether it is nullable, and which it allows the developer to use as a *[String] or as a *[String?], at their own risk. On line 58, the variable [variable] is of the platform type, and we treat it cautiously as [?.]. On line 61, we assert that it is not null by declaring it as [String]: the compiler then inserts a check, and the [NullPointerException] exception is thrown immediately upon assignment, with a message that identifies the offending method ([getenv(...) must not be null]), rather than later, during use. When the Java code is annotated ([@Nullable], [@NonNull] from JSpecify, or from JetBrains), Kotlin takes this into account, and the type is no longer a platform type: this is the case for much of Spring Framework 7, which we will be using.

Results:

> gradlew -q run -Pmain=classes26.Classes26Kt
Jean : middle name=Marie, length=5
Paul : middle name=(none), length=null
Durand found: false
Durand: person not found
Martin : Paul
Louise
Paul
Error: Durand cannot be found
Error: java.lang.NullPointerException
Paul's email initialized: false
Error: lateinit property email has not been initialized
Paul's email: paul.martin@example.com, initialized: true
variable=null, length=null
Error: getenv(...) must not be null

The Java class *[Optional]* remains usable in Kotlin when a Java library provides one (the extension function ***[getOrNull()] from the package *[kotlin.jvm.optionals] then converts it to a nullable type), but we do not use it in the Kotlin code: a nullable type does the same thing—verified by the compiler—without creating an object. In this course, we declare only optional values as nullable; we prefer objects that are always complete (constructed with all their required values) to objects that are populated incrementally; we reserve [lateinit] for properties initialized by a grid, and we handle values coming from Java with caution.

4.15. Example Application - V2

We revisit the tax calculation already covered in the previous chapter (version 1) and now implement it using classes and interfaces. Let’s review the problem. We aim to write a program to calculate a taxpayer’s tax. We’ll consider the simplified case of a taxpayer who has only their salary to report (the 2004 tax schedule for 2003 income, used as-is: this is an exercise):

  • we calculate the employee’s number of shares: shares = nbChildren / 2 + 1 if they are unmarried, nbChildren / 2 + 2 if he is married, where nbChildren is the number of his 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 three fields. To calculate tax I, find the first row where QF ≤ the limit. For example, if QF = 5,000, the row (8,382; 0.068; 291.09) will be found. Tax I is then equal to 0.068 × R - 291.09 × shares. If QF is such that the condition QF ≤ limit is never satisfied, then the coefficients of the last row are used, resulting in tax I = 0.481 × R - 9,505.54 × shares.

The application creates the package [taxv2]. The calculation classes are organized in a subpackage [taxv2.tax]:

1
2
3
4
5
6
7
src/main/kotlin/taxv2/
  Main.kt                   the dialog program with the user
  tax/
    TaxBracket.kt           a bracket of the tax scale
    ITax.kt                 the tax calculation interface
    AbstractTax.kt          an abstract class implementing ITax
    HardwiredTax.kt         a derived class, with hard-coded data

First, we define a type capable of encapsulating a row from the previous array [TaxBracket.kt]:

1
2
3
4
5
6
package taxv2.tax

import java.math.BigDecimal

// a bracket of the tax scale
data class TaxBracket(val limit: BigDecimal, val coeffR: BigDecimal, val coeffN: BigDecimal)

We use a data class:

  • because a bracket is a small piece of data without an identity, defined solely by its value, and there is no reason for it to be modified once created: its properties are [val];
  • because the compiler generates [toString], the equality operator, [copy], and destructuring from a single line.

The properties are of type [BigDecimal] (package [java.math], hence the import on line 3): Kotlin uses this class directly from JDK. The types [Float] and [Double] represent numbers in binary: a decimal number such as 0.068 has no exact representation in these types, and calculations accumulate small rounding errors. The [BigDecimal] class represents decimal numbers exactly: this is the type to use for amounts. In Java, its drawback is the lack of operators: you have to write [a.add(b)], [a.multiply(b)], [a.compareTo(b)]. The Kotlin library adds the operators [+], [-], [*], [/], [%], and the unary minus operator; and since [BigDecimal] implements [Comparable], the comparison operators [<], [<=]… also apply. We will take advantage of this, with one exception: division.

Then we define an interface [ITax] capable of calculating the tax [ITax.kt]:

1
2
3
4
5
6
package taxv2.tax

interface ITax {
    // tax calculation for a taxpayer
    fun calculate(married: Boolean, nbChildren: Int, salary: Int): Int
}
  • Line 5: the method for calculating the tax based on three pieces of data: the taxpayer’s marital status, the number of children, and annual salary. It rounds the tax down to the nearest euro.

Next, we define an abstract class that implements this interface [AbstractTax.kt]:

package taxv2.tax

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

abstract class AbstractTax : ITax {
    // the tax brackets needed for the calculation come from an external source
    // that only the derived classes know
    protected abstract val taxBrackets: List<TaxBracket>

    // tax calculation
    override fun calculate(married: Boolean, nbChildren: Int, salary: Int): Int {
        // calculation of the number of shares
        var shares = nbChildren.toBigDecimal().divide(BigDecimal.TWO) + (if (married) 2 else 1).toBigDecimal()
        if (nbChildren >= 3) {
            shares += BigDecimal("0.5")
        }
        // calculation of the taxable income and of the family quotient
        val income = BigDecimal("0.72") * salary.toBigDecimal()
        val quotient = income.divide(shares, MathContext.DECIMAL128)
        // search for the tax bracket: the last one is used if no limit fits
        val brackets = taxBrackets
        val bracket = brackets.dropLast(1).firstOrNull { quotient <= it.limit } ?: brackets.last()
        // tax calculation
        return (income * bracket.coeffR - shares * bracket.coeffN).toInt()
    }
}
  • Line 1: The class belongs to the package [taxv2.tax]; its file is stored in the folder [taxv2/tax];
  • line 6: the class [AbstractTax] implements the interface [ITax];
  • line 9: the scale data, provided by a protected abstract property (the Java version used an abstract method [getTaxBrackets()]). The class [AbstractTax] does not know where this data comes from (written in the code, read from a file, a database, etc.). It leaves this task to the derived classes. This is why it is declared abstract, which prevents it from being instantiated. The abstract property requires derived classes to provide the tax brackets: the compiler verifies this. Its type, [List<TaxBracket>], is that of a read-only list: the class cannot modify the pay scale provided to it;
  • Lines 12–25: The implementation of the [calculate] method of the [ITax] interface. Derived classes will not need to override this method. The [AbstractTax] class thus serves as a base class for the derived classes: it contains what is common to all of them;
  • line 14: the number of shares. The extension function [toBigDecimal()] converts an integer to [BigDecimal] (such as [BigDecimal.valueOf(n)]); division by [BigDecimal.TWO] is exact (3 / 2 = 1.5), unlike integer division by [nbChildren / 2] (3 / 2 = 1). The expression [if (married) 2 else 1] equals 2 for a married taxpayer, 1 otherwise. The addition is written using the operator [+]. Division, on the other hand, is performed using Java’s [divide] method, not the [/] operator: Kotlin’s [/] operator applied to [BigDecimal] preserves the number of decimal places in the dividend and rounds the result; [3.toBigDecimal() / BigDecimal.TWO] equals 2, not 1.5! This is a known pitfall that is best avoided by always writing divisions of [BigDecimal] with [divide], which forces you to consider the desired precision;
  • Lines 15–16: the additional half-share. A [BigDecimal] is immutable: [shares += x] is converted to [shares = shares + x], which creates a new object assigned to the variable; which is why [shares] is a [var];
  • Lines 19–20: taxable income and the family quotient. Decimal constants are created from strings ([BigDecimal("0.72")]) to ensure they are exact. Dividing a [BigDecimal] can result in an infinite number of decimal places (1 / 3): we specify the desired precision, in this case [MathContext.DECIMAL128] (34 significant digits);
  • line 23: search for the interval, in a single line. [brackets.dropLast(1)] returns the list of brackets except for the last one; [firstOrNull { quotient <= it.limit }] searches for the first bracket whose limit is greater than or equal to the family quotient (the operator [<=] calls [compareTo]), or returns [null] if there are none; the Elvis operator then returns the last bracket ([brackets.last()]). The Java version used a [for] loop and a [break]; the two expressions are equivalent;
  • line 25: the tax calculation, written using the operators [*] and [-], is much more readable than [income.multiply(bracket.coeffR()).subtract(shares.multiply(bracket.coeffN()))]. The function [toInt] truncates the result to its integer part.

A class that implements the [ITax] interface can be created by deriving from the [AbstractTax] class. That is what we are doing now with [HardwiredTax.kt]:

package taxv2.tax

class HardwiredTax : AbstractTax() {
    // the tax brackets, built from the hard-coded data
    override val taxBrackets: List<TaxBracket> = LIMITS.indices.map { i ->
        TaxBracket(LIMITS[i].toBigDecimal(), COEFF_R[i].toBigDecimal(), COEFF_N[i].toBigDecimal())
    }

    companion object {
        // tax calculation data (2004 tax scale on 2003 income), "hard-coded"
        private val LIMITS = doubleArrayOf(4962.0, 8382.0, 14753.0, 23888.0, 38868.0, 47932.0, 0.0)
        private val COEFF_R = doubleArrayOf(0.0, 0.068, 0.191, 0.283, 0.374, 0.426, 0.481)
        private val COEFF_N = doubleArrayOf(0.0, 291.09, 1322.92, 2668.39, 4846.98, 6883.66, 9505.54)
    }
}
  • Lines 9–13: The [HardwiredTax] class hardcodes the data needed to calculate the tax. This data is stored in the companion object and is shared by all instances, similar to the [static final] fields in the Java version. These are arrays of [Double] ([DoubleArray], created by [doubleArrayOf]); their elements are written as [4962.0] and not [4962], since there is no implicit conversion from [Int] to [Double]. By convention, constant names are written in uppercase;
  • Lines 5–6: The implementation of the parent class's abstract property, initialized when the object is constructed. [LIMITS.indices] is the range of array indices (0..6); the function [map] constructs the list of lambda results for each of them, in this case one bracket per index. The extension function [Double.toBigDecimal()] converts a [Double] to a [BigDecimal] via its shortest decimal representation: [0.068.toBigDecimal()] is exactly 0.068, just like [BigDecimal.valueOf(0.068)] in Java. Do not use the [BigDecimal(0.068)] constructor, which reproduces all the decimal places of the approximated binary value (0.068000000000000004884981308350688…);
  • the list returned by [map] is exposed as type [List<TaxBracket>], read-only: no class will be able to modify the scale through this property (the Java version created a read-only copy using [List.copyOf]). The redefined property retains the visibility ([protected]) of the abstract property.

The next chapter will introduce additional classes derived from [AbstractTax], which will read the brackets from a text file or JSON: they will only need to provide the property [taxBrackets].

A test program might look like the following [Main.kt]:

// interactive tax calculation program
// the user types three data items on the keyboard: married nbChildren salary
// the program then displays the tax to pay
package taxv2

import taxv2.tax.HardwiredTax
import taxv2.tax.ITax

// the help message
private val SYNTAX = """
    syntax: Married NbChildren Salary
    Married: y for married, n for not married
    NbChildren: number of children
    Salary: annual salary in euros
""".trimIndent()

fun main() {
    // creation of an ITax object
    val tax: ITax = HardwiredTax()

    // input loop
    while (true) {
        // we ask for the tax calculation parameters
        print("Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: ")
        val parameters = readlnOrNull()
        // end of standard input or empty line: we stop
        if (parameters.isNullOrBlank()) {
            break
        }
        // checking the number of fields of the typed line
        val fields = parameters.trim().split(Regex("\\s+"))
        if (fields.size != 3) {
            println(SYNTAX)
            continue
        }
        // checking the validity of the parameters
        // married
        val married = fields[0].lowercase()
        if (married != "y" && married != "n") {
            println("$SYNTAX\nIncorrect married argument: type y or n")
            continue
        }
        // nbChildren
        val nbChildren = positiveInteger(fields[1])
        if (nbChildren == null) {
            println("$SYNTAX\nIncorrect NbChildren argument: type a positive or zero integer")
            continue
        }
        // salary
        val salary = positiveInteger(fields[2])
        if (salary == null) {
            println("$SYNTAX\nIncorrect salary argument: type a positive or zero integer")
            continue
        }
        // the parameters are correct: we calculate the tax
        println("Tax=${tax.calculate(married == "y", nbChildren, salary)} euros")
    }
}

// returns the positive or zero integer represented by the string, null if the string does not represent one
fun positiveInteger(string: String): Int? = string.toIntOrNull()?.takeIf { it >= 0 }

The program above allows the user to perform repeated tax calculation simulations.

  • lines 4–7: The main program is in the package [taxv2]; it imports the two types it needs from the subpackage [taxv2.tax]. The header comment may precede the package declaration;
  • Lines 10–15: The help message is a top-level property, private to the file, written as a raw string delimited by three quotation marks; the string may span multiple lines. The function [trimIndent()] removes the common indentation from the lines, as well as the first and last lines if they are empty: the string therefore does not end with a line break. A [const val] property is not possible here, since the value is calculated by a function;
  • Line 19: Creation of a [tax] object that implements the [ITax] interface. This object is obtained by instantiating a type [HardwiredTax], a type that implements the interface [ITax]. Note that we did not let the compiler infer the type *[HardwiredTax] from the variable *[tax]*; instead, we explicitly assigned it the type *[ITax]. By writing this, we indicate that we are only interested in the method [calculate] of the object [tax] and not in the rest. To use data from a file, simply change this single line;
  • lines 22–56: the loop for tax calculation simulations;
  • lines 24–25: the three parameters required for the [calculate] method are entered in a single line typed on the keyboard. [print] displays the message without wrapping to the next line, then [readlnOrNull()] reads a line; it returns [null] if standard input is closed;
  • line 27: the extension function [isNullOrBlank], defined on the type [String?], is true if the string is [null] or contains only spaces; the program then terminates. After this test, the compiler knows that [parameters] is not [null] (the function indicates this to the compiler via a “contract”): it allows the call to [parameters.trim()] without [?.];
  • line 31: the function [split] splits the line—stripped of its leading and trailing spaces—into words separated by one or more whitespace characters (regular expression ["\\s+"]). It returns a [List<String>], whose size is tested using [size];
  • Lines 38–39: The function [lowercase] converts the string to lowercase; the strings are compared using [==] and [!=], which compare their contents;
  • lines 44–51: The function [positiveInteger] (line 61) checks that the last two fields are positive integers or zero. It returns a [Int?]: [null] signals an error, whereas the Java version returned the value -1. [toIntOrNull()] returns [null] if the string does not represent an integer (instead of throwing an exception like [Integer.parseInt]), and [takeIf { it >= 0 }] returns the value if the condition is true, and [null] otherwise. After the tests [nbChildren == null] and [salary == null], followed by [continue], the compiler knows that the two variables are not equal to [null]: they are of type [Int] for the remainder of the loop;
  • line 56: call to the [calculate] method of the [tax] object implementing the [ITax] interface.

Here is an example of the program’s execution, launched from the [04-classes] project folder. The input, provided via standard input redirection, has been reinserted into the display at the points where the program reads it, just as it would appear when entered via the keyboard:

> gradlew -q run -Pmain=taxv2.MainKt
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: q s d
syntax: Married NbChildren Salary
Married: y for married, n for not married
NbChildren: number of children
Salary: annual salary in euros
Incorrect married argument: type y or n
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: y 2 d
syntax: Married NbChildren Salary
Married: y for married, n for not married
NbChildren: number of children
Salary: annual salary in euros
Incorrect salary argument: type a positive or zero integer
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: q s d f
syntax: Married NbChildren Salary
Married: y for married, n for not married
NbChildren: number of children
Salary: annual salary in euros
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: y -1 60000
syntax: Married NbChildren Salary
Married: y for married, n for not married
NbChildren: number of children
Salary: annual salary in euros
Incorrect NbChildren argument: type a positive or zero integer
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: y 2 60000
Tax=4282 euros
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: y 3 60000
Tax=2959 euros
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop: n 0 60000
Tax=11519 euros
Tax calculation parameters in the format: Married (y/n) NbChildren Salary, or nothing to stop:

The tax calculation values are correct: 4,282 euros for a married taxpayer with two children and an annual salary of 60,000 euros (line 25 of the results), and 2,959 euros with three children (line 27). An unmarried taxpayer with no children pays 11,519 euros for the same salary (line 29). These are the results from the Java version.

Compared to Version 1, the calculation is now encapsulated in classes behind an interface: the dialog program only recognizes the [ITax] interface. Subsequent versions will take advantage of this separation: Version 3 (next chapter) will change the data source by deriving new classes from [AbstractTax], and then the application will be organized into layers (Chapter 6).