How to construct objects in Scala

By | 31st October 2020

Scala’s object-oriented programming style comes with some syntactic sugar and a few tricks.

In order to get a better understanding of how Scala works, we will examine a few Scala examples and the corresponding byte code (in reality, the Java code resulting from decompiling the class files).

Examples

Empty class

Simplest possible example, an empty class. No surprises here, just a class with a no-args constructor. By the way, as it is revealed by this example, the default visibility modifier in Scala is public

//Scala
class Empty
//Java
public class Empty {}

Class with parameters but not fields

Class with a constructor that takes parameters: it results in a class with an all-args constructor.
The body of the class outside any method is considered part of the constructor definition.

//Scala
class Fraction(n: Int, d: Int) {
  System.out.println("numerator:" + n + ", denominator:" + d)
}
//Java
public Fraction(final int n, final int d) {
  System.out.println("numerator:" + n + ", denominator:" + d);
}

Class with parameters promoted to fields

When the parameters of the constructor are used by members of the class other than the constructor, those parameters are promoted to fields so they can be accessed.

//Scala
class FractionString(n: Int, d: Int){
  override def toString: String = n + "/" + d
}
//Java
public class FractionString {
   private final int n;
   private final int d;

   public String toString() {
      return this.n + "/" + this.d;
   }

   public FractionString(final int n, final int d) {
      this.n = n;
      this.d = d;
   }
}

It’s worth noticing that the visibility of the fields generated is object-private (private[this]), meaning that they cannot be accessed by other objects of the same class. For instance, this example won’t compile:

//Scala
class FractionEqual(n: Int, d: Int){
  override def equals(other: Any): Boolean = other match {
    case that: FractionEqual => n == that.n && d == that.d
    case _ => false
  }
}

Error:(32, 43) value n is not a member of FractionEqual
    case that: FractionEqual => n == that.n && d == that.d

Interestingly, the modifier private[this] has no meaning in the JVM and therefore it is compiled to private.

Class with parametric fields

The keyword val can be used as a shorthand to define at the same time a parameter and a field with the same name. The field is accompanied by a getter named as the field.

//Scala
class FractionVal(val n: Int, val d: Int)
//Java
public class FractionVal {
   private final int n;
   private final int d;

   public int n() {
      return this.n;
   }

   public int d() {
      return this.d;
   }

   public FractionVal(final int n, final int d) {
      this.n = n;
      this.d = d;
   }
}

However, everything is designed to create the illusion that there are no methods. In this case, the getter is defined as a parameterless method and as a consequence the client is oblivious to whether they are calling a val or a def.
This is the way Scala supports the uniform access principle:

the client code should not be affected by a decision to implement an attribute as a field or method.

Unfortunately, this cannot be appreciated in the Java version as it shows the method defined with parentheses, an empty-paren method, given that parameterless methods don’t exist in the realm of the JVM.

However, the following example illustrate how this works in Scala:

scala> class FractionVal(val n: Int, val d: Int)
defined class FractionVal

scala> val f = new FractionVal(1,2)
f: FractionVal = FractionVal@2102eb7a

scala> f.n
res17: Int = 1

scala> f.d
res18: Int = 2

scala> f.n()
<console>:13: error: Int does not take parameters
       f.n()
          ^

Now it would be possible to implement the method equals as the fields can be accessed through the corresponding public getters.

Parametric fields with var

It’s also possible to define parametric fields with the keyword var. In this case, a setter named field_= is also generated.

//Scala
class FractionVar(var n: Int, var d: Int)
//Java
public class FractionVar {
   private int n;
   private int d;

   public int n() {
      return this.n;
   }

   public void n_$eq(final int x$1) {
      this.n = x$1;
   }

   public int d() {
      return this.d;
   }

   public void d_$eq(final int x$1) {
      this.d = x$1;
   }

   public FractionVar(final int n, final int d) {
      this.n = n;
      this.d = d;
      super();
   }
}

Similarly to the previous example, the setter field_= is interpreted as the assignment operator to create the illusion that there is no method. Although in this case, it is also possible to invoke the setter directly:

scala> class FractionVar(var n: Int, var d: Int)
defined class FractionVar

scala> val f = new FractionVar(1,2)
f: FractionVar = FractionVar@1ecdfe1e

scala> f.n
res20: Int = 1

scala> f.n = 9
f.n: Int = 9

scala> f.n
res21: Int = 9

scala> f.n_=(10)

scala> f.n
res24: Int = 10

The visibility of the generated getter/setter is the same as the visibility of the corresponding val/var

Class with custom getter/setter

In the Java tradition, we can also create our own getter/setter methods. However, the access to the variables is still done through the synthetic getter/setter.

//Scala
class FractionVar(var n: Int, var d: Int){
  def setN(n: Int) = this.n = n
  def getN() = n
}
//Java
public class FractionVar {
   private int n;
   private int d;

   public int n() {
      return this.n;
   }

   public void n_$eq(final int x$1) {
      this.n = x$1;
   }

   public int d() {
      return this.d;
   }

   public void d_$eq(final int x$1) {
      this.d = x$1;
   }

   public void setN(final int n) {
      this.n_$eq(n);
   }

   public int getN() {
      return this.n();
   }

   public FractionVar(final int n, final int d) {
      this.n = n;
      this.d = d;
      super();
   }
}

Here’s an example of how to use the new methods. Notice how getN can be invoked with or without parentheses

scala> :paste
// Entering paste mode (ctrl-D to finish)

class FractionVar(var n: Int, var d: Int){
  def setN(n: Int) = this.n = n
  def getN() = n
}

// Exiting paste mode, now interpreting.

defined class FractionVar

scala> val f = new FractionVar(1,2)
f: FractionVar = FractionVar@125490ea

scala> f.getN()
res26: Int = 1

scala> f.getN
res27: Int = 1

scala> f.setN(9)

scala> f.getN
res29: Int = 9

Object

Objects are compiled to two classes:

  • a singleton with the suffix $ added to its name and containing the actual functionality
  • a class with static forwarders to access the singleton functionality.
//Scala
object FractionObject {
  def fraction(n: Int, d: Int) = n + "/" + d
}
//Java
public final class FractionObject$ {
   public static FractionObject$ MODULE$;

   static {
      new FractionObject$();
   }

   public String fraction(final int n, final int d) {
      return n + "/" + d;
   }

   private FractionObject$() {
      MODULE$ = this;
   }
}

public final class FractionObject {
   public static String fraction(int var0, int var1) {
      return FractionObject$.MODULE$.fraction(var0, var1);
   }
}

Companion object

A companion object in Scala is an object that’s declared in the same file as a class, and has the same name as the class. A singleton and a forwarder are created like in the previous case.
The only difference is that now the singleton implements AbstractFunction (with the corresponding -arity) and Serializable.

//Scala
class FractionVal(val n: Int, val d: Int)
object FractionVal
//Java
public final class FractionVal$ extends AbstractFunction2 implements Serializable {
   public static FractionVal$ MODULE$;

   static {
      new FractionVal$();
   }

   public final String toString() {
      return "FractionVal";
   }

   public FractionVal apply(final int n, final int d) {
      return new FractionVal(n, d);
   }

   public Option unapply(final FractionVal x$0) {
      return (Option)(x$0 == null ? .MODULE$ : new Some(new sp(x$0.n(), x$0.d())));
   }

   private Object readResolve() {
      return MODULE$;
   }

   // $FF: synthetic method
   // $FF: bridge method
   public Object apply(final Object v1, final Object v2) {
      return this.apply(BoxesRunTime.unboxToInt(v1), BoxesRunTime.unboxToInt(v2));
   }

   private FractionVal$() {
      MODULE$ = this;
   }
}

public class FractionVal {
   private final int n;
   private final int d;

   public int n() {
      return this.n;
   }

   public int d() {
      return this.d;
   }

   public FractionVal(final int n, final int d) {
      this.n = n;
      this.d = d;
   }
}

Interestingly, none of the functionality implemented by the singleton is available in Scala as FractionVal does not have any reference to FractionVal$.
For instance, the method apply is not available whereas toString uses the implementation given by Object.toString instead of FractionVal$.toString

scala> :paste
// Entering paste mode (ctrl-D to finish)

class FractionVal(val n: Int, val d: Int)
object FractionVal

// Exiting paste mode, now interpreting.

defined class FractionVal
defined object FractionVal

scala> FractionVal(1,2)
<console>:13: error: FractionVal.type does not take parameters
       FractionVal(1,2)
                  ^
scala> new FractionVal(1,2)
res5: FractionVal = FractionVal@7f2c995b

Case class

Case classes are regular classes with tons of features.

  • by default, the constructor parameters are val
  • for each case class, the compiler creates a companion object
  • the resulting classes implement Serializable
  • methods equal and hashCode are overridden based on the attributes of the class fields

Here’s the structure of the resulting classes:

case class structure
//Scala
case class FractionVal(n: Int, d: Int)
//Java
import scala.Option;
import scala.Serializable;
import scala.Some;
import scala.None.;
import scala.Tuple2.mcII.sp;
import scala.runtime.AbstractFunction2;
import scala.runtime.BoxesRunTime;

public final class FractionVal$ extends AbstractFunction2 implements Serializable {
   public static FractionVal$ MODULE$;

   static {
      new FractionVal$();
   }

   public final String toString() {
      return "FractionVal";
   }

   public FractionVal apply(final int n, final int d) {
      return new FractionVal(n, d);
   }

   public Option unapply(final FractionVal x$0) {
      return (Option)(x$0 == null ? .MODULE$ : new Some(new sp(x$0.n(), x$0.d())));
   }

   private Object readResolve() {
      return MODULE$;
   }

   // $FF: synthetic method
   // $FF: bridge method
   public Object apply(final Object v1, final Object v2) {
      return this.apply(BoxesRunTime.unboxToInt(v1), BoxesRunTime.unboxToInt(v2));
   }

   private FractionVal$() {
      MODULE$ = this;
   }
}

import scala.Function1;
import scala.Option;
import scala.Product;
import scala.Serializable;
import scala.collection.Iterator;
import scala.reflect.ScalaSignature;
import scala.runtime.BoxesRunTime;
import scala.runtime.Statics;
import scala.runtime.ScalaRunTime.;

public class FractionVal implements Product, Serializable {
   private final int n;
   private final int d;

   public static Option unapply(final FractionVal x$0) {
      return FractionVal$.MODULE$.unapply(var0);
   }

   public static FractionVal apply(final int n, final int d) {
      return FractionVal$.MODULE$.apply(var0, var1);
   }

   public static Function1 tupled() {
      return FractionVal$.MODULE$.tupled();
   }

   public static Function1 curried() {
      return FractionVal$.MODULE$.curried();
   }

   public int n() {
      return this.n;
   }

   public int d() {
      return this.d;
   }

   public FractionVal copy(final int n, final int d) {
      return new FractionVal(n, d);
   }

   public int copy$default$1() {
      return this.n();
   }

   public int copy$default$2() {
      return this.d();
   }

   public String productPrefix() {
      return "FractionVal";
   }

   public int productArity() {
      return 2;
   }

   public Object productElement(final int x$1) {
      Integer var10000;
      switch(x$1) {
      case 0:
         var10000 = BoxesRunTime.boxToInteger(this.n());
         break;
      case 1:
         var10000 = BoxesRunTime.boxToInteger(this.d());
         break;
      default:
         throw new IndexOutOfBoundsException(BoxesRunTime.boxToInteger(x$1).toString());
      }

      return var10000;
   }

   public Iterator productIterator() {
      return .MODULE$.typedProductIterator(this);
   }

   public boolean canEqual(final Object x$1) {
      return x$1 instanceof FractionVal;
   }

   public int hashCode() {
      int var1 = -889275714;
      var1 = Statics.mix(var1, this.n());
      var1 = Statics.mix(var1, this.d());
      return Statics.finalizeHash(var1, 2);
   }

   public String toString() {
      return .MODULE$._toString(this);
   }

   public boolean equals(final Object x$1) {
      boolean var10000;
      if (this != x$1) {
         label51: {
            boolean var2;
            if (x$1 instanceof FractionVal) {
               var2 = true;
            } else {
               var2 = false;
            }

            if (var2) {
               FractionVal var4 = (FractionVal)x$1;
               if (this.n() == var4.n() && this.d() == var4.d() && var4.canEqual(this)) {
                  break label51;
               }
            }

            var10000 = false;
            return var10000;
         }
      }

      var10000 = true;
      return var10000;
   }

   public FractionVal(final int n, final int d) {
      this.n = n;
      this.d = d;
      Product.$init$(this);
   }
}

Case object

It is also possible to define case objects. Again, a singleton and a forwarder are created. The most remarkable thing is that the singleton implements Serializable and Product.

//Scala
case object FractionObject
//Java
import scala.collection.Iterator;
public final class FractionObject {
   public static String toString() {
      return FractionObject$.MODULE$.toString();
   }

   public static int hashCode() {
      return FractionObject$.MODULE$.hashCode();
   }

   public static boolean canEqual(final Object x$1) {
      return FractionObject$.MODULE$.canEqual(var0);
   }

   public static Iterator productIterator() {
      return FractionObject$.MODULE$.productIterator();
   }

   public static Object productElement(final int x$1) {
      return FractionObject$.MODULE$.productElement(var0);
   }

   public static int productArity() {
      return FractionObject$.MODULE$.productArity();
   }

   public static String productPrefix() {
      return FractionObject$.MODULE$.productPrefix();
   }
}

import scala.Product;
import scala.Serializable;
import scala.collection.Iterator;
import scala.runtime.BoxesRunTime;
import scala.runtime.ScalaRunTime.;

public final class FractionObject$ implements Product, Serializable {
   public static FractionObject$ MODULE$;

   static {
      new FractionObject$();
   }

   public String productPrefix() {
      return "FractionObject";
   }

   public int productArity() {
      return 0;
   }

   public Object productElement(final int x$1) {
      throw new IndexOutOfBoundsException(BoxesRunTime.boxToInteger(x$1).toString());
   }

   public Iterator productIterator() {
      return .MODULE$.typedProductIterator(this);
   }

   public boolean canEqual(final Object x$1) {
      return x$1 instanceof FractionObject$;
   }

   public int hashCode() {
      return 1259148289;
   }

   public String toString() {
      return "FractionObject";
   }

   private Object readResolve() {
      return MODULE$;
   }

   private FractionObject$() {
      MODULE$ = this;
      Product.$init$(this);
   }
}

Trait

Traits are compiled as interfaces

//Scala
trait FractionTrait{
  val n: Int
  val d: Int
}
//Java
public interface FractionTrait {
   int n();
   int d();
}

Abstract class

The rules for concrete classes also apply to abstract classes

//Scala
abstract class AbstractFraction(val n: Int, val d: Int)
//Java
public abstract class AbstractFraction {
   private final int n;
   private final int d;

   public int n() {
      return this.n;
   }

   public int d() {
      return this.d;
   }

   public AbstractFraction(final int n, final int d) {
      this.n = n;
      this.d = d;
   }
}

Package object

Package objects are treated as Objects. Two classes are created: a singleton called package$ and the forwarder class called package

//Scala
package object mypackage {
  def greet() = "hello"
}
//Java
package mypackage;

public final class package {
   public static String greet() {
      return package$.MODULE$.greet();
   }
}

public final class package$ {
   public static package$ MODULE$;

   static {
      new package$();
   }

   public String greet() {
      return "hello";
   }

   private package$() {
      MODULE$ = this;
   }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.