Книга: Learn Scala for Java Developers
Назад: Classes and Objects
Дальше: Control Structures

Inheritance

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.

Subtype Inheritance

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.

Anonymous Classes

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 / Traits

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 = ???   } 

What’s the Question?

The ??? above is actually a method. It’s a handy method you can use to say “I don’t know yet”. It throws a runtime exception if you call it, a bit like UnsupportedOperationException in Java. It gets things compiling when you really don’t know what you need yet.

Methods on Traits

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 

Converting Anonymous Classes to Lambdas

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

Concrete Fields on Traits

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   } 

Abstract Fields on Traits

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.

Abstract Classes

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     }   } 

Polymorphism

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:

Traits vs. Abstract Classes

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.

 

Does Scala Support Multiple-Inheritance?

If by “inheritance” you mean classic class extension, then Scala doesn’t support multiple inheritance. Scala allows only a single class to be “extended”. It’s the same as Java in that respect. However, if you mean can behaviour be inherited by other means, then yes, Scala does support multiple inheritance.

A Scala class can mixin behaviour from any number of traits, just as Java 8 can mixin behaviour from multiple interfaces with default methods. The difference is in how they resolve clashes. Scala uses linearization to predictably resolve a method call at runtime, whereas Java 8 relies on compilation failure.

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 
Fig. 2.1. Basic Animal class hierarchy.

Fig. 2.1. Basic Animal class hierarchy.

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 
Fig. 2.2. Concrete class `FlyingHorse` extends everything.

Fig. 2.2. Concrete class FlyingHorse extends everything.

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?

Fig. 2.3. How should a call to `move` resolve?

Fig. 2.3. How should a call to move resolve?

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.

Fig. 2.4. Class `FlyingHorse` extends `Animal with Bird with HasFourLegs`.

Fig. 2.4. Class FlyingHorse extends Animal with Bird with HasFourLegs.

If we change the order of the traits and swap HasFourLegs with Birds, the linearization changes and we get a different evaluation order.

Fig. 2.5. Class `FlyingHorse` extends `Animal with HasFourLegs with Bird`.

Fig. 2.5. Class FlyingHorse extends Animal with HasFourLegs with Bird.

 

So side by side, the examples look like this:

Fig. 2.6. The linearization of the two hierarchies.

Fig. 2.6. The linearization of the two hierarchies.

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.

Deciding Between the Options

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

 

Назад: Classes and Objects
Дальше: Control Structures