Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Constructors

Constructors are responsible for creating instances of a class. Kotlin classes are generally defined with a primary constructor, providing the main way of creating an object:

class Point(var x: Double, var y: Double)

In this example, the primary constructor specifies that two values of type Double, representing a point’s \(x\) and \(y\) coordinates, need to be provided whenever we try to create a Point. These values will be used to initialize properties named x and y.

But what if we want to create objects of this class in other ways? For example, what if we want there to be a ‘default point’ of \( (0,0) \), created when no values are provided for x and y?

This could achieved by specifying default arguments of 0.0 in the constructor:

class Point(var x: Double = 0.0, var y: Double = 0.0)

However, this isn’t entirely satisfactory. It allows us to create a Point by supplying a value for only one of the two coordinates, and this feels unintuitive.

Secondary Constructors

A better solution to this problem is to define an additional constructor with an empty parameter list. This secondary constructor is specified within the body of the class, using the constructor keyword:

class Point(var x: Double, var y: Double) {
    constructor(): this(0.0, 0.0)
}

The parameter list of this secondary constructor is followed immediately by a colon, then this(0.0, 0.0). This syntax invokes the primary constructor, providing values of 0.0 for parameters x and y.

Important

A secondary constructor must always delegate either to the primary constructor or to another secondary constructor, using the this() syntax shown above.

If required, the secondary constructor can have a body, enclosed in {}, which can do other work, but it always has to delegate.

This ensures that the primary constructor is ultimately always used as part of the process of initializing an object.

Task 12.3.1

  1. Edit the file Construct.kt, in the tasks/task12_3_1 subdirectory of your repository. You should see the following code:

    class Point(var x: Double, var y: Double)
    
    fun main() {
        val p = Point(4.5, 7.0)
        println("(${p.x}, ${p.y})")
    }
    

    Make sure that this code compiles.

  2. Change main() so that it attempts to create the Point object without specifying values for x and y. Try compiling the code. What errors do you see?

  3. Modify the class definition so that it includes the secondary constructor described above. Compile and run the program to verify that this fixes the issue.

  4. Now change the first line of main() to be

    val p = Point(4, 7)
    

    Try recompiling the code. What happens?

  5. Add another secondary constructor to Point that fixes the issue.

    Hints

    • Your secondary constructor will need parameters. What type do they need to be?

    • Note that these parameters should NOT be prefixed with val or var. Those keywords are used to specify properties, and you are only allowed to do that in the primary constructor parameter list or separately in the body of the class.

    • Remember that Kotlin has extension functions to support type conversion. For example, you can invoke toDouble() on a number to create a Double representation of that number…

Another Example

Consider this class definition:

import java.time.LocalDate

class Person(var name: String, val birth: LocalDate) {
    var isMarried = false
}

This represents a person using three properties: their name, their date of birth, and their marital status. Notice that the latter is defined and initialized in the body of the class, rather than being specified as part of the primary constructor.

We are free to mix these different styles of property definition and initialization as we see fit. The design decision made here was that when we represent a person, it should be assumed that they are not married by default.

Notice also that properties name and isMarried are defined using var, but birth is defined using val. This is because a person’s date of birth is a fixed value that can never change, whereas their name and marital status can both change during their lifetime.

Task 12.3.2

  1. Edit the file named Person.kt, in the tasks/task12_3_2 subdirectory of your repository. Add to this file the Person class definition shown above. Be sure to include the import statement as well.

  2. Now add a main() function that creates a Person object and then prints the person’s name, date of birth and marital status.

    Note: you can create a LocalDate object to represent date of birth like this:

    val date = LocalDate.of(1997, 8, 23)
    

    Check that your program compiles and runs successfully.

  3. Dates are often manipulated as strings, in ISO 8601 format (e.g., "1997-08-23"). Add a secondary constructor to Person that allows date of birth to be supplied as such a string.

    Hint

    Your secondary constructor will need to convert a string to a LocalDate object before delegating to the primary constructor. You can do this by passing the string to the LocalDate.parse() method.

  4. Modify main() so that when the Person object is created, date of birth is now specified as a string. Recompile and run the program to verify that it behaves as expected.

Initializer Blocks

There’s a problem with the Person class. In the implementation shown above, it is possible to create a Person object using any string as the person’s name. It would be better if we could impose some restrictions—e.g., preventing use of an empty string, or a string consisting solely of spaces. However, there is no way do to this currently, because the primary constructor just assigns values to properties.

There are a couple of different ways of solving this problem. One of them is to give the Person class an initializer block.

Initializer blocks are blocks of code enclosed in braces, marked with the init prefix. Any blocks marked in this way will be injected into the object construction process. You can do almost anything you like inside an initializer block, but a common thing to do is property validation:

import java.time.LocalDate

class Person(var name: String, val birth: LocalDate) {
    var isMarried = false

    init {
        require(name.isNotBlank()) { "Name cannot be blank" }
    }
}

In the example above, the name parameter is checked to make sure that it is not blank, i.e., that its length is greater than zero and its contents do not consist solely of whitespace characters. If this is not true, an IllegalArgumentException will be thrown and object construction will fail.

Note that initializer blocks are effectively incorporated into the primary constructor, even in cases where that primary constructor is implicit rather than explicit. Thus they always execute when an instance of the class is created.

Task 12.3.3

  1. Copy the file Person.kt from the task12_3_2 subdirectory of your repository to the task12_3_3 subdirectory.

  2. Edit the copied file and modify main() so that it reads the user’s name and date of birth using readln(), then creates a Person object using these strings.

    Compile the program and run it to verify that you can create a Person with a zero-length name, or a name consisting only of spaces.

  3. Modify the class so that it has the initializer block shown above. Recompile the program and try running it again to verify that invalid names trigger an exception.