Skip to main content

CS 3100: Program Design and Implementation II

Quiz Review 1

User Friendly cartoon showing two people taking an exam at desks.
Sound effects for Student 1: 'doink doink doink doink doing'
Student 2: 'Hey...is that the sweet sound of a multiple-choice section I hear?'
Student 1: 'Nope...essay question you have to answer in binary

©2026 Ellen Spertus, CC-BY-SA

How much of the practice quiz did you do?

A. None yet

B. I've skimmed it

C. A few problems

D. Many problems

E. All problems

Poll Everywhere QR Code or Logo

How hard do you think it is?

Poll image
Poll Everywhere QR Code or Logo

Click below the faces if you haven't tried it yet.

Cover Sheet

Write your roster name and your NUID neatly for OCR.

Screenshot of cover page, with spaces to write name and NUID

Question 1: Write Once Run Anywhere

Which statement best describes how Java achieves "write once, run anywhere"?

  1. Java source code is directly interpreted by the operating system
  2. Java source code is compiled to bytecode, which runs on the JVM available for each platform
  3. Java programs must be recompiled for each target operating system
  4. Java uses only ahead-of-time compilation to native machine code

Different CPUs and OSes Require Different Code

  • CPUs have different instruction sets
  • OS's have different APIs
  • How do you run a program on any machine?

Bytecode Provides Platform Independence

Bytecode visualization

Java compilation/execution

Block diagram showing Java compilation-run process

Question 1 Answer

Which statement best describes how Java achieves "write once, run anywhere"?

  1. Java source code is directly interpreted by the operating system
  2. Java source code is compiled to bytecode, which runs on the JVM available for each platform
  3. Java programs must be recompiled for each target operating system
  4. Java uses only ahead-of-time compilation to native machine code

Java Evolution

Cartoon of Java as a coffee maker evolving from 1995 to today. A simple original machine labeled 'Write Once, Run Anywhere' appears ghosted in the corner. The modern version is covered with bolted-on features: Generics funnel, Lambda arrow pipes, Streams conveyor belt, Records module, Sealed Classes padlock, and Pattern Matching tubes. An old developer in a Sun shirt reminisces while a young developer adds String Templates. Banner reads: 'Still backward compatible. Still brewing. Still Java?'

How could new keywords be added to Java?

Why didn't adding keywords (record, sealed, etc.) break existing programs?

They are contextual keywords (or "restricted identifiers").

These words only have special meaning in specific syntactic contexts—so int sealed = 42; still compiles fine.

Question 2: Type Checking

Consider this code:

Quantity q = new ExactQuantity(2.5, Unit.CUP);

The compiler verifies that ExactQuantity is assignable to Quantity. This is an example of:

  1. Dynamic typing - the type is checked when the program runs
  2. Static typing - the type is checked at compile time before the program runs
  3. Duck typing - any object with similar methods would work
  4. No type checking occurs

Quantity q = new ExactQuantity(2.5, Unit.CUP);

Declaration uses abstract supertype.

Liskov Substitution Principle tells us assignment is legal.

Kinds of Type Checking

  • Static Typing (compile-time, Java) → Assignment is legal because ExactQuantity extends Quantity
  • Dynamic Typing (run-time)
    • Duck Typing is a specific kind of dynamic typing (Python)

Duck Typing vs Static Typing

Python (duck typing): Type checked at runtime

def make_sound(animal):
animal.quack() # No compile-time check—just tries it at runtime

make_sound(Duck()) # works because duck has quack()
make_sound(Person()) # works if Person has quack(), crashes otherwise

Java (static typing): Type checked at compile time

void makeSound(Quackable animal) {
animal.quack(); // Compiler verifies animal implements Quackable
}

"If it walks like a duck and quacks like a duck, it's a duck."

Duck Typing (meme)

Meme captioned 'duck typing' in which a person is connecting a plug to a pig's snout

Question 2 Answer

Consider this code:

Quantity q = new ExactQuantity(2.5, Unit.CUP);

The compiler verifies that ExactQuantity is assignable to Quantity. This is an example of:

  1. Dynamic typing - the type is checked when the program runs
  2. Static typing - the type is checked at compile time before the program runs
  3. Duck typing - any object with similar methods would work
  4. No type checking occurs

Question 3: Liskov Substitution Principle

In CookYourBooks, ExactQuantity, FractionalQuantity, and RangeQuantity all extend Quantity.

According to the Liskov Substitution Principle:

  1. Each subclass can implement toDecimal() to return any value it wants
  2. Each subclass must be usable anywhere a Quantity is expected without breaking the program
  3. Subclasses must have identical implementations of all methods
  4. Only ExactQuantity can be used where Quantity is expected

UML

In CookYourBooks, ExactQuantity, FractionalQuantity, and RangeQuantity all extend Quantity.

What does the Liskov Substitution Principle tell us?

An instance of the subtype can be passed where an instance of the supertype is expected.

Superman is Above the Submarine

Cartoon image showing Superman in the sky and a submarine under water

Question 3 Answer

In CookYourBooks, ExactQuantity, FractionalQuantity, and RangeQuantity all extend Quantity.

According to the Liskov Substitution Principle:

  1. Each subclass can implement toDecimal() to return any value it wants
  2. Each subclass must be usable anywhere a Quantity is expected without breaking the program
  3. Subclasses must have identical implementations of all methods
  4. Only ExactQuantity can be used where Quantity is expected

Question 4: Abstract Classes

Why is Quantity declared as an abstract class rather than a regular class?

  1. Abstract classes use less memory
  2. It has abstract methods like toDecimal() that subclasses must implement, and it should not be instantiated directly
  3. Java requires all parent classes to be abstract
  4. It makes the class immutable

A Closer Look at Quantity

public abstract class Quantity {
public Quantity(Unit unit) { ... }

public abstract double toDecimal();

@Override public String toString() { ... }
}

Abstract vs Concrete Classes

Abstract ClassConcrete Class
Can contain concrete methods
Can contain abstract methods
Can be instantiated
Can be extended

Question 4 Answer

Why is Quantity declared as an abstract class rather than a regular class?

  1. Abstract classes use less memory
  2. It has abstract methods like toDecimal() that subclasses must implement, and it should not be instantiated directly
  3. Java requires all parent classes to be abstract
  4. It makes the class immutable

Abstract Classes (meme)

A meme from Family Guy showing Noah on the ark looking confused at two mixed-up animal pairs. A penguin stands with an elephant labeled 'Abstract Classes' while another elephant stands with a penguin labeled 'Interfaces'. A third label reads 'Inheritance'. Noah gestures in confusion with the caption 'What the hell is this?'

Question 5: Exceptions

What distinguishes checked exceptions (e.g., IOException) from unchecked exceptions (e.g., IllegalArgumentException)?

  1. Checked exceptions are faster to throw
  2. Checked exceptions must be declared in the method signature or caught; unchecked exceptions do not
  3. Unchecked exceptions cannot be caught
  4. Checked exceptions extend RuntimeException

Checked and Unchecked Exceptions

Airport customs: green channel for unchecked exceptions, red channel for checked exceptions

Java's Exception Hierarchy Distinguishes Error Types

All exceptions extend Throwable

Subclasses of Error and RuntimeException are unchecked.

Parameter Validation

  • Validate constructor and method parameters as early as possible
  • Throw IllegalArgumentException
  • Document requirements and exceptions in Javadoc
  /**
* Creates an ingredient with the given name, preparation, and notes.
*
* @param name the ingredient name (must not be null or blank)
* @param preparation the preparation instructions (may be null)
* @param notes additional notes (may be null)
* @throws IllegalArgumentException if name is blank (empty or whitespace-only)
*/
protected Ingredient(@NonNull String name, @Nullable String preparation, @Nullable String notes) {
if (name.isBlank()) {
throw new IllegalArgumentException("Name must not be blank.");
}
...
}

Two Ways to Handle Checked Exceptions

Option 1: Catch it

public void processFile(String path) {
try {
String data = readFile(path);
// use data
} catch (IOException e) {
System.err.println("Could not read file: "
+ e.getMessage());
}
}

Handle the problem here.

Option 2: Propagate it

public void processFile(String path) throws IOException {
String data = readFile(path);
// use data
}

Let the caller handle it.

Two-panel storybook illustration titled 'Babysitter Exception Handling'. Left panel labeled 'The Good Babysitter (Catch It)' shows a calm babysitter in a rocking chair holding a content baby in a peaceful nursery at night. A code snippet reads 'try { baby.putToBed(); } catch (CryingBabyException e) { baby.soothe(); baby.singLullaby(); }'. Speech bubble says 'I got this.' Caption: 'Handles the problem. Parents sleep peacefully.' Right panel labeled 'The Bad Babysitter (Declare It)' shows a frazzled babysitter handing a crying baby to exhausted parents in pajamas. Speech bubble says 'Above my pay grade.' Caption: 'Propagates the problem. Parents deal with it.' Bottom banner reads 'Checked exceptions: Handle them or declare them. Someone has to deal with it eventually.'

Question 5 Answer

What distinguishes checked exceptions (e.g., IOException) from unchecked exceptions (e.g., IllegalArgumentException)?

  1. Checked exceptions are faster to throw
  2. Checked exceptions must be declared in the method signature or caught; unchecked exceptions do not
  3. Unchecked exceptions cannot be caught
  4. Checked exceptions extend RuntimeException

Question 6: Raw Types

If CookYourBooks stored ingredients in a List without generics, what problem (if any) would occur in compiling or running the following code?

List ingredients = new ArrayList();
ingredients.add(new MeasuredIngredient("flour", ...));
ingredients.add("A pinch of salt");
  1. Nothing; this is a fine practice
  2. Without generics, the compiler can't prevent adding wrong types, leading to potential ClassCastException at runtime
  3. The program runs slower
  4. ArrayList doesn't work without generics

Raw Types vs Generics

Raw Types (avoid)

List ingredients = new ArrayList();
ingredients.add(new MeasuredIngredient("flour", ...));
ingredients.add("A pinch of salt"); // Compiles!

// Later...
for (Object item : ingredients) {
Ingredient ingred = (Ingredient) item;
// ClassCastException on the String!
System.out.println(ingred.getPreparation());
}

Error happens at run-time! 🤬

With Generics (preferred)

List<MeasuredIngredient> ingredients = new ArrayList<>();
ingredients.add(new MeasuredIngredient("flour", ...));
ingredients.add("A pinch of salt"); // Compile error!

// Later...
for (Ingredient ingred: ingredients) {
// No cast needed, type is guaranteed
System.out.println(ingred.getPreparation());
}

No run-time errors! 🙂

Question 6 Answer

If CookYourBooks stored ingredients in a List without generics, what problem (if any) would occur in compiling or running the following code?

List ingredients = new ArrayList();
ingredients.add(new MeasuredIngredient("flour", ...));
ingredients.add("A pinch of salt");
  1. Nothing; this is a fine practice
  2. Without generics, the compiler can't prevent adding wrong types, leading to potential ClassCastException at runtime
  3. The program runs slower
  4. ArrayList doesn't work without generics

Question 7 Hashing

In MeasuredIngredient.hashCode(), the name is converted to lowercase before hashing: getName().toLowerCase(). Why?

  1. Lowercase strings hash faster
  2. Since equals() compares names case-insensitively, hashCode() must also be case-insensitive so equal objects have equal hash codes
  3. Java requires lowercase in hashCode()
  4. It's just a style preference

MeasuredIngredient

public class MeasuredIngredient extends Ingredient {

/**
* Compares this measured ingredient with the specified object for equality.
*
* <p>Two MeasuredIngredient objects are equal if they have the same
* name (case-insensitive), quantity, preparation, and notes.
*/
@Override
public boolean equals(@Nullable Object o) {
// null-checking and type-conversion omitted
return this.getName().equalsIgnoreCase(that.getName())
&& Objects.equals(this.quantity, that.quantity)
&& Objects.equals(this.getPreparation(), that.getPreparation())
&& Objects.equals(this.getNotes(), that.getNotes());
}

@Override
public int hashCode() {
return Objects.hash(getName().toLowerCase(Locale.ROOT),
quantity, getPreparation(), getNotes());
}
}

What if hashCode() Did Not Convert to Lower-Case?

MeasuredIngredient flour1 = new MeasuredIngredient("Flour", ...);
MeasuredIngredient flour2 = new MeasuredIngredient("flour", ...);

flour1.equals(flour2); // true (case-insensitive comparison)
flour1.hashCode(); // "Flour".hashCode() → 67847617
flour2.hashCode(); // "flour".hashCode() → 97526364 ← Different!
Set<MeasuredIngredient> pantry = new HashSet<>();
pantry.add(flour1); // Goes into bucket based on 67847617
pantry.contains(flour2); // Looks in bucket based on 97526364
// → Returns false! (Wrong bucket)

The rule: If a.equals(b), then a.hashCode() == b.hashCode()

Question 7 Answer

In MeasuredIngredient.hashCode(), the name is converted to lowercase before hashing: getName().toLowerCase(). Why?

  1. Lowercase strings hash faster
  2. Since equals() compares names case-insensitively, hashCode() must also be case-insensitive so equal objects have equal hash codes
  3. Java requires lowercase in hashCode()
  4. It's just a style preference

Question 8: Criteria for Specifications

Which of the following is NOT one of the three criteria for evaluating a good specification?

  1. Restrictiveness (rules out bad implementations)
  2. Generality (doesn't over-constrain the implementation)
  3. Efficiency (specifies performance requirements)
  4. Clarity (easy to understand)

The Vending Machine Contract

A pixel art illustration titled "The Vending Machine Contract" showing three vending machines with identical interfaces (Chips $1.00, Soda $1.50, Candy $0.75) but different internal mechanisms: one uses gravity and ramps, one uses pneumatic tubes with steam gauges, and one uses hamsters running on wheels and conveyor belts. All three successfully deliver the correct products. In the corner, a rejected fourth machine lies broken in a trash heap—it promised chips but dispensed a boot, violating the contract. The caption reads "Same Promise. Many Machines." illustrating how multiple implementations can satisfy the same specification, but violations are rejected.

What about performance?

Specifications generally describe what a system does, rather than how it does it.

There are hard real-time systems that do have performance requirements, such as airbag deployment, antilock braking systems, pacemakers, and industrial robots.

There are also soft real-time systems, such as GPS, videogames, and music synthesis.

This class is not about real-time systems.

Question 8 Answer

Which of the following is NOT one of the three criteria for evaluating a good specification?

  1. Restrictiveness (rules out bad implementations)
  2. Generality (doesn't over-constrain the implementation)
  3. Efficiency (specifies performance requirements)
  4. Clarity (easy to understand)

Question 9: Specification Generality

Which specification for a reverse(String s) method is more general?

Spec A:

/**
* Creates a char[] from the string `s`,
* iterates from the last character to the
* first, copying each to the array, then
* returns a new string from the array.
* @param s a non-null string
*/

Spec B:

/**
* Returns a new string with the characters
* of s in reverse order.
* @param s a non-null string
*/
  1. Spec A, because it's more detailed
  2. Spec B, because it defines the result without mandating a specific algorithm
  3. They are equally general
  4. Spec B, because it handles edge cases that Spec A doesn't

Specification Criteria

  • A spec is restrictive if it rules out unacceptable implementations.

  • A spec is general if does not rule out correct implementations.

  • A spec is clear if readers can understand it correctly.

Question 9 Answer

Which specification for a reverse(String s) method is more general?

Spec A:

/**
* Creates a char[] from the string `s`,
* iterates from the last character to the
* first, copying each to the array, then
* returns a new string from the array.
* @param s a non-null string
*/

Spec B:

/**
* Returns a new string with the characters
* of s in reverse order.
* @param s a non-null string
*/
  1. Spec A, because it's more detailed
  2. Spec B, because it defines the result without mandating a specific algorithm
  3. They are equally general
  4. Spec B, because it handles edge cases that Spec A doesn't

Question 10: Overriding equals() and hashCode()

MeasuredIngredient overrides both equals() and hashCode(). Why is it important to override both together?

  1. The compiler enforces that both must be overridden together
  2. Equal objects must have equal hash codes for hash-based collections to work correctly
  3. hashCode() must call equals() internally to compute its value
  4. Overriding only equals() causes a compilation error in classes that use generics

Question 10 Answer

MeasuredIngredient overrides both equals() and hashCode(). Why is it important to override both together?

  1. The compiler enforces that both must be overridden together
  2. Equal objects must have equal hash codes for hash-based collections to work correctly
  3. hashCode() must call equals() internally to compute its value
  4. Overriding only equals() causes a compilation error in classes that use generics

Question 11: Comparable

What should x.compareTo(y) return if x is less than y?

  1. true
  2. false
  3. A negative integer
  4. Zero

The Comparable Interface

public interface Comparable<T> {
/**
* Compares this object with the
* specified object for order.
*
* @return negative if this < o,
* zero if this == o,
* positive if this > o
*/
int compareTo(T o);
}

The Comparator Interface

public interface Comparator<T> {
/**
* Compares two objects for order.
*
* @return negative if o1 < o2,
* zero if o1 == o2,
* positive if o1 > o2
*/
int compare(T o1, T o2);
}

Comparable vs Comparator

ComparableComparator
MethodcompareTo(T o)compare(T o1, T o2)
Comparesthis vs another objectTwo separate objects
Where definedInside the class being comparedIn a separate class
Orderings per classOne (the "natural" ordering)Many (different comparators)
Example useCollections.sort(list)Collections.sort(list, comp)

Question 11 Answer

What should x.compareTo(y) return if x is less than y?

  1. true
  2. false
  3. A negative integer
  4. Zero

Question 12: Lambdas

What is the primary advantage of using a lambda expression instead of an anonymous class for a functional interface?

  1. Lambdas are faster at runtime
  2. Lambdas are more concise and reduce boilerplate code
  3. Lambdas can implement multiple methods
  4. Lambdas can access private fields that anonymous classes cannot

Java Supports Named and Anonymous Classes

// Named class
class BrightnessComparator implements Comparator<DimmableLight> {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
}

// Usage:
lights.sort(new BrightnessComparator());
lights.sort(
// Anonymous class
new Comparator<DimmableLight>() {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
});

This still has a lot of boilerplate code.

Lambdas Remove the Boilerplate

lights.sort(
// Anonymous class
new Comparator<DimmableLight>() {
@Override
public int compare(DimmableLight l1, DimmableLight l2) {
return Integer.compare(l1.getBrightness(), l2.getBrightness());
}
});
lights.sort((l1, l2) -> Integer.compare(l1.getBrightness(), l2.getBrightness()));

UML

Terminology: Boilerplate

In the field of printing, the term dates back to the early 1900s. Starting in the late 1800s, printing plates of text that were going to be used over and over, such as advertisements or syndicated columns, started being stamped in steel instead of the much softer and less durable lead. They came to be known as 'boilerplates'.

metal plate with reversed letters

Source: Why Name It That?: Boiler Plate

Question 12 Answer

  1. Lambdas are faster at runtime
  2. Lambdas are more concise and reduce boilerplate code
  3. Lambdas can implement multiple methods
  4. Lambdas can access private fields that anonymous classes cannot

Question 13: Method References

What does the following method reference represent?

lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));
  1. A call to getBrightness() on the DimmableLight class itself
  2. A reference to the getBrightness method that will be called on each DimmableLight instance
  3. A static method that returns brightness values
  4. A constructor reference for creating DimmableLight objects

Three Ways to Sort by Brightness

Lambda expression

lights.sort((a, b) -> Integer.compare(a.getBrightness(), b.getBrightness()));

Using comparingInt with a lambda

lights.sort(Comparator.comparingInt(light -> light.getBrightness()));

Using comparingInt with a method reference

lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));

Question 13 Answer

What does the following method reference represent?

lights.sort(Comparator.comparingInt(DimmableLight::getBrightness));
  1. A call to getBrightness() on the DimmableLight class itself
  2. A reference to the getBrightness method that will be called on each DimmableLight instance
  3. A static method that returns brightness values
  4. A constructor reference for creating DimmableLight objects

Question 14: Records

Which of the following is NOT automatically provided by a Java record?

  1. A constructor that initializes all fields
  2. equals(), hashCode(), and toString() implementations
  3. Mutable setter methods for each field
  4. Accessor methods for each field

Records: Before and After

public final class Point {           // final = can't extend
private final int x; // final = immutable
private final int y;

public Point(int x, int y) { this.x = x; this.y = y; }

public int x() { return x; } // accessors
public int y() { return y; }

@Override public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
if (!(obj instanceof Point other)) return false;
return x == other.x && y == other.y;
}

@Override public int hashCode() { return Objects.hash(x, y); }

@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
public record Point(int x, int y) {}

Records (Java 16, 2021): Data Classes Without Boilerplate

A record is all 30 lines in one:

public record Point(int x, int y) {}

You get automatically:

  • Constructor: new Point(1, 2)
  • Accessors: point.x(), point.y()
  • Correct equals, hashCode, toString
  • Immutability: all fields are final, class is final

Question 14 Answer

Which of the following is NOT automatically provided by a Java record?

  1. A constructor that initializes all fields
  2. equals(), hashCode(), and toString() implementations
  3. Mutable setter methods for each field
  4. Accessor methods for each field

Question 15: Coupling

Consider this method signature for code that compiles and runs correctly:

void notifyStudent(Submission submission) {
sendEmail(submission.student.email, "Your submission was received");
}

The method only uses submission.student.email. What is the main problem with this design?

  1. The method cannot be tested without constructing a full Submission object with a Student
  2. The method is slower than passing a String directly
  3. The method violates the Liskov Substitution Principle
  4. The method cannot handle multiple submissions

Stamp Coupling

A wooden rubber stamp with a red rubber base showing a complex Student data structure with many fields including name, email, address, street, city, state, zip, enrollmentDate, GPA, courses array, advisor, projects, and achievements. The stamp has been pressed onto cream paper, leaving a red ink impression of the entire data structure. A handwritten note with an arrow points to just the email field saying 'Only needed this part.' Natural window light illuminates the scene.

Question 15: Answer

Consider this method signature for code that compiles and runs correctly:

void notifyStudent(Submission submission) {
sendEmail(submission.student.email, "Your submission was received");
}

The method only uses submission.student.email. What is the main problem with this design?

  1. The method cannot be tested without constructing a full Submission object with a Student
  2. The method is slower than passing a String directly
  3. The method violates the Liskov Substitution Principle
  4. The method cannot handle multiple submissions

Question 16: Utility Classes

A Utility class contains unrelated methods like formatDate(), calculateTax(), and validateEmail(). What is the main problem with this design?

  1. The class will run out of memory
  2. The class becomes a dumping ground that's hard to navigate, and changes to one method risk affecting unrelated functionality
  3. Java does not allow static utility methods
  4. The methods cannot be tested individually

Static Utility Classes

Static utility classes contain static helper methods and cannot be instantiated.

final public class Utility { // prevent subclassing

private Utility() {} // prevent instantiation

public static String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"));
}

public static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}

public static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}

Question 16 Answer

A Utility class contains unrelated methods like formatDate(), calculateTax(), and validateEmail(). What is the main problem with this design?

  1. The class will run out of memory
  2. The class becomes a dumping ground that's hard to navigate, and changes to one method risk affecting unrelated functionality
  3. Java does not allow static utility methods
  4. The methods cannot be tested individually

Question 17: SOLID

Which SOLID principle states that "a class should have only one reason to change"?

  1. Open/Closed Principle
  2. Single Responsibility Principle
  3. Interface Segregation Principle
  4. Dependency Inversion Principle

SOLID guidelines

  • Single Responsibility Principle: A class should have only one reason to change.
  • Open/Closed Principle: Software entities should be open for extension and closed for modification.
  • Liskov Substitution Principle: Objects of a supertype should be replaceable with objects of its subtypes without breaking the application.
  • Interface Segregation Principle: Clients should not be forced to depend on interfaces they don't use.
  • Dependency Inversion Principle: Depend on abstractions, not on concretions.

Question 17 Answer

Which SOLID principle states that "a class should have only one reason to change"?

  1. Open/Closed Principle
  2. Single Responsibility Principle
  3. Interface Segregation Principle
  4. Dependency Inversion Principle

Question 18: Decorator Pattern

Consider Java's I/O stream design:

InputStream in = new BufferedInputStream(new FileInputStream("data.txt"));

BufferedInputStream wraps FileInputStream, adding buffering without modifying FileInputStream.

What is the main advantage of this design over using inheritance (e.g., class BufferedFileInputStream extends FileInputStream)?

  1. The wrapper approach is faster at runtime
  2. Behaviors can be combined flexibly at runtime without creating a subclass for every combination
  3. The wrapper approach uses less memory
  4. Inheritance is not allowed for stream classes in Java

Visual Decorator

InputStream Decorators

BufferedInputStream — adds buffering for efficiency

new BufferedInputStream(new FileInputStream("data.txt"));

DataInputStream — reads primitive types (int, double, etc.)

new DataInputStream(new FileInputStream("data.bin"));

GZIPInputStream — decompresses gzip data

new GZIPInputStream(new FileInputStream("data.gz"));

Combining decorators:

new DataInputStream(new BufferedInputStream(new GZIPInputStream(
new FileInputStream("data.gz"))));

InputStream Decorator Pattern

Dynamic Decorator Combinations

public InputStream openFile(String path, boolean compressed, boolean buffered) {
InputStream in = new FileInputStream(path);

if (compressed) {
in = new GZIPInputStream(in);
}

if (buffered) {
in = new BufferedInputStream(in);
}

return in;
}

Possible combinations at runtime:

compressedbufferedResult
FileInputStream
BufferedInputStream(FileInputStream)
GZIPInputStream(FileInputStream)
BufferedInputStream(GZIPInputStream(FileInputStream))

With inheritance, we would need a separate class for each combination!

Question 18 Answer

Consider Java's I/O stream design:

InputStream in = new BufferedInputStream(new FileInputStream("data.txt"));

BufferedInputStream wraps FileInputStream, adding buffering without modifying FileInputStream.

What is the main advantage of this design over using inheritance (e.g., class BufferedFileInputStream extends FileInputStream)?

  1. The wrapper approach is faster at runtime
  2. Behaviors can be combined flexibly at runtime without creating a subclass for every combination
  3. The wrapper approach uses less memory
  4. Inheritance is not allowed for stream classes in Java

Other topics

  • The Four Pillars of OOP
  • Access Modifiers in Java
  • Java Wrapper Classes
  • Dynamic Dispatch
  • Fragile Base Classes
  • Strategy Pattern
  • Inheritance and Overriding

The Four Pillars of OOP

The four pillars of OOP: Abstraction, Encapsulation, Inheritance, and Polymorphism

Access Modifiers in Java

Every class, method, and field has an access modifier:

ModifierVisibility
publicAccessible from anywhere
protectedAccessible from package and subclasses
(default)Package-private: same package only
privateAccessible only within the class

Rule: Minimize accessibility of classes and members.

Primitive Types vs Reference Types

Primitive Types

  • int, double, boolean, char
  • Store actual values
  • No methods
  • == compares values
  • Cannot use in generics

Reference Types

  • Integer, String, ArrayList, Object
  • Store memory addresses
  • Have methods
  • == compares references (use .equals())
  • Required for generics

Auto-boxing & Auto-unboxing

Java automatically converts between primitive and wrapper types:

// Auto-boxing: primitive → wrapper
int primitive = 42;
Integer wrapped = primitive; // int → Integer

// Auto-unboxing: wrapper → primitive
Integer obj = 100;
int value = obj; // Integer → int

// Works in method calls too!
ArrayList<Integer> nums = new ArrayList<>();
nums.add(5); // auto-boxes int to Integer
int first = nums.get(0); // auto-unboxes Integer to int

⚠️ Performance concern: Auto-boxing in tight loops creates new wrapper objects each time.

Java is Always Pass by Value

Primitives: Copy the value

void change(int x) {
x = 100; // local copy only
}

int num = 5;
change(num);
// num is still 5

References: Copy the reference

void cutPower(Light light) {
light.turnOff(); // modifies object
}

Light light = new DimmableLight();
light.turnOn(); // light is now on
cutPower(light);
// light is now off

Key insight: The reference itself is copied, but it points to the same object.

  • You can't change which object the caller's reference points to
  • You can modify the object's state

Wrapper Classes Summary

PrimitiveWrapper Class
intInteger
floatFloat
doubleDouble
charCharacter
booleanBoolean

When to use each:

  • Primitives: Performance-critical code, local variables, simple calculations
  • Wrapper classes: Collections, generics, when you need null, when you need methods

Dynamic Dispatch

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:

  1. If T declares m, use that
  2. Otherwise, check T's superclass recursively
  3. If still not found, use default interface method (if any)

Runtime type matters, not compile-time type!

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!

Fragile Base Classes

Imagine we want a Set that counts how many items are passed to add() and addAll():

public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;

@Override
public boolean add(E e) {
addCount++;
return super.add(e); // return true if added
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() { return addCount; }
}

What does getAddCount() return after s.addAll(Arrays.asList("A", "B", "C"))?

Composition Delegates Without Depending on Implementation

public class InstrumentedSet<E> implements Set<E> {
private final Set<E> s; // Composition: HAS-A Set
private int addCount = 0;

public InstrumentedSet(Set<E> s) {
this.s = s;
}

@Override
public boolean add(E e) {
addCount++;
return s.add(e); // Delegate to wrapped set
}

@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c); // Delegate - don't care how it's implemented!
}

public int getAddCount() { return addCount; }

// Forward all other Set methods to s...
@Override public int size() { return s.size(); }
@Override public boolean isEmpty() { return s.isEmpty(); }
// ... etc ...
}

HashSet Class Hierarchy

Composition Keeps Method Calls Inside the Wrapped Object

Inheritance: count = 6 ❌

Composition: count = 3 ✓

Strategy Pattern

Each strategy class has functional cohesion—one language, one job. Adding TypeScript? Just add a new class.

Inheritance and Overriding

1. Inherit As-Is

class DimmableLight  extends Light {
// inherits turnOn()
}

2. Override Completely

class DimmableLight extends Light {
@Override
void turnOn() {
System.out.println( "dimmed on");
}
}

3. Override + Extend

class DimmableLight extends Light {
@Override
void turnOn() {
super.turnOn(); // call superclass method
System.out.println("set to 50%");
}
}