The latest instance of the course can be found at: O1: 2024
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.
Kieli vaihtuu A+:n sivujen yläreunan 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 or more.
Points Available: B70.
Related Modules: Subtypes.
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.
Subclasses and Superclasses
Let’s define Square
like this instead:
class Square(size: Double) extends Rectangle(size, size) {
}
Square
has only one constructor parameter that determines the
length of each side.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.)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 programs 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
}
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.PhoneCall
and TextMessage
each take multiple
constructor parameters. Most of these parameters are associated
with these specific subclasses, but ...vatAdded
gets passed on as a constructor
parameter to the superclass. The processing of that information
is left entirely to Transaction
.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
}
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 a class extend several of them
(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
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.
Scala API’s ready-made classes 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.
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)
There’s a small class hierarchy associated with the Option
type, too.
Since Chapter 4.3, 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 directly extend a trait except within the file that defines the trait itself. A superclass
can also be sealed, and Option
is just such a sealed class.
As we noted in Chapter 4.3, 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)
Int
, a String
, a Boolean
, a Vector
of Int
,
and a Square
.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 ^
Any
to
refer to any object. The object can be a string or a square,
for instance.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.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
:
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.
A bit more on AnyRef
and AnyVal
Some readers (who already know something about the JVM/Java)
may wish to know that in the JVM-based Scala implementation,
subclasses of AnyVal
have been implemented as JVM primitives
such as int
and double
, whereas classes that derive from
AnyRef
are implemented as classes within the JVM.
The names AnyRef
and AnyVal
reflect this implementation. The
former’s implementation relies of object references, the latter’s
implementation on primitive values.
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.4), 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 classAnyRef
.
- the event handlers on
View
s (such asonClick
; Chapter 3.1): the default implementations in classView
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.
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.
B
doesn’t define an overriding implementation, so a B
object simply inherits the test
implementation of A
.C
overrides test
.D
doesn’t override the method. A D
object uses the
implementation from its immediate superclass C
(which overrides
the one from A
).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.
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.")
}
}
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
? Because it is “salty”.
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. Such a language feature, which makes it harder to write bad code, is sometimes called “syntactic salt” (cf. the more common term syntactic sugar).
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 the Subtypes module and run o1.cruising.Cruising
(which is
the above program). Was the output precisely what you expected? If
not, find out why.
You may wish to use the debugger as an aid.
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.
- They have an
addContent
method, which places an item within the container. - And a
contents
method that returns the previously added items in aVector
. - 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)
container1.contentsres7: Vector[o1.items.Item] = Vector(ring)
container2.contentsres8: Vector[o1.items.Item] = Vector(book, box containing 1 item(s))
The toString
method’s return value 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 12.1.)
Instructions and hints
- There is some starter code in
o1.items
within the Subtypes module. - Don’t forget to pass a constructor parameter to the superclass
Item
. - Don’t use
val
to (re)define the instance variablename
within theContainer
subclass. That variable is already defined in the superclass. It’s perfectly fine to giveContainer
’s constructor parameter the namename
, though. - Note that what you
override
in this assignment isn’t the generictoString
fromAnyRef
, as in many other programs, but thetoString
implementation inItem
. - Can you implement the
toString
inContainer
so that you use the superclass’s implementation rather than directly accessing the container’s name? (This is optional.) - Do not add any public members in the class beyond what was requested. Private members you are free to add as you see fit.
A+ presents the exercise submission form here.
Assignment: Legal Entities
Task description
Study the documentation of o1.legal
in module Subtypes. The documentation lays out a
number of classes that represent court cases and the legal entities
involved in such cases (also known as “legal persons”).
Implement the classes as Scala files in the same module.
There are quite a few classes, but they are very simple. This assignment is primarily concerned with the relationships between these classes, which are illustrated in the diagram below.
Recommended steps and other hints
You may wish to proceed as follows.
Browse the documentation for
CourtCase
,Entity
,NaturalPerson
, andJuridicalPerson
to get an overall sense of those four classes.Write
CourtCase
. Note that each court case is associated with two variables whose type is defined by the superclassEntity
. The referred objects are legal entities of some kind, butCourtCase
doesn’t care which.Write class
Entity
in a file of its own.- As noted in the documentation, the class is
abstract. Use
abstract
. - Some methods are also marked as abstract in the documentation. However, you neither need nor should use the keyword on methods. For defining an abstract method, it suffices to omit the method body as shown in earlier examples.
- As noted in the documentation, the class is
abstract. Use
Write
NaturalPerson
in a file of the same name.- As stated in the docs, you’ll need to extend
a class. Also remember that when inheriting
from a class, you need to supply the
superclass with values for any constructor
parameters it defines. (Cf.
Container
above.) - This class passes one of its constructor
parameters on to the superclass but not the
other one. The superclass already defines
a variable that corresponds to the
name
parameter, so you don’t need aval
for that. - The Scaladocs tell you which methods of each class are inherited from a superclass and which ones are new to the specific class. Near the top of each Scaladoc page, there is a gray area labeled Inherited that contains buttons for adjusting which methods show on the page. Try the buttons.
- As stated in the docs, you’ll need to extend
a class. Also remember that when inheriting
from a class, you need to supply the
superclass with values for any constructor
parameters it defines. (Cf.
Write
FullCapacityPerson
.- Especially since we’re dealing with short,
closely related class definitions, it
makes sense to define the subclasses of
NaturalPerson
in the same file with their superclass.
- Especially since we’re dealing with short,
closely related class definitions, it
makes sense to define the subclasses of
Open
Restriction.scala
, which will help you represent people whose capacity to act on their own behalf in court is reduced for one reason or another. The abstract classRestriction
is there already, as is a singleton objectIllness
that inherits it. AddUnderage
, a similar singleton.Write
ReducedCapacityPerson
.If you have implemented the earlier methods correctly, this should work as an implementation for the
kind
method:override def kind = super.kind + " with " + this.restriction
Write
JuridicalPerson
. Just one line of code will do (since no additional methods are needed and you can omit the empty curly brackets).Implement
HumanOrganization
andGeographicalFeature
.- A parameterless, abstract
def
in a superclass can be implemented with a variable in a subclass. Define acontact
variable inHumanOrganization
and akind
variable inGeographicalFeature
.
- A parameterless, abstract
You don’t have to bother writing
Nation
,Municipality
, andCorporation
; there is nothing novel about these classes. You can just uncomment the given implementations. If you have correctly defined the superclasses, these subclasses should work as given.Implement
Group
.- In this program, groups don’t have distinct names. Read the documentation carefully and pass an appropriate string literal as a constructor parameter to the superclass.
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. 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 Animal
s as well as all the
properties defined in the Spitting
trait:
pet.speciesres9: String = llama pet.spitres10: 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
andSpitting
; and - implements the abstract method
numberOfHumps
in theHumped
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
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).methodres11: String = b's method (new BA).methodres12: String = a's method
For more information, look up scala linearization or follow this link here.
The Liskov substitution principle
“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 thesuper
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 onthis
only if they are common to all instances of the supertype. There is nosub
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 direct
inheritance to within a single 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;final
.
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!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
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 developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, 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 did 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 O1Library for simple graphical programming (such as Pic
) 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+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It was created by Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi, and Paweł Stróżański with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.
For O1’s current teaching staff, please see Chapter 1.1.
extends
keyword, but this time we follow it with the name of a regular class rather than a trait. We say: the classSquare
inherits the classRectangle
. The inheriting class is known as a subclass (aliluokka); the class being inherited from is known as a superclass (yliluokka).Square
s. This gives allSquare
objects the additional type ofRectangle
.