
CS 3100: Program Design and Implementation II
Lecture 2: Inheritance and Polymorphism in Java
©2026 Jonathan Bell & Ellen Spertus, CC-BY-SA
Learning Objectives
After this lecture, you will be able to:
- Describe why inheritance is a core concept of OOP
- Define a type hierarchy and understand superclass/subclass relationships
- Explain the role of interfaces and abstract classes
- Describe the JVM's implementation of dynamic dispatch
- Describe the difference between static and instance methods
- Describe the JVM exception handling mechanism
- Recognize common Java exceptions and when to use them
Can students complete polls when outside of the classroom?
A. No, it's physically impossible.
B. Yes, and you should complete polls whether or not you're physically present.
C. Yes, but it's an academic integrity violation.

Text espertus to 22333 if the
URL isn't working for you.
Ellen's Office Hours
- Mondays and Thursdays, 3-4 PM, online and in CPM 201
- Other days by appointment
Would you attend in-person office hours?
A. Yes, often
B. Occasionally
C. Rarely, if ever

Text espertus to 22333 if the
URL isn't working for you.
Software Models Real-World Domains

IS-A relationship
Would you agree that:
- A Cat is-a Animal
- A Fish is-a Animal
We say that Cat and Fish are subtypes of Animal.
The Liskov Substitution Principle
"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program."
— Barbara Liskov, 1987
Subtypes Can Be Substituted for Their Supertypes
Given:
- A Cat is-a Animal
- A Fish is-a Animal
- You can feed an Animal
Would you agree that:
- You can feed a Cat
- You can feed a Fish
Because Cat and Fish are subtypes of Animal, anything you can do to/with an Animal
can be done to/with a Cat.
In other words, you can substitute an instance of a subtype for an instance of the supertype.
Abstract Methods
Animals breathe, but they breathe in different ways.
We say that breathe() is an abstract method in Animal because it is
declared but not implemented.
It is a concrete method in Cat and Fish because it is implemented.
Java interfaces
interface Animal {
void breathe();
}
class Cat implements Animal {
void breathe() {
// use lungs
}
}
class Fish implements Animal {
void breathe() {
// use gills
}
}
An interface declares but does not implement methods. It consists entirely
of abstract methods.
Subtypes implement the interface and provide concrete implementations of the required methods.
How Can We Share Code?
Different types of mob move and take damage the same way, but they make different sounds. How can they share code?
abstract class BaseMob {
void takeDamage() {
// shared implementation
}
void move() {
// shared implementation
}
abstract void makeSound();
}
class Cow extends BaseMob {
void makeSound() {
System.out.println("Moo!");
}
}
class Skeleton extends BaseMob {
void makeSound() {
System.out.println("Rattle!");
}
}
We can't put code in an interface. Instead, we use an abstract class: a class that can contain both concrete methods and abstract methods.
Subclasses inherit the code from their superclasses.
Not How Inheritance Is Supposed To Work

Y2K Bug

Example Domain: Smart Home IoT
Let's design a system to control smart home devices
What concepts exist in this domain?
- Devices (lights, fans, thermostats, sensors...)
- Rooms, zones
- Schedules, automations
- Users, permissions
Today we'll focus on devices
Real-World Concepts Form Natural Hierarchies
In the real world, concepts have natural relationships:
- A dimmable light is a kind of light
- A light is a kind of device
- A fan is a kind of device
These "is-a" relationships form a hierarchy
Inheritance Captures "Is-A" Relationships
Inheritance is a core OOP concept that lets us model "is-a" relationships
- Child types inherit behavior from parent types
- Child types can extend with new capabilities
- Child types can override to specialize behavior
Bonus: Avoid duplicating code → easier to maintain
Base Types Define Shared Capabilities
Every device in our system shares some basic capabilities:
identify()— help a human find the device (e.g., flash a light, wiggle a shade or fan)isAvailable()— check if the device is reachable
+— indicates that a method is public- italics — indicates a method is abstract (not implemented)
Subtypes Add Specialized Behavior
A skeletal implementation provides shared behavior; subclasses specialize:
Discussion: Why Separate Types?
Lights and fans both have on/off behavior...
Why not just use one SwitchableDevice type?
Think about: What does the domain tell us? How would users think about these?
Hierarchies Can Go Multiple Levels Deep
Not all lights are the same — some have more features:
The Complete Hierarchy
Each Level Inherits Everything Above It
| Type | Inherits From | Adds |
|---|---|---|
IoTDevice | — | identify(), isAvailable() |
BaseIoTDevice | IoTDevice | Implements isAvailable(), adds deviceId |
Light | BaseIoTDevice | turnOn(), turnOff(), isOn() |
DimmableLight | Light | setBrightness(), getBrightness() |
TunableWhiteLight | DimmableLight | setColorTemperature(), getColorTemperature() |
Each level inherits everything from above and adds new capabilities
Test Your Understanding: Animal
Where should getSpecies() be implemented (not abstract)?
Cat cat = new Cat();
System.out.println(cat.getSpecies()); // Felis catus
Dog dog = new Dog();
System.out.println(dog.getSpecies()); // Canis familiaris
Animal Class Diagram and Code 1

Your Code Should Tell a Story About the Domain
- Understand the domain before writing code
- "Is-a" relationships in the domain → inheritance in code
- Common behavior goes in parent types
- Specialized behavior goes in child types
- Good names come from domain vocabulary
Animal Class Diagram and Code 2

Sketch Your Types Before You Code

Java Uses Classes and Interfaces for Hierarchies
Every language represents type hierarchies differently
Java uses two constructs:
Classes Extend One Superclass, Implement Many Interfaces
- Can extend exactly one superclass
- Can implement multiple interfaces
- Inherit fields and methods from superclass
- Can override inherited methods
- Can be concrete or abstract
No Multiple Inheritance

Defining a Class
public class TunableWhiteLight extends DimmableLight {
private int startupColorTemperature;
public TunableWhiteLight(String deviceId, int colorTemp, int brightness) {
super(deviceId, brightness); // Call parent constructor
this.startupColorTemperature = colorTemp;
}
@Override
public void turnOn() {
setColorTemperature(startupColorTemperature);
super.turnOn(); // Call parent's turnOn
}
public void setColorTemperature(int colorTemperature) { /* ... */ }
public int getColorTemperature() { /* ... */ }
}
Key Syntax Points
extends | Specifies the superclass |
super(...) | Call the parent's constructor (must be first line) |
@Override | Annotation indicating method override |
super.method() | Call the parent's version of a method |
this.field | Refers to instance field (not parameter) |
Visibility Controls Who Can Access Your Code
| Modifier | Accessible By |
|---|---|
public | Any class |
protected | Same class, subclasses, same package |
private | Same class only |
| (none) | Package-private — avoid! |
Always specify visibility explicitly
Subtypes Must Behave Like Their Supertypes

Subclasses Work Wherever Supertypes Are Expected
Light[] lights = new Light[2];
TunableWhiteLight light1 = new TunableWhiteLight("light-1", 2700, 100);
lights[0] = light1; // ✓ TunableWhiteLight IS-A Light
DimmableLight light2 = new DimmableLight("light-2", 100);
lights[1] = light2; // ✓ DimmableLight IS-A Light
for (Light light : lights) {
light.turnOn(); // Works for any Light!
}
A subclass can always be used where a superclass is expected
Downcasting Accesses Subclass-Specific Features
To access subclass-specific methods, cast the reference:
// Explicit cast to access subclass method
TunableWhiteLight twl = (TunableWhiteLight) lights[0];
twl.setColorTemperature(2200);
// Or inline:
((TunableWhiteLight) lights[0]).setColorTemperature(2200);
⚠️ Throws ClassCastException if the cast is invalid!
Interfaces Define Contracts Without Implementation

- Define a contract — methods a class must implement
- A class can implement multiple interfaces
- Cannot be instantiated directly
- No instance fields (only constants)
Defining an Interface
public interface IoTDevice {
/** Identify the device to a human (flash, spin, beep). */
void identify();
/** Check if the device is available. */
boolean isAvailable();
}
Note: public is optional in interface methods (always public)
Abstract Classes Provide Skeletal Implementations
A skeletal implementation provides shared behavior while leaving specifics abstract:
public abstract class BaseIoTDevice implements IoTDevice {
protected String deviceId;
protected boolean isConnected;
public BaseIoTDevice(String deviceId) {
this.deviceId = deviceId;
}
@Override
public boolean isAvailable() { return this.isConnected; }
public abstract void identify(); // Subclasses must implement
}
Use Interfaces for Contracts, Abstract Classes for Shared State
| Interface | Abstract Class | |
|---|---|---|
| Multiple inheritance | Yes | No |
| Instance fields | No | Yes |
| Concrete methods | Default only | Yes |
| Constructor | No | Yes |
Rule of thumb: prefer interfaces; use abstract classes when you need shared state or implementation
The JVM Chooses Methods at Runtime, Not Compile Time
How does the JVM know which method to call?
Light[] lights = new Light[] {
new TunableWhiteLight("light-1", 2700, 100),
new DimmableLight("light-2", 100)
};
for (Light light : lights) {
light.turnOn(); // Which turnOn() is called?
}
Method Lookup Walks Up the Type Hierarchy
To call method m on object o of runtime type T:
- If T declares m, use that
- Otherwise, check T's superclass recursively
- If still not found, use default interface method (if any)
Runtime type matters, not compile-time type!
Which Method Gets Called?
Call:twl.turnOn()
TunableWhiteLight twl =
new TunableWhiteLight("living-room", 2700, 100);
twl.turnOn();
Runtime type:TunableWhiteLight
Step 1: Does TunableWhiteLight declare turnOn()?
✓ Yes! Use TunableWhiteLight.turnOn()
Which Method Gets Called?
Call:twl.setBrightness(50)
TunableWhiteLight twl =
new TunableWhiteLight("living-room", 2700, 100);
twl.setBrightness(50);
Runtime type:TunableWhiteLight
Step 1: Does TunableWhiteLight declare setBrightness()? No
Step 2: Check superclass DimmableLight...
✓ Found! Use DimmableLight.setBrightness()
Which Method Gets Called?
Call:light.turnOn()
Light light =
new TunableWhiteLight("living-room", 2700, 100);
light.turnOn();
Compile-time type:Light
Runtime type:TunableWhiteLight
Step 1: Does TunableWhiteLight declare turnOn()?
✓ Yes! Use TunableWhiteLight.turnOn()
The variable type (Light) doesn't matter — runtime type does!
Poll: What does this do? 1
abstract class Animal {
public abstract String getSpecies();
}
class Cat extends Animal {
@Override
public String getSpecies() {
return "Felis catus";
}
public static void main(String[] args) {
Animal animal = new Cat();
System.out.println(animal.getSpecies());
}
}
A. nothing
B. prints "Felis catus"
C. does not compile
D. has runtime error

Text espertus to 22333 if the
URL isn't working for you.
Poll: What does this do? 2
abstract class Animal {
public abstract String getSpecies();
}
class Cat extends Animal {
@Override
public String getSpecies() {
return "Felis catus";
}
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal.getSpecies());
}
}
A. nothing
B. prints "Felis catus"
C. does not compile
D. has runtime error

Text espertus to 22333 if the
URL isn't working for you.
Casting Doesn't Change Which Method Runs
Light light = new TunableWhiteLight("living-room", 2700, 100);
light.turnOn();
// Calls TunableWhiteLight.turnOn()
((DimmableLight) light).turnOn();
// STILL calls TunableWhiteLight.turnOn()!
The cast doesn't change which method is called
The actual object type at runtime determines the method
Instance Methods Dispatch Dynamically; Static Methods Don't
Instance Methods
- Belong to an object
- Can access
this - Dynamically dispatched
Static Methods
- Belong to the class
- No
thisreference - Statically bound
Static Methods Belong to Classes, Not Objects
public class TunableWhiteLight extends DimmableLight {
// ...
/**
* Convert color temperature from Kelvin to mireds.
*/
public static int degreesKelvinToMired(int degreesKelvin) {
return 1000000 / degreesKelvin;
}
}
// Invocation — use the class name, not an instance
int mireds = TunableWhiteLight.degreesKelvinToMired(2700);
No object needed — method is resolved at compile time
Some Objects Belong to Multiple Categories
What if a class has more than one supertype? For example, a Cat is both an Animal and a FuzzyThing.
interface Animal {
void feed();
}
interface FuzzyThing {
void shed();
}
class Cat implements Animal, FuzzyThing {
public void feed() {
System.out.println("Nom nom nom");
}
public void shed() {
System.out.println("Hair everywhere!");
}
}
Can Cat be a subtype of both? Yes! A class can implement multiple interfaces.
Multiple Inheritance
Can a class be a subtype of two classes?
abstract class Animal {
boolean causesAllergies() {
return false; // most animals don't
}
}
abstract class FuzzyThing {
boolean causesAllergies() {
return true; // fuzzy things do
}
}
// NOT ALLOWED IN JAVA!
class Cat extends Animal, FuzzyThing {
// Which causesAllergies() do we inherit?
}
Which implementation would Cat use?
To avoid this problem, Java disallows multiple inheritance. (C++ and Python permit it.)
Real Objects Often Have Multiple "Is-A" Relationships
Back to our IoT domain... what about a ceiling fan with a light?
It's both a light AND a fan!
More Examples of Multiple Inheritance
Real-world objects often have multiple "is-a" relationships:
- A smart thermostat is both a temperature sensor and a controllable device
- A smartphone is a phone, a camera, and a computer
- A teaching assistant is both a student and an employee
- An amphibious vehicle is both a boat and a car
In each case, the object legitimately belongs to multiple categories
Multiple Class Inheritance Creates Ambiguity
Why don't most languages allow multiple class inheritance?
Which identify() does CeilingFanWithLight inherit?
The Compiler Can't Choose Between Conflicting Implementations

Java Avoids the Diamond Problem with Interfaces
Java avoids the diamond problem by restricting multiple inheritance to interfaces:
- Interfaces don't provide implementations (usually)
- No ambiguity about which implementation to inherit
- The implementing class must provide the implementation
public class CeilingFanWithLight implements Light, Fan {
@Override
public void identify() {
// We decide: flash AND spin!
flashLight();
spinBlades();
}
// ... implement all other methods from both interfaces
}
Composition: "Has-A" Instead of "Is-A"
Instead of being both types, contain instances of them
Composition Lets You Control How Behaviors Combine
With composition, we decide how to combine behaviors:
public class CeilingFanWithLight implements IoTDevice {
private Light light; // has-a light
private Fan fan; // has-a fan
@Override
public void identify() {
// No ambiguity — we explicitly delegate to both!
light.identify(); // flashes the light
fan.identify(); // wiggles the blades forward/backward
}
public void turnOnLight() { light.turnOn(); }
public void setFanSpeed(int speed) { fan.setSpeed(speed); }
}
✓ Diamond problem solved — we control the delegation
Composition Trades Substitutability for Flexibility
Advantages
- No diamond problem
- Can swap implementations at runtime
- More flexible coupling
- Easier to test (mock components)
Disadvantages
- Can't substitute for Light or Fan
- Must manually delegate methods
- More boilerplate code
"Prefer composition over inheritance" — but know when inheritance fits better
Combine Interfaces and Composition for Maximum Flexibility
Best of both worlds: composition with interface implementation
public class CeilingFanWithLight implements Light, Fan, IoTDevice {
private Light lightComponent; // Delegate to this
private Fan fanComponent; // Delegate to this
@Override
public void identify() {
lightComponent.identify();
fanComponent.identify();
}
@Override
public void turnOn() { lightComponent.turnOn(); }
@Override
public void setSpeed(int speed) { fanComponent.setSpeed(speed); }
// ... other delegated methods
}
✓ Can substitute for Light, Fan, and IoTDevice!
Java's Exception Hierarchy Distinguishes Error Types
All exceptions extend Throwable
Checked Exceptions Must be Declared

public void turnOn() throws IOException {
// ...
}
Errors Are Fatal; Exceptions Are Recoverable
Error | Exception |
|---|---|
| Typically fatal | Recoverable |
| JVM-detected problems | Application-level problems |
OutOfMemoryError | IOException |
StackOverflowError | NullPointerException |
Don't throw Error in your code!
Checked Exceptions Force Callers to Handle Failure
| Checked | Unchecked |
|---|---|
| Must be caught or declared | No requirement to handle |
Subclass of Exception | Subclass of RuntimeException |
IOException | NullPointerException |
SQLException | IllegalArgumentException |
Python: all exceptions are unchecked
Prefer Standard Exceptions Over Custom Ones
IllegalArgumentException | Invalid method argument |
NullPointerException | Unexpected null value |
IllegalStateException | Object in wrong state for operation |
IndexOutOfBoundsException | Index outside valid range |
UnsupportedOperationException | Operation not supported |
Prefer standard exceptions over custom ones!
Fail Fast: Check Parameters and Throw Clear Exceptions
/**
* Set the color temperature of the light.
* @param colorTemperature Temperature in degrees Kelvin.
* @throws IllegalArgumentException if temperature is out of range.
*/
public void setColorTemperature(int colorTemperature) {
if (colorTemperature < 1000 || colorTemperature > 10000) {
throw new IllegalArgumentException(
"Color temperature must be between 1,000 and 10,000 Kelvin");
}
// ... proceed with valid input
}
Check parameters early — fail fast with clear messages
⚠️ Exceptions Are for Exceptional Cases
// DON'T DO THIS!
try {
int i = 0;
while (true) {
lights[i].turnOn();
i++;
}
} catch (ArrayIndexOutOfBoundsException e) {
// Using exception for flow control — bad!
}
// DO THIS INSTEAD
for (int i = 0; i < lights.length; i++) {
lights[i].turnOn();
}
Summary
- Inheritance models "is-a" relationships & enables code reuse
- Interfaces define contracts; abstract classes share implementation
- Multiple inheritance: desirable but causes diamond problem; Java uses interfaces
- Liskov Substitution: subtypes must be substitutable for supertypes
- Dynamic dispatch: runtime type determines which method runs
- Exceptions: checked vs unchecked; favor standard exceptions
Next Steps
- Complete Java flashcard sets 1 and 2
- Read: Core Java Volume I: Fundamentals, Ch 3
- Lab 2: Java abstraction and inheritance
Questions?