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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With