(9) Class Hierarchies
About
:octocat: GitHub: All of the example code: repo (link)
:page_facing_up: blog link: https://purrgramming.life/cs/programming/fp/ :star:
Terminology
-
Abstract Classes: An abstract class may include abstract methods or abstract properties shared by its subclasses.
-
Extend: the extends keyword indicates that the class being defined is derived from the base class using inheritance. i.e. extend the functionality of the parent class to the subclass.
image source: https://www.geeksforgeeks.org/difference-between-abstract-class-and-interface-in-java/
-
Conformance: an object of subtypes can be used wherever an object of
parent type is required -
Base Class: A class from which other classes are derived.
Abstract Classes
Definition
Scala has a concept of an abstract class similar to Java’s abstract class, constructed using the abstract
keyword.
Need to use an abstract class when:
- You want to create a base class that requires
constructor
arguments - Your Scala code will be called from Java code
Consider the task of writing a class for sets of integers with the following operations.
abstract class IntSet {
def incl(x: Int): IntSet
def contains(x: Int): Boolean
}
IntSet is an abstract class.
Abstract classes can contain members who are missing an implementation (in our case, both incl
and contains);
These are called abstract members.
Consequently, no direct instances of an abstract class can be created. For instance, an IntSet()
call would be illegal.
Abstract Function
An abstract function) is a virtual function for which we can (and must) implement the derived class. Otherwise, the derived class will also become an abstract class
The definitions of contains
and incl
in the classes Empty
and NonEmpty
implement the abstract functions in the base class IntSet
abstract class IntSet {
def incl(x: Int): IntSet // Abstract Function
def contains(x: Int): Boolean // Abstract Function
}
Abstract classes can contain both abstract and non-abstract methods.
class Empty() extends IntSet {
def contains(x: Int): Boolean = false // Implementation
def incl(x: Int): IntSet = NonEmpty(x, Empty(), Empty()) // Implementation
}
Class Extensions
IntSet is called the superclass of Empty and NonEmpty.
Empty and NonEmpty are subclasses of IntSet.
In Scala, any user-defined class extends another class.
If no superclass is given, the standard class Object in the Java package. So, the base classes of NonEmpty include IntSet and Object.
Let’s consider implementing sets as binary trees.
There are two types of possible trees:
- a tree for the empty set
- a tree consisting of an integer and two sub-trees.
Here are their implementations:
class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
def contains(x: Int): Boolean =
if x < elem then left.contains(x)
else if x > elem then right.contains(x)
else true
def incl(x: Int): IntSet =
if x < elem then NonEmpty(elem, left.incl(x), right)
else if x > elem then NonEmpty(elem, left, right.incl(x))
else this
}
class Empty() extends IntSet {
def contains(x: Int): Boolean = false
def incl(x: Int): IntSet = NonEmpty(x, Empty(), Empty())
}
Empty
andNonEmpty
bothextend
the classIntSet
.- This implies that the types Empty and NonEmpty conform to the type IntSet, i.e. an object of type Empty or NonEmpty can be used wherever an object of type IntSet is required.
Dynamic Binding
Object-oriented languages (including Scala) implement dynamic method dispatch.
This means that the code invoked by a method call depends on the object’s runtime type that contains the method.
i.e. Dynamic binding is determining the method to invoke at runtime
instead of at compile-time.
Dynamic binding is also referred to as late binding.
// val res1: Boolean = false
Empty.contains(1)
//val res2: Boolean = true
(NonEmpty(7, Empty, Empty)).contains(7)
Summary
Abstract classes
- can have companion objects
- can extend other classes (there are no restrictions on this, abstract classes can extend and be extended by abstract or non-abstract classes)
- cannot be instantiated (abstract classes contain members without implementation. Therefore, they cannot be instantiated)
- can be extended
Clean Code
In the IntSet
example, there is really only a single empty IntSet.
So overkill to have the user create many instances of it.
We can express this case better with an object definition:
object Empty extends IntSet {
def contains(x: Int): Boolean = false
def incl(x: Int): IntSet = NonEmpty(x, Empty, Empty)
}
This defines a singleton object named Empty.
No other Empty
instance can be (or needs to be) created.
Singleton objects are values, so Empty evaluates itself.
i.e.
// val res0: Boolean = false
Empty.contains(1)
// Error: value contains is not a member of object NonEmpty
NonEmpty.contains(1)
Also, we can create a companion object for IntSet.
object IntSet {
def singleton(x: Int) = NonEmpty(x, Empty, Empty)
}
This defines a method to build sets with one element, called IntSet.singleton(elem).
Overriding
It is also possible to redefine an existing, non-abstract definition in a subclass by using override.
i.e., a feature that enables a child class to provide a different implementation for a method already defined and/or implemented in its parent class or one of its parent classes.
image source: https://codepumpkin.com/method-overriding-interview-questions/
// Creating a super class
abstract class Shapes {
def draw() = println("Draw the shape")
}
// Creating a subclass
class Rectangle extends Shapes
// Creating a subclass
class Circle extends Shapes {
// Overriding
override def draw() = println("Draw the Circle")
}
val rectangle = new Rectangle
rectangle.draw()
val circle = new Circle
circle.draw()
Results:
val rectangle: Rectangle = Rectangle@47a8e61f
Draw the shape
val circle: Circle = Circle@15c2d397
Draw the Circle