Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin override abstract val behavior, object vs class

I've just started using and started messing with abstract classes, override val's and singeltons. But, I've just encountered a really odd behaviour. My goal was to have an abstract class, and then create several singeltons that extends that abstract class. Since i want to require certain variables i created abstract val that then could be overridden in the subclasses (Instead of passing them through the constructor).

So i got 4 classes:

MainActivity:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val instance = Instance()
        Log.d("MainActivity", "instance randObject: ${instance.randObject}")
        Log.d("MainActivity", "instance randObject: ${instance.randObject.myProp}")
        Log.d("MainActivity", "instance randObject: ${instance.randObject.myProp2}")

        Log.d("MainActivity", "singleton randObject: ${Object.randObject}")
        Log.d("MainActivity", "singleton randObject: ${Object.randObject.myProp}")
        Log.d("MainActivity", "singleton randObject: ${Object.randObject.myProp2}")    
    }
}

Instance:

class Instance: AClass(){
    override val testString: String = "test"
    override val testUriString: String = "https://www.google.se"
    override val testUri: Uri = Uri.parse(testUriString)!!
    override val randObject: RandomObject = RandomObject("Herp")
}

Object

object Object : AClass(){
    override val testString: String = "test"
    override val testUriString: String = "https://www.google.se"
    override val testUri: Uri = Uri.parse(testUriString)!!
    override val randObject: RandomObject = RandomObject("Herp")
}

AClass:

abstract class AClass{
    abstract val testString: String
    abstract val testUriString: String
    abstract val testUri: Uri
    abstract val randObject: RandomObject

    init {
        Log.d("AClass", "testString: $testString")
        Log.d("AClass", "testUriString: $testUriString")
        Log.d("AClass", "testUri: $testUri")
        Log.d("AClass", "randObject: $randObject")
    }
}

Output:

D/AClass: testString: null
D/AClass: testUriString: null
D/AClass: testUri: null
D/AClass: randObject: null
D/MainActivity: instance randObject: com.technocreatives.abstracttest.RandomObject@4455b26
D/MainActivity: instance randObject: derp
D/MainActivity: instance randObject: Herp

D/AClass: testString: test
D/AClass: testUriString: https://www.google.se
D/AClass: testUri: null
D/AClass: randObject: null
D/MainActivity: singleton randObject: com.technocreatives.abstracttest.RandomObject@8b19367
D/MainActivity: singleton randObject: derp
D/MainActivity: singleton randObject: Herp

After this I came to the realization that overridden probably are not initialized until after the init{} has been executed. But then I saw what happened when i created a singleton. The value testUriString is set in the init. Why is this the case? Is it a bug? What is the expected behaviour of singleton and override val?

I tried searching for the documentation but have not found any information about this in the documentation.

like image 738
Rawa Avatar asked Dec 11 '17 16:12

Rawa


1 Answers

The difference in the behavior that you observe is caused by how backing fields are generated for properties in classes and objects, and also by how those are initialized.

  • When a class overrides a property using a backing field, under the hood, there is a separate instance field in the derived class, and the overridden getter returns the value of that field.

    So, when you access the property from inside the super class constructor, it's the overridden getter that is called, which returns the null value of the field (it is uninitialized at that moment, since the super constructor is called before the class' own initialization logic).

  • On contrary, when you define an object overriding a class, then the underlying class Object has its backing fields defined as JVM static fields.

    The Object class has an instance as well (it can even be accessed in Java as Object.INSTANCE), and this instance is initialized at some point in time and calls the super constructor.

    Now the interesting part: when the class Object class is loaded by the JVM, its static fields that are be initialized by constant values already contain those values, even before the PUTSTATIC instruction in the Object's <clinit> is executed.

    If you change the testString initializer to a non-constant value, it will not be initialized by the time it is accessed, e.g. override val testString: String = "test".also { println(it) }

    Here's a gist with the bytecode of such a singleton with a few marks made by me. Note that the field is accessed by the constructor of the abstract class before the value is put in the <clinit> of Object.

I'm not sure it's actually a bug, but at least the behavior is inconsistent. I've reported this inconsistency to the issue tracker: https://youtrack.jetbrains.com/issue/KT-21764

like image 110
hotkey Avatar answered Oct 05 '22 22:10

hotkey