我遇到了一个奇怪的问题.我有一个基类和一堆扩展基类的对象.我在基类中添加了一个伴随对象,其中包含了扩展基类的所有对象的列表.但是,在某些情况下,当试图调用该列表上的函数时,似乎没有初始化对象.

下面是一个基类,它包含了一个伴随对象:

abstract class MyBaseClass {
    abstract fun getName(): String

    companion object {
        val list = listOf(Foo, Bar)

        fun getByName(name: String): MyBaseClass? {
            return list.firstOrNull { it.getName() == name }
        }
    }
}

对象在另一个文件中定义:

object Foo: MyBaseClass() {
    override fun getName(): String {
        return "Foo"
    }
}

object Bar: MyBaseClass() {
    override fun getName(): String {
        return "Bar"
    }
}

现在,如果我调用getByName函数,它会抛出一个NullPointerException.但是,如果我先删除列表并打印项目,它会正确地打印出所有内容,之后,getByName工作正常.

// Throws NPE
@Test
fun testOne() {
    assertEquals(Foo, MyBaseClass.getByName("Foo"))
}

// Works correctly
@Test
fun testTwo() {
    for (o in MyBaseClass.list) {
        println("Object: $o")
    }
    assertEquals(Foo, MyBaseClass.getByName("Foo"))
}

这是故意的行为,还是我错过了什么?

推荐答案

这种行为是Kotlin/JVM特有的.在Kotlin/JS上,这是预期的.

Kotlin objects被编译成JVM类,其中包含一个名为INSTANCE的静态字段,例如,对于Foo,编译后的代码将大致转换回:

public class Foo {
    private Foo() {}
    public static final Foo INSTANCE = new Foo();

    // ...
}

如果是MyBaseClass块,就像这样:

public abstract class MyBaseClass {
    public static class Companion {
        private List<MyBaseClass> list = listOf(Foo.INSTANCE, Bar.INSTANCE);

        public List<MyBaseClass> getList() {
            return list;
        }

        private Companion() {}
    }

    public static final Companion Companion = new Companion();
}

Foo.INSTANCE什么时候开始?Java语言规范说"当类初始化时"(不要与创建类的新实例混淆).第Foo章什么时候开始?第一次使用Foo.这意味着类Foo在计算第一个参数assertEquals时被初始化:

assertEquals(Foo, MyBaseClass.getByName("Foo"))
             ^^^

注意,此时,MyBaseClass.getByName("Foo")尚未被判断,并且类MyBaseClass.Companion尚未被初始化.

好,让我们初始化Foo类.第一步是递归地初始化它的超类和它实现的接口.因此,我们初始化MyBaseClass,这导致初始化MyBaseClass.Companion,执行listOf(Foo, Bar).

我们需要计算Foo(即Foo.INSTANCE),但是类Foo还没有初始化(实际上,它是undergoing初始化)!Java语言规范告诉我们,在这种情况下什么都不会做.也就是说,当我们想要初始化一个已经被初始化的类时,什么都不做.INSTANCE的初始化器,即new Foo()不运行,INSTANCE保持为空.

结果,listOf创建一个包含null和实例Bar的列表.最后,MyBaseClass的初始化完成,我们回到初始化Foo,这就是运行INSTANCE的初始化器的地方.

通过先迭代列表,您实际上是在初始化Foo之前初始化MyBaseClass.在表达式listOf(Foo, Bar)中计算Foo时,将初始化Foo.再次,我们try 初始化它的超类MyBaseClass,它已经被初始化,所以我们什么也不做.然后,对Foo.INSTANCE的初始化器进行判断,最后,listOf得到非空的Foo对象.

你不必循环查看名单.任何导致MyBaseClassFoo之前初始化的东西都可以工作.即使是像这样的表情

val x = MyBaseClass.Companion

将导致初始化MyBaseClass.

有关Java语言规范中的确切引号,参见my answer here,这实际上是关于相同的问题,但在Java中.

Kotlin/JVM规范还没有发布,所以我不确定这是不是intended.

Kotlin相关问答推荐

在KMP中使用koin将来自Android的上下文注入到SQLDelight Driver中

在Kotlin中处理结果的高阶函数

只能在元素区域中点击的Jetpack Compose列

为什么onEach不是挂起函数,而Collect是?

在Kotlin中,我是否可以访问已知的WHEN子句值?

有没有办法在 jetpack compose 中将 TextField 密码点图标增加得更大?

如何注入返回通用列表的转换器?

Spring Boot Kotlin 数据类未使用 REST 控制器中的默认值初始化

init中的NPE抽象函数变量

Kotlin 有垃圾收集器吗?如果是这样,它基于哪种算法?

如何为你的 Flutter 元素添加 Kotlin 支持?

在 Koin 中提供一个 Instance 作为其接口

作为 Kotlin 中的函数的结果,如何从 Firestore 数据库返回列表?

kotlin-bom 库是做什么的?

封闭 lambda 的隐式参数被shadowed

未找到导入 kotlinx.coroutines.flow.*

Kotlin中OnclickListener方法之间的差异

如何在Kotlin中将字符串转换为InputStream?

Recyclerview: listen to padding click events

Kotlin:访问 when 语句的参数