Требование в блоке инициализации суперкласса вызывает IllegalArgumentException [Kotlin]

Доброе утро, котлинские гуру.

У меня есть структура наследования, в которой абстрактный суперкласс реализует некоторые общие проверки данных. Компилятор не жалуется, но после выполнения JVM создает исключение IllegalArgumentException

Код

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
)

Исключение выглядит следующим образом

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)

Спасибо за ваше время

всего наилучшего

2 ответа

Решение

На name.isNotBlank(), вы должны получить предупреждение Lint, как это:

Доступ к неконечному имени свойства в конструкторе

Вы получаете доступ к name собственность в то время, когда Parent строится, однако, name переопределяется в Child, Это переопределение означает, что внутри Parent а также Child классы имеют частные поля для nameи так как Parent Конструктор это первое, что называется, когда Child создан, Child"s name пока не будет инициализирован, но регистрация Parent будет иметь доступ к Childпереопределение свойства при проверке.

Это может показаться сложным, вот соответствующие части декомпилированного байт-кода вашего примера (упрощенно):

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
    }
}

Некоторый код для понимания порядка выполнения инициализаторов kotlin [1]

  open class Parent {
    private val a = println("Parent.a")

    constructor(arg: Unit=println("Parent primary constructor default argument")) {
        println("Parent primary constructor")
    }

    init {
        println("Parent.init")
    }

     private val b = println("Parent.b")
    }

    class Child : Parent {
    val a = println("Child.a")

    init {
        println("Child.init 1")
    }

    constructor(arg: Unit=println("Child primary constructor default argument")) : super() {
        println("Child primary constructor")
    }

    val b = println("Child.b")

    constructor(arg: Int, arg2:Unit= println("Child secondary constructor default argument")): this() {
        println("Child secondary constructor")
    }

    init {
        println("Child.init 2")
    }
   }

Выход для ребенка (1)

Child secondary constructor default argument
Child primary constructor default argument
Parent primary constructor default argument
Parent.a
Parent.init
Parent.b
Parent primary constructor
Child.a
Child.init 1
Child.b
Child.init 2
Child primary constructor
Child secondary constructor

По сути, Parent.init вызывается до того, как реальный объект создан, и, следовательно, создается исключение.

Ссылка: [1] https://medium.com/keepsafe-engineering/an-in-depth-look-at-kotlins-initializers-a0420fcbf546

Другие вопросы по тегам