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

因此,我正在重写android应用程序的一些旧代码。
该更改的一部分包括引入视图模型。其中一部分包括将以前是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节)。
在返回对新创建对象的引用作为结果之前,使用以下过程处理指示的构造函数以初始化新对象:

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

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

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

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

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

如果我正确地阅读了此内容,则实例变量应始终在执行构造函数主体之前进行初始化。
这意味着init{...}在构造函数之前执行。
但这也没有意义,因为根据these docs,

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

在实例变量初始化之后将执行哪些操作,不是吗?
所以...这是怎么回事?
为什么userData在上面的类null中不应该出现?

参考方案

TL; DR

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

Kotlin的初始化顺序

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

在实例初始化期间,将执行初始化程序块
顺序与出现在班级正文中的顺序相同,
属性初始化器

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

输出:
{}
{}
{}

--

您的问题表明行为很怪异,因为字段userData是不可为空的val,并且不会产生编译错误,如果将init块写入字段上方,则会发生编译错误!因此,仅涉及Kotlin时,必须先对该字段进行初始化。
我没有Android开发方面的经验,也不知道初始化部分在那里的工作方式,但是我强烈建议在那儿寻找问题。

绑定Java库Xamarin.Android - java

我花了两天时间在每个论坛,文档,tuto,博客等上寻找答案。我为实习生启动了一个Android应用程序,因为我不懂Java,所以用xamarin C#开发了它。直到最近一切都还不错,但现在我需要集成一个SDK才能在应用程序中使用POS(销售点),但是该库是用Java编写的,即使跟随文档或辅导老师,我也无法将其与xamarin绑定(我什至无法调试)。这里有人已…

Java:线程池如何将线程映射到可运行对象 - java

试图绕过Java并发问题,并且很难理解线程池,线程以及它们正在执行的可运行“任务”之间的关系。如果我创建一个有10个线程的线程池,那么我是否必须将相同的任务传递给池中的每个线程,或者池化的线程实际上只是与任务无关的“工人无人机”可用于执行任何任务?无论哪种方式,Executor / ExecutorService如何将正确的任务分配给正确的线程? 参考方案 …

JAVA:字节码和二进制有什么区别? - java

java字节代码(已编译的语言,也称为目标代码)与机器代码(当前计算机的本机代码)之间有什么区别?我读过一些书,他们将字节码称为二进制指令,但我不知道为什么。 参考方案 字节码是独立于平台的,在Windows中运行的编译器编译的字节码仍将在linux / unix / mac中运行。机器代码是特定于平台的,如果在Windows x86中编译,则它将仅在Win…

java:继承 - java

有哪些替代继承的方法? java大神给出的解决方案 有效的Java:偏重于继承而不是继承。 (这实际上也来自“四人帮”)。他提出的理由是,如果扩展类未明确设计为继承,则继承会引起很多不正常的副作用。例如,对super.someMethod()的任何调用都可以引导您通过未知代码的意外路径。取而代之的是,持有对本来应该扩展的类的引用,然后委托给它。这是与Eric…

Java:BigInteger,如何通过OutputStream编写它 - java

我想将BigInteger写入文件。做这个的最好方式是什么。当然,我想从输入流中读取(使用程序,而不是人工)。我必须使用ObjectOutputStream还是有更好的方法?目的是使用尽可能少的字节。谢谢马丁 参考方案 Java序列化(ObjectOutputStream / ObjectInputStream)是将对象序列化为八位字节序列的一种通用方法。但…