Why is the setter of a property not called on construction?

Recently I placed some validation logic into a custom setter and expected that this logic would be called when the object gets instantiated. But this is not the case – it is only called when the property value is changed after construction.

The following example shows that the setter is only called once

class Demo(x: Int) {
        
    var x = x
    	set(value) {
            println("field: $field, value: $value")
            field = value
        }
}

fun main() {
	val d = Demo(1)
    d.x = 2 // setter only called here
}

Output

field: 1, value: 2

Executable example

The expected output would be

field: 0, value: 1
field: 1, value: 2

There is another topic in this forum showing a nice solution. But what is the reason the setter is not called on object construction? Is there a deeper reason? Is that something that should be improved in the language (principle of least astonishment …)?

I’m not really sure why this is the case. One reason is that you can read the old value in the setter which can lead to NPEs during construction since there is no value yet.

When I need validation logic I prefer to call it as part of the init block. Something like this is an alternative:

class Demo(x: Int) {
        
    var x: Int = -1 // setter not called, we need a value for the not initialized case
    	set(value) {
            println("field: $field, value: $value")
            field = value
        }

    init {
        this.x = x  // this calls the setter
    }
}

fun main() {
    val d = Demo(1)
    d.x = 2 
}

I’m not sure, maybe. It is a surprising fact about kotlin and thus can lead to many bugs, but on the other hand it is valid to have setters that require the class to be properly initialized. Both scenarios can lead to bugs where code expects the class to be “valid” but has to handle “invalid” data.

This should however be documented and I can’t find this in the documentation. Well it kinda is documented here as part of a comment in the example. Not sure that is good enough.

I’ve used the init block at first, but that doesn’t solve all problems since the value is actually set twice then. In cases where the former value is relevant, like in a validation where the new value must be greater than the existing value, it simply doesn’t work with a setter + init block.

In that case there are 2 options:

  1. you don’t want the setter called in the constructor so you can start with any value => just use a normal property initialization setting the field directly
  2. you want a “min value” so you set the field to that value.

But how is that setter supposed to work without an initial value?

Option 2 is probably acceptable in most cases, but it is still a bit counter intuitive that the setter is not called in my my first example where the property is set directly (var x = x). However, since the previous value doesn’t matter in the case of the object construction, setting a default value would be ok. But then again: why doesn’t the compiler just set field to a reasonable default value like null, 0 or false then and initialize the field with the setter on construction?

I have no idea why the kotlin team decided to do this, but I guess one reason is that a variable of type T does not have a good default value. It’s not nullable so null does not work, but you can’t use anything else either.
But as I mentioned above, maybe it’s due to the fact that setters might require the object to be properly initialized, so calling the setter would create bugs that way.
I don’t think there is a perfect solution to this problem.

I would say the biggest reason is that when the property is set the object is not fully initialised (it cannot be guaranteed that calling the setter is valid). Imagine a setter that invokes change listeners. If the list of change listeners is declared later in the source than the setter the change list (even if a non-nullable list) will actually have a null value and looping over all listeners will throw a null pointer exception. There are many other things that can be validly done to a fully initialised object, but not to a partially initialised object.

3 Likes

I created an issue here for this to be documented more clearly. I’m not sure how many people will actually read the docs, but I guess it might help some people.
Also KT-6624 confirms that this is intended behavior.

3 Likes