When writing a class in Java we religiously avoid using public fields. The industry-accepted pattern is to declare a field as private with getter and setter methods, which allows us, the class owners, greater control over its fields. This pair of getter and setter methods is known as a property. Properties are preferred for exposing fields outside of the class because they allow the logic and visibility of the get and set semantics to be different and evolve independently as required.
Getters and setters
Auto-generated properties
Scala helps reduce boilerplate by automatically providing a getter and setter method for every field that is declared. But before we get into the properties, let's remind ourselves of the different ways to declare fields in Scala.
This can be done using the var and val keywords. The former is used to declare a mutable field, while the latter is used to declare an immutable field. A mnemonic to help remember this is that var is short for variable, which means that it can vary over time, ergo, mutable.
The getter and setter methods that Scala provides for a field depend on the kind of field that we declare. If we declare a field using var, then Scala will create a public setter and getter. Let's look at the following example:
class Car {
var color = "red"
}
val car = new Car()
println(car.color) // "red"
car.color = "green"
println(car.color) // "green"
As expected, we can access the color field and also change its value.
On the other hand, if we declare the field using val, then Scala will create a public getter but not a setter. Let's look at the following example:
class Car {
val color = "red"
}
val car = new Car()
println(car.color) // "red"
car.color = "green" // compile-time error!
Again, nothing unexpected here. We can access the color field, but we cannot change its value. At this point, you might be thinking that we're accessing the field directly; that there's no getter or setter involved. It certainly looks that way, but that's not the case. Scala is smart enough to generate the getter and setter methods for us, we just don't see them.
Bytecode
Don't take my word for it though; let's look at what's happening behind the scenes by examining the JVM bytecode that is generated from the Scala code. To do so, let's use the scalac and javap commands on the first version of Car, which declares the color field using var:
$ scalac Car_var.scala
$ javap -p Car
This results in the following bytecode:
Compiled from "Car_var.scala"
public class Car {
private java.lang.String color; // field
public java.lang.String color(); // getter
public void color_$eq(java.lang.String); // setter
public Car();
}
As you can see, Scala has generated a private field named color and public getter and setter methods for it. The getter and setter methods are named color and color_$eq, respectively. The $eq suffix is used to distinguish the setter method from the getter method and represents the character =, because the JVM does not allow = in a method name.
Let's contrast this with the second version of Car, where color is declared using val:
Compiled from "Car_val.scala"
public class Car {
private final java.lang.String color; // field
public java.lang.String color(); // getter
public Car();
}
In this case, there is no setter method, because the field is immutable. Note that the field also has the final modifier, which means it cannot be changed (even within the class).
Custom properties
Even though Scala generates properties for us, there are still some cases where we might want to write our own getter and setter methods. For instance, we might want to add some validation logic to the setter method. Let's look at the following example:
class Car {
private var _color = "red"
def color: String = _color
def color_=(value: String): Unit = {
if (value == "red" || value == "green" || value == "blue") {
_color = value
} else {
throw new IllegalArgumentException("Invalid color")
}
}
}
Let's unpack a few things before we examine the bytecode. First, we're using a private field named _color to store the value of the color field. This is a common convention in Scala to avoid name clashes with the getter and setter methods. The getter and setter methods are named color and color_=, respectively, in order to differentiate between the two. The reason we add an underscore before the = sign is due to the Scala language spec, which allows operators in identifiers only when they're preceded by an underscore.
Now let's look at the bytecode:
Compiled from "Car_var.scala"
public class Car {
private java.lang.String _color; // field
private java.lang.String _color(); // private getter
private void _color_$eq(java.lang.String); // private setter
public java.lang.String color(); // getter
public void color_$eq(java.lang.String); // setter
public Car();
}
As expected, we get our custom public getter and setter methods, but we also get private getter and setter methods for the _color field. This is a consequence of the fact that Scala automatically generates a getter and setter for the _color field itself, even if we define our own.
Additional modifiers
Private
We can also use the private and private[this] modifiers for greater control of the auto-generated getter and setter methods. When we define a private field, the getter and setter methods will also be private. We saw a glimpse of this in the previous example, where the _color field was private, so I won't show another example here.
The private[this] modifier can be used when we don't want any getter or setter (note that this is not the only side-effect: private[this] will also make the field inaccessible from other instances of the same class). Let's look at the following example:
class Car {
private[this] var color = "red"
}
The bytecode for this class is as follows:
Compiled from "Car_var_private_this.scala"
public class Car {
private java.lang.String color; // field
public Car();
}
As expected, there is no getter or setter method.
JavaBeans
If you're a seasoned Java developer, you might have noticed that the getter and setter methods that Scala provides do not follow the JavaBeans specification, which defines a Java property as a pair of getFoo and setFoo methods. This can be problematic when you want to use your Scala classes in Java code.
Fortunately, Scala provides a way to generate JavaBeans-style getter and setter methods using the @BeanProperty annotation:
import scala.beans.BeanProperty
class Car {
@BeanProperty var color = "red"
}
Note that we had to import the scala.beans.BeanProperty annotation in order to use it. The bytecode for this class is as follows:
Compiled from "Car_var_bean.scala"
public class Car {
private java.lang.String color; // field
public java.lang.String color(); // getter
public void color_$eq(java.lang.String); // setter
public java.lang.String getColor(); // JavaBeans getter
public void setColor(java.lang.String); // JavaBeans setter
public Car();
}
As you can see, Scala has generated the JavaBeans-style getter and setter methods for us. This is a neat feature for those writing Scala code that should be interoperable with Java code.
Primary constructor
Every class in Scala has a primary constructor. Unlike Java, the primary constructor is interwoven with the class definition, and the parameters are placed immediately after the class name. For instance:
class Car(val color: String) {
...
}
Expand to see equivalent Java code
public class Car {
private final String color;
public Car(String color) {
this.color = color;
}
public String color() {
return color;
}
}
The same modifiers that we use when declaring fields can also be used in the primary constructor. For instance, we could have used var instead of val, or we could have added the private or private[this] modifier. Whichever format we use, all parameters in the primary constructor will turn into fields (with one exception, explained in the next paragraph), which in turn means they're subject to the exact same getter and setter rules as we've seen so far.
However, there is one additional case to consider with parameters in the primary constructor. Unlike a field, a parameter can be defined without the val or var keyword, which will lead to one of two possible scenarios.
If the parameter is used in at least one method in the class, then it becomes an immutable, object-private field (equivalent to private[this] val). Let's look at the following example:
class Car(color: String) {
def description: String = s"A $color car"
}
Here we define a description method that uses the color parameter. The bytecode for this class is:
Compiled from "Car_method.scala"
public class Car {
private final java.lang.String color; // field
public java.lang.String description(); // method
public Car(java.lang.String);
}
As expected, the color parameter has been converted into a private, immutable field.
However, if the parameter is not used in any method in the class, then it becomes a local variable in the primary constructor and not a field (i.e., it can be used in construction logic but not anywhere else). Let's look at the following example:
class Car(color: String) { }
Which generates the following bytecode:
Compiled from "Car_empty.scala"
public class Car {
public Car(java.lang.String);
}
As you can see, there is no field for the color parameter. This is because the color parameter is not used in any method in the class.
Comparison
Let's summarize what we've learned so far into a table so we can easily compare the different options and additionally have something we can refer to.
| Declaration | Generated getter | Generated setter |
|---|---|---|
| val name | public name | none |
| var name | public name | public name_= |
| @BeanProperty val name | public name public getName() | none |
| @BeanProperty var name | public name public getName() | public name_= public setName() |
| private val name | private name | none |
| private var name | private name | private name_= |
| private[this] var/val name | none | none |
| name (parameter) | none | none |
Conclusion
The Scala language designers have effectively eliminated the need to write cumbersome but necessary getter and setter code by incorporating it into the language. This saves developers from the hassle of writing boilerplate code or relying on third-party libraries like Lombok.
While this is great for developers as it enables us to focus on the business logic, it also adds a layer of complexity to the language due to the magic happening behind the scenes. This can be particularly challenging for junior developers or those new to the language, as the operations being performed are not immediately apparent and may lead to confusion later on.
Whether this tradeoff is worthwhile or not is personally difficult to tell at this point, as I'm still new to the language and in the process of learning more about Scala. While the pragmatic side of me as a programmer appreciates this feature, I'm more skeptical as an engineer when considering the implications it may have when reading or reviewing code.
What do you think? Add a comment below!