Inheritance

As in Java, classes can extend other classes. If class B extends class A, then B inherits the fields and methods of A. (Of course these fields and methods must be public or protected in order for B to access them.)

For example, assume a calculator class is declared in Scala:

class Calculator {
  protected var result = 0.0
  def add(x: Double) { result += x }
  def mul(x: Double) { result *= x }
  def div(x: Double) { result /= x }
  def sub(x: Double) { result -= x }
  def clear { result = 0.0 }
  def getResult = result
  def help {
    println("Commands: add, mul, div, sub, clear, getResult, help")
  }
}

We can create a math calculator by extending the Calculator class:

class MathCalculator extends Calculator {
  def sin { result = math.sin(result) }
  def cos { result = math.cos(result) }
  def tan { result = math.tan(result) }
  override def help {
    super.help
    println("Also: sin, cos, tan")
  }
}

The Calculator field and methods are inherited:

scala> val calc = new MathCalculator
calc: MathCalculator = MathCalculator@7bb58ca3

scala> calc.add(12)

scala> calc.mul(10)

scala> calc.div(4)

scala> calc.result
<console>:15: error: variable result in class Calculator cannot be accessed in MathCalculator
 Access to protected method result not permitted because
 enclosing object $iw is not a subclass of
 class Calculator where target is defined
       calc.result
            ^

scala> calc.getResult
res4: Double = 30.0

scala> calc.sin

scala> calc.getResult
res6: Double = -0.9880316240928618

Notes:

·       Class members can have public, private, and protected scope. Public members (the default) can be accessed by any client of the class. Private members can only be accessed by other members of the class. Protected members can be accessed by class members and members of subclasses. (How is this different from Java's definition of protected scope?)

·       A class can override (i.e., redefine) inherited fields and methods, but must declare the override.

·       An override may call the method overridden.

Classes as Types and Runtime Type Identification

Continuing with the above example:

scala> var calc2: Calculator = calc
calc2: Calculator = MathCalculator@7bb58ca3

scala> calc2.add(.5)

scala> calc2.sin
<console>:16: error: value sin is not a member of Calculator
       calc2.sin
             ^

scala> calc2.isInstanceOf[MathCalculator]
res11: Boolean = true

scala> calc2.asInstanceOf[MathCalculator].sin

scala> calc2.getResult
res12: Double = 0.005984152237500469

scala> calc2.help
Commands: add, mul, div, sub, clear, getResult, help
Also: sin, cos, tan

scala> calc2 = new Calculator
calc2: Calculator = Calculator@29e495ff

scala> calc2.add(.5)

scala> calc2.asInstanceOf[MathCalculator].sin
java.lang.ClassCastException: Calculator cannot be cast to MathCalculator
  ... 32 elided

Notes:

·       A class can be thought of as a type. (It might be more accurate to say that every class is associated with a type.)

·       If we don't explicitly declare the type of calc2 to be Calculator, then Scala will infer from the initial value, calc2, that the type is MathCalculator.

·       Polymorphism: if class A extends class B, then A is a subtype of B, and all instances of A can masquerade as instances of B. So math calculators like calc can be stored in calculator variables like calc2.

·       However, MathCalculator-specific fields and methods can't be accessed for a masquerading math calculator. (Why?) That's why the calc.sin can't be called, even though calc.sin exists.

·       We can "unmask" a masquerading math calculator by asking if it is, in fact, an instance of the MathCalculator class.

·       We can temporarily retype the masqueraded math calculator by performing a cast.

·       Casting an instance of Calculator to a MathCalculator results in disaster. (Why?) Check before you cast using isInstanceOf. This is called a safe cast.

·       However, calling the help method of a math calculator masquerading as a calculator invokes the overridden method.

UML Class Diagrams

Here's how subclasses are represented in UML

image006

The Scala class hierarchy

Scala data can be divided into values and references. Values are objects that wrap primitive values such as Int, Boolean, and Double. All other objects are references. Here's the Scala inheritance hierarchy:

 

 

Notes:

·       Any is the root of the Scala class hierarchy. Its methods include:

==, !=, equals
hashCode, ##
isInstanceOf, asInstanceOf
toString

·       AnyVal is the root of all value types (i.e., wrappers for primitive values). Implicit subtype conversions are defined between these classes.

·       AnyRef is the root class for all reference types (pointers to objects in the heap). In addition to the inherited methods, it adds:

getClass // returns an instance of java.lang.Class

·       Unit is analogous to Java's void type. Its only instance is ().

·       The only instance of Null is null, the null reference pointer. It can masquerade as any reference.

·       Nothing is the empty class. It is the universal subtype.

Initializing Inherited Fields

Constructor parameter lists my also contain ordinary parameters that are neither var or val parameters. These don't turn into fields unless they are used in the body of the constructor. One application of such parameters is to initialize inherited fields:


  class Date(val month: Int, val day: Int, val year: Int) {
     if (month <= 0 || 12 < month || day <= 0 || 31 < day || year < 0)
     throw new Exception("Invalid date")
    
     override def toString = month + "/" + day + "/" + year
  }
 
  class Time(val hour: Int, val minute: Int, month: Int, day: Int, year: Int)
  extends Date(month, day, year) {
     if (hour < 0 || 24 < hour || minute < 0 || 60 <= minute)
     throw new Exception("Invalid time")
    
   override def toString = hour + ":" + minute + ":" + super.toString
  }

Here are some sample calls:

  var day1 = new Date(10, 2, 2018)                //> day1  : time.Date = 10/2/2018
  day1                                            //> res0: time.Date = 10/2/2018
 
  // day1.month = 5 error, month is a val field
 
  var time1 = new Time(10, 30, 10, 2, 1018)       //> time1  : time.Time = 10:30:10/2/1018
  time1                                           //> res1: time.Time = 10:30:10/2/1018
 
 
  try {
     day1 = new Date(13, 1, 2019)
  } catch {
     case e: Exception => println(e)
  }                                               //> java.lang.Exception: Invalid date
 
    try {
     time1 = new Time(10, 30, 13, 1, 2019)
  } catch {
     case e: Exception => println(e)
  }                                               //> java.lang.Exception: Invalid date
}

Abstract Classes

An abstract class contains one or more abstract methods. An abstract method is simply a method without an implementation:

abstract class Shape {
     def area: Double
}

Abstract classes can't be instantiated for obvious reasons.

A class that inherits an abstract method is also abstract unless it provides implementations for those methods:

class Rectangle(length: Double, width: Double) extends Shape {
     def area = length * width
}

Notes:

·       We don't need to declare that a method overrides an inherited abstract method.

·       We don't need to perform casts. Thanks to polymorphism, the right method always gets called:

var shape: Shape = new Rectangle(3, 4)