Почему тело основного конструктора выполняется перед инициализацией поля? - PullRequest
4 голосов
/ 26 января 2020

Итак, я переписываю какой-то устаревший код приложения android.

Часть этого изменения включает в себя представление моделей представлений. И часть , которая включает в себя изменение класса UserManager, который раньше был object, на 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{...} выполняется перед конструктором.

Но это также не имеет смысла, поскольку согласно эти документы ,

Компилятор Java копирует блоки инициализатора в каждый конструктор.

, который будет выполнять их после инициализации переменной экземпляра, нет?

Итак ... что здесь происходит?

Почему userData в классе выше null, когда это не должно быть?

Ответы [ 2 ]

1 голос
/ 27 января 2020

TL; DR

Не удается найти ошибку на стороне Kotlin, возможно поведение Android.

Kotlin 'порядок инициализации

  1. Первичный конструктор
  2. Вторичные конструкторы
  3. Свойство и блоки инициализации - в зависимости от их (нисходящего) порядка

Во время инициализации экземпляра блоки инициализатора выполняются в том же порядке, в каком они появляются в теле класса, чередуясь с инициализаторами свойств

Посмотрите пример кода в kotlindo c

Обратите внимание, что код в блоках инициализатора фактически становится частью основного конструктора. Делегирование первичному конструктору происходит как первый оператор вторичного конструктора, поэтому код во всех блоках инициализатора и инициализаторах свойства выполняется перед телом вторичного конструктора. Даже если у класса нет первичного конструктора, делегирование все равно происходит неявно, а блоки инициализатора по-прежнему выполняются

Заключение

Ваш код должен выполнить
сначала 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 и я не знаю, как там работает инициализирующая часть, но я настоятельно рекомендуем поискать там проблему.

0 голосов
/ 27 января 2020

На стороне java:

Конструктор UserManager

  1. Поля обнуляются: null / 0 / 0.0 / false
  2. Вызывается конструктор AndroidViewModel (явный / неявный super(...))
  3. Инициализированные поля (X x = y;)
  4. Оставшийся код конструктора

Конструктор AndroidViewModel

  1. Поля обнуляются
  2. Вызывается супер-конструктор (явный / неявный super(...))
  3. Поля инициализируются (X x = y;)
  4. Остальной код Конструктор

Это может иметь странные последствия: поля UserManager все еще неинициализированы в коде, вызываемом из конструктора AndroidViewModel, и в UserManager, перезаписанном при инициализации.

Не уверен, как Kotlin реализована собственная логика инициализации c.

...