Topics in This Chapter A1
In this chapter, you will learn the most important ways in which inheritance in Scala differs from inheritance in other programming languages. (I assume that you are familiar with the general concept of inheritance.) The highlights are:
The extends
keyword denotes inheritance.
You must use override
when you override a method.
A final
class cannot be extended. A final
method cannot be overridden.
An open
class is explicitly designed for being extended.
Only the primary constructor can call the primary superclass constructor.
You can override fields.
You can define classes whose instances can only be compared with each other or other suitable types.
In this chapter, we only discuss the case in which a class inherits from another class. See Chapter 10 for inheriting Traits for inheriting traits—the Scala concept that generalizes Java interfaces.
To form a subclass in Scala, use the extends
keyword:
class Employee extends Person :
var salary = 0.0
...
In the body of the subclass, specify fields and methods that are new to the subclass or that override methods in the superclass.
You can declare a class final
so that it cannot be extended. You can also declare individual methods or fields final
so that they cannot be overridden. (See Section 8.8, “Overriding Fields,” on page 102 for overriding fields.) Note that this is different from Java, where a final
field is immutable, similar to val
in Scala.
In Scala, you must use the override
modifier when you override a method that isn’t abstract. (See Section 8.6, “Abstract Classes,” on page 101 for abstract methods.) For example,
class Person :
...
override def toString = s"${getClass.getName}[name=$name]"
The override
modifier can give useful error messages in a number of common situations, such as:
When you misspell the name of the method that you are overriding
When you accidentally provide a wrong parameter type in the overriding method
When you introduce a new method in a superclass that clashes with a subclass method
Note
The last case is an instance of the fragile base class problem where a change in the superclass cannot be verified without looking at all the subclasses. Suppose programmer Alice defines a Person
class, and, unbeknownst to Alice, programmer Bob defines a subclass Student
with a method id
yielding the student ID. Later, Alice also defines a method id
that holds the person’s national ID. When Bob picks up that change, something may break in Bob’s program (but not in Alice’s test cases) since Student
objects now return unexpected IDs.
In Java, one is often advised to “solve” this problem by declaring all methods as final
unless they are explicitly designed to be overridden. That sounds good in theory, but programmers hate it when they can’t make even the most innocuous changes to a method (such as adding a logging call). That’s why Java eventually introduced an optional @Overrides
annotation.
To invoke a superclass method in Scala, use the keyword super
:
class Employee extends Person :
...
override def toString = s"${super.toString}[salary=$salary]"
The call super.toString
invokes the toString
method of the superclass—that is, the Person.toString
method.
To test whether an object belongs to a given class, use the isInstanceOf
method. If the test succeeds, you can use the asInstanceOf
method to convert a reference to a subclass reference:
if p.isInstanceOf[Employee] then
val s = p.asInstanceOf[Employee] // s has type Employee
...
The p.isInstanceOf[Employee]
test succeeds if p
refers to an object of class Employee
or its subclass (such as Manager
).
If p
is null
, then p.isInstanceOf[Employee]
returns false
and p.asInstanceOf[Employee]
returns null
.
If p
is not an Employee
, then p.asInstanceOf[Employee]
throws an exception.
If you want to test whether p
refers to an Employee
object, but not a subclass, use
if p.getClass == classOf[Employee] then ...
The classOf
method is defined in the scala.Predef
object that is always imported.
Table 8–1 shows the correspondence between Scala and Java type checks and casts.
Table 8–1 Type Checks and Casts in Scala and Java
Scala | Java |
---|---|
|
|
|
|
|
|
However, pattern matching is usually a better alternative to using type checks and casts. For example,
p match
case s: Employee => ... //
Process s as an Employeecase _ => ... //
p wasn't an Employee
See Chapter 14 for more information.
Recall from Chapter 5 that a class has one primary constructor and any number of auxiliary constructors, and that all auxiliary constructors must start with a call to a preceding auxiliary constructor or the primary constructor.
As a consequence, an auxiliary constructor can never invoke a superclass constructor directly.
The auxiliary constructors of the subclass eventually call the primary constructor of the subclass. Only the primary constructor can call a superclass constructor.
Recall that the primary constructor is intertwined with the class definition. The call to the superclass constructor is similarly intertwined. Here is an example:
class Employee(name: String, age: Int, var salary : Double) extends
Person(name, age) :
...
This defines a subclass
class Employee(name: String, age: Int, var salary : Double) extends
Person(name, age)
and a primary constructor that calls the superclass constructor
class Employee(name: String, age: Int, var salary : Double) extends
Person(name, age)
Intertwining the class and the constructor makes for very concise code. You may find it helpful to think of the primary constructor parameters as parameters of the class. Here, the Employee
class has three parameters: name
, age
, and salary
, two of which it “passes” to the superclass.
In Java, the equivalent code is quite a bit more verbose:
public class Employee extends Person { //
Javaprivate double salary;
public Employee(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
}
Note
In a Scala constructor, you can never call super(params)
, as you would in Java, to call the superclass constructor.
A Scala class can extend a Java class. Its primary constructor must invoke one of the constructors of the Java superclass. For example,
class ModernPrintWriter(p: Path, cs: Charset = StandardCharsets.UTF_8) extends
java.io.PrintWriter(Files.newBufferedWriter(p, cs))
You can construct an instance of an anonymous subclass by providing construction arguments and a block with overrides. For example, suppose we have the following superclass:
class Person(val name: String) :
def greeting = s"Hello, my name is $name"
Here, we construct an object that belongs to an anonymous subclass of Person
, overriding the greeting
method:
val alien = new Person("Tweel") :
override def greeting = "Greetings, Earthling!"
Caution
The new
keyword is required to construct an instance of an anonymous class.
A class declared as abstract
cannot be instantiated. This is usually done because one or more of its methods are not defined. For example,
abstract class Person(val name: String) :
def id: Int //
No method body—this is an abstract method
Here we say that every person has an ID, but we don’t know how to compute it. Each concrete subclass of Person
needs to specify an id
method. Note that you do not use the abstract
keyword for an abstract method. You simply omit its body. A class with at least one abstract method must be declared abstract
.
In a subclass, you need not use the override
keyword when you define a method that was abstract in the superclass.
class Employee(name: String) extends Person(name) :
def id = name.hashCode //
override keyword not required
In addition to abstract methods, a class can also have abstract fields. An abstract field is simply a field without an initial value. For example,
abstract class Person :
val id: Int
//
No initializer—this is an abstract field with an abstract getter methodvar name: String
//
Another abstract field, with abstract getter and setter methods
This class defines abstract getter methods for the id
and name
fields and an abstract setter for the name
field. The generated Java class has no fields.
Concrete subclasses must provide concrete fields, for example:
class Employee(val id: Int) extends Person : //
Subclass has concrete id propertyvar name = "" //
and concrete name property
As with methods, no override
keyword is required in the subclass when you define a field that was abstract in the superclass.
You can always customize an abstract field by using an anonymous type:
val fred = new Person() {
val id = 1729
var name = "Fred"
}
Recall from Chapter 5 that a field in Scala consists of a private field and accessor/mutator methods. You can override a val
(or a parameterless def
) with another val
field of the same name. The subclass has a private field and a public getter, and the getter overrides the superclass getter (or method).
For example,
class Person(val name: String) :
override def toString = s"${getClass.getName}[name=$name]"
class SecretAgent(codename: String) extends Person(codename) :
override val name = "secret" //
Don’t want to reveal name...override val toString = "secret" //
...or class name
This example shows the mechanics, but it is rather artificial. A more common case is to override an abstract def
with a val
, like this:
abstract class User :
def id: Int //
Each user has an ID that is computed in some way
class Student(override val id: Int) extends User
//
A student ID is simply provided in the constructor
Note the following restrictions (see also Table 8–2):
A def
can only override another def
.
A val
can only override another val
or a parameterless def
.
A var
can only override an abstract var
.
Table 8–2 Overriding val
, def
, and var
With | With | With | |
---|---|---|---|
Override |
| Error. | Error. |
Override |
| Like in Java. | A |
Override | Error. | Error. | Only if the superclass |
Caution
In Chapter 5, I said that it’s OK to use a var
because you can always change your mind and reimplement it as a getter/setter pair. However, the programmers extending your class do not have that choice. They cannot override a var
with a getter/setter pair. In other words, if you provide a var
, all subclasses are stuck with it.
Inheritance is a powerful feature—and it can be abused. Sometimes, programmers extend a class just because it is convenient to pick up some methods. Chapter 7 discussed the exports
feature that helps Scala programmers avoid this trap by making it easy to use composition and delegation instead of inheritance.
However, many classes are explicitly designed for inheritance. In Scala, use the open
keyword to mark those classes:
open class Person :
...
Starting with Scala 3.1, there will be a warning if you extend a non-open
class outside the source file in which it is declared. To avoid the warning, the file containing the extending class must contain the following import statement:
import scala.language.adhocExtensions
This mechanism is a very modest nudge away from inheritance overuse.
Within a single file, there are no restrictions. Presumably the author of the file understands the superclass well enough to extend it.
If you extend a non-open
class from another file and get a compiler warning, consider whether inheritance is appropriate in your situation. If you decide that it is, include the adhocExtensions
import. It will signal to others that you either made a conscious choice, or you just wanted to shut up the compiler.
Note
It is a syntax error to declare a final
class as open
. Conversely, an abstract
class (Section 8.6, “Abstract Classes,” on page 101) is automatically open
.
A related concept is that of a sealed
class. A sealed class has a fixed number of direct subclasses. They must all be declared in the same file.
// PostalRates.scala
sealed abstract class PostalRate :
...
class DomesticRate extends PostalRate :
...
class InternationalRate extends PostalRate :
...
Trying to extend a sealed class in another file is an error, not a warning.
Sealed classes are intended for pattern matching. If the compiler knows all subclasses of a class, it can verify that a match against subclasses is exhaustive. See Chapter 14 for the details.
As in Java or C++, you can declare a field or method as protected
. Such a member is accessible from any subclass.
class Employee(name: String, age: Int, protected var salary: Double) :
...
class Manager(name: String, age: Int, salary: Double)
extends Employee(name, age, salary) :
def setSalary(newSalary: Double) = // A manager’s salary can never decrease
if newSalary > salary then salary = newSalary
def outranks(other: Manager) =
salary > other.salary
Note that a Manager
method can access the salary
field in any Manager
object.
Unlike in Java, a protected
member is not visible throughout the package to which the class belongs. (If you want this visibility, you can use a package modifier—see Chapter 7.)
When you override a val
in a subclass and use the value in a superclass constructor, the resulting behavior is unintuitive.
Here is an example. A creature can sense a part of its environment. For simplicity, we assume the creature lives in a one-dimensional world, and the sensory data are represented as integers. A default creature can see ten units ahead.
class Creature :
val range: Int = 10
val env: Array[Int] = Array.ofDim[Int](range)
Ants, however, are near-sighted:
class Ant extends Creature :
override val range = 2
Unfortunately, we now have a problem. The range
value is used in the superclass constructor, and the superclass constructor runs before the subclass constructor. Specifically, here is what happens:
The Ant
constructor calls the Creature
constructor before doing its own construction.
The Creature
constructor sets its range
field to 10
.
The Creature
constructor, in order to initialize the env
array, calls the range()
getter.
That method is overridden to yield the (as yet uninitialized) range
field of the Ant
class.
The range
method returns 0
. (That is the initial value of all integer fields when an object is allocated.)
env
is set to an array of length 0
.
The Ant
constructor continues, setting its range
field to 2
.
Even though it appears as if range
is either 10
or 2
, env
has been set to an array of length 0
. The moral is that you should not rely on the value of a val
in the body of a constructor.
As a remedy, you can make range
into a lazy val
(see Chapter 2).
Note
At the root of the construction order problem lies a design decision of the Java language—namely, to allow the invocation of subclass methods in a superclass constructor. In C++, an object’s virtual function table pointer is set to the table of the superclass when the superclass constructor executes. Afterwards, the pointer is set to the subclass table. Therefore, in C++, it is not possible to modify constructor behavior through overriding. The Java designers felt that this subtlety was unnecessary, and the Java virtual machine does not adjust the virtual function table during construction.
Tip
You can have the compiler check for initialization errors, such as the one in this section, by compiling with the experimental -Ysafe-init
flag.
Figure 8–1 shows the inheritance hierarchy of Scala classes. The classes that correspond to the primitive types in the Java virtual machine, as well as the type Unit
, extend AnyVal
. You can also define your own value classes—see Section 8.15, “Value Classes,” on page 111.
All other classes are subclasses of the AnyRef
class. When compiling to the Java virtual machine, this is a synonym for the java.lang.Object
class.
Both AnyVal
and AnyRef
extend the Any
class, the root of the hierarchy.
The Any
class defines methods isInstanceOf
, asInstanceOf
, and the methods for equality and hash codes that we will look at in Section 8.13, “Object Equality,” on page 109.
AnyVal
does not add any methods. It is just a marker for value types.
The AnyRef
class adds the monitor methods wait
and notify
/notifyAll
from the Object
class. It also provides a synchronized
method with a function parameter. That method is the equivalent of a synchronized
block in Java. For example,
account.synchronized { account.balance += amount }
Note
Just like in Java, I suggest you stay away from wait
, notify
, and synchronized
unless you have a good reason to use them instead of higher-level concurrency constructs.
At the other end of the hierarchy are the Nothing
and Null
types.
Null
is the type whose sole instance is the value null
. You can assign null
to any reference, but not to one of the value types. For example, setting an Int
to null
is not possible. This is better than in Java, where it would be possible to set an Integer
wrapper to null
.
Note
With the experimental “explicit nulls” feature, the type hierarchy changes, and Null
is no longer a subtype of the types extending AnyRef
. Object references cannot be Null
. If you want nullable values, you need to declare a union type T | Null
. To opt in to this feature, compile with the -Yexplicit-nulls
flag.
The Nothing
type has no instances. It is occasionally useful for generic constructs. For example, the empty list Nil
has type List[Nothing]
, which is a subtype of List[T]
for any T
.
The ???
method is declared with return type Nothing
. It never returns but instead throws a NotImplementedError
when invoked. You can use it for methods that you still need to implement:
class Person(val name: String) :
def description: String = ???
The Person
class compiles since Nothing
is a subtype of every type. You can start using the class, so long as you don’t call the description
method.
The Nothing
type is not at all the same as void
in Java or C++. In Scala, void
is represented by the Unit
type, the type with the sole value ()
.
Caution
Unit
is not a supertype of any other type. However, a value of any type can be replaced by a ()
. Consider this example:
def showAny(o: Any) = println(s"${o.getClass.getName}: $o")
def showUnit(o: Unit) = println(s"${o.getClass.getName}: $o")
showAny("Hello") //
Yields "java.lang.String: Hello"showUnit("Hello") //
Yields "void: ()"//
"Hello" is replaced with () (with a warning)
Caution
When a method has a parameter of type Any
or AnyRef
, and it is called with multiple arguments, then the arguments are placed in a tuple:
showAny(3) //
Prints class java.lang.Integer: 3showAny(3, 4, 5) //
Prints class scala.Tuple3: (3,4,5)
You can override the equals
method to provide a natural notion of equality for the objects of your classes.
Consider the class
class Item(val description: String, val price: Double) :
...
You might want to consider two items equal if they have the same description and price. Here is an appropriate equals
method:
final override def equals(other: Any) =
other.isInstanceOf[Item] && {
val that = other.asInstanceOf[Item]
description == that.description && price == that.price
}
Or better, use pattern matching:
final override def equals(other: Any) = other match
case that: Item => description == that.description && price == that.price
case _ => false
Tip
Generally, it is very difficult to correctly extend equality in a subclass. The problem is symmetry. You want a.equals(b)
to have the same result as b.equals(a)
, even when b
belongs to a subclass. Therefore, an equals
method should usually be declared as final
.
Caution
Be sure to define the equals
method with parameter type Any
. The following would be wrong:
final def equals(other: Item) = ... //
Don’t!
This is a different method which does not override the equals
method of Any
.
When you define equals
, remember to define hashCode
as well. The hash code should be computed only from the fields that you use in the equality check, so that equal objects have the same hash code. In the Item
example, combine the hash codes of the fields.
final override def hashCode = (description, price).##
The ##
method is a null-safe version of the hashCode
method that yields 0
for null
instead of throwing an exception.
Caution
Do not supply your own ==
method instead of equals
. You can’t override the ==
method defined in Any
, but you can supply a different one with an Item
argument:
final def ==(other: Item) = //
Don’t supply == instead of equals!description == other.description && price == other.price
This method will be invoked when you compare two Item
objects with ==
. But other classes, such as scala.collection.mutable.HashSet
, use equals
to compare elements, for compatibility with Java objects. Your ==
method will not get called!
Tip
You are not compelled to override equals
and hashCode
. For many classes, it is appropriate to consider distinct objects unequal. For example, if you have two distinct input streams or radio buttons, you will never consider them equal.
Unlike in Java, don’t call the equals
method directly. Simply use the ==
operator. For reference types, it calls equals
after doing the appropriate check for null
operands.
In Java, the equals
method is universal. It can be invoked to compare objects of any class.
That sounds nice, but it limits the ability of the compiler to find errors. In Scala, the ==
operator can be made to fail at compile time when its arguments could not possibly be comparable.
There are two mechanisms for opting into this multiversal equality.
If you want to ensure that all equality checks use multiversal equality, add the import:
import scala.language.strictEquality
Alternatively, you can activate multiversal equality selectively. A class can declare that it wants to disallow comparison with instances of other classes:
class Item(val description: String, val price: Double) derives CanEqual :
...
The derives
keyword is a general mechanism that you will see in Chapter 20.
Now it is impossible to compare Item
instances with unrelated objects:
Item("Blackwell toaster", 29.95) == Product("Blackwell toaster")
//
Compile-time error
Even with multiversal equality, you can implement equality checks between different classes. This is not something that you should attempt in general, but it happens with a number of classes in the Scala library. You can check for equality between any two sequences or sets (subtypes of scala.collection.Seq
and scala.collection.Set
), provided the element types can also be equal.
You can test for equality between any numeric primitive types, or between any primitive types and their wrapper types. Finally, any reference type can be compared for equality with null
.
For backwards compatibility, mixed equality checks are still permitted when neither operand opts in to multiversal equality.
Some classes have a single field, such as the wrapper classes for primitive types, and the “rich” or “ops” wrappers that Scala uses to add methods to existing types. It is inefficient to allocate a new object that holds just one value. Value classes allow you to define classes that are “inlined,” so that the single field is used directly.
A value class has these properties:
The class extends AnyVal
.
Its primary constructor has exactly one parameter, which is a val
, and no body.
The class has no other fields or constructors.
The automatically provided equals
and hashCode
methods compare and hash the underlying value.
As an example, let us define a value class that wraps a “military time” value:
class MilTime(val time: Int) extends AnyVal :
def minutes = time % 100
def hours = time / 100
override def toString = f"$time%04d"
When you construct a MilTime(1230)
, the compiler doesn’t allocate a new object. Instead, it uses the underlying value, the integer 1230
. You can invoke the minutes
and hours
methods on the value:
val lunch = MilTime(1230)
println(lunch.hours) //
OK
Just as importantly, you cannot invoke Int
methods:
println(lunch * 2) //
Error
To guarantee proper initialization, make the primary constructor private and provide a factory method in the companion object:
class MilTime private(val time: Int) extends AnyVal :
...
object MilTime :
def apply(t: Int) =
if 0 <= t && t < 2400 && t % 100 < 60 then MilTime(t)
else throw IllegalArgumentException()
Caution
In some programming languages, value types are any types that are allocated on the runtime stack, including structured types with multiple fields. In Scala, a value class can only have one field.
Note
If you want a value class to implement a trait (see Chapter 10), the trait must explicitly extend Any
, and it may not have fields. Such traits are called universal traits.
Opaque types are an alternative to value types. An opaque type alias lets you give a name to an existing type, so that the original type cannot be used. For example, MilTime
can be an opaque type alias to Int
. In Chapter 18, you will see how to declare opaque types and define their behavior with “extension methods.”
In the not too distant future, the Java virtual machine will support native value types that can have multiple fields. It is expected that Scala value will evolve to align with JVM value types.
1. Extend the following BankAccount
class to a CheckingAccount
class that charges $1 for every deposit and withdrawal:
class BankAccount(initialBalance: Double) :
private var balance = initialBalance
def currentBalance = balance
def deposit(amount: Double) = { balance += amount; balance }
def withdraw(amount: Double) = { balance -= amount; balance }
2. Extend the BankAccount
class of the preceding exercise into a class SavingsAccount
that earns interest every month (when a method earnMonthlyInterest
is called) and has three free deposits or withdrawals every month. Reset the transaction count in the earnMonthlyInterest
method.
3. Consult your favorite Java, Python, or C++ textbook which is sure to have an example of a toy inheritance hierarchy, perhaps involving employees, pets, graphical shapes, or the like. Implement the example in Scala.
4. Define an abstract class Item
with methods price
and description
. A SimpleItem
is an item whose price and description are specified in the constructor. Take advantage of the fact that a val
can override a def
. A Bundle
is an item that contains other items. Its price is the sum of the prices in the bundle. Also provide a mechanism for adding items to the bundle and a suitable description
method.
5. Design a class Point
whose x and y coordinate values can be provided in a constructor. Provide a subclass LabeledPoint
whose constructor takes a label value and x and y coordinates, such as
LabeledPoint("Black Thursday", 1929, 230.07)
6. Define an abstract class Shape
with an abstract method centerPoint
and subclasses Rectangle
and Circle
. Provide appropriate constructors for the subclasses and override the centerPoint
method in each subclass.
7. Provide a class Square
that extends java.awt.Rectangle
and has three constructors: one that constructs a square with a given corner point and width, one that constructs a square with corner (0, 0)
and a given width, and one that constructs a square with corner (0, 0)
and width 0
.
8. Compile the Person
and SecretAgent
classes in Section 8.8, “Overriding Fields,” on page 102 and analyze the class files with javap
. How many name
fields are there? How many name
getter methods are there? What do they get? (Hint: Use the -c
and -private
options.)
9. In the Creature
class of Section 8.11, “Construction Order,” on page 105, replace val range
with a def
. What happens when you also use a def
in the Ant
subclass? What happens when you use a val
in the subclass? Why?
10. The file scala/collection/immutable/Stack.scala
contains the definition
class Stack[A] protected (protected val elems: List[A])
Explain the meanings of the protected
keywords. (Hint: Review the discussion of private constructors in Chapter 5.)
11. Write a class Circle
with a center and a radius. Add equals
and hashCode
methods. Activate multiversal equality.
12. Define a value class Point
that packs integer x and y coordinates into a Long
(which you should make private).