Skip to main content
Lecture 3 Cover

CS 3100: Program Design and Implementation II

Lecture 3: Inheritance and Polymorphism in Java II

©2025 Jonathan Bell & Ellen Spertus, CC-BY-SA

Poll: What pre-college CS courses did you take?

A. AP CS A

B. AP CS Principles

C. Other Java class or self-study

D. Other non-Java class or self-study

E. None

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

How's Assignment 1 going?

A. Haven't looked at it

B. Looked at it but haven't started

C. Started it but got stuck

D. Started it and doing alright

E. Pretty far along

F. Completed it

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Learning Objectives

After this lecture, you will be able to:

  1. Describe the role of generics in the Collections API
  2. Recognize and apply Java's core data structures (List, Set, Map)
  3. Describe the purpose of primitive wrapper types
  4. Utilize Java methods for reading input and writing output to streams

The JVM Chooses Methods Based on Runtime Type

Dynamic dispatch: The JVM chooses methods at runtime based on actual object type

Java's Standard Libraries Model Good OOP Design

Inheritance and polymorphism aren't just theory...

They shape how Java's built-in libraries work:

  • Collections API — Lists, Sets, Maps with type safety
  • I/O Streams — Uniform abstraction for reading/writing

Understanding these APIs deepens your grasp of OOP design

Collections Need Flexibility AND Safety

Warehouse metaphor: unlabeled Object boxes vs type-safe color-coded sections

Without Generics, Collections Are Type-Unsafe

public interface List {
void add(Object o);
Object get(int index);
}
List list = new ArrayList();
list.add(new TunableWhiteLight(2700));
list.add(new Fan()); // No compiler complaint!

// Runtime explosion! ClassCastException
TunableWhiteLight light = (TunableWhiteLight) list.get(1);

The compiler can't help you — errors hide until runtime

Java programmers demanded generic types

1990s protest scene: programmers demanding generics outside Sun Microsystems

Generics Catch Type Errors at Compile Time

/**
* A list
* @param <E> the type of elements in the list
*/
public interface List<E> {
void add(E element);
E get(int index);
}
List<Light> lights = new ArrayList<Light>();
lights.add(new TunableWhiteLight(2700)); // ✓ Light subtype
lights.add(new Fan()); // Compile-time error! ✗

Errors detected when you write them, not when customers find them

Type Inference Eliminates Redundant Declarations

// Explicit type parameters (verbose but clear)
List<Light> lights = new ArrayList<Light>();

// Java 7+: Diamond operator infers the type
List<Light> lights = new ArrayList<>();

// Works with complex types too
Map<String, List<IoTDevice>> devicesByRoom = new HashMap<>();

The compiler infers the type parameter from the variable declaration

Three Rules Keep Your Generic Code Safe

RuleWhy
Don't use raw typesType safety is the whole point
Eliminate unchecked warningsThey're errors in disguise
Favor generic types & methodsDesign for reuse

See Effective Java, Items 26, 27, 29, 30.

Generic Type Information Disappears at Runtime

List<Light> lights = new ArrayList<>();
List<Fan> fans = new ArrayList<>();

// At runtime, both are just "List" — type info is gone!
System.out.println(lights.getClass() == fans.getClass()); // true!

// This won't even compile:
// if (lights instanceof List<Light>) { } // Error!

// You can only check the raw type:
if (lights instanceof List) { } // This works, but less useful

Generic type information is erased at runtime — a backwards compatibility compromise from 2004

Collections Form an Inheritance Hierarchy

Lists Maintain Order and Support Index Access

List<Light> lights = new ArrayList<>();
lights.add(new DimmableLight("office", 100)); // Index 0
lights.add(new TunableWhiteLight("den", 2700)); // Index 1

// Direct access by index
Light first = lights.get(0);

// Insert at specific position
lights.add(0, new SwitchedLight("porch")); // Now at index 0

// Iterate in guaranteed order
for (Light light : lights) {
light.turnOn(); // porch, then office, then den
}

Key property: Elements maintain insertion order and are accessible by index

Arrays Are Fast but Fixed in Size

// Arrays are built into the language — fixed size, declared with []
Light[] lights = new Light[3]; // Create array of exactly 3 slots
lights[0] = new DimmableLight("office", 100);
lights[1] = new TunableWhiteLight("den", 2700);
lights[2] = new SwitchedLight("porch");

// Direct access by index — O(1)
Light first = lights[0];

// Iteration
for (Light light : lights) {
light.turnOn();
}

// But... what if we need a 4th light?
lights[3] = new Fan("oops"); // ArrayIndexOutOfBoundsException!

Problem: Size is fixed at creation — can't grow or shrink

ArrayList Trades Slight Overhead for Flexibility

Array (Light[])ArrayList (ArrayList<Light>)
SizeFixed at creationGrows automatically
Syntaxlights[i]lights.get(i)
Type safetyBuilt-inVia generics
PrimitivesYes (int[])No (need Integer)
Length.length (field).size() (method)
MethodsNone (just indexing)add, remove, contains...

Rule: Use ArrayList unless you need primitives or know the exact size

What's your understanding of Big-O notation?

A. I've never heard of it

B. I've heard of it but don't remember it

C. I know the basics

D. I've mastered it

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Absolute Time Doesn't Measure Algorithm Quality

Library triptych comparing O(1), O(n), and O(n²) search algorithms

Big-O Describes How Work Scales with Input Size

Big-O describes how an algorithm's time (or space) scales with input size n

NotationNameExample10 items1000 items
O(1)ConstantArray access by index1 step1 step
O(n)LinearSearch unsorted list10 steps1,000 steps
O(n²)QuadraticCompare all pairs100 steps1,000,000 steps

The "O" stands for "order of" — we care about the growth pattern, not exact counts

Big-O Patterns Appear in Everyday Tasks

O(1) — Constant

  • Looking up a word in a dictionary by page number
  • Getting the first item from a shelf
  • Checking if a light switch is on

O(n) — Linear

  • Finding a specific book on an unsorted shelf
  • Counting people in a room
  • Reading every line of a file

O(n²) — Quadratic

  • Comparing every student's answer to every other student's (plagiarism check)
  • Bubble sort

Big-O Described in Emojis

Meme humorously describing big-O notation

The Wrong Algorithm Fails at Scale

// O(1) — accessing element at index: instant at any size
Light light = lights.get(500000); // Same speed as get(0)

// O(n) — finding an element: slower as list grows
for (Light light : lights) {
if (light.getId().equals("target")) { /* found! */ }
}
// 1 million lights = up to 1 million comparisons

// O(n²) — nested loops: gets very slow, very fast
for (Light a : lights) {
for (Light b : lights) {
// Compare a to b
}
}
// 1000 lights = 1,000,000 comparisons
// 10000 lights = 100,000,000 comparisons!

ArrayList Is Just an Array with Bookkeeping

// Simplified view of ArrayList's internal structure
public class ArrayList<E> {
private Object[] elementData; // The backing array!
private int size; // How many elements we actually have

public ArrayList() {
this.elementData = new Object[10]; // Default capacity: 10
this.size = 0;
}

public E get(int index) {
return (E) elementData[index]; // O(1) — just array access
}

public boolean add(E element) {
if (size == elementData.length) {
grow(); // Array full — need to resize!
}
elementData[size] = element;
size++;
return true;
}
}

ArrayList is an array — it just manages resizing for you!

ArrayList Grows by Copying to a Larger Array

private void grow() {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // Grow by 50%
// >> is the right shift operator, which is equivalent to dividing by 2
// So oldCapacity + (oldCapacity >> 1) is equivalent to oldCapacity * 1.5
// but faster to compute because it's a bitwise operation

// Create a new, bigger array
Object[] newArray = new Object[newCapacity];

// Copy all elements to the new array
for (int i = 0; i < size; i++) {
newArray[i] = elementData[i];
}

// Switch to the new array
elementData = newArray;
// Old array becomes garbage — GC will clean it up
}

Before: [A][B][C][D] (capacity 4, full!)

After: [A][B][C][D][ ][ ] (capacity 6)

Classroom Analogy

Three-panel classroom metaphor showing ArrayList resizing by doubling capacity

Infrequent Resizes Average Out to O(1) Per Add

Operation #Array SizeResize?Elements Copied
1-1010No0
1110→15Yes10
12-1515No0
1615→22Yes15
17-2222No0
2322→33Yes22
............

After N adds: ~2N total copies → O(1) per add on average

Doubling strategy means resizes become increasingly rare

LinkedList: Use It Only for Queues

How it works:

  • Chain of nodes, each pointing to neighbors
  • O(1) add/remove at both ends
  • O(n) to access middle elements (must walk chain)

When to use it:

  • As a Queue (FIFO)
  • As a Deque (double-ended)
  • Almost never as a general List
// Good use of LinkedList: as a Queue
Queue<Task> taskQueue = new LinkedList<>();
taskQueue.offer(new Task("process")); // Add to end — O(1)
Task next = taskQueue.poll(); // Remove from front — O(1)

// For everything else, use ArrayList
List<Light> lights = new ArrayList<>(); // Default choice

Rule: Use ArrayList unless you specifically need queue behavior

HashSets Guarantee Uniqueness with O(1) Lookup

Set<String> deviceIds = new HashSet<>();
deviceIds.add("light-001");
deviceIds.add("fan-002");
deviceIds.add("light-001"); // Silently ignored — already present!

System.out.println(deviceIds.size()); // 2, not 3
System.out.println(deviceIds.contains("fan-002")); // true — O(1)!

// Iteration order is NOT guaranteed with HashSet
for (String id : deviceIds) {
System.out.println(id); // Could be either order
}

Key property: No duplicates; extremely fast contains() check

Maps Let You Find Values by Meaningful Keys

Map<String, IoTDevice> deviceRegistry = new HashMap<>();

// Store devices by a meaningful key
deviceRegistry.put("living-room-main", new DimmableLight("lr", 100));
deviceRegistry.put("bedroom-fan", new Fan("bf"));
deviceRegistry.put("living-room-main", new TunableWhiteLight("lr", 2700));
// ^ Replaces the previous value for this key!

// Retrieve by key — O(1)
IoTDevice device = deviceRegistry.get("living-room-main");
device.identify();

// Check before accessing
if (deviceRegistry.containsKey("kitchen-light")) {
deviceRegistry.get("kitchen-light").identify();
}

Poll: What should be used for a playlist?

A. ArrayList

B. LinkedList

C. Set

D. Map

E. I have no idea

Assume that songs can be played in order or referenced by number and that duplicates are allowed.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Answer: Data structure for playlist

Scenario: Store a playlist of songs in the order they should play

Use: List<Song> (ArrayList)

  • Order matters — songs play in sequence
  • May contain duplicates — same song can appear twice
  • Need indexed access — "skip to track 5"
List<Song> playlist = new ArrayList<>();
playlist.add(new Song("Bohemian Rhapsody"));
playlist.add(new Song("Stairway to Heaven"));
playlist.add(new Song("Bohemian Rhapsody")); // Play it again!
Song track5 = playlist.get(4);

Poll: What should be used for a voter list?

A. ArrayList

B. LinkedList/Queue

C. Set

D. Map

E. I have no idea

We need to track which users have voted to prevent double-voting.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Answer: Data Structure for Voter List

Scenario: Track which users have already voted (prevent double-voting)

Use: Set<String> (HashSet)

  • No duplicates — each user votes once
  • Order doesn't matter — just need "has this user voted?"
  • Fast contains() check — O(1)
Set<String> voterIds = new HashSet<>();

public boolean recordVote(String voterId, String choice) {
if (voterIds.contains(voterId)) {
return false; // Already voted!
}
voterIds.add(voterId);
// ... record the vote
return true;
}

Poll: What should be used for a student grade?

A. ArrayList

B. LinkedList/Queue

C. Set

D. Map

E. I have no idea

Given a student ID, we want to find their course average.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Answer: Data Structure for Student Grade

Scenario: Look up a student's average by student ID

Use: Map<String, Double> (HashMap)

  • Need to find values by a key (student ID)
  • Each student has exactly one grade
  • Fast lookup — O(1)
Map<String, Double> grades = new HashMap<>();
grades.put("12345", 92.5);
grades.put("67890", 87.0);

double grade = grades.get("12345"); // 92.5
grades.put("12345", 94.0); // Update after extra credit

Poll: What should be used for office hours requests?

A. ArrayList

B. LinkedList/Queue

C. Set

D. Map

E. I have no idea

We want to process office hour requests in the order they arrive.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Answer: Data Structure for Office Hours Requests

Scenario: Process office hour requests in the order they arrived

Use: Queue<Ticket> (LinkedList)

  • First-in, first-out (FIFO) order
  • Add at the end, remove from the front
  • Don't need random access
Queue<Ticket> supportQueue = new LinkedList<>();
supportQueue.offer(new Ticket("build problem"));
supportQueue.offer(new Ticket("debug help"));

Ticket next = supportQueue.poll(); // Gets "build problem"

Poll: What should be used to store devices by room?

A. ArrayList

B. LinkedList/Queue

C. Set

D. Map

E. I have no idea

Given a room, we want to retrieve all of the devices in it.

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Answer: Data Structures for Devices by Room

Scenario: Store all devices in each room of a smart home

Use: Map<String, List<IoTDevice>> or Use: Map<String, Set<IoTDevice>> Or

  • Look up by room name (key)
  • Each room has multiple devices (value is a List)
  • Nested generics — Map of String to List of IoTDevice
Map<String, List<IoTDevice>> devicesByRoom = new HashMap<>();
devicesByRoom.put("living-room", new ArrayList<>());
devicesByRoom.get("living-room").add(new DimmableLight("lr1", 100));
devicesByRoom.get("living-room").add(new Fan("lr-fan"));

// Turn off all devices in living room
for (IoTDevice device : devicesByRoom.get("living-room")) {
// ...
}

Match Your Data Structure to Your Access Pattern

NeedUseWhy
Ordered sequence, indexed accessArrayListO(1) random access
FIFO queueLinkedListO(1) add/remove at ends
Unique elements, fast lookupHashSetO(1) contains
Key-value lookupHashMapO(1) by key

Primitives Need Wrapper Classes for Collections

Retro factory scene showing primitives being boxed into wrapper objects with == trap

Java Automatically Boxes and Unboxes Primitives

PrimitiveWrapper
intInteger
doubleDouble
booleanBoolean
charCharacter
longLong
// Must use wrapper for generics
List<Integer> numbers = new ArrayList<>();

// Autoboxing: int → Integer
numbers.add(42);

// Autounboxing: Integer → int
int x = numbers.get(0);

// Can do arithmetic directly
numbers.set(0, numbers.get(0) + 1);

Never Use == to Compare Wrapper Objects

// Primitives: == compares values
int x = 1, y = 1;
System.out.println(x == y); // true ✓

// Wrappers: == compares object identity!
Integer a = 128, b = 128;
System.out.println(a == b); // false! 😱

Always use .equals() for object comparison, or prefer primitives

Wrapper Objects Add Overhead—Prefer Primitives

// Prefer this:
int count = 0;
for (int i = 0; i < 1000000; i++) {
count += i;
}

// Over this (much slower!):
Integer count = 0;
for (Integer i = 0; i < 1000000; i++) {
count += i; // Unbox, add, rebox... million times
}
  • Use primitives for local variables, loop counters, arithmetic
  • Use wrappers only when required (generics, nullability)
  • Be careful with == on wrappers

Streams Provide a Uniform Abstraction for All I/O

House cross-section showing Java I/O streams as plumbing with input/output pipes

Every Java Program Inherits Three Standard Streams

StreamPurposeType
System.inStandard input (keyboard)InputStream
System.outStandard output (console)PrintStream
System.errStandard error (console)PrintStream

Unix heritage from 1969 that every Java program inherits

System.out.println("Normal output");
System.err.println("Error message"); // Often shown in red

Stream Classes Use Inheritance for Flexibility

PrintStream Makes Writing Formatted Output Easy

// We've been doing this all along!
System.out.println("Hello, world!");

// println accepts any type — calls toString()
System.out.println(42);
System.out.println(new DimmableLight("test", 100));

// Write to a file instead of console
PrintStream fileOut = new PrintStream("output.txt");
fileOut.println("Written to file!");
fileOut.println("Line 2");
fileOut.close(); // Don't forget to close!

PrintStream wraps the complexity of byte-level I/O

Scanner Parses Formatted Input from Any Source

Scanner scanner = new Scanner(System.in);  // Wrap standard input

System.out.print("Enter device name: ");
String name = scanner.nextLine(); // Read entire line

System.out.print("Enter brightness (0-100): ");
int brightness = scanner.nextInt(); // Parse as integer

DimmableLight light = new DimmableLight(name, brightness);
System.out.println("Created: " + light);

scanner.close();

Scanner parses formatted input from any source

The Same Scanner API Works for Files or Console

Scanner fileScanner = new Scanner(new File("devices.txt"));

List<String> deviceNames = new ArrayList<>();
while (fileScanner.hasNextLine()) {
String line = fileScanner.nextLine();
deviceNames.add(line);
}

fileScanner.close(); // Always close when done!
System.out.println("Loaded " + deviceNames.size() + " devices");

Same methods, different source — polymorphism in action

Try-With-Resources Guarantees Streams Are Closed

The verbose way (pre-Java 7):

Scanner scanner = null;
try {
scanner = new Scanner(
new File("input.txt"));
while (scanner.hasNext()) {
System.out.println(
scanner.nextLine());
}
} catch (FileNotFoundException e) {
System.err.println("Error!");
} finally {
if (scanner != null) {
scanner.close();
}
}

The modern way (Java 7+):

try (Scanner scanner =
new Scanner(
new File("input.txt"))) {
while (scanner.hasNext()) {
System.out.println(
scanner.nextLine());
}
} catch (FileNotFoundException e) {
System.err.println("Error!");
}
// Automatically closed!

Combine Collections, I/O, and Parsing for Real Programs

public List<IoTDevice> loadDevices(String filename) {
List<IoTDevice> devices = new ArrayList<>();

try (Scanner scanner = new Scanner(new File(filename))) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
String[] parts = line.split(",");
String type = parts[0];
String id = parts[1];

if (type.equals("light")) {
int brightness = Integer.parseInt(parts[2]);
devices.add(new DimmableLight(id, brightness));
} else if (type.equals("fan")) {
devices.add(new Fan(id));
}
}
} catch (FileNotFoundException e) {
System.err.println("Could not load devices: " + e.getMessage());
}

return devices;
}

Commonly Confused Term: Overloading

Overloading is having multiple methods with the same name and different parameter types.

public class Vet {
public void treat(Animal a) { System.out.println("Treating animal"); }
public void treat(Dog d) { System.out.println("Treating dog"); }
}

Vet vet = new Vet();
Animal a = new Animal();
Animal d = new Dog(); // declared: Animal, runtime: Dog
vet.treat(a); // prints "Treating animal"
vet.treat(d); // prints "Treating animal" - uses declared type!

Methods are resolved at compile-time based on declared parameter types.

Commonly Confused Term: Overriding

Overriding is providing a new implementation of an inherited method in a subclass.

public class Animal {
public String speak() { return "..."; }
}
public class Dog extends Animal {
@Override
public String speak() { return "Woof!"; }
}

Animal a = new Animal();
Animal d = new Dog(); // declared: Animal, runtime: Dog
a.speak(); // returns "..."
d.speak(); // returns "Woof!" - uses runtime type

Methods are resolved at run-time based on the actual object type.

Summary

GenericsCompile-time type safety for collections
CollectionsList (ordered), Set (unique), Map (key-value)
Wrapper typesBridge primitives to the object world; beware ==
StreamsUniform abstraction for I/O; InputStream, OutputStream
ScannerParse formatted input from any source
try-with-resourcesAutomatic cleanup for Closeable resources

Next Steps

  • Complete flashcard set 3 (Collections & I/O)
  • Read: Core Java Volume I, Chapter 3 (Fundamental Programming Structures)
  • Assignment 1 due Thursday, January 15 at 11:59 PM