This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 4.13, “How to define properties in an abstract base class or trait.”
Problem
Using Scala, you want to define abstract or concrete properties in an abstract base class (or trait) that can be referenced in all child classes.
Solution
You can declare both val
and var
fields in an abstract Scala class (or trait), and those fields can be abstract or have concrete implementations. All of these variations are shown in this recipe.
Abstract val
and var
fields
The following example demonstrates an Animal
trait with abstract val
and var
fields, along with a simple concrete method named sayHello
, and an override of the toString
method:
abstract class Pet (name: String) { val greeting: String var age: Int def sayHello { println(greeting) } override def toString = s"I say $greeting, and I'm $age" }
The following Dog
and Cat
classes extend the Animal
class and provide values for the greeting
and age
fields. Notice that the fields are again specified as val
or var
:
class Dog (name: String) extends Pet (name) { val greeting = "Woof" var age = 2 } class Cat (name: String) extends Pet (name) { val greeting = "Meow" var age = 5 }
The functionality can be demonstrated with a simple driver object:
object AbstractFieldsDemo extends App { val dog = new Dog("Fido") val cat = new Cat("Morris") dog.sayHello cat.sayHello println(dog) println(cat) // verify that the age can be changed cat.age = 10 println(cat) }
The resulting output looks like this:
Woof Meow I say Woof, and I'm 2 I say Meow, and I'm 5 I say Meow, and I'm 10
Concrete field implementations are presented in the Discussion, because it helps to understand how the Scala compiler translates your code in the preceding examples.
Discussion
As shown, you can declare abstract fields in an abstract class as either val
or var
, depending on your needs. The way abstract fields work in abstract classes (or traits) is interesting:
- An abstract
var
field results in getter and setter methods being generated for the field. - An abstract
val
field results in a getter method being generated for the field. - When you define an abstract field in an abstract class or trait, the Scala compiler does not create a field in the resulting code; it only generates the methods that correspond to the
val
orvar
field.
In the example shown in the Solution, if you look at the code that’s created by scalac
using the -Xprint:all
option, or by decompiling the resulting Pet.class file, you won’t find greeting
or age
fields. For instance, if you decompile the class, the output shows only methods in the class, no fields:
import scala.*; import scala.runtime.BoxesRunTime; public abstract class Pet { public abstract String greeting(); public abstract int age(); public abstract void age_$eq(int i); public void sayHello() { Predef$.MODULE$.println(greeting()); } public String toString(){ // code omitted } public Pet(String name){} }
Because of this, when you provide concrete values for these fields in your concrete classes, you must again define your fields to be val
or var
. Because the fields don’t actually exist in the abstract base class (or trait), the override
keyword is not necessary.
As another result of this, you may see developers define a def
that takes no parameters in the abstract base class rather than defining a val
. They can then define a val
in the concrete class, if desired. This technique is demonstrated in the following code:
abstract class Pet (name: String) { def greeting: String } class Dog (name: String) extends Pet (name) { val greeting = "Woof" } object Test extends App { val dog = new Dog("Fido") println(dog.greeting) }
Given this background, it’s time to examine the use of concrete val
and var
fields in abstract classes.
Concrete val fields in abstract classes
When defining a concrete val
field in an abstract class, you can provide an initial value, and then override that value in concrete subclasses:
abstract class Animal { val greeting = "Hello" // provide an initial value def sayHello { println(greeting) } def run } class Dog extends Animal { override val greeting = "Woof" // override the value def run { println("Dog is running") } }
In this example, the greeting variable is created in both classes. To demonstrate this, running the following code:
abstract class Animal { val greeting = { println("Animal"); "Hello" } } class Dog extends Animal { override val greeting = { println("Dog"); "Woof" } } object Test extends App { new Dog }
results in this output, showing that both values are created:
Animal Dog
To prove this, you can also decompile both the Animal
and Dog
classes, where you’ll find the greeting
declared like this:
private final String greeting = "Hello";
To prevent a concrete val
field in an abstract base class from being overridden in a subclass, declare the field as a final val
:
abstract class Animal { final val greeting = "Hello" // made the field 'final' } class Dog extends Animal { val greeting = "Woof" // this line won't compile }
Concrete var
fields in abstract classes
You can also give var
fields an initial value in your trait or abstract class, and then refer to them in your concrete subclasses, like this:
abstract class Animal { var greeting = "Hello" var age = 0 override def toString = s"I say $greeting, and I'm $age years old." } class Dog extends Animal { greeting = "Woof" age = 2 }
In this case, these fields are declared and assigned in the abstract base class, as shown in the decompiled code for the Animal
class:
private String greeting; private int age; public Animal(){ greeting = "Hello"; age = 0; } // more code ...
Because the fields are declared and initialized in the abstract Animal
base class, there’s no need to redeclare the fields as val
or var
in the concrete Dog
subclass.
You can verify this by looking at the code the Scala compiler generates for the Dog
class. When you compile the code with scalac -Xprint:all
, and look at the last lines of output, you’ll see how the compiler has converted the Dog
class:
class Dog extends Animal { def <init>(): Dog = { Dog.super.<init>(); Dog.this.greeting_=("Woof"); Dog.this.age_=(2); () } }
Because the fields are concrete fields in the abstract base class, they just need to be reassigned in the concrete Dog
class.
Don’t use null
As discussed in many recipes in this book, including Recipe 20.5, “Eliminate null Values from Your Code”, you shouldn’t use null
values in these situations. If you’re tempted to use a null
, instead initialize the fields using the Option/Some/None pattern. The following example demonstrates how to initialize val
and var
fields with this approach:
trait Animal { val greeting: Option[String] var age: Option[Int] = None override def toString = s"I say $greeting, and I'm $age years old." } class Dog extends Animal { val greeting = Some("Woof") age = Some(2) } object Test extends App { val d = new Dog println(d) }
Running this Test
object yields the following output:
I say Some(Woof), and I'm Some(2) years old.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- See Recipe 5.2, “Calling a Method on a Superclass in Scala”, for more examples of how to call methods on superclasses.