zsmb.coEst. 2017



Primaries Matter (a discussion of constructors)

2019-08-12 • Márton Braun

Primary constructors play a fundamental role in Kotlin classes. Let’s take a close look at them, and really understand what exactly is part of a primary constructor, and what makes this constructor so special.

Back to Java for a moment

In Java, class creation isn’t exactly strict. The language lets you leave variables uninitialized without any complaints. Take this class for example:

public class Car {
    String model;
    int year;
    double miles;
}

It has three fields, and an implicit constructor with no parameters. When you call its constructor with new Car(), all of these fields will be initialized to implicit default values: null, 0, and 0.0, respectively. In general, primitive types are initialized to some resemblance of 0, while reference types are initialized to null.

Note that the latter will happen even when annotations such as @NotNull are present on a field, as these are just markers, and are not enforced by the compiler.

Kotlin’s safety guarantees

In contrast, Kotlin is very strict about creating instances. A class like this does not compile in Kotlin, because initializing each property when an instance is created is mandatory. Even nullable variables won’t initialize to null by default - you have to be explicit.

class Car {
    val model: String
    val year: Int
    var miles: Double
}

e: Property must be initialized or be abstract

This forces you to explicitly initialize every value in one way or another, and guarantees that your properties won’t have implicit values stored in them. Whenever you read a property, you’ll get a value out of it that you put there. In the case of non-nullable properties, this also guarantees that you won’t ever read null unexpectedly, as you can’t assign a null value to a non-nullable property.

The primary constructor

There are two ways to initialize these properties. You can initialize them inline at their declarations, or in one or more initializer (init) blocks.

class Car(model: String, year: Int) {
    val model: String = model
    val year: Int
    var miles: Double = 0.0

    init {
        this.year = year
    }
}

These two kinds of initializations are performed from top to bottom, in order. In the example, model and miles would be initialized first, and then finally year would get its value. Any parameters that the primary constructor takes may be used for these initializations.

The IDE warnings at this point will suggest for us to join the declaration and assignment of year, and to move both model and year into the primary constructor. Properties in the primary constructor will be initialized before anything in the body of the class, and again, they’ll be initialized in order.

class Car(val model: String, val year: Int) {
    var miles: Double = 0.0
}

Previously initialized variables will also be in scope during initialization if you want to rely on their values:

class Car(val model: String, val year: Int) {
    var miles: Double = 0.0

    val age: Int

    init {
        age = getCurrentYear() - year
    }

    val description: String = "$model ($age years, $miles miles)"
}

We can only initialize description this way after age has been initialized. If we placed it before the init block, we’d again see an error:

e: Variable ‘age’ must be initialized

(This example class has a bug in it - see if you can spot it!)


If we decompile the bytecode produced for this class using the decompiler of the Kotlin IDEA plugin, we’ll see this corresponding Java source (comments added):

public final class Car {
    private double miles;
    private final int age;
    @NotNull
    private final String description;
    @NotNull
    private final String model;
    private final int year;

    // Getters & setters ...

    public Car(@NotNull String model, int year) {
        // Runtime null checks for params with reference types
        Intrinsics.checkParameterIsNotNull(model, "model");

        // Properties in the primary constructor
        this.model = model;
        this.year = year;

        // Initialization at the declaration
        // (This is actually optimized away if we init to 0)
        this.miles = 0.0D;

        // init block
        this.age = Utils.getCurrentYear() - this.year;

        // Initialization at the declaration
        this.description = this.model + " (" + this.age + " years, " + this.miles + " miles)";
    }
}

This shows us how all the different kinds of initializations end up in a constructor body together.


To review, the initialization order:

  • Properties in the primary constructor, in declaration order.
  • Initializations at property declarations and in initializer blocks, interleaved, in the order that they appear in the class body.

Essentially, you can read the initialization statements in the class top to bottom, and that’s what you’ll get in the “body” of the primary constructor.

In each of these initializations, you can use the values of:

  • Constructor parameters, whether or not they’re stored in properties.
  • Previously initialized (not just declared!) properties.

Due to these restrictions and safety guarantees, classes created via the primary constructor will always be in a valid state.

A representation of our single, primary constructor, which is in a valid state.

Secondary constructors

Of course, there are cases when you want to create class instances with different sets of parameters, which normally requires multiple constructors. Kotlin’s default parameter values make this possible to some extent while still keeping just a primary constructor. However, if you need a constructor that has entirely new parameters or parameters with entirely new types, you’ll need a secondary constructor.

For our example, let’s say we need to be able to create cars with a model, year, and mileage, all provided as strings. Our primary constructor can’t accommodate these parameters, so it’s time to write a new one. This could be our first attempt:

constructor(
        model: String,
        year: String,
        mileage: String
) {
    this.model = model
    this.year = year.toInt()
    this.miles = mileage.toDouble()
}

This code would fail at constructing a valid Car instance, and so it doesn’t compile (though it certainly would in Java). For example, it doesn’t set the age and description properties of the instance, which we expect to be initialized by every constructor.

We also get an error for trying to set year and miles here: Val cannot be reassigned. As a val can only be initialized once, that one initialization will always have to happen in the primary constructor.

A secondary constructor that doesn’t call the primary constructor is invalid.

The fix, and the rule for secondary constructors is simple: it has to first call the primary constructor, and only after that can it perform further initialization on the instance that was created. After the primary constructor is called by the secondary constructor, the instance is already in a guaranteed valid state, so it’s safe to operate on it in the body of the secondary constructor.

Let’s fix our constructor to invoke the primary constructor first:

constructor(
        model: String,
        year: String,
        mileage: String
): this(model, year.toInt()) {
    miles = mileage.toDouble()
}

A secondary constructor that calls the primary constructor directly.

The call to the primary constructor doesn’t have to be direct, it can also happen indirectly through calling another secondary constructor, but this chain eventually has to end in a call to a primary constructor.

Here’s an example of yet another new constructor, which calls the previous secondary constructor:

constructor(data: Array<String>) : this(
        model = data[1],
        year = data[3],
        mileage = data[7]
)

A secondary constructor that calls the primary constructor indirectly.

What we really have here is a graph of the various constructors in our class calling each other.

  • A primary constructor is valid if it initializes all properties.
  • A secondary constructor is valid if it eventually calls the primary constructor, i.e. if there’s a directed path to the node representing the primary constructor from the node of the secondary constructor.
    • This also means no cycles within secondary constructor nodes, and no disconnected nodes.
  • The entire class is valid if all constructors are valid.

A graph of constructors.

Without primaries

One last approach you can take is to not use a primary constructor at all. This is useful when trying to convert the inputs of the various constructors into one canonical form is just too cumbersome to do.

In this case, the strict initialization requirements will apply to the secondary constructors. If you declare a property that’s not initialized at its declaration or in an initializer block, it must be initialized by all secondary constructors.

You may still use those two ways of initializing properties that were used in the primary constructor before, and these will run before each of your secondary constructor’s bodies. This means that any initialization that makes use of constructor parameters will now have to be placed inside the secondary constructors, and potentially needs to be duplicated. This is the downside of not using a primary constructor: losing its superpowers of accessing parameters in the class body.

For example, you can no longer initialize age this way, since the initialization of year will only happen after the initializer block has run, in the body of each secondary constructor:

class Car {
    val model: String
    val year: Int
    var miles: Double = 0.0
    val age: Int

    init {
        age = getCurrentYear() - year
    }

    constructor(
            model: String,
            year: Int
    ) {
        this.model = model
        this.year = year
    }

    constructor(
            model: String,
            year: String,
            mileage: String
    ) {
        this.model = model
        this.year = year.toInt()
        this.miles = mileage.toDouble()
    }
}

Thankfully, this results in a compile time check, and an error:

e: Variable ‘year’ must be initialized

One solution for this would be to move the age initialization into each constructor separately:

constructor(
        model: String,
        year: Int
) {
    this.model = model
    this.year = year
    this.age = getCurrentYear() - year
}

constructor(
        model: String,
        year: String,
        mileage: String
) {
    this.model = model
    this.year = year.toInt()
    this.miles = mileage.toDouble()
    this.age = getCurrentYear() - year
}

In certain cases, you can get rid of this kind of duplication by calling one secondary constructor from another. In this case, the first constructor will guarantee that the class is already fully initialized, and the second one has no strict initialization requirements imposed on it.

constructor(
        model: String,
        year: Int
) {
    this.model = model
    this.year = year
    age = getCurrentYear() - year
}

constructor(
        model: String,
        year: String,
        mileage: String
) : this(model, year.toInt()) {
    miles = mileage.toDouble()
}

A secondary constructor delegating to another.

Visually, we’ve now lost the single node in the graph that we can delegate safe initialization to. Therefore, each secondary constructor has to perform correct initialization on its own merits, or call another, already known to be correct secondary constructor. We now have a forest instead of a tree.

A forest of secondary constructors

Thanks to /u/mariusmora and Ilya Gorbunov for pointing out that I was missing this section in the original version of the article.

Conclusion

That’s all you need to know about primary and secondary constructors in Kotlin! Hopefully you now have a better feel for how these constructors interact with each other, and how all initialization “flows” towards the primary constructor.

A similar topic is how constructor calls work with inheritance, which gets quite interesting when you have multiple constructors in your subclass. You can read about this - with similar visualizations - in my article about implementing View constructors with @JvmOverloads.



You might also like...

Building a macOS screen saver in Kotlin

This article documents how I created my own custom screen saver for macOS, built almost entirely in Kotlin, using the powers of Kotlin Multiplatform compiled to macOS. Join me for a tale of Kotlin success and macOS failures.

All About Opt-In Annotations

Have you ever encountered APIs that show warnings or errors when you use them, saying that they're internal or experimental? In this guide, you'll learn everything you need to know about opt-in APIs in Kotlin: how to create and use them, and all their nuances.

The conflation problem of testing StateFlows

StateFlow behaves as a state holder and a Flow of values at the same time. Due to conflation, a collector of a StateFlow might not receive all values that it holds over time. This article covers what that means for your tests.

Wrap-up 2021

Another year over, a new one's almost begun. Here's a brief summary of what I've done in this one.