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:
//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; } }