Chapter 8. Inheritance

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.

  • A value class wraps a single value without the overhead of a separate object.

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.

8.1 Extending a Class

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.

8.2 Overriding Methods

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

Images 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.

8.3 Type Checks and Casts

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

obj.isInstanceOf[Cl]

obj instanceof Cl

obj.asInstanceOf[Cl]

(Cl) obj

classOf[Cl]

Cl.class

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 Employee
  case _ => ... // p wasn't an Employee

See Chapter 14 for more information.

8.4 Superclass Construction

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 { // Java
  private double salary;
  public Employee(String name, int age, double salary) {
    super(name, age);
    this.salary = salary;
  }
}

Images 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))

8.5 Anonymous Subclasses

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!"

Images Caution

The new keyword is required to construct an instance of an anonymous class.

8.6 Abstract Classes

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

8.7 Abstract Fields

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 method
  var 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 property
  var 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"
}

8.8 Overriding Fields

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 val

With def

With var

Override val

  • Subclass has a private field (with the same name as the superclass field—that’s OK).

  • Getter overrides the superclass getter.

Error.

Error.

Override def

  • Subclass has a private field.

  • Getter overrides the superclass method.

Like in Java.

A var can override a getter/setter pair. Overriding just a getter is an error.

Override var

Error.

Error.

Only if the superclass var is abstract.

Images 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.

8.9 Open and Sealed Classes

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.

Images 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.

8.10 Protected Fields and Methods

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.)

8.11 Construction Order

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:

  1. The Ant constructor calls the Creature constructor before doing its own construction.

  2. The Creature constructor sets its range field to 10.

  3. The Creature constructor, in order to initialize the env array, calls the range() getter.

  4. That method is overridden to yield the (as yet uninitialized) range field of the Ant class.

  5. The range method returns 0. (That is the initial value of all integer fields when an object is allocated.)

  6. env is set to an array of length 0.

  7. 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).

Images 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.

Images 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.

8.12 The Scala Inheritance Hierarchy

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.

Images

Figure 8–1 The inheritance hierarchy of Scala classes

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 }

Images 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.

Images 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 ().

Images 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)

Images 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: 3
showAny(3, 4, 5) // Prints class scala.Tuple3: (3,4,5)

8.13 Object Equality L1

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

Images 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.

Images 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.

Images 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!

Images 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.

8.14 Multiversal Equality L2

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.

8.15 Value Classes L2

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:

  1. The class extends AnyVal.

  2. Its primary constructor has exactly one parameter, which is a val, and no body.

  3. The class has no other fields or constructors.

  4. 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()

Images 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.

Images 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.

Exercises

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.26.90