为什么在字段初始化之前执行主构造函数主体?

问题描述 投票:4回答:1

所以我要重写一些Android应用程序的旧代码。

部分更改包括引入视图模型。 that的一部分包括将以前是UserManagerobject类更改为AndroidViewModel

class UserManager(application: Application) : AndroidViewModel(application) {

  private val userData: MutableMap<User, MutableMap<String, Any>> = object : HashMap<User, MutableMap<String, Any>>() {
      override fun get(key: User): MutableMap<String, Any>? {
          val former = super.get(key)
          val current = former ?: mutableMapOf()
          if (current !== former) this.put(key, current)
          return current
      }
  }

  init {
    restoreActiveUsers()
  }

  override fun onCleared() {
      persistActiveUsersData()
  }

  private fun restoreActiveUsers() {
    val decodedUsers: List<User> = ... load users from persistent storage ...

    decodedUsers.forEach { userData[it] } //create an entry in [userData] with the user as key, if none exists

    ...
  }
}

init块是新的,因为在我进行转换之前,曾经从外部在Object实例上调用它,这是我感到困惑的根源。

因为试图像这样运行应用程序,所以在decodedUsers.forEach { userData[it] }处给了我一个例外,因为

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.bla.bla.bla.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class com.bla.bla.bla..user.service.UserManager
          at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
          at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
          at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
          at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
          ...
       Caused by: java.lang.RuntimeException: Cannot create an instance of class com.,bla.blab.bla.user.service.UserManager
          at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:275)
          at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)
          ...
          at com.,bla.blab.bla.app.ui.MainActivity.getUserManager(Unknown Source:7)
          at com.,bla.blab.bla.app.ui.MainActivity.onCreate(MainActivity.kt:71)
          at android.app.Activity.performCreate(Activity.java:7802)
          ...
      Caused by: java.lang.reflect.InvocationTargetException
          at java.lang.reflect.Constructor.newInstance0(Native Method)
          at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
          at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:267)
          at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:106)
          ...
      Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.Map.get(java.lang.Object)' on a null object reference
          at com.bla.bla.bla.user.service.UserManager.restoreActiveUsers(UserManager.kt:178)
          at com.bla.bla.bla.user.service.UserManager.<init>(UserManager.kt:60)

我检查了调试器,然后userData确实是null

但是那没有道理。

由于我没有其他想法,尽管有AndroidStudio的抗议,我还是改用了二级构造器。

constructor(application: Application) : super(application) {
    restoreActiveUsers()
}

这就是窍门。

不过,我正在努力理解原因。

根据jvm specs

[每当创建新的类实例时,都会为其分配内存空间,并为该类类型中声明的所有实例变量和该类类型的每个超类中声明的所有实例变量(包括可能是隐藏(§8.3)。

如果没有足够的空间为该对象分配内存,则该类实例的创建将突然完成,并出现OutOfMemoryError。否则,新对象中的所有实例变量(包括在超类中声明的实例变量)都将初始化为其默认值(第4.12.5节)。

仅在返回对新创建对象的引用作为结果之前,使用以下过程处理所指示的构造函数以初始化新对象:

  1. 将构造函数的参数分配给此构造函数调用的新创建的参数变量。

  2. 如果此构造函数以同一个类中的另一个构造函数的显式构造函数调用(第8.8.7.1节)开头(使用此方法),则使用相同的五个步骤评估参数并以递归方式处理该构造函数调用。如果该构造函数调用突然完成,则该过程由于相同的原因而突然完成;否则,请继续执行步骤5。

  3. 此构造函数并不以对同一类中的另一个构造函数的显式构造函数调用开头(使用此方法)。如果此构造函数用于Object以外的其他类,则此构造函数将以显式或隐式调用超类构造函数(使用super)开始。使用这五个相同的步骤来递归评估超类构造函数调用的参数和过程。如果该构造函数调用突然完成,则出于相同原因,此过程也会突然完成。否则,请继续执行步骤4。

  4. 执行此类的实例初始值设定项和实例变量初始值设定项,并按照从左到右的顺序将实例变量初始值设定项的值在文本中显示在该类的源代码中,并将它们分配给相应的实例变量。如果执行这些初始化程序中的任何一个导致异常,则不会再处理其他初始化程序,并且该过程会因相同的异常而突然完成。否则,请继续执行步骤5。

  5. 执行此构造函数的其余部分。如果该执行突然完成,则出于相同原因,此过程也会突然完成。否则,此过程将正常完成。

如果我正确地阅读了此内容,则实例变量应始终在执行构造函数主体之前进行初始化。

这意味着init{...}在构造函数之前执行。

但是这也没有意义,因为根据these docs

Java编译器将初始化程序块复制到每个构造函数中。

将让它们执行after实例变量初始化,不是吗?

所以...这是怎么回事?

为什么userData不在上级null中?

java android kotlin constructor initialization
1个回答
0
投票

TL; DR

找不到Kotlin的任何错误,可能是Android行为。

Kotlin的初始化顺序

  1. 主要构造函数
  2. 二级构造函数
  3. 属性和初始化块-取决于它们(从上到下)的顺序

在实例初始化期间,将执行初始化程序块按照它们在班级正文中出现的顺序排列,与属性初始值设定项

Check out the code example in kotlindoc

请注意,初始化程序块中的代码实际上已成为主要构造函数。向主要构造函数的委派发生在辅助构造函数的第一条语句,因此所有代码初始化程序块和属性初始化程序在执行之前次要构造函数主体。即使班级没有小学构造函数,委派仍然隐式发生,并且初始化程序块仍在执行

结论

您的代码应该执行首先UserManager(application: Application),然后是AndroidViewModel(application),然后是private val userData: MutableMap<User, MutableMap<String, Any>> = ...,然后init { restoreActiveUsers() }

我尝试在EDI中编写此示例(扩展普通类而不是AndroidViewModel,但我无法重现异常:

private open class Boo (private val input: Int)

private class Foo : Boo(1) {

    private val logger = LoggerFactory.getLogger(this::class.java)

    val userData: MutableMap<String, MutableMap<String, Any>> = object : HashMap<String, MutableMap<String, Any>>() {
        override fun get(key: String): MutableMap<String, Any>? {
            val former = super.get(key)
            val current = former ?: mutableMapOf()
            if (current !== former) this.put(key, current)
            return current
        }
    }

    init {
        restoreActiveUsers()
    }

    private fun restoreActiveUsers() {
        (1..3).forEachIndexed { index, i -> userData["$index"] = mutableMapOf<String, Any>() }
        logger.info { "$userData" }
    }
}

输出:{0={}, 1={}, 2={}}

-

您的问题表明行为很怪异,因为字段userDatanon-nullable val并且不会产生编译错误,如果将init块写入字段上方,则会发生编译错误” !因此,当出现[[purely to kotlin-必须先初始化该字段。我没有Android开发经验,也不知道初始化部分在那里的工作原理,但我强烈建议在那儿寻找问题。

© www.soinside.com 2019 - 2024. All rights reserved.