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
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 interfaceI
by providing implementations for allI
’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:
- comparing objects - ex Java’s
Comparable
- iterating oer an object - ex Java’s
Iterable
- (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 inheritsB
’s public interface and its method implementations - Derived class
D
may overrideB
’s methods, or add its own new methods, potentially expandingD
’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.