首页 > 解决方案 > 为什么在字段初始化之前执行主构造函数体?

问题描述

所以我正在重写一个android应用程序的一些遗留代码。

部分更改包括引入视图模型。其中一部分包括UserManager将曾经是 an的类更改objectAndroidViewModel.

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 规格

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

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

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

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

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

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

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

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

如果我没看错的话,实例变量应该总是在构造函数体被执行之前被初始化。

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

但这也没有意义,因为根据这些文档

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

哪个会在实例变量初始化之后执行它们,不是吗?

那么……这是怎么回事?

为什么不应该userData在上面的课程null中?

标签: javaandroidkotlinconstructorinitialization

解决方案


TL;博士

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

Kotlin 的初始化顺序

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

在实例初始化期间,初始化程序块的执行顺序与它们在类主体中出现的顺序相同,并与属性初始化程序交错

查看 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).forEach { _ -> logger.info {  "${userData["notInside"]}"  } }
    }
}

输出: {} {} {}

-

您的问题表明了一种非常奇怪的行为,因为该字段userData不可为空的 val并且不会产生编译错误,如果将 init 块写入该字段上方就会发生这种情况!因此,当纯粹涉及 kotlin时- 该字段必须先初始化。
我没有 Android 开发经验,也不知道那里的初始化部分是如何工作的,但我强烈建议在那里寻找问题所在。


推荐阅读