Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is primary constructor body executed before field initialisation?

So I'm rewriting some legacy code of an android app.

Part of that change includes introducing view models. And part of that includes changing a UserManager class that used to be an object to an 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

    ...
  }
}

The init block is new because it used to be called on the Object instance from outside, prior to my transformation, and it's the source of my confusion.

Because trying to run the application like this gives me an exception at decodedUsers.forEach { userData[it] } because

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)

I checked with the debugger and userData really IS null.

But that doesn't make sense.

Because I had no other ideas and in spite of AndroidStudio's protests, I switched to a secondary constructor.

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

And that did the trick.

I'm struggling to understand why, though.

According to jvm specs:

Whenever a new class instance is created, memory space is allocated for it with room for all the instance variables declared in the class type and all the instance variables declared in each superclass of the class type, including all the instance variables that may be hidden (§8.3).

If there is not sufficient space available to allocate memory for the object, then creation of the class instance completes abruptly with an OutOfMemoryError. Otherwise, all the instance variables in the new object, including those declared in superclasses, are initialized to their default values (§4.12.5).

Just before a reference to the newly created object is returned as the result, the indicated constructor is processed to initialize the new object using the following procedure:

  1. Assign the arguments for the constructor to newly created parameter variables for this constructor invocation.

  2. If this constructor begins with an explicit constructor invocation (§8.8.7.1) of another constructor in the same class (using this), then evaluate the arguments and process that constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason; otherwise, continue with step 5.

  3. This constructor does not begin with an explicit constructor invocation of another constructor in the same class (using this). If this constructor is for a class other than Object, then this constructor will begin with an explicit or implicit invocation of a superclass constructor (using super). Evaluate the arguments and process that superclass constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, continue with step 4.

  4. Execute the instance initializers and instance variable initializers for this class, assigning the values of instance variable initializers to the corresponding instance variables, in the left-to-right order in which they appear textually in the source code for the class. If execution of any of these initializers results in an exception, then no further initializers are processed and this procedure completes abruptly with that same exception. Otherwise, continue with step 5.

  5. Execute the rest of the body of this constructor. If that execution completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, this procedure completes normally.

If I'm reading this correctly, instance variables should always get initialised before the constructor body is executed.

That would imply that the init{...} is executed before the constructor.

But that also doesn't make sense because according to these docs,

The Java compiler copies initializer blocks into every constructor.

which would have them executed after the instance variable initialisation, no?

So ... what is going on, here?

Why is userData in above class nullwhen it shouldn't be?

like image 574
User1291 Avatar asked Nov 06 '22 10:11

User1291


1 Answers

TL;DR

Can't find any error on Kotlin' side, may be Android behavior.

Kotlin' order of initialization

  1. Primary constructor
  2. Secondary constructors
  3. Property and init blocks - depending on their (top-down) order

During an instance initialization, the initializer blocks are executed in the same order as they appear in the class body, interleaved with the property initializers

Check out the code example in kotlindoc

Note that code in initializer blocks effectively becomes part of the primary constructor. Delegation to the primary constructor happens as the first statement of a secondary constructor, so the code in all initializer blocks and property initializers is executed before the secondary constructor body. Even if the class has no primary constructor, the delegation still happens implicitly, and the initializer blocks are still executed

Conclusion

Your code should execute
first UserManager(application: Application),
then AndroidViewModel(application),
then private val userData: MutableMap<User, MutableMap<String, Any>> = ...,
then init { restoreActiveUsers() }

I tried writing this example in my EDI (extending normal class instead of AndroidViewModel), but I could not reproduce the exception:

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"]}"  } }
    }
}

Output: {} {} {}

-

Your problem indicates a very weird behavior, since the field userData is a non-nullable val and does not produce a compile error, which it would do if the init block would be written above the field! So when it comes purely to kotlin - the field has to be initialized before.
I have no experience with Android development and don't know how the initializing part works there, but I strongly suggest looking there for the problem.

like image 79
Neo Avatar answered Nov 12 '22 19:11

Neo