Companion objects

on waitingforcode.com

Companion objects

Scala doesn't come with static keyword as Java does. However, with object singleton type it allows to define static properties and methods. It goes even further and thanks to object "class" lets us to build an instruction called companion objects.

This post is composed of 2 parts. The first one starts with the definition of companion objects. The second section focuses on their internal implementations.

Definition

You've already met the idea of companion objects in the post about Scala extractors. They were presented as objects holding properties extraction definition inside unapply and unapplySeq functions. But more formally speaking, a companion object is an object called exactly the same as the class it's accompanying and it's located in the same physical file. Having both of them (accompanied class + companion object) in a single place has some organization advantages (common logic in a single place) but also technical ones because instances and companion object can access their private fields and methods.

All this looks interesting but for what purposes we could use companion classes ? I quoted one of them when I talked about extractors. And since unapply's opposite is apply(), companion objects can be also used to create new class instances, as shown in the following snippet:

it("should create new instance of the class") {
  class Person(val firstName: String, val lastName: String) {}
  object Person {
    def apply(firstAndLastNames: String): Person = {
      val Array(firstName, lastName) = firstAndLastNames.split(" ", 2)
      new Person(firstName, lastName)
    }
  }

    val person = Person("a b c d")

    person.firstName shouldEqual "a"
    person.lastName shouldEqual "b c d"
}
it("should define extractor") {
  class Sentence(val value: String) {}
  object Sentence {
    def unapply(sentence: Sentence): Option[Seq[String]] = Some(sentence.value.split(" "))
  }

  val testedSentence = new Sentence("a b c")
  val matchedResult = testedSentence match {
    case Sentence(phrase) => phrase.mkString(" => ")
    case _ => ""
  }

  matchedResult shouldEqual "a => b => c"
}

And since companion object can access properties of accompanied class without visibility limitations, it can also do some operations exploiting these properties, as for instance:

it("should have direct access to members") {
  class Letters(word: String) {
    val letter1 = word.split(" ")(0)

    private val printableFirstLetter = s"${Letters.prefix}${letter1}"
  }
  object Letters {
    private val prefix = "letter="

    def convertToPrintableFirstLetter(letters: Letters): String = {
      s"#print ${letters.printableFirstLetter}"
    }
  }
  object SomeLetters {

    // Just for comparison. If you try to compile this class, the operation will fail because of
    // Error:(50, 30) value printableFirstLetter in class Letters cannot be accessed in Letters
    //          s"#print ${letters.printableFirstLetter}"
    // def convertToPrintableFirstLetter(letters: Letters): String = {
    //   s"#print ${letters.printableFirstLetter}"
    // }
  }

  val lettersABC = new Letters("a b c")

  val printableFirstLetter = Letters.convertToPrintableFirstLetter(lettersABC)

  printableFirstLetter shouldEqual "#print letter=a"
}

Companion object under-the-hood

Especially the last property of companion objects is interesting. Let's see how this mutual access is orchestrated at bytecode level. In the analysis we'll look at the following class:

class CompanionObject {

  private val Letter = "A"

}

object CompanionObject {

  def printLetter(companionObject: CompanionObject): String = companionObject.Letter

}

After decompiling both classes with javap -v command we get the following code (only fragments about printLetter are used):

# Instance class
public class CompanionObject

  public static java.lang.String printLetter(CompanionObject);
    descriptor: (LCompanionObject;)Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #18                 // Field CompanionObject$.MODULE$:LCompanionObject$;
         3: aload_0
         4: invokevirtual #20                 // Method CompanionObject$.printLetter:(LCompanionObject;)Ljava/lang/String;
         7: areturn

  public java.lang.String CompanionObject$$Letter();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #23                 // Field CompanionObject$$Letter:Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LCompanionObject;
      LineNumberTable:
        line 3: 0

# Companion object
public final class CompanionObject$

  public java.lang.String printLetter(CompanionObject);
    descriptor: (LCompanionObject;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: invokevirtual #21                 // Method CompanionObject.CompanionObject$$Letter:()Ljava/lang/String;
         4: areturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LCompanionObject$;
            0       5     1 companionObject   LCompanionObject;
      LineNumberTable:
        line 9: 0
    MethodParameters:
      Name                           Flags
      companionObject                final
}

For the ones of you who aren't familiar with bytecode expressions, $ in class name is reserved to objects, so from that we can easily find companion object's bytecode. Each object has also its representation without dollar sign that exposes static methods. They delegate real execution to the class with the dollar sign. And it's pretty much visible in this snippet:

# forwarding method
public static java.lang.String printLetter(test.CompanionObject);
    descriptor: (LCompanionObject;)Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #18                 // Field test.CompanionObject$.MODULE$:LCompanionObject$;
         3: aload_0
         4: invokevirtual #20                 // Method test.CompanionObject$.printLetter:(LCompanionObject;)Ljava/lang/String;
         7: areturn

As you can see, this delegator retrieves the singleton instance for CompanionObject object and calls its printLetter method by passing the instance class in the parameter. Later there is no magic and the target method does what we've defined before in the code:

public java.lang.String printLetter(test.CompanionObject);
  descriptor: (LCompanionObject;)Ljava/lang/String;
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=2, args_size=2
        0: aload_1
        1: invokevirtual #21                 // Method test.CompanionObject.test.CompanionObject$$Letter:()Ljava/lang/String;
        4: areturn

Companion objects are an interesting construction that let us put common and instance-independent logic there. It's interesting also because it "breaks" visibility rules and in consequence companion object and instance, classes can access even private fields and methods. Aside from that, companion objects are also used in extractors and factory methods. Internally they're implemented with the help of forwarding static method defined inside the same .class file as the instance class.

Read also about Companion objects here: Why does Scala place a dollar sign at the end of class names? .

Share, like or comment this post on Twitter:

Share on: