Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit. Myös suomenkielisessä materiaalissa käytetään ohjelmaprojektien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.

Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta painikkeesta. Tai tästä: Vaihda suomeksi.


Chapter 7.3: Inheritance and Class Hierarchies

About This Page

Questions Answered: Say I have a regular class; how can I define a subtype for it? How do the classes and traits of the Scala API form a family tree? How can I form such a family tree of data types for my program?

Topics: Inheritance: extending a superclass with subclasses. Some key classes in Scala’s class hierarchy. Abstract classes.

What Will I Do? Read and program.

Rough Estimate of Workload:? Three hours.

Points Available: B70.

Related Projects: Subtypes.

../_images/person04.png

Introduction

In the previous chapter, we used traits to define supertypes for multiple classes. In this chapter, we’ll use a technique known as inheritance (periytyminen) that defines supertypes as regular classes rather than traits.

Let’s Continue with Shapes

In the previous chapter, we defined the trait Shape and an implementing class Rectangle:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double

}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {

  def area = this.sideLength * this.anotherSideLength

}

What if we also want to represent squares, that is, rectangles with four sides of equal length? We’d like to be able to write new Square(10) and the like.

Of course, we might simply extend Shape in a Square class:

class Square(val sideLength: Double) extends Shape {

  def area = this.sideLength * this.sideLength

}

The bothersome thing about this solution is that the code is quite redundant: the algorithm for computing the area of a square is the same as that for a rectangle; it just happens that the sides are equal in length. This approach isn’t up to par in terms of conceptual modeling, either: it makes squares a subtype of Shape alongside rectangles, whereas we humans prefer to think of a squares as a special case of a rectangle; each square is a square, a shape, and a rectangle.

The fix is simple: we can define squares as a subtype of rectangles, as shown in the diagram below. We can do this even though Rectangle is a regular class, not a trait.

../_images/inheritance_shape_square.png

Subclasses and Superclasses

Let’s define Square like this instead:

class Square(size: Double) extends Rectangle(size, size) {

}
We again use the extends keyword, but this time we follow it with the name of a regular class rather than a trait. We say: the class Square inherits the class Rectangle. The inheriting class is known as a subclass (aliluokka); the class being inherited from is known as a superclass (yliluokka). Squares. This gives all Square objects the additional type of Rectangle.
Square has only one constructor parameter that determines the length of each side.
To create an instance of a subclass, any initialization steps that the superclass demands must also be performed (such as initializing the superclass’s instance variables). It’s common to pass constructor parameters from a subclass to the superclass, as shown here. In this example, we state that whenever a Square object is created, we initialize a Rectangle so that each of its two constructor parameters (each side length) gets the value of the new Square’s single constructor parameter. (See the animation below.)
We could have simply left out the empty curly brackets. (In this example, we haven’t given squares any methods or instance variables that rectangles don’t have.)

Inheritance vs. traits

Extending a superclass with a subclass looks much like extending a trait with a class. The two techniques are closely related.

There are differences, though; let’s compare. First, consider traits and how the Rectangle class extends the Shape trait:

When using a trait Example
The supertype is represented by a trait. trait Shape
Each subtype extends (“mixes in”) that trait. class Rectangle extends Shape
A trait can define abstract methods and variables. def area: Double
A trait cannot be instantiated directly. One does not simply write new Shape.
A trait cannot take constructor parameters. trait Shape(...) is invalid.
A class (or trait) may directly extend multiple traits. class X extends Trait1 with Trait2 with Trait3 is fine.

And here is a similar table about inheritance and the example of Rectangle as a superclass for Square:

When inheriting from a regular class Example
The supertype is represented by a (super)class. class Rectangle
Each subtype extends (“inherits”) that superclass. class Square extends Rectangle
An ordinary class cannot define abstract methods or variables. (But you can... see below.) All the methods on Rectangle have a function body.
A superclass can be instantiated directly. new Rectangle works.
A superclass can take constructor parameters. class Rectangle(val x: Int, val y: Int) extends Shape is fine.
(In Scala and many other languages:) A class can have only one immediate superclass. class X extends Super1 with Super2 is invalid. (But class X extends Super1 with Trait1 with Trait2 is fine.)

There are many scenarios where it’s reasonable to use either of these techniques.

Abstract Classes

The distinction between traits and class-based inheritance is further complicated by the fact that it’s possible to define so-called abstract classes. We’ll approach this concept through an example.

Let’s return briefly to the domain of phone bills, previously featured in Chapter 2.3. In that chapter, you used a given class PhoneCall, which represented aspects of phone calls that were relevant for billing purposes. Below is an implementation for such a class. (For simplicity, the version shown here doesn’t add a network surcharge like the earlier class did.)

class PhoneCall(initialFee: Double, pricePerMinute: Double, val duration: Double) {
  def totalPrice = this.initialFee + this.pricePerMinute * this.duration
}

Suppose we now want to add text messages to phone bills. Moreover, we’d like to record, for each billable call and message, whether or not its price already includes a 24-percent value-added tax (VAT). Finally, we’d like to have a method for computing the tax-free price.

In other words, we’d like phone bills to list “billable transactions” that can be either phone calls or text messages. Here’s a first draft:

class Transaction(val vatAdded: Boolean) {
  def priceWithoutTax = if (this.vatAdded) this.totalPrice / 1.24 else this.totalPrice
}
class PhoneCall(val duration: Double,
             val initialFee: Double,
             val pricePerMinute: Double,
             vatAdded: Boolean) extends Transaction(vatAdded) {
  def totalPrice = this.initialFee + this.pricePerMinute * this.duration
}
class TextMessage(val price: Double, vatAdded: Boolean) extends Transaction(vatAdded) {
  def totalPrice = this.price
}
We’d like to pass in a constructor parameter vatAdded to each Transaction in order to indicate whether the given prices include a 24% VAT. If the tax is included, priceWithoutTax subtracts the amount of tax in order to return the tax-free price.
The subclasses PhoneCall and TextMessage each take multiple constructor parameters. Most of these parameters are associated with these specific subclasses, but ...
... the value of vatAdded gets passed on as a constructor parameter to the superclass. The processing of that information is left entirely to Transaction.
This example code has a small but critical problem, however. The priceWithoutTax method calls totalPrice on a transaction object, but the program doesn’t ensure that all possible transaction objects actually have this method. (It so happens that each of the two subclasses that we’ve defined here do have the method, but more generally, it’s not clear that any instance of any subclass of Transaction will have it.) The compiler rejects this code.

What we need is a totalPrice method that is common to all possible objects of type Transaction but that is implemented in different ways by different subclasses. Something like this:

class Transaction(val vatAdded: Boolean) {

  def totalPrice: Double

  def priceWithoutTax = if (this.vatAdded) this.totalPrice / 1.24 else this.totalPrice

}

In this version, the totalPrice method is abstract, with no implementation. It exists in order to guarantee that such a method is somehow implemented on all transaction objects, which makes it possible to implement priceWithoutTax in general terms within the Transaction superclass.

But ordinary classes weren’t supposed to have abstract methods, right!?

Indeed, the above second draft of Transaction doesn’t make it past the compiler, either. But this one does:

abstract class Transaction(val vatAdded: Boolean) {

  def totalPrice: Double

  def priceWithoutTax = if (this.vatAdded) this.totalPrice / 1.24 else this.totalPrice

}
If you want abstract methods on a class, you can add
abstract to the class definition.

Such a class is called an abstract class. A class that is not abstract may be referred to as a concrete class.

An abstract class is similar to a trait. Let’s include it in our comparison:

  Trait Abstract superclass Concrete superclass
Can it have abstract methods? Yes. Yes. No.
Can it be directly instantiated using new? No. No. Yes.
Can it take constructor parameters? No. Yes. Yes.
Can several of them be extended by a class (using extends and with)? Yes. No. No.

An alternative implementation for TextMessage

Above, we implemented text messages like this:

class TextMessage(val price: Double, vatAdded: Boolean) extends Transaction(vatAdded) {
  def totalPrice = this.price
}

Perhaps you feel it’s redundant to use two names, price and totalPrice, that give you access to the same information. And it is unnecessary. This works, too:

class TextMessage(val totalPrice: Double, vatAdded: Boolean) extends Transaction(vatAdded)

Simple as that. The only new thing about this is that the subclass turns totalPrice into a variable; there’s no def that implements the superclass method. But there’s nothing wrong with that; this is a valid way to implement a parameterless abstract method. The important thing is that an expression such as myTextMessage.totalPrice must have a Double value, one way or another. For a user of the text-message class, it’s generally irrelevant whether they are dealing with a val or an effect-free, parameterless method that efficiently returns the same value every time. (Further reading: the Wikipedia article on the uniform access principle.)

Couldn’t we have used a trait there?

As an alternative to the approach we adopted above, we could have made Transaction a trait, replacing class with trait in its definition. However, that solution is unsatisfactory in one respect: Scala prohibits traits from having constructor parameters, so we would have needed to make other changes to work around that.

Optional assignment: Rewrite (mentally, at least) Transaction and its subclasses. Use a trait instead of an abstract superclass.

Generally speaking, should I use a trait or a superclass?

Rule of thumb: Unless you have a specific reason to use an abstract class, use a trait instead, since traits are more flexibly extendable.

The convenience of constructor parameters might be a reason to use a superclass rather than a trait.

It can be tricky to choose among a trait, an abstract superclass, or a concrete superclass. However, in O1, such decisions are generally made for you and provided in the assignment specifications. You can worry about learning to make these decisions yourself later, in Programming Studio 2, for example. For more information, you can also take a look at Section 12.7 of Programming in Scala (Third Edition), one of the book recommendations on our Books and Other Resources page.

Class Hierarchies and the Scala API

../_images/inheritance_animals-en.png

Chapter 7.2 demonstrated that we can represent conceptual hierarchies with traits. Inheritance is likewise useful in forming such hierarchies. Even though a class can have only one immediate superclass, it can have multiple indirect superclasses. In the diagram above, for instance, the immediate superclass of Spider is Arthropod, but Animal is also one of its superclasses.

The ready-made classes of the Scala API also form hierarchies. Let’s consider a few examples.

A hierarchy of GUI elements

The package scala.swing provides a selection of classes that represent GUI elements, building blocks for graphical user interfaces. The diagram below depicts a part of this hierarchy.

../_images/inheritance_swing.png

Some of these classes from package scala.swing are traits, some are regular classes. Each supertype defines generic properties common to all the extending types. At the top level, UIElement defines properties common to all kinds of GUI elements, such as background color and size.

About the Swing GUI library

Chapter 12.3 contains an introduction to the GUI library known as Swing. That optional chapter is at the rear end of O1 and this ebook, but if you’re itching to read more about GUIs, go ahead and read it. Now that you’ve been introduced to inheritance, you should have the sufficient prerequisite knowledge for the chapter.

The Option hierarchy (and sealed superclasses)

../_images/inheritance_option1.png

There’s a small class hierarchy associated with the Option type, too.

Since Chapter 4.2, you’ve known that there are two kinds of Option objects. Any object of type Option is either a Some object that contains a value or None. This is actually an example of inheritance: Option is an abstract class that the concrete class Some and the singleton object None extend.

Chapter 7.2 mentioned how to seal a trait: the sealed keyword makes it impossible to extend a trait outside the file where the trait itself is defined. A superclass can also be sealed, and Option is just such a sealed class: as noted in Chapter 4.2, an Option can be either a Some or None (which are defined in the same file) but it can never, ever be anything else. Which is exactly what you want when you use Option. You can’t extend Option with a class of your own, and that’s good.

The mother of all classes: Any

Let’s examine some objects in the Scala REPL:

val miscellany = Vector(123, "llama", true, Vector(123, 456), new Square(10))miscellany: Vector[Any] = Vector(123, llama, true, Vector(123, 456), o1.shapes.Square@114c3c7)
We create a vector that contains some completely disparate objects: an Int, a String, a Boolean, a Vector of Int, and a Square.
Scala infers that the type of the vector’s elements is Any. That is, what we have is a “vector of any sorts of objects”. This suggests that integers, vectors, squares, and what have you, are all of type Any.

All Scala classes and singleton objects — including those you write — automatically inherit from a class called Any, even though we don’t usually record this inheritance explicitly in code. All the objects in a Scala program always have the Any type in addition to any other types they may have.

It’s possible to use this top-level class as a type for a variable, as illustrated below.

var someObject: Any = "kumquat"someObject: Any = kumquat
someObject.isInstanceOf[Any]res0: Boolean = true
someObject.isInstanceOf[String]res1: Boolean = true
someObject.isInstanceOf[Square]res2: Boolean = false
someObject = new Square(10)someObject: Any = o1.shapes.Square@ecfb83
someObject.isInstanceOf[Any]res3: Boolean = true
someObject.isInstanceOf[String]res4: Boolean = false
someObject.area<console>:12: error: value area is not a member of Any
            someObject.area
                       ^
As the name suggests, you can use a variable of type Any to refer to any object. The object can be a string or a square, for instance.
So, the static type of someObject is Any, and the expression someObject.isInstanceOf is valid because (and only because) the isInstanceOf method is defined in the Any class and is therefore available on any Scala object.
The attempted method call someObject.area fails even though the variable happens to store a Square object which has an area method. The variable’s static type limits what you can do with it.

In most cases, it’s not sensible to use variables of type Any, since the type’s genericity so constricts what you can do with such variables. With Any as the static type, you can use the variable only for the operations that are defined on Any; those extremely generic methods that are common to all Scala objects include isInstanceOf, toString, ==, !=, and a handful of others. Generally, it makes sense to use variables with more specific types, which is of course what we’ve been doing already. (See the optional material below for a further discussion.)

Something you may have noticed in the docs

The Scaladoc pages for many classes say extends AnyRef near the top. Here’s a familiar example:

But we haven’t seen extends AnyRef in the Scala code; why is it there in the docs? And why AnyRef, not Any?

The mother of most classes: AnyRef a.k.a. Object

Scala’s root type Any divides in two “branches”. It has two immediate subclasses, AnyVal and AnyRef:

../_images/inheritance_any.png

This division to AnyRef and AnyVal has to do with the way the Scala language is implemented and isn’t too significant to the Scala beginner or for learning the basics of programming more generally. Nevertheless, it’s good to be aware that these types exist so you know why they sometimes crop up in Scaladocs, REPL outputs, and error messages.

AnyVal is a superclass for some API classes that define certain data types that are simple but operate more efficiently than other classes do. The familiar data types Int, Double, Boolean, Char, and Unit descend from AnyVal, as do a few others. It’s relatively uncommon to extend AnyVal in an application; in O1, we don’t do that at all.

AnyRef, on the other hand, is a superclass for all other classes and singleton objects. The classes String and Vector, for instance, derive from AnyRef, as does the Transaction class we just wrote.

When using the common JVM-based implementation of Scala (as we do; Chapter 5.2), AnyRef is sometimes (and somewhat confusingly) also known by the name Object for JVM-specific technical reasons.

We can spot AnyVal and AnyRef a.k.a. Object in the REPL, too:

val miscellany2 = Vector(123, true)miscellany2: Vector[AnyVal] = Vector(123, true)
val miscellany3 = Vector("llama", Vector(123, 456), new Square(10))miscellany3: Vector[Object] = Vector(llama, Vector(123, 456), o1.shapes.Square@667113)
Int and Boolean descend from AnyVal.
String, Vector, and Square descend from Object, which is effectively synonymous with AnyRef.

Overriding Supertype Methods

Since Week 2, we have used override for replacing default method implementations with class-specific ones. In particular, we have used this keyword in:

  • toString methods (Chapter 2.5): the methods that we’ve
    written have overridden the default implementation (which produces descriptions such as o1.shapes.Square@ecfb83). That default implementation comes from class AnyRef.
  • the event handlers on Views (such as onClick; Chapter 2.8): the default implementations in class View react to events by doing nothing at all but you can override them with whichever behavior you wish your application to have.

You can also override other methods in type hierarchies. As an experiment, let’s write a few mini-classes.

../_images/inheritance_a.png
class A {
  def test() = {
    println("Greetings from class A.")
  }
}
class B extends A {
}
class C extends A {
  override def test() = {
    println("Greetings from class C.")
  }
}
class D extends C {
}
class E extends D {
  override def test() = {
    println("Greetings from class E.")
  }
}

Now let’s use our classes in the REPL:

(new A).test()Greetings from class A.
(new B).test()Greetings from class A.
(new C).test()Greetings from class C.
(new D).test()Greetings from class C.
(new E).test()Greetings from class E.
Class B doesn’t define an overriding implementation, so a B object simply inherits the test implementation of A.
Class C overrides test.
Class D doesn’t override the method. A D object uses the implementation from its immediate superclass C (which overrides the one from A).
Class E does supply an overriding implementation that supersedes both the one in C and the one in A.

Let’s explore further:

var myObject = new AmyObject: A = A@e1ee21
myObject.test()Greetings from class A.
myObject = new CmyObject: A = C@c081a6
myObject.test()Greetings from class C.
Notice: The variable’s static type is A. Its value has the dynamic type C.
We can call test on any expression with the static type A (or one of A’s subtypes); that is, we can call it on any object that is guaranteed to have the method. On the other hand, what happens when we do so depends on the dynamic type of the object that receives the message. Here, we execute the test implementation defined on C objects, even though the variable is of type A.

One more class:

class F extends E {
  override def test() = {
    super.test()
    println("Greetings from class F.")
  }
}
You can use the super keyword to refer to an implementation defined in a supertype. Here, we first call the superclass’s version of test. As a consequence, the test method of an F object first does whatever the test method on its superclass E does and then produces a printout specific to F. See below for an example.
(new F).test()Greetings from class E.
Greetings from class F.

In Scala, you must always explicitly use the override keyword whenever you wish to override a method.

Why require an explicit override?

By writing override in your program, you acknowledge that you are deliberately superseding a method implementation defined on a supertype. If the keyword was optional, you might very easily give your method a name that is already used in a supertype. Such a thing could happen quite accidentally and unknowingly, possibly spawning exotic bugs.

Like static typing, this requirement is a language feature that reduces the chance of human error.

As an added benefit, the keyword makes overriding explicit to the program’s readers.

Practice on inheritance and overriding

The frivolous program below features a combination of many of the techniques that we just discussed. You can use it for checking your understanding. If you can tell — in detail! — what this program does, then you will have grasped some of the main principles of inheritance.

A story about driving

Read the code below. Painstakingly consider which exact lines of text it outputs and in which order. Ideally, you should write down what you think the program outputs.

object Cruising extends App {
  val car = new Car
  car.receivePassenger(new Schoolkid("P. Pupil"))
  car.receivePassenger(new ChemicalEngineer)
  car.receivePassenger(new MechanicalEngineer)
  car.receivePassenger(new ElectricalEngineer)
  car.receivePassenger(new ComputerScientist)
  car.start()
}
class Car {
  private val passengers = Buffer[Passenger]()

  def receivePassenger(passenger: Passenger) = {
    passenger.sitDown()
    this.passengers += passenger
  }

  def start() = {
    println("(The car won't start.)")
    for (passenger <- this.passengers) {
      passenger.remark()
    }
  }
}
abstract class Passenger(val name: String) {
  def sitDown() = {
    println(this.name + " finds a seat.")
  }

  def speak(sentence: String) = {
    println(this.name + ": " + sentence)
  }

  def diagnosis: String

  def remark() = {
    this.speak(this.diagnosis)
  }
}
abstract class Student(name: String) extends Passenger(name) {
  def diagnosis = "No clue what's wrong."
}
class Schoolkid(name: String) extends Student(name)
abstract class TechStudent(name: String) extends Student(name) {
  override def remark() = {
    super.remark()
    this.speak("Clear as day.")
  }
}
class ChemicalEngineer extends TechStudent("C. Chemist") {
  override def diagnosis = "It's the wrong octane. Next time, I'll do the refueling."
}
class MechanicalEngineer extends TechStudent("M. Machine") {

  override def diagnosis = "Nothing wrong with the gas. It must be the pistons."

  override def speak(sentence: String) = {
    super.speak(sentence.replace(".", "!"))
  }
}
class ElectricalEngineer extends TechStudent("E. Electra") {
  override def sitDown() = {
    println(this.name + " claims a front seat.")
  }

  override def diagnosis = "Hogwash. The spark plugs are faulty."
}
class ComputerScientist extends TechStudent("C.S. Student") {
  override def remark() = {
    this.speak("No clue what's wrong.")
    this.speak(this.diagnosis)
  }

  override def diagnosis = "Let's all get out of the car, close the doors, reopen, and try again."
}

Did you read the program carefully? Did you write down the output you expect?

Now open project Subtypes and run o1.cruising.Cruising (which is the above program). Was the output precisely what you expected? If not, find out why.

Assignment: Items within Items

Introduction

Imagine we’re working on an application that needs to represent items of various sorts. We have a simple class Item:

class Item(val name: String) {
  override def toString = this.name
}

For this small programming assigment, let’s adopt the following goal: we wish our program to have not only “simple items”, as defined above, but also items that can contain other items. For instance, we might have a bag that contains a book and a box, and the box might further contain a ring.

Task description

Write a subclass for Item. Name it Container. This subclass will represent items that can contain other items, such as bags and boxes. Containers each have a name, just like other items do. Moreover, they have an addContent method that places an item within the container. Their toString implementation differs from that of other items.

Your class should work as shown below.

val container1 = new Container("box")container1: o1.items.Container = box containing 0 item(s)
container1.addContent(new Item("ring"))
container1res5: o1.items.Container = box containing 1 item(s)
val container2 = new Container("bag")container2: o1.items.Container = bag containing 0 item(s)
container2.addContent(new Item("book"))
container2.addContent(container1)
container2res6: o1.items.Container = bag containing 2 item(s)

Instructions and hints

  • There is some starter code in o1.items within the Subtypes project.
  • Don’t forget to pass a constructor parameter to the superclass Item.
  • Don’t use val to (re)define the instance variable name within the Container subclass. That variable is already defined in the superclass. It’s perfectly fine to give Container’s constructor parameter the name name, though.
  • The return value of toString should count only the container’s immediate contents. For instance, in the REPL session above, the bag is shown as containing only two items, even though the box in the bag further contains the ring. (How could we count all the “contents of the contents”, too? You’ll see in Chapter 11.2.)
  • Note that what you override in this assignment isn’t the generic toString from AnyRef, as in many other programs, but the toString implementation in Item.
  • Can you implement the toString in Container so that you use the superclass’s implementation rather than directly accessing the container’s name? (This is optional.)
  • You don’t need to implement any other methods for, say, examining the contents of a container or removing them.

Submission form

A+ presents the exercise submission form here.

Tailoring Instances of a Supertype

In Chapter 2.4, you learned to create an object that has an instance-specific method in addition to the methods defined in a class:

val superman = new Person("Clark") {
 def fly = "WOOSH!"
}superman: Person{def fly: String} = $anon$1@25ba32e0

Since then, we’ve used this technique when creating instances of the View class: we have given each of our View objects a set of tailor-made methods that suits the application.

Tailoring instances in this way is actually a form of inheritance. For example, what the above command actually does is define a nameless subclass of Person “on the fly” and immediately create a single instance of that subclass. Along the same lines, we have written diverse implementations for the abstract makePic method of the superclass View.

The same works in general for traits and superclasses. Read on if you wish.

Defining a subtype “on the fly”

You can also extend a trait “on the fly” as you create an object.

To set up the next example, let’s define a couple of traits and one concrete class. Note that these three types are entirely distinct from each other.

class Animal(val species: String) {
  override def toString = "an animal, more specifically a " + this.species
}
trait Spitting {
  def spit = "ptui!"
}
trait Humped {
  def numberOfHumps: Int
}defined class Animal
defined trait Spitting
defined trait Humped

Let’s now define a new subclass of Animal that has no name and that has the trait Spitting. While we’re at it, let’s instantiate that class immediately. The result is an object of the combined type Animal with Spitting, which we’ve defined on the fly:

val pet = new Animal("llama") with Spittingpet: Animal with Spitting = an animal, more specifically a llama

This object has all the properties of Animals as well as all the properties defined in the Spitting trait:

pet.speciesres7: String = llama
pet.spitres8: String = ptui!

Of course, we could have also defined a named class, as shown below, and instantiated it as per usual.

class Llama extends Animal("llama") with Spittingdefined class Llama

Adding methods on a type defined “on the fly”

Let’s continue our toy example. We’ll create an object of a type that:

  • is a subclass of Animal;
  • additionally extends the traits Humped and Spitting; and
  • implements the abstract method numberOfHumps in the Humped trait in a specific way.
val shipOfTheDesert = new Animal("dromedary") with Humped with Spitting {
  def numberOfHumps = 1
}shipOfTheDesert: Animal with Humped with Spitting = an animal, more specifically a dromedary

Finally, let’s create an object of a nameless class that has the Spitting trait and a couple of additional properties (a name variable and a particular implementation of toString):

val fisherman = new Spitting {
  val name = "Eemeli"
  override def toString = this.name
}fisherman: Spitting{val name: String} = Eemeli

Note that here we followed new with the name of a trait rather than a concrete class. However, this command does not directly instantiate the trait (which isn’t possible; Chapter 7.2). Instead, it defines a new type that extends the trait and additionally has the properties listed inside the curly brackets. It is that new type that is then instantiated.

Terms for googling: scala trait mixin, scala anonymous subclass.

Further Reading

Choosing static types for variables

Rule of thumb: static types in general and the static types of parameters in particular should be as “wide” as possible. That is, where possible, we’d like our variables to have a superclass or a trait as their type rather than a more specific class. That way, our methods and classes will be more generally useful and easier to modify.

Compare:

def doSomething(circle: Circle) = {
  // ...
}

vs.

def doSomething(shape: Shape) = {
  // ...
}

The first definition is sensible in case doSomething should work only on circles (say, because it depends on being able to access the given object’s radius, which other shapes don’t have). Otherwise, the second definition is probably better, since it works on different shapes, including future subclasses of Shape that haven’t even been written yet.

You can’t always generalize; otherwise we’d just give everything the Any type. But do generalize when you can.

Between public and private: protected

A subclass’s instances also have the superclasses’ private variables as part of their state. Inherited methods commonly use those private variables. However, you can’t directly refer to a private member of a superclass from the subclass’s program code. The same goes for private members on traits.

However, Scala, like some other languages, features another access modifier, protected. This modifier allows precisely what we just said private disallows: a protected variable or method is accessible not only to the class or trait itself but also within the code of its subtypes. (But not just anywhere, like a public member is.) An internet search should bring up many examples.

Multiple inheritance

As noted above, a class can extend only a single superclass directly. Why?

Search online for multiple inheritance (moniperintä). You may also wish to look up the “deadly diamond of death” that is sometimes associated with multiple inheritance and that is held to be a serious problem by some but not all programmers.

And here is some slightly provocative but interesting reading (mostly to those students who already know something about Java and its interface construct):'Interface' Considered Harmful.

If you extend multiple traits, which method implementations do you get?

Here’s a simple example:

trait X {
  def method: String
}
trait A extends X {
  override def method = "a's method"
}
trait B extends X {
  override def method = "b's method"
}defined trait X
defined trait A
defined trait B
class AB extends A with B
class BA extends B with Adefined class AB
defined class BA
(new AB).methodres9: String = b's method
(new BA).methodres10: String = a's method

For more information, look up scala linearization or follow this link here.

The Liskov substitution principle

../_images/liskov-en.png

“Square as a subclass of rectangle” is a classic example when discussing the Liskov substitution principle, which we’ll only paraphrase here. This well-known design guideline is often held up as a criterion of object-oriented program quality.

For a class S to follow this principle, the following must be true:

If S is a subclass of T, all operations that are available and meaningful on instances of T are equally available and meaningful on instances of S.

In other words: where you have an instance of T, you can also use an instance of S.

Let’s return to the shape example and these two classes that we defined earlier:

class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
  def area = this.sideLength * this.anotherSideLength
}
class Square(size: Double) extends Rectangle(size, size)

Does this code follow the Liskov substitution principle? What if we used var instead of val in class Rectangle? Recall: our goal when writing Square was to model the fact that a square is a rectangle whose sides are equal in length.

More in Wikipedia:

On dependencies between classes/traits

The chapter on traits (7.2) already brought up the notion that there is an asymmetric relationship between a subtype and a supertype: we define each subtype in terms of its supertype(s), not the other way around. This applies to inheritance, too:

  • In a subclass definition, we record the superclass that the subclass extends. Given that relationship, we can rely on the fact that the inherited methods from the superclass are available for us to use within the subclass. We can write things like this.methodInSuperclass. We may also use the super keyword to refer to the superclass definition.
  • We do not record the inheritance relationship in the superclass, and the superclass’s code is independent of the subclasses in this respect. We can't call this.methodInSubclass; we can invoke methods on this only if they are common to all instances of the supertype. There is no sub keyword.

If superclasses were dependent on their subclasses, changing those subclasses would necessitate changes in the superclass. That would be quite vexing. For instance: It’s fairly common to extend a superclass defined in a library (e.g., View). There is no way that the library’s creator can know all the possible subclasses that may be created for their class or react to changes in those subclasses.

On the other hand, it’s great that we can add methods to a superclass and those methods will then be available in all extending classes. Often, this works fine, but sometimes, we run into a weakness in inheritance-based program design:

On fragile superclasses

The fragile base class problem refers to situations where it’s impossible to modify a superclass without knowing details about its subclasses.

As one example, consider adding a method to a superclass: what if the new method happens to have the same name as one or more of the methods in the subclasses? We have a serious problem.

In a language such as Scala, our program will break down with a compile-time error that complains about a missing override modifier. In languages that don’t have such a modifier or that make it optional, we may get surprising and erroneous runtime behavior instead.

O1’s follow-on courses will discuss object-oriented design more deeply. In the meantime, you may find the Wikipedia article on fragile base classes interesting; see also composition over inheritance.

Preventing overriding and inheritance: final

If you add the final modifier in front of a def, the method can’t be overridden: instances of all subtypes will inherit the method as is. An attempt to override it will bring a compile-time error.

In front of class, the same final keyword disallows inheritance from that class altogether. (Cf. sealed, which limits inheritance to classes defined in the same file.)

When used appropriately, the final modifier may make programs easier to comprehend and prevent inappropriate use of a class. Some programs’ efficiency is improved by final, since the compiler doesn’t need to worry about overriding. Many of the classes in the standard Scala API are final .

Summary of Key Points

  • Inheritance is an object-oriented technique in which subclasses extend a superclass. The subclasses, which represent more specific concepts, inherit the generic properties defined in the superclass.
  • Traits and superclasses have much in common, but there are differences, too. Specifically:
    • A class may have only one immediate superclass but it may extend multiple traits.
    • Unlike a trait, a superclass may take constructor parameters.
  • You can define a class as abstract, in which case it can contain abstract methods and variables just like a trait can. An abstract class can’t be instantiated directly but only via its subclasses.
  • Classes and traits form hierarchies that represent hierarchies of concepts. In Scala, all classes are part of a hierarchy whose base is the API class Any.
  • A subclass may override a superclass method with a subtype-specific implementation.
  • Links to the glossary: inheritance, subclass, superclass, class hierarchy, Any; abstract class; static type, dynamic type; to override; sealed class.

Feedback

Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.

Credits

Thousands of students have given feedback that has contributed to this ebook’s design. Thank you!

Weeks 1 to 13 of the ebook, including the assignments and weekly bulletins, have been written in Finnish and translated into English by Juha Sorva.

Weeks 14 to 20 are by Otto Seppälä. That part of the ebook isn’t available during the fall term, but we’ll publish it when it’s time.

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been programmed by Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio have done the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using tools from O1Library (such as Pic) for simple graphical programming is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ has been created by Aalto’s LeTech research group and is largely developed by students. The current lead developer is Jaakko Kantojärvi; many other students of computer science and information networks are also active on the project.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits for this page

Thanks to whoever came up with the joke that forms the basis of the driving example.

../_images/imho7.png
Posting submission...

Submission received.