Home > Software engineering >  Safe initialization of property using other abstract proprety
Safe initialization of property using other abstract proprety

Time:07-28

I want an abstract class declaring abstract val values: List<String> and val totalLen: Int. I'd like to initialize totalLen like this:

abstract class MyClass {
    abstract val values: List<String>
    val totalLen: Int = values.sumOf { it.length }
}

My editor warns me that I'm Accessing non-final property values in constructor . Is this something I can ignore, or will it come back and bite me. Using init block yields the same warning.

Putting values in primary constructor removes the warning. I'd like to avoid that so that the code extending the class would prettier.

// prettier
abstract class MyClass {
    abstract val values: List<String>
    val totalLen = values.sumOf {it.length}
}
class Child : MyClass() {
    override val values = listOf("a", "b")
    ...
}


// uglier
abstract class MyClassOther (val values: List<String>) {
    val totalLen = values.sumOf {it.length}
}
class ChildOther : MyClassOther(listOf("a", "b")) {
    ...
}

Here the aesthetic difference isn't big, but if i had more abstact and non abstract properties it would result in crowded call to the super-class constructor in extending class. Also, using sealed class for parrent doesn't help to remove IDE warning.

EDIT: found out about lazy delegate property. It solves this, but I'd still like to know if there is another solution without using delegates.

CodePudding user response:

The issue is a subtle one, but it can have serious consequences down the line.

Object construction works on the general principle that a superclass should be fully initialised before the subclass is initialised. (That includes any code in the constructor itself, along with any property initialisers and init blocks.)

This means that if you call anything overridable from a constructor, initialisers, or init blocks, it will run before the class is fully initialised, and before anything in any subclasses is initialised. Properties might not have their initial values set, invariants might not be established, and you can't rely on anything working.

The only safe course is never to call anything overridable from a constructor or initialiser. That's what your IDE is warning you about.

(In practice, if you control all the code, you may be able to get away with calling something that has no dependency on any subclass's state, nor on anything in the superclass that's not yet set up. But that's fragile, and can break if someone adds some innocent-looking code in a subclass.)

Have you tried to run your ‘prettier’ case? That demonstrates the problem very neatly, by throwing a NullPointerException when you try to construct a Child! Here's the order in which things would happen:

  • External code calls the Child() constructor.
    • That calls the MyClass constructor.
      • That initialises totalLen.
        • That gets the value of values. ← NullPointerException
    • Having completed the MyClass construction, it continues with the Child() constructor.
      • That initialises values.

I hope you can see the problem: when the MyClass initialiser runs, values is not yet initialised. At that point, its value is undefined; on the JVM, it'll always be null (or 0, or false, depending on the type) — on other platforms, it might be something random or impossible.


Your ‘uglier’ case works around that well: it creates the list before calling the superclass constructor, so you know it's safe to use. But, as you say, it would be pretty unclear if there were multiple properties and/or multiple levels of inheritance along the way.

Unfortunately, I don't know of any general workarounds for this.

As you say, making superclass initialisers lazy is one approach, but it doesn't fit every case.

Spring has a @PostConstruct annotation which lets you specify code to be run after the class (and any subclasses) have all be initialised (including any Spring autowiring and other magic), which can be a good solution if you're using Spring. Other frameworks might provide something similar.

You could do something similar in an ad-hoc way, if you know your lowest-level class(es) will be final: they could call a construction-completed superclass method at the end of their constructors. But of course that's not a general solution.

Ultimately, giving superclasses dependencies on their subclasses (instead of the other way around) seems like a bit of a code smell, and this is only one of the problems it can cause. You might look at your code and see whether inheritance is really needed, or whether composition might be a better approach.

  • Related