In this chapter we’ll look at inheritance in Scala: how you create subclasses and override methods, the Scala equivalent of interfaces and abstract classes, and the mechanisms Scala offers for mixing in reusable behaviour. We’ll finish by discussing how to pick between all the options.
Creating a subtype of another class is the same as in Java. You use the extends
keyword and you can prevent subclassing with the final
modifier on a class definition.
Let’s suppose we want to extend the basic Customer
class from earlier and create a special subtype to represent a DiscountedCustomer
. A shopping basket might belong to the Customer
super-class, along with methods to add items to the basket and total its value.
// java
public
class
Customer
{
private
final
String
name
;
private
final
String
address
;
private
final
ShoppingBasket
basket
=
new
ShoppingBasket
();
public
Customer
(
String
name
,
String
address
)
{
this
.
name
=
name
;
this
.
address
=
address
;
}
public
void
add
(
Item
item
)
{
basket
.
add
(
item
);
}
public
Double
total
()
{
return
basket
.
value
();
}
}
Let’s say the DiscountedCustomer
is entitled to a 10% discount on all purchases. We can extend Customer
, creating a constructor to match Customer
, and call super
in it. We can then override the total
method to apply the discount.
// java
public
class
DiscountedCustomer
extends
Customer
{
public
DiscountedCustomer
(
String
name
,
String
address
)
{
super
(
name
,
address
);
}
@Override
public
Double
total
()
{
return
super
.
total
()
*
0.90
;
}
}
We do exactly the same thing in Scala. Here’s the basic Customer
class:
// scala
class
Customer
(
val
name
:
String
,
val
address
:
String
)
{
private
final
val
basket
:
ShoppingBasket
=
new
ShoppingBasket
def
add
(
item
:
Item
)
{
basket
.
add
(
item
)
}
def
total
:
Double
=
{
basket
.
value
}
}
When it comes to extending Customer
to DiscountedCustomer
, there are a few things to consider. First, we’ll create the DiscountedCustomer
class.
class
DiscountedCustomer
If we try and extend Customer
to create DiscountedCustomer
, we get a compiler error.
class
DiscountedCustomer
extends
Customer
// compiler error
We get a compiler error because we need to call the Customer
constructor with values for its arguments. We had to do the same thing in Java when we called super
in the new constructor.
Scala has a primary constructor and auxiliary constructors. Auxiliary constructors must be chained to eventually call the primary constructor and in Scala, only the primary constructor can call the super-class constructor. We can add arguments to the primary constructor like this:
class
DiscountedCustomer
(
name
:
String
,
address
:
String
)
extends
Customer
But we can’t call super
directly like we can in Java.
class
DiscountedCustomer
(
val
name
:
String
,
val
address
:
String
)
extends
Customer
{
super
(
name
,
address
)
// compiler error
}
In Scala, to call the super-class constructor you pass the arguments from the primary constructor to the super-class. Notice that the arguments to DiscountedCustomer
aren’t set as val
. They’re not fields; instead, they’re locally scoped to the primary constructor and passed directly to the super-class.
class
DiscountedCustomer
(
name
:
String
,
address
:
String
)
extends
Customer
(
name
,
address
)
Finally, we can implement the discounted total
method in the subclass.
override
def
total
:
Double
=
{
super
.
total
*
0.90
}
There are two things to note here: the override
keyword is required, and to call the super-classes total
method, we use super
and a dot, just like in Java.
The override
keyword is like the @Override
annotation in Java. It allows the compiler to check for mistakes like misspelling the name of the method or providing the wrong arguments. The only real difference between the Java annotation and Scala’s is that it’s mandatory in Scala when you override non-abstract methods.
You create anonymous subclasses in a similar way to Java.
In the Java version of the ShoppingBasket
class, the add
method takes an Item
interface. So to add an item to your shopping basket, you could create an anonymous subtype of Item
. Below, we’ve created a program to add two fixed-price items to Joe’s shipping basket. Each item is an anonymous subclass of Item
. The basket total after discount would be $5.40.
// java
public
class
ShoppingBasket
{
private
final
Set
<
Item
>
basket
=
new
HashSet
<>();
public
void
add
(
Item
item
)
{
basket
.
add
(
item
);
}
public
Double
value
()
{
return
basket
.
stream
().
mapToDouble
(
Item:
:
price
).
sum
();
}
}
// java
public
class
TestDiscount
{
public
static
void
main
(
String
...
args
)
{
Customer
joe
=
new
DiscountedCustomer
(
"Joe"
,
"128 Bullpen Street"
);
joe
.
add
(
new
Item
()
{
@Override
public
Double
price
()
{
return
2.5
;
}
});
joe
.
add
(
new
Item
()
{
@Override
public
Double
price
()
{
return
3.5
;
}
});
System
.
out
.
println
(
"Joe's basket will cost $ "
+
joe
.
total
());
}
}
In Scala, it’s pretty much the same. You can drop the brackets on the class name when newing up an Item
, and the type from the method signature of price
. The override
keyword in front of the price
method is also optional.
// scala
object
DiscountedCustomer
{
def
main
(
args
:
Array
[
String
])
{
val
joe
=
new
DiscountedCustomer
(
"Joe"
,
"128 Bullpen Street"
)
joe
.
add
(
new
Item
{
def
price
=
2.5
})
joe
.
add
(
new
Item
{
def
price
=
3.5
})
println
(
"Joe`s basket will cost $ "
+
joe
.
total
)
}
}
You create anonymous instances of classes, abstract classes, or Scala traits in just the same way.
Interfaces in Java are similar to traits in Scala. You can use traits in much the same way as you can use an interface. You can implement specialised behaviour in implementing classes, yet still treat them polymorphically in code. However:
In this section, we’ll look at these differences in more detail.
In Java, we might create an interface called Readable
to read some data and copy it into a character buffer. Each implementation may read something different into the buffer. For example, one might read the content of a web page over HTTP whilst another might read a file.
// java
public
interface
Readable
{
public
int
read
(
CharBuffer
buffer
);
}
In Scala, the Java interface would become a trait and it would look like this:
// scala
trait
Readable
{
def
read
(
buffer
:
CharBuffer
)
:
Int
}
You just use trait
rather than class
when you define it. There’s no need to declare methods as abstract
, as any unimplemented methods are automatically abstract.
Implementing the interface in Java uses the implements
keyword. For example, if we implement a file reader, we might take a File
object as a constructor argument and override the read
method to consume the file. The read
method would return the number of bytes read.
// java
public
class
FileReader
implements
Readable
{
private
final
File
file
;
public
FileReader
(
File
file
)
{
this
.
file
=
file
;
}
@Override
public
int
read
(
CharBuffer
buffer
)
{
int
read
=
0
;
// ...
return
read
;
}
}
In Scala, you use extends
just like when you extend regular classes. You’re forced to use the override
keyword when overriding an existing concrete method, but not when you override an abstract method.
// scala
class
FileReader
(
file
:
File
)
extends
Readable
{
override
def
read
(
buffer
:
CharBuffer
)
:
Int
=
{
// override optional
val
linesRead
:
Int
=
0
return
linesRead
}
}
In Java, if you want to implement multiple interfaces you append the interface name to the Java class definition, so we could add AutoClosable
behaviour to our FileReader
.
// java
public
class
FileReader
implements
Readable
,
AutoCloseable
{
private
final
File
file
;
public
FileReader
(
File
file
)
{
this
.
file
=
file
;
}
@Override
public
int
read
(
CharBuffer
buffer
)
{
int
read
=
0
;
// ...
return
read
;
}
@Override
public
void
close
()
throws
Exception
{
// close
}
}
In Scala, you use the with
keyword to add additional traits. You do this when you want to extend a regular class, abstract class or trait. Just use extends
for the first and then with
for any others. However, just like in Java, you can have only one super-class.
// scala
class
FileReader
(
file
:
File
)
extends
Readable
with
AutoCloseable
{
def
read
(
buffer
:
CharBuffer
)
:
Int
=
{
val
linesRead
:
Int
=
0
// ...
return
linesRead
}
def
close
()
:
Unit
=
???
}
Java 8 introduced default methods where you can create default implementations on interfaces. You can do the same thing in Scala with a few extra bits besides.
Let’s see where Java interfaces might benefit from having default implementations. We could start by creating a Sortable
interface to describe any class that can be sorted. More specifically, any implementations should be able to sort things of the generic type A
. This implies it’s only useful for collection classes so we’ll make the interface extend Iterable
to make that more obvious.
// java
interface
Sortable
<
A
>
extends
Iterable
<
A
>
{
public
List
<
A
>
sort
();
}
If lots of classes implement this, many may well want similar sorting behaviour. Some will want finer-grained control over the implementation. With Java 8, we can provide a default implementation for the common case. We mark the interface method as default
indicating that it has a default implementation, then go ahead and provide an implementation.
Below we’re taking advantage of the fact that the object is iterable, and copying its contents into a new ArrayList
. We can then use the built-in sort
method on List
. The sort
method takes a lambda to describe the ordering, and we can take a shortcut to reuse an object’s natural ordering if we say the objects to compare must be Comparable
. A slight tweak to the signature to enforce this and then we can use the comparator’s compareTo
method. It means that we have to make type A
something that is Comparable
, but it’s still in keeping with the intent of the Sortable
interface.
// java
public
interface
Sortable
<
A
extends
Comparable
>
extends
Iterable
<
A
>
{
default
public
List
<
A
>
sort
()
{
List
<
A
>
list
=
new
ArrayList
<>();
for
(
A
elements:
this
)
list
.
add
(
elements
);
list
.
sort
((
first
,
second
)
->
first
.
compareTo
(
second
));
return
list
;
}
}
The default
keyword above means that the method is no longer abstract and that any subclasses that don’t override it will use it by default. To see this, we can create a class, NumbersList
extending Sortable
, to contain a list of numbers, and use the default sorting behaviour to sort these. There’s no need to implement the sort
method as we’re happy to use the default provided.
// java
public
class
NumbersUsageExample
{
private
static
class
NumberList
implements
Sortable
<
Integer
>
{
private
Integer
[]
numbers
;
private
NumberList
(
Integer
...
numbers
)
{
this
.
numbers
=
numbers
;
}
@Override
public
Iterator
<
Integer
>
iterator
()
{
return
Arrays
.
asList
(
numbers
).
iterator
();
}
}
public
static
void
main
(
String
...
args
)
{
Sortable
<
Integer
>
numbers
=
new
NumberList
(
1
,
34
,
65
,
23
,
0
,
-
1
);
System
.
out
.
println
(
numbers
.
sort
());
}
}
We can apply the same idea to our Customer
example and create a Customers
class to collect customers. All we have to do is make sure the Customer
class is Comparable
and we’ll be able to sort our list of customers without implementing the sort method ourselves.
// java
// You'll get a compiler error if Customer isn't Comparable
public
class
Customers
implements
Sortable
<
Customer
>
{
private
final
Set
<
Customer
>
customers
=
new
HashSet
<>();
public
void
add
(
Customer
customer
)
{
customers
.
add
(
customer
);
}
@Override
public
Iterator
<
Customer
>
iterator
()
{
return
customers
.
iterator
();
}
}
In our Customer
class, if we implement Comparable
and the compareTo
method, the default natural ordering will be alphabetically by name.
// java
public
class
Customer
implements
Comparable
<
Customer
>
{
// ...
@Override
public
int
compareTo
(
Customer
other
)
{
return
name
.
compareTo
(
other
.
name
);
}
}
If we add some customers to the list in random order, we can print them sorted by name
(as defined in the compareTo
method above).
// java
public
class
CustomersUsageExample
{
public
static
void
main
(
String
...
args
)
{
Customers
customers
=
new
Customers
();
customers
.
add
(
new
Customer
(
"Velma Dinkley"
,
"316 Circle Drive"
));
customers
.
add
(
new
Customer
(
"Daphne Blake"
,
"101 Easy St"
));
customers
.
add
(
new
Customer
(
"Fred Jones"
,
"8 Tuna Lane,"
));
customers
.
add
(
new
DiscountedCustomer
(
"Norville Rogers"
,
"1 Lane"
));
System
.
out
.
println
(
customers
.
sort
());
}
}
In Scala, we can go through the same steps. Firstly, we’ll create the basic trait.
// scala
trait
Sortable
[
A
]
{
def
sort
:
Seq
[
A
]
}
This creates an abstract method sort
. Any extending class has to provide an implementation, but we can provide a default implementation by just providing a regular method body.
// scala
trait
Sortable
[
A
<:
Ordered
[
A
]]
extends
Iterable
[
A
]
{
def
sort
:
Seq
[
A
]
=
{
this
.
toList
.
sorted
// built-in sorting method
}
}
We extend Iterable
and give the generic type A
a constraint that it must be a subtype of Ordered
. Ordered
is like Comparable
in Java and is used with built-in sorting methods. The <:
keyword indicates the upper bound of A
. We’re using it here just as we did in the Java example to constrain the generic type to be a subtype of Ordered
.
Recreating the Customers
collection class in Scala would look like this:
// scala
class
Customers
extends
Sortable
[
Customer
]
{
private
val
customers
=
mutable
.
Set
[
Customer
]()
def
add
(
customer
:
Customer
)
=
customers
.
add
(
customer
)
def
iterator
:
Iterator
[
Customer
]
=
customers
.
iterator
}
We have to make Customer
extend Ordered
to satisfy the upper-bound constraint, just as we had to make the Java version implement Comparable
. Having done that, we inherit the default sorting behaviour from the trait.
// scala
object
Customers
{
def
main
(
args
:
Array
[
String
])
{
val
customers
=
new
Customers
()
customers
.
add
(
new
Customer
(
"Fred Jones"
,
"8 Tuna Lane,"
))
customers
.
add
(
new
Customer
(
"Velma Dinkley"
,
"316 Circle Drive"
))
customers
.
add
(
new
Customer
(
"Daphne Blake"
,
"101 Easy St"
))
customers
.
add
(
new
DiscountedCustomer
(
"Norville Rogers"
,
"1 Lane"
))
println
(
customers
.
sort
)
}
}
The beauty of the default method is that we can override it and specialise it if we need to. For example, if we want to create another sortable collection class for our customers but this time sort the customers by the value of their baskets, we can override the sort
method.
In Java, we’d create a new class which extends Customers
and overrides the default sort method.
// java
public
class
CustomersSortableBySpend
extends
Customers
{
@Override
public
List
<
Customer
>
sort
()
{
List
<
Customer
>
customers
=
new
ArrayList
<>();
for
(
Customer
customer:
this
)
customers
.
add
(
customer
);
customers
.
sort
((
first
,
second
)
->
second
.
total
().
compareTo
(
first
.
total
()));
return
customers
;
}
}
The general approach is the same as the default method, but we’ve used a different implementation for the sorting. We’re now sorting based on the total basket value of the customer. In Scala we’d do pretty much the same thing.
// scala
class
CustomersSortableBySpend
extends
Customers
{
override
def
sort
:
List
[
Customer
]
=
{
this
.
toList
.
sorted
(
new
Ordering
[
Customer
]
{
def
compare
(
a
:
Customer
,
b
:
Customer
)
=
b
.
total
.
compare
(
a
.
total
)
})
}
}
We extend Customers
and override the sort
method to provide our alternative implementation. We’re using the built-in sort method again, but this time using a different anonymous instance of Ordering
; again, comparing the basket values of the customers.
If you want to create a instance of the comparator as a Scala object rather than an anonymous class, we could do something like the following:
class
CustomersSortableBySpend
extends
Customers
{
override
def
sort
:
List
[
Customer
]
=
{
this
.
toList
.
sorted
(
BasketTotalDescending
)
}
}
object
BasketTotalDescending
extends
Ordering
[
Customer
]
{
def
compare
(
a
:
Customer
,
b
:
Customer
)
=
b
.
total
.
compare
(
a
.
total
)
}
To see this working we could write a little test program. We can add some customers to our CustomersSortableBySpend
, and add some items to their baskets. I’m using the PricedItem
class for the items, as it saves us having to create a stub class for each one like we saw before. When we execute it, we should see the customers sorted by basket value rather than customer name.
// scala
object
AnotherExample
{
def
main
(
args
:
Array
[
String
])
{
val
customers
=
new
CustomersSortableBySpend
()
val
fred
=
new
Customer
(
"Fred Jones"
,
"8 Tuna Lane,"
)
val
velma
=
new
Customer
(
"Velma Dinkley"
,
"316 Circle Drive"
)
val
daphne
=
new
Customer
(
"Daphne Blake"
,
"101 Easy St"
)
val
norville
=
new
DiscountedCustomer
(
"Norville Rogers"
,
"1 Lane"
)
daphne
.
add
(
PricedItem
(
2.4
))
daphne
.
add
(
PricedItem
(
1.4
))
fred
.
add
(
PricedItem
(
2.75
))
fred
.
add
(
PricedItem
(
2.75
))
norville
.
add
(
PricedItem
(
6.99
))
norville
.
add
(
PricedItem
(
1.50
))
customers
.
add
(
fred
)
customers
.
add
(
velma
)
customers
.
add
(
daphne
)
customers
.
add
(
norville
)
println
(
customers
.
sort
)
}
}
The output would look like this:
Norville
Rogers
$
7.641
Daphne
Blake
$
3.8
Fred
Jones
$
2.75
Velma
Dinkley
$
0.0
In the Java version of the sort
method, we could use a lambda to effectively create an instance of Comparable
. The syntax is new in Java 8 and in this case, is an alternative to creating an anonymous instance in-line.
// java
customers
.
sort
((
first
,
second
)
->
second
.
total
().
compareTo
(
first
.
total
()));
To make the Scala version more like the Java one, we’d need to pass in a lambda instead of the anonymous instance of Ordering
. Scala supports lambdas so we can pass anonymous functions directly into other functions, but the signature of the sort
method wants an Ordering
, not a function.
Luckily, we can coerce Scala into converting a lambda into an instance of Ordering
using an implicit conversion. All we need to do is create a converting method that takes a lambda or function and returns an Ordering
, and mark it as implicit
. The implicit
keyword tells Scala to try and use this method to convert from one to the other if otherwise things wouldn’t compile.
// scala
implicit
def
functionToOrdering
[
A
](
f
:
(
A
,
A
)
=>
Int
)
:
Ordering
[
A
]
=
{
new
Ordering
[
A
]
{
def
compare
(
a
:
A
,
b
:
A
)
=
f
.
apply
(
a
,
b
)
}
}
The signature takes a function and returns an Ordering[A]
. The function itself has two arguments and returns an Int
. So our conversion method is expecting a function with two arguments of type A
, returning an Int
((A, A) => Int
).
Now we can supply a function literal to the sorted
method that would otherwise not compile. As long as the function conforms to the (A, A) => Int
signature, the compiler will detect that it can be converted to something that does compile and call our implicit
method to do so. We can therefore modify the sort
method of CustomersSortableBySpend
like this:
// scala
this
.
toList
.
sorted
((
a
:
Customer
,
b
:
Customer
)
=>
b
.
total
.
compare
(
a
.
total
))
…passing in a lambda rather than an anonymous class. It’s very similar to the equivalent line of the Java version below.
// java
list
.
sort
((
first
,
second
)
->
first
.
compareTo
(
second
));
We’ve looked at default methods on traits, but Scala also allows you to provide default values. You can specify fields in traits.
// scala
trait
Counter
{
var
count
=
0
def
increment
()
}
Here, count
is a field on the trait. All classes that extend Counter
will have their own instance of count
copied in. It’s not inherited — it’s a distinct value specified by the trait as being required and supplied for you by the compiler. Subtypes are provided with the field by the compiler and it’s initialised (based on the value in the trait) on construction.
For example, count
is magically available to the class below and we’re able to increment it in the increment
method.
// scala
class
IncrementByOne
extends
Counter
{
override
def
increment
()
:
Unit
=
count
+=
1
}
In this example, increment
is implemented to multiply the value by some other value on each call.
// scala
class
ExponentialIncrementer
(
rate
:
Int
)
extends
Counter
{
def
increment
()
:
Unit
=
if
(
count
==
0
)
count
=
1
else
count
*=
rate
}
Incidentally, we can use protected
on the var
in Counter
and it will have similar schematics as protected
in Java. It gives visibility to subclasses but, unlike Java, not to other types in the same package. It’s slightly more restrictive than Java. For example, if we change it and try to access the count
from a non-subtype in the same package, we won’t be allowed.
// scala
trait
Counter
{
protected
var
count
=
0
def
increment
()
}
class
Foo
{
val
foo
=
new
IncrementByOne
()
// a subtype of Counter but
foo
.
count
// count is now inaccessible
}
You can also have abstract values on traits by leaving off the initialising value. This forces subtypes to supply a value.
// scala
trait
Counter
{
protected
var
count
:
Int
// abstract
def
increment
()
}
class
IncrementByOne
extends
Counter
{
override
var
count
:
Int
=
0
// forced to supply a value
override
def
increment
()
:
Unit
=
count
+=
1
}
class
ExponentialIncrementer
(
rate
:
Int
)
extends
Counter
{
var
count
:
Int
=
1
def
increment
()
:
Unit
=
if
(
count
==
0
)
count
=
1
else
count
*=
rate
}
Notice that IncrementByOne
uses the override
keyword whereas ExponentialIncrementer
doesn’t. For both fields and abstract methods, override
is optional.
Vanilla abstract classes are created in Java with the abstract
keyword. For example, we could write another version of our Customer
class but this time make it abstract. We could also add a single method to calculate the customer’s basket value and mark that as abstract
.
// java
public
abstract
class
AbstractCustomer
{
public
abstract
Double
total
();
}
In the DiscountedCustomer
subclass, we could implement our discounted basket value like this:
// java
public
class
DiscountedCustomer
extends
AbstractCustomer
{
private
final
ShoppingBasket
basket
=
new
ShoppingBasket
();
@Override
public
Double
total
()
{
return
basket
.
value
()
*
0.90
;
}
}
In Scala, you still use the abstract
keyword to denote a class that cannot be instantiated. However, you don’t need it to qualify a method; you just leave the implementation off.
// scala
abstract
class
AbstractCustomer
{
def
total
:
Double
// no implementation means it's abstract
}
Then we can create a subclass in the same way we saw earlier. We use extends
like before and simply provide an implementation for the total
method. Any method that implements an abstract method doesn’t require the override
keyword in front of the method, although it is permitted.
// scala
class
DiscountedCustomer
extends
AbstractCustomer
{
private
final
val
basket
=
new
ShoppingBasket
def
total
:
Double
=
{
return
basket
.
value
*
0.90
}
}
Where you might use inheritance in Java, there are more options available to you in Scala. Inheritance in Java typically means subtyping classes to inherit behaviour and state from the super-class. You can also view implementing interfaces as inheritance where you inherit behaviour but not state.
In both cases the benefits are around substitutability: the idea that you can replace one type with another to change system behaviour without changing the structure of the code. This is referred to as inclusion polymorphism.
Scala allows for inclusion polymorphism in the following ways:
There are a couple of differences between traits and abstract classes. The most obvious is that traits cannot have constructor arguments. Traits also provide a way around the problem of multiple inheritance that you’d see if you were allowed to extend multiple classes directly. Like Java, a Scala class can only have a single super-class, but can mixin as many traits required. So despite this restriction, Scala does support multiple inheritance. Kind of.
Multiple inheritance can cause problems when subclasses inherit behaviour or fields from more than one super-class. In this scenario, with methods defined in multiple places, it’s difficult to reason about which implementation should be used. The is a relationship breaks down when a type has multiple super-classes.
Scala allows for a kind of multiple inheritance by distinguishing between the class hierarchy and the trait hierarchy. Although you can’t extend multiple classes, you can mixin multiple traits. Scala uses a process called linearization to resolve duplicate methods in traits. Specifically, Scala puts all the traits in a line and resolves calls to super
by going from right to left along the line.
Linearization means that the order in which traits are defined in a class definition is important. For example, we could have the following:
class
Animal
trait
HasWings
extends
Animal
trait
Bird
extends
HasWings
trait
HasFourLegs
extends
Animal
If we add a concrete class that extends Animal
but also Bird
and HasFourLegs
, we have a creature (FlyingHorse
) which has all of the behaviours in the hierarchy.
class
Animal
trait
HasWings
extends
Animal
trait
Bird
extends
HasWings
trait
HasFourLegs
extends
Animal
class
FlyingHorse
extends
Animal
with
Bird
with
HasFourLegs
The problem comes when we have a method that any of the classes could implement and potentially call that method on their super-class. Let’s say there’s a method called move
. For an animal with legs, move
might mean to travel forwards, whereas an animal with wings might travel upwards as well as forwards. If you call move
on our FlyingHorse
, which implementation would you expect to be called? How about if it in turn calls super.move
?
Scala addresses the problem using the linearization technique. Flattening the hierarchy from right to left would give us FlyingHorse
, HasForLegs
, Bird
, HasWings
and finally Animal
. So if any of the classes call a super-class’s method, it will resolve in that order.
If we change the order of the traits and swap HasFourLegs
with Birds
, the linearization changes and we get a different evaluation order.
So side by side, the examples look like this:
With default methods in Java 8 there is no linearization process: any potential clash causes the compiler to error and the programmer has to refactor around it.
Apart from allowing multiple inheritance, traits can also be stacked or layered on top of each other to provide a call chain, similar to aspect-oriented programming, or using decorators. There’s a good section on layered traits in by Cay S. Horstmann if you want to read more.
Here are some tips to help you choose when to use the different inheritance options.
Use traits without state when you would have used an interface in Java; namely, when you define a role a class should play where different implementations can be swapped in. For example, when you want to use a test double when testing and a “real” implementation in production. “Role” in this sense implies no reusable concrete behaviour, just the idea of substitutability.
When your class has behaviour and that behaviour is likely to be overridden by things of the same type, use a regular class and extend. Both of these are types of inclusion polymorphism.
Use an abstract class in the case when you’re more interested in reuse than in an OO is a relationship. For example, data structures might be a good place to reuse abstract classes, but our Customer
hierarchy from earlier might be better implemented as non-abstract classes.
If you’re creating reusable behaviour that may be reused by unrelated classes, make it a mixin trait as they have fewer restrictions on what can use them compared to a abstract class.
Odersky also talks about some other factors, like performances and Java interoperability, in