Lecture 15

2023-05-22 | Week 8 | edited by Matt Wang

(originally written 2022-11-16 by Carey Nachenberg)

Matt here! This lecture covers OOP Palooza’s inheritance section, up to destructors - slides 67 to 110.

Table of Contents

Inheritance

You learned about one type of inheritance in CS32, but there are actually four types:

  • Interface Inheritance: This is about creating many classes that share a common public base interface I. A class supports interface I by providing implementations for all I’s functions.
  • Subtype Inheritance: A base class provides a public interface and implementations for its methods. A derived class inherits both the base class’s interface and its implementations.
  • Implementation Inheritance: This is about reusing a base class’s method implementations. A derived class inherits method implementations from a base class.
  • Prototypicall Inheritance: One object may inherit the fields/methods from a parent object. This is used in JavaScript.

Let’s learn all of them!

Interface Inheritance

Here’s how it works. You define a public interface, which is basically a collection of related function prototypes (aka declarations). Here’s an example of an interface in C++ - C++ just uses fully-abstract classes to define interfaces (where none of the functions has an implementation):

// C++ pure interface inheritance example
class Shape {
public:
  virtual double area() const = 0;
  virtual double perimeter() const = 0;
  virtual void scale(double factor) = 0;
};

Notice that these are pure-virtual methods - there is no implementation specified. They specify “what” to do, but not “how.” Then you can define a class that “inherits” (aka) implements this interface:

class Circle : public Shape {
public:
 Circle(double radius) { rad_ = radius; }
 virtual double area() const
   { return PI * rad_ * rad_; }
 virtual double perimeter() const
  { return 2 * PI * rad_; }
 virtual void scale(double factor)
  { rad_ *= factor; }
private:
  double rad_;
};

With interface inheritance we can have entirely unrelated/different classes that support a common interface:

class Washable {
public:
  virtual void wash() = 0;
  virtual void dry() = 0;
};

class Car: public Washable {
public:
  void drive() { cout << "Vroom vroom"; }
  void wash() { cout << "Use soap"; }
  void dry()  { cout << "Use rag"; }
};

class Person: public Washable {
public:
  void talk() { cout << "Hello!"; }
  void wash() { cout << "Use shampoo"; }
  void dry()  { cout << "Use blowdryer"; }
};

Notice that even though Car and Person are unrelated, they both support a Washable interface, and can both be washed!

With interface inheritance we can also have a single class support multiple interfaces. For example, in the example Below a Car can be both washed and driven:

class Washable {
public:
  virtual void wash() = 0;
  virtual void dry() = 0;
};

class Drivable {
public:
  virtual void accelerate() = 0;
  virtual void brake() = 0;
};

class Car: public Washable, public Drivable {
public:
  // Car methods
  void turnOn() { cout << "The car is on!"; }

  // Washable methods
  void wash() { cout << "Use soap"; }
  void dry()  { cout << "Use rag"; }

  // Drivable methods
  void accelerate() { cout << "Vroom vroom"; }
  void brake() { cout << "Screeeeeech"; }
};

Once we define an interface we can find functions that specify the interface as the type of a parameter:

// We can use the interface as a reference type, so now w can refer to any washable object
void cleanUp(Washable& w) {
  w.wash();
  w.dry();
}

// We can use the interface as a reference type, so now w can refer to any drivable object
void driveToUCLA(Drivable& d) {
 d.accelerate();
 // waste 1 hour in traffic
 d.brake();
}

int main() {
  Car forrester;
  cleanUp(forrester);
  driveToUCLA(forrester);
}

Neat, right? Now our above Car class has three personas. It is a Car, it is a Drivable thing, and it is a Washable thing.

Examples in Other Languages

Here’s an example from Swift; it uses the protocol keyword to mean interface:

protocol Shape {
  public func area() -> Double
  public func perimeter() -> Double
  public func scale(factor: Double)
}

class Circle: Shape {
  init(radius: Double)
    { rad_ = radius; }
  public func area() -> Double
    { return PI * rad_ * rad_; }
  public func perimeter() -> Double
    { return 2 * PI * rad_; }
  func scale(factor: Double)
    { rad_ *= factor; }

  private var rad_: Double
}

Here’s a similar example in Java, which uses implements:

public interface Shape {
  public double area();
  public double perimeter();
  public void scale(double factor);
};

class Circle implements Shape {
  public Circle(double radius)
    { rad_ = radius; }
  public double area()
    { return PI * rad_ * rad_; }
  public double perimeter()
   { return 2 * PI * rad_; }
  public void scale(double factor)
   { rad_ *= factor; }

  private double rad_;
};

Go has an interesting approach: they use structural inheritance, where you don’t need to explicitly say that a Circle implements a Shape; all that needs to be true is that it implements the relevant functions.

type Shape interface {
  Area() float64
  Perimeter() float64
  Scale(f float64)
}

type Circle struct {
  x      float64
  y      float64
  radius float64
}

func (c Circle) Area() float64 {
	return PI * c.radius * c.radius }
func (c Circle) Perimeter() float64 {
	return 2 * PI * c.radius }
func (c Circle) Scale(f float64) {
	c.radius *= f }

This is also how languages like Rust work!

Common Use Cases

Interfaces are ubiquitous in object-oriented programming. Here are a few places you might encounter them:

  1. comparing objects - ex Java’s Comparable
  2. iterating oer an object - ex Java’s Iterable
  3. (from class: serializing an object!)

First, let’s take a look at Java’s Comparable:

// This interface is part of the Java language distribution:
public interface Comparable<T>
{
  // Compares two objects, e.g., for sorting
  public int compareTo(T o);
}

public class Dog implements Comparable<Dog> {
  private int bark;
  private int bite;
  Dog(int bark, int bite) {
    this.bark = bark;
    this.bite = bite;
  }

    public int compareTo(Dog other) {
    int diff = this.bark  other.bark;
    if (diff != 0) return diff;
    return this.bite  other.bite;
  }
}

ArrayList<Dog> doggos = new ArrayList<>();
doggos.add(new Dog(10, 20));
doggos.add(new Dog( 3, 48));
doggos.add(new Dog(15, 4));

doggos.sort(Comparator.naturalOrder()); // sort() uses the Comparable interface to compare objects

We can also use interfaces to make objects iterable:

// This interface is part of the Java language distribution:
public interface Iterable<T>
{
  Iterator<T> iterator();
}

public class Dog { ... }

class Kennel implements Iterable<Dog> {
    private ArrayList<Dog> list;

    public Kennel() {
      list = new ArrayList<Dog>();
    }
    public void addDog(Dog doggo)
      { list.add(doggo); }

    public Iterator<Dog> iterator()
      { return list.iterator(); }
}

...
Kennel kennel = new Kennel();
kennel.addDog(new Dog("Fido",...));
kennel.addDog(new Dog("Nellie",...));

for (Dog d : kennel) {  // Java's for loop automatically uses the Iterable interface to iterate
  d.bark();
}

Best Practices (Pros & Cons)

Use Interface Inhertance when you have a “can-support” relationship between a class and a group of behaviors.

  • The Car class can support washing.
  • The kennel class can support iteration.

Use Interface Inhertance when you have different classes that all need to support related behaviors, but aren’t related to the same base class.

  • Cars and Dogs can both be washed, but aren’t related by a common base.

Pros:

  • You can write functions focused on an interface, and have it apply to many different classes.
  • A single class can implement multiple interfaces and play different roles

Cons:

  • Doesn’t facilitate code reuse

Subclassing Inheritance

This is the inheritance we learned about in CS32, and the one that most people think of when they think of inheritance.

Here’s how it works:

  • We create a base class B, which provides a public interface and method implementations
  • We create a derived class D which inherits B’s public interface and its method implementations
  • Derived class D may override B’s methods, or add its own new methods, potentially expanding D’s public interface.

Here’s an example from C++:

// C++ subclassing example
class Shape {
public:
  Shape(float x, float y)  { x_ = x; y_ = y; }
  void setColor(Color c)   { color_ = c; }
  virtual void disappear() { setColor(Black); }
  virtual float area() const = 0;
...
protected:
  void printShapeDebugInfo() const { ... }
};

class Circle: public Shape {
public:
  Circle(float x, float y, float r) :  Shape(x,y)
  	{ rad_ = r; }
  float radius() const { return rad_; }
  virtual float area() const { return PI * rad_ * rad_; }
  virtual void disappear() {
    for (int i=0;i<10;++i)
    	rad_ *= .1;
        Shape::disappear();
  }
};

Notice that that the derived class, Circle, not only inherits the public interface of Shape (e.g., setColor, disappear, and area), but also inherits the implementation/code of some of these functions. Because the derived class inherits the base’s public interface and its implementation, it can do anything the base class can do! And you can pass the derived class anywhere the base class is expected (because the derived class is a subtype of the base class). And since the derived class’s interface is expanded, it can also do its own specialized derived things.

Examples in Other Languages

Here’s an example from Java:

// Java class for Shapes
abstract class Shape {
  Shape(float x, float y)
    { x_ = x; y_ = y; }
  public final void setColor(Color c)
    { color_ = c; }
  public void disappear()
    { setColor(Black); }
  public abstract float area();

  protected final void printShapeDebugInfo()
    { ... }

  private float x_;
  private float y_;
  private Color color_;
}

class Circle extends Shape {
  Circle(float x, float y, float r) {
    super(x, y);
    rad_ = r;
  }
  public float radius()
    { return rad_; }
  public void disappear() {
    for (int i=0;i<10;++i)      rad_ *= .1;    super.disappear();
  }
  public float area()
    { return PI*rad_*rad_; }

  private float rad_;
}

Here’s a similar example in Swift:

class Shape {
  init(x : Double, y : Double) {
    x_ = x
    y_ = y
  }
  public func setColor(c : Color)
    { color_ = c }
  public func disappear()
    { setColor(c:Black) }
  public func area() -> Double
    { return 0 }
  func printShapeDebugInfo()
     { ... }

  private var x_ : Double;
  private var y_ : Double;
  private var color_ : Int = White;
}

class Circle:  Shape {
  init(x : Double, y : Double, r : Double) {
    rad_ = r
    super.init(x : x, y : y)
  }
  public func radius() -> Double
    { return rad_  }
  override public func disappear() {
    for i in 1...10 {
      rad_ *= 0.1
    }
    super.disappear()
  }
  override public func area() -> Double
    { return PI * self.rad_ * self.rad_ }

  private var rad_ : Double;
}

Some quick differences:

  • Swift doesn’t support abstract methods (this is a holdover from Objective-C)
  • Swift requires the override keyword, rather than implicit behaviour (like in Java)

And, we even have subclassing in Python!

class Shape:
  def __init__(self, x, y):
    self.x = x
    self.y = y
  def set_color(self,c):
    self.color = c
  def disappear(self):
    self.setColor(Black)
  def area(self):
    pass
  def _printShapeDebugInfo(self):
    # ...

class Circle(Shape):
  def __init__(self, x, y, r):
    super().__init__(x, y)
    self.rad = r
  def radius(self):
    return self.rad
  def disappear(self):
    for i in range(10):
      self.rad *= 0.1
    super().disappear()
  def area(self):
    return PI * self.rad * self.rad

Some interesting things:

  • we’ve used the pass keyword to specify an abstract method (though, it turns out there are a few ways to do it)
  • we need to explicitly call the constructor of the superclass!

Subtype Inheritance Best Practices

Use subtype inheritance when:

  • When there’s an is-a relationship:
    • A Circle is a type of Shape
    • A Car is a type of Vehicle
  • When you expect your subclass to share the entire public interface of the superclass AND maintain the semantics of the super’s methods
  • When you can factor out common implementations from subclasses into a superclass:
    • All Shapes (Circles, Squares) share x,y coordinates and a color along with methods to get/set these things.

When should you not use subtype inheritance? Consider this example:

// C++ Collection class
class Collection {
public:
  void insert(int x) { ... }
  bool delete(int x) { ... }
  void removeDuplicates(int x) { ... }
  int count(int x) const { ... }
  ...

private:
  int *arr_;
  int length_;
};

// C++ Set class
class Set: public Collection {
public:
  void insert(int x) { ... }
  bool delete(int x) { ... }
  bool contains(int x) const { ... }
  ...

private:
  int *arr_;
  int length_;
};

Why wouldn’t we want to use it in this case? Because:

  • Reason #1: Our derived class doesn’t support the full interface of the base class.
  • Reason #2: The derived class doesn’t share the same semantics as the base class.

In these cases, where you’d like to inherit the functionality of a base class but don’t want to inherit the full public interface (because it doesn’t make sense to do so in your derived class) you can use composition and delegation. Composition is when you make the original class a member variable of your new class. Delegation is when you call the original class’s methods from the methods of the new class. Here’s an example:

class Collection {
public:
  void insert(int x) { ... }
  bool delete(int x) { ... }
  int count(int x) const { ... }
};

class Set
public:
  void insert(int x) {
    if (c_.howMany() == 0) c_.insert();   // delegation
  }
  bool delete(int x) {
    return c_.delete(x)
  }
  bool contains(int x) const {
    return c_.count(x) == 1;
  }
  ...
private:
  Collection c_;    // composition
};

Composition/delegation has the benefit of hiding the public interface of the original class (Collection) and at the same time the second class can leverage all of its functionality.

Pros and Cons

OK, let’s sum up the pros/cons of subtype inheritance:

Pros:

  • Eliminates code duplication/facilitates code reuse
  • Simpler maintenance – fix a bug once and it affects all subclasses
  • If you understand the base class’s interface, you can generally use any subclass easily
  • Any function that can operate on a superclass can operate on a subclass w/o changes

Cons:

  • Often results in poor encapsulation (derived class uses base class details)
  • Changes to superclasses can break subclasses (aka Fragile Base Class problem, which we’ll learn about in week 9)

Implementation Inheritance

In Implementation Inheritance, the derived class inherits the method implementations of the base class, but NOT its public interface. The public interface of the base class is hidden from the outside world. This is often done with private or protected inheritance

// C++ Collection class
class Collection {
public:
  void insert(int x) { ... }
  bool delete(int x) { ... }
  int count(int x) const { ... }
  ...
};

// C++ Set class
class Set: private Collection	// private inheritance
public:
  void add(int x)
    { if (howMany() == 0) insert(x); }
  bool erase(int x)
    { return delete(x); }
  bool contains(int x) const
    { return count(x) > 0; }
  ...
};

Since Set privately inherits from Collection, it inherits all of its public/protected methods, but it does NOT expose the public interface (insert(), delete(), count(), …) of the base Collection class publicly. And for that matter, Set is NOT a subtype of Collection. The Set class hides the fact that it has anything to do with the Collection base class. So you CAN’T pass a Set to a function that accepts a Collection, since as far as the language is concerned, they’re unrelated types.

In general, implementation inheritance is frowned upon. If you want to have a class “inherit” all of the functionality of a base class, but hide the public interface of the base class, it’s always better to use composition + delegation to do so.

Classify That Language: Inheritance

// Equivalent of a Dog class, with bark method.
struct Dog {
  name: String,
  stomach: String
}

impl Dog {
  fn bark(&self) { println!("Woof!") }
}

trait Ingests {
  fn eat(&mut self, food: &str);
  fn drink(&mut self, liquid: &str);
}

impl Ingests for Dog {
  fn eat(&mut self, food: &str) {
    println!("{} eats {}", self.name, food);
    self.stomach = food.to_string()
  }
  fn drink(&mut self, liquid: &str) {
    println!("{} laps {}", self.name, liquid);
    self.stomach = liquid.to_string()
  }
}

fn main() {
  let mut fido = Dog {...};
  fido.bark();
  fido.eat("grass");
  fido.drink("toilet water")
}

What type of inheritance is this program using?

  • Interface Inheritance
  • Subclassing/Hybrid Inheritance
  • Implementation Inheritance

Answer: It’s using interface inheritance. In this language (Rust), the name for an interface is a “trait.”

Prototypal Inheritance

This one’s mostly for completeness, but we won’t test you on it. Prototypal Inheritance is used in languages that only have objects, not classes. An example would be JavaScript. In this case, an object may inherit the fields and methods from one or more “prototype” objects.

As it turns out, every JavaScript object has a hidden reference to a parent (aka “prototype”) object. An object may specify that it inherits the fields/methods of some other object. Any fields/methods of the same name in the “derived” object as in the “base” object, shadow those in the “base” object:

obj1 = {
  name: "",
  job: "",
  greet: function()
          { return "Hi, I'm " + this.name; },
  my_job: function()
             { return "I'm a " + this.job; }
}

obj2 = {
  name: "Carey",
  job: "comedian",
  tell_joke: function()
    { return "USC education"; },
  __proto__: obj1
}

Notice the proto field in obj2? This says that obj2 inherits all of the fields/methods of obj1. So you could call obj2.greet(), or obj2.my_job() and it works fine. Since obj2 redefines name to “Carey”, any functions called on obj2, including those originally defined in obj1, like greet(), will use obj2’s version of the field! So a call to obj2.greet() would print out “Hi I’m Carey”.

Shallow Dive into Inheritance

Here, we’ll focus more on the mechanics of inheritance, rather than comparing paradigms.

Construction

Construction follows the same basic pattern in all languages! When you instantiate an object of a derived class, this calls the derived class’s constructor. Every derived class constructor does two things:

  • It calls its superclass’s constructor to initialize the immediate superclass part of the object
  • It initializes its own parts of the derived object

In most languages, you must call the superclass constructor first from the derived class constructor, before performing any initialization of the derived object:

// Construction with inheritance in Java
class Person {
 public Person(String name) {
   this.name = name;
 }
 private String name;
}

class Nerd extends Person {
  public Nerd(String name, int IQ) {
    super(name);  // calls superclass constructor first
    this.IQ = IQ; // then initializes members of the drived class
  }
  private int IQ;
}

class ComedianNerd extends Nerd {
  public ComedianNerd(String name, int IQ, String joke) {


  }
  private String joke;
}

In other languages, like Swift, the derived constructor initializes the derived object’s members first, then calls the base class constructor.

Some languages will auto-call the base-class constructor if it’s a default constructor (has no parameters), but other languages, like Python always require you to call the base constructor explicitly:

# Python inheritance+construction
class Person:
  def __init__(self):
    self.name = "anonymous"

  ...

class Nerd(Person):
  def __init__(self, IQ):
    super().__init__()  // if we leave this out, we simply won't initialize our Person base part!
    self.IQ = IQ
  ...

Destruction

When destructing a derived object, the derived destructor runs its code first and then implicitly calls the destructor of its superclass. This occurs all the way to the base class.

Finalization

With Finalization, some languages require an explicit call from a derived class finalizer to the base class. In other languages, the derived finalizer automatically/implicitly calls the base class finalizer. This varies by language, so make sure to figure out what your language does.