CS 3100: Program Design and Implementation II
Quiz Review 1

©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

How hard do you think it is?


Click below the faces if you haven't tried it yet.
Cover Sheet
Write your roster name and your NUID neatly for OCR.

Question 1: Write Once Run Anywhere
Which statement best describes how Java achieves "write once, run anywhere"?
- Java source code is directly interpreted by the operating system
- Java source code is compiled to bytecode, which runs on the JVM available for each platform
- Java programs must be recompiled for each target operating system
- 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

Java compilation/execution
Question 1 Answer
Which statement best describes how Java achieves "write once, run anywhere"?
- Java source code is directly interpreted by the operating system
- Java source code is compiled to bytecode, which runs on the JVM available for each platform
- Java programs must be recompiled for each target operating system
- Java uses only ahead-of-time compilation to native machine code
Java Evolution

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:
- Dynamic typing - the type is checked when the program runs
- Static typing - the type is checked at compile time before the program runs
- Duck typing - any object with similar methods would work
- No type checking occurs
Why This is Legal
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)

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:
- Dynamic typing - the type is checked when the program runs
- Static typing - the type is checked at compile time before the program runs
- Duck typing - any object with similar methods would work
- No type checking occurs
Question 3: Liskov Substitution Principle
In CookYourBooks, ExactQuantity, FractionalQuantity, and RangeQuantity all extend Quantity.
According to the Liskov Substitution Principle:
- Each subclass can implement toDecimal() to return any value it wants
- Each subclass must be usable anywhere a Quantity is expected without breaking the program
- Subclasses must have identical implementations of all methods
- 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

Question 3 Answer
In CookYourBooks, ExactQuantity, FractionalQuantity, and RangeQuantity all extend Quantity.
According to the Liskov Substitution Principle:
- Each subclass can implement toDecimal() to return any value it wants
- Each subclass must be usable anywhere a Quantity is expected without breaking the program
- Subclasses must have identical implementations of all methods
- 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?
- Abstract classes use less memory
- It has abstract methods like toDecimal() that subclasses must implement, and it should not be instantiated directly
- Java requires all parent classes to be abstract
- 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 Class | Concrete 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?
- Abstract classes use less memory
- It has abstract methods like toDecimal() that subclasses must implement, and it should not be instantiated directly
- Java requires all parent classes to be abstract
- It makes the class immutable
Abstract Classes (meme)

Question 5: Exceptions
What distinguishes checked exceptions (e.g., IOException) from unchecked exceptions (e.g., IllegalArgumentException)?
- Checked exceptions are faster to throw
- Checked exceptions must be declared in the method signature or caught; unchecked exceptions do not
- Unchecked exceptions cannot be caught
- Checked exceptions extend RuntimeException
Checked and Unchecked 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.

Question 5 Answer
What distinguishes checked exceptions (e.g., IOException) from unchecked exceptions (e.g., IllegalArgumentException)?
- Checked exceptions are faster to throw
- Checked exceptions must be declared in the method signature or caught; unchecked exceptions do not
- Unchecked exceptions cannot be caught
- 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");
- Nothing; this is a fine practice
- Without generics, the compiler can't prevent adding wrong types, leading to potential ClassCastException at runtime
- The program runs slower
- 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");
- Nothing; this is a fine practice
- Without generics, the compiler can't prevent adding wrong types, leading to potential ClassCastException at runtime
- The program runs slower
- ArrayList doesn't work without generics
Question 7 Hashing
In MeasuredIngredient.hashCode(), the name is converted to lowercase before hashing: getName().toLowerCase(). Why?
- Lowercase strings hash faster
- Since equals() compares names case-insensitively, hashCode() must also be case-insensitive so equal objects have equal hash codes
- Java requires lowercase in hashCode()
- 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?
- Lowercase strings hash faster
- Since equals() compares names case-insensitively, hashCode() must also be case-insensitive so equal objects have equal hash codes
- Java requires lowercase in hashCode()
- 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?
- Restrictiveness (rules out bad implementations)
- Generality (doesn't over-constrain the implementation)
- Efficiency (specifies performance requirements)
- Clarity (easy to understand)
The Vending Machine Contract

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?
- Restrictiveness (rules out bad implementations)
- Generality (doesn't over-constrain the implementation)
- Efficiency (specifies performance requirements)
- 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
*/
- Spec A, because it's more detailed
- Spec B, because it defines the result without mandating a specific algorithm
- They are equally general
- 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
*/
- Spec A, because it's more detailed
- Spec B, because it defines the result without mandating a specific algorithm
- They are equally general
- 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?
- The compiler enforces that both must be overridden together
- Equal objects must have equal hash codes for hash-based collections to work correctly
- hashCode() must call equals() internally to compute its value
- 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?
- The compiler enforces that both must be overridden together
- Equal objects must have equal hash codes for hash-based collections to work correctly
- hashCode() must call equals() internally to compute its value
- 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?
- true
- false
- A negative integer
- 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
| Comparable | Comparator | |
|---|---|---|
| Method | compareTo(T o) | compare(T o1, T o2) |
| Compares | this vs another object | Two separate objects |
| Where defined | Inside the class being compared | In a separate class |
| Orderings per class | One (the "natural" ordering) | Many (different comparators) |
| Example use | Collections.sort(list) | Collections.sort(list, comp) |
Question 11 Answer
What should x.compareTo(y) return if x is less than y?
- true
- false
- A negative integer
- Zero
Question 12: Lambdas
What is the primary advantage of using a lambda expression instead of an anonymous class for a functional interface?
- Lambdas are faster at runtime
- Lambdas are more concise and reduce boilerplate code
- Lambdas can implement multiple methods
- 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'.

Source: Why Name It That?: Boiler Plate
Question 12 Answer
- Lambdas are faster at runtime
- Lambdas are more concise and reduce boilerplate code
- Lambdas can implement multiple methods
- 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));
- A call to getBrightness() on the DimmableLight class itself
- A reference to the getBrightness method that will be called on each DimmableLight instance
- A static method that returns brightness values
- 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));
- A call to getBrightness() on the DimmableLight class itself
- A reference to the getBrightness method that will be called on each DimmableLight instance
- A static method that returns brightness values
- A constructor reference for creating DimmableLight objects
Question 14: Records
Which of the following is NOT automatically provided by a Java record?
- A constructor that initializes all fields
- equals(), hashCode(), and toString() implementations
- Mutable setter methods for each field
- 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 isfinal
Question 14 Answer
Which of the following is NOT automatically provided by a Java record?
- A constructor that initializes all fields
- equals(), hashCode(), and toString() implementations
- Mutable setter methods for each field
- 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?
- The method cannot be tested without constructing a full Submission object with a Student
- The method is slower than passing a String directly
- The method violates the Liskov Substitution Principle
- The method cannot handle multiple submissions
Stamp Coupling

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?
- The method cannot be tested without constructing a full Submission object with a Student
- The method is slower than passing a String directly
- The method violates the Liskov Substitution Principle
- 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?
- The class will run out of memory
- The class becomes a dumping ground that's hard to navigate, and changes to one method risk affecting unrelated functionality
- Java does not allow static utility methods
- 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?
- The class will run out of memory
- The class becomes a dumping ground that's hard to navigate, and changes to one method risk affecting unrelated functionality
- Java does not allow static utility methods
- The methods cannot be tested individually
Question 17: SOLID
Which SOLID principle states that "a class should have only one reason to change"?
- Open/Closed Principle
- Single Responsibility Principle
- Interface Segregation Principle
- 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"?
- Open/Closed Principle
- Single Responsibility Principle
- Interface Segregation Principle
- 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)?
- The wrapper approach is faster at runtime
- Behaviors can be combined flexibly at runtime without creating a subclass for every combination
- The wrapper approach uses less memory
- 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:
| compressed | buffered | Result |
|---|---|---|
| ✗ | ✗ | 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)?
- The wrapper approach is faster at runtime
- Behaviors can be combined flexibly at runtime without creating a subclass for every combination
- The wrapper approach uses less memory
- 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

Access Modifiers in Java
Every class, method, and field has an access modifier:
| Modifier | Visibility |
|---|---|
public | Accessible from anywhere |
protected | Accessible from package and subclasses |
| (default) | Package-private: same package only |
private | Accessible only within the class |
Rule: Minimize accessibility of classes and members.
Primitive Types vs Reference Types
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
| Primitive | Wrapper Class |
|---|---|
int | Integer |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
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:
- 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: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%");
}
}