Saturday 4 May 2024

Kotlin Companion Objects

Some weeks ago I wrote a post reviewing how static members work in different languages. I was prompted to perform such review to better understand the motivations for Kotlin designers to come up with that odd feature called companion objects (I think the only other well-known language with such concept is Scala).

As you can read here Kotlin features object expressions (let's think of it as an advanced form of object literals) and object declarations that initially I thought were only used for declaring singletons. Well, object declarations are also used for declaring companion objects. I won't repeat here the basis explained there in the official documentation.

So, what's the point with this companion objects thing? Honestly, for the moment I don't see them as providing any revolutionary advantage over static members in Python or JavaScript (as per my previous post static members in these 2 languages seem a bit richer than those in Java and C#), but it's an interesting approach. I should also note that this nice article explains that thinking of companion objects as just a different way to implement static members is missing part of the picture and that they should be seen more like "a powerful upgrade to static nested classes". As I very rarely use inner/nested classes I have little to say about this.

At one point in this discussion someone gives a good summary of how to think about companion objects:

Think of each member of the ‘real class’, being instanced with a reference to the same singleton of a special ‘secondary class’ designed to hold data shared by all members of the ‘main class’. You are actually calling a method the secondary class with calling a companion object method, and not actually having a static method at all.

I remember having read somewhere that from a theoretical point companion objects are superior to static members because we use objects to gather together all the members belonging to the class rather than to an instance of the class, instead of throwing them "in the middle of the class" as we do with static members. That's right, and thanks to using a single object for all those elements (methods and properties) belonging to the class, that object can inherit from another object, which seems to me (and others) like the main distinctive feature of companions.

On the other side, there are a couple of things that can be seen as limitations. First, companion objects are not inherited. If I define a companion object in class A, and have a class B that inherits from A, I can not access to the companion through B (while in Python and JavaScript static members are inherited). Second, I can not access the companion through an instance of the class, only through the class (this is possible in Python, but not in JavaScript). Notice that inside a method of the companion "this" (the receiver) points to the companion itself, so we can use "this" explicitly or omit it (implicit this). We can also use just the class name. I'll add here some code showing this:


package companionObjects


open class Country(var name: String) {
    var xPos = 0
    var yPos = 0
    companion object {
        val planetName = "Earth"
        fun formatPlanet(): String {
            // KO. We have no access to the "name" property in the Country instance 
            //return "${name} - ${planetName}"
            
            // OK, "this" refers to the companion
            //return "${this.planetName}"

            //so we can just use the implicit "this" (that refers to the companion itself)    
            return "${planetName}"

            // OK, access through the class rather than through "this"
            //return "${Country.planetName}"
            
        }
    }

    fun move(x: Int, y: Int) {
        xPos += x
        yPos += y
    }
}

class ExtendedCountry(name: String): Country(name) {}


// kotlinc Main.kt -d myApp.jar
// kotlin -cp myApp.jar companionObjects.MainKt

fun main(){
    val country1 = Country("France")
    
    // KO: Unresolved reference (so I can not access the companion through an instance, only through the class)
    //println(country1.formatPlanet())
    
    println(Country.formatPlanet())
    //Earth
    
    // KO: Unresolved reference (I can not access the companion from the derived class)
    //println(ExtendedCountry.formatPlanet())
}

So all in all we can say that a companion object is like having a single static member in our class, with that member pointing to an object. The members in that object are accessible directly through the containing class (no need to write MyClass.companion.member, just MyClass.member), so it's a form of delegation.

No comments:

Post a Comment