© Toby Weston 2018

Toby Weston, Scala for Java Developers, https://doi.org/10.1007/978-1-4842-3108-1_11

11. Inheritance

Toby Weston

(1)London, UK

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 behavior. 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 from 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(name: String, 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, like this:

  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. Here is 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();
      }
  }


  public class AnonymousCLass {
      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 a trait in much the same way as you can use an interface. You can implement specialized behavior in implementing classes, yet still treat them polymorphically in code. However:

  • Traits can have default implementations for methods. These are just like Java’s virtual extension methods (new in Java 8 and otherwise known as default methods) but there’s no equivalent pre-Java 8.

  • Traits can also have fields and even default values for these, something which Java interfaces cannot do. Therefore, traits can have both abstract and concrete methods and have state.

  • A class can implement any number of traits just as a class can implement any number of interfaces, although extending traits with default implementations in Scala is more like mixing in behaviors than traditional interface inheritance in Java.

  • There’s a cross-over with Java 8 as you can mixin behavior with Java 8, although there are some differences in semantics and how duplicate methods are handled.

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

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 behavior. 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. With a slight tweak to the signature to enforce this 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 here 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 behavior 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).

  // 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]         // no body means abstract
  }

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
  import scala.collections._


  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 behavior 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 specialize 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 that 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 an 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 CustomersUsageExample {
    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.1 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 similar to the following equivalent Java version and means we don’t need the BasketTotalDescending object anymore.

  // 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 initialized (based on the value in the trait) on construction.

For example, count is magically available to the following class 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 NotASubtype {
    val counter = new IncrementByOne()            // a subtype of Counter but
    counter.count                                 // count is now inaccessible
  }

Abstract Fields on Traits

You can also have abstract values on traits by leaving off the initializing 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 subclass, we could implement our discounted basket 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 HeavilyDiscountedCustomer 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 behavior and state from the super-class. You can also view implementing interfaces as inheritance where you inherit behavior but not state.

In both cases the benefits are around substitutability: the idea that you can replace one type with another to change system behavior without changing the structure of the code. This is referred to as inclusion polymorphism.

Scala allows for inclusion polymorphism in the following ways:

  • Traits without default implementations.

  • Traits with default implementations (because these are used to “mix in” behavior, they’re often called mixin traits).

  • Abstract classes (with and without fields).

  • Traditional class extension.

  • Structural types, a kind of duck typing familiar to Ruby developers but which uses reflection.

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 behavior 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 (see Figure 11-1). For example, we could have the following:

  class Animal
  trait HasWings extends Animal
  trait Bird extends HasWings
  trait HasFourLegs extends Animal
A456960_1_En_11_Fig1_HTML.jpg
Figure 11-1 Basic Animal class hierarchy

If we add a concrete class that extends Animal but also Bird and HasFourLegs, we have a creature (FlyingHorse) that has all of the behaviors in the hierarchy (see Figure 11-2).

  class Animal
  trait HasWings extends Animal
  trait Bird extends HasWings
  trait HasFourLegs extends Animal
  class FlyingHorse extends Animal with Bird with HasFourLegs
A456960_1_En_11_Fig2_HTML.jpg
Figure 11-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, as shown in Figure 11-3. If you call move on our FlyingHorse, which implementation would you expect to be called? How about if it in turn calls super.move?

A456960_1_En_11_Fig3_HTML.jpg
Figure 11-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, (see Figure 11-4 ). So, if any of the classes call a super-class’s method, it will resolve in that order.

A456960_1_En_11_Fig4_HTML.jpg
Figure 11-4 Class FlyingHorse extends Animal with Bird with HasFourLegs

If we change the order of the traits and swap HasFourLegs with Birds, as shown in Figure 11-5, the linearization changes and we get a different evaluation order.

A456960_1_En_11_Fig5_HTML.jpg
Figure 11-5 Class FlyingHorse extends Animal with HasFourLegs with Bird

Figure 11-6 shows what the examples look like side by side.

A456960_1_En_11_Fig6_HTML.jpg
Figure 11-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 Scala for the Impatient 2 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 behavior, just the idea of substitutability.

When your class has behavior and that behavior 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 behavior that may be reused by unrelated classes, make it a mixin trait as they have fewer restrictions on what can use them compared to abstract classes.

Odersky also talks about some other factors, like performances and Java interoperability, in Programming in Scala.3

Footnotes

1 Since Scala 2.12, anonymous classes can be converted into Java SAMs automatically.

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

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