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

Use*_*291 5 java android constructor initialization kotlin

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

该更改的一部分包括引入视图模型。和的部分包括改变UserManager曾经是一个类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

    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

我检查了调试器,userData真的是null

但这没有意义。

因为我没有其他想法,并且不顾 AndroidStudio 的抗议,我切换到了二级构造函数。

constructor(application: Application) : super(application) {
    restoreActiveUsers()
}
Run Code Online (Sandbox Code Playgroud)

这就是诀窍。

不过,我很难理解为什么。

根据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

Neo*_*Neo 1

长话短说

在 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"]}"  } }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出: {} {} {}

-

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