Search code examples
kotlinkotlin-null-safety

Require in superclass' init block raises IllegalArgumentException [Kotlin]


Good Morning Kotlin gurus.

I have an inheritance structure in which the abstract superclass implements some shared of data checks. The compiler does not complain, but upon execution an IllegalArgumentException is thrown by the JVM

The Code

fun main(args: Array<String>) {
    val foo = Child("NOT_BLANK")
}

abstract class Parent(
    open val name: String = "NOT_BLANK"
) {
    init {
        require(name.isNotBlank()) { "Firstname must not be blank" }
    }
}

data class Child(
    override val name: String = "NOT_BLANK"
) : Parent(
    name = name
)

The exception looks as follows

Exception in thread "main" java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.text.StringsKt__StringsJVMKt.isBlank, parameter $receiver
at kotlin.text.StringsKt__StringsJVMKt.isBlank(StringsJVM.kt)
at com.systemkern.Parent.<init>(DataClassInheritance.kt:24)
at com.systemkern.Child.<init>(DataClassInheritance.kt:30)
at com.systemkern.DataClassInheritanceKt.main(DataClassInheritance.kt:17)

Thanks for your time

all the best


Solution

  • On name.isNotBlank(), you should be getting a lint warning like this:

    Accessing non-final property name in constructor

    You're accessing the name property at the time when the Parent is being constructed, however, name is overridden in Child. This override means that internally, both the Parent and Child classes have private fields for name, and since the Parent constructor is the first thing called when a Child is created, the Child's name won't be initialized yet, but the check in Parent will access the Child's override of the property when it does the check.

    That might sound complicated, here are the relevant parts of the decompiled bytecode of your example (simplified):

    public abstract class Parent {
        @NotNull
        private final String name;
    
        @NotNull
        public String getName() {
            return this.name;
        }
    
        public Parent(@NotNull String name) {
            super();
            this.name = name;
            if(!StringsKt.isBlank(this.getName())) { // uses getName, which is overridden in
                                                     // Child, so Child's field is returned
                throw new IllegalArgumentException("Firstname must not be blank");
            }
        }
    }
    
    public final class Child extends Parent {
        @NotNull
        private final String name;
    
        @NotNull
        @Override
        public String getName() {
            return this.name;
        }
    
        public Child(@NotNull String name) {
            super(name); // calls super constructor
            this.name = name; // sets own name field
        }
    }