8.5 Lab: Inheritance, Interfaces, and Polymorphism
Prerequisites: Inheritance, overriding, dynamic dispatch, abstract classes, and interfaces
Estimated time: 40 minutes
Practice environment: JDK 17+ and Bash; all files go in a temporary directory
You are ready to continue when: You can test subtype contracts with both successful and failing calls
chapter8_oop_root=$(mktemp -d)
readonly chapter8_oop_root
mkdir -p "$chapter8_oop_root/out" "$chapter8_oop_root/broken"
cd "$chapter8_oop_root"Warm-up 1: Animal Sounds and Polymorphism
Save as Animal.java:
abstract class Animal {
private final String name;
Animal(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("blank name");
}
this.name = name;
}
final String name() { return name; }
abstract String sound();
}
final class Dog extends Animal {
Dog(String name) { super(name); }
@Override String sound() { return "Woof"; }
}
final class Cat extends Animal {
Cat(String name) { super(name); }
@Override String sound() { return "Meow"; }
}
final class Zoo {
static String introduce(Animal animal) {
java.util.Objects.requireNonNull(animal, "animal");
return animal.name() + ": " + animal.sound();
}
}Test Zoo.introduce(new Dog("Blackie")) and Cat, and verify that blank names and null parameters fail by contract. Then add another animal, Zoo should not be modified.
Warm-up 2: Verify @Override and final
Save the intentionally failed minimal source code as BrokenOverride.java:
abstract class Animal {
abstract String sound();
}
class Dog extends Animal {
@Override String sounds() { return "Woof"; }
}The class lacks a sound implementation, and sounds overrides nothing. Compile it and confirm a failing exit status:
broken_override_status=0
javac --release 17 -encoding UTF-8 -d broken BrokenOverride.java \
> broken-override.log 2>&1 || broken_override_status=$?
test "$broken_override_status" -ne 0
sed -n '1,12p' broken-override.logMake the overridable method final to ensure that subclass overriding results in a compilation error. Failed examples must be verified by javac, rather than guessed from comments alone.
Challenge: A Payment Contract Using Integer Cents
Return a structured result so callers do not have to parse printed text. Save as PaymentMethod.java:
interface PaymentMethod {
PaymentResult pay(long cents);
}
final class PaymentResult {
private final boolean successful;
private final String reference;
private PaymentResult(boolean successful, String reference) {
this.successful = successful;
this.reference = reference;
}
static PaymentResult success(String reference) {
if (reference == null || reference.isBlank()) {
throw new IllegalArgumentException("blank reference");
}
return new PaymentResult(true, reference);
}
static PaymentResult declined() {
return new PaymentResult(false, "");
}
boolean successful() { return successful; }
String reference() { return reference; }
}Implement a test payment method that can spend only its available balance and save it as TestWallet.java:
final class TestWallet implements PaymentMethod {
private long balanceCents;
TestWallet(long balanceCents) {
if (balanceCents < 0) {
throw new IllegalArgumentException("negative balance");
}
this.balanceCents = balanceCents;
}
long balanceCents() { return balanceCents; }
@Override
public PaymentResult pay(long cents) {
if (cents <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
if (cents > balanceCents) {
return PaymentResult.declined();
}
balanceCents -= cents;
return PaymentResult.success("test-" + cents);
}
}
final class Checkout {
static PaymentResult charge(PaymentMethod method, long cents) {
return java.util.Objects.requireNonNull(method, "method").pay(cents);
}
}Test success, insufficient balance, zero, negative amounts, and a null payment method. Verify that failed calls leave the balance unchanged. The test-amount reference is only a demonstration label: equal payment amounts produce the same label, so it is not a unique transaction ID. Real payments require idempotency keys, timeouts, retry bounds, external failures, auditing, and concurrent transactions; polymorphism only replaces implementations and doesn't automatically solve these issues.
Boundary Exercise: Place pattern matching only where appropriate
static String describe(Object value) {
if (value == null) return "null";
if (value instanceof Animal animal) {
return "animal:" + animal.name();
}
if (value instanceof String text) {
return "text:" + text;
}
return value.getClass().getSimpleName();
}This code is suitable for logging or external input classification. If the core payment flow keeps adding instanceof branches for CreditCard, Wallet, Cash, the shared actions should be moved back into the PaymentMethod contract.
Object Methods Practice
Implement toString, equals, and hashCode for an immutable value type and verify:
- Reflexive, symmetric, transitive;
- Equal objects have the same hashCode;
- null is not equal;
- After being added to a HashSet, equal copies don't increase size.
While an object remains in a hash-based set or serves as a map key, do not change fields used by equals/hashCode. This rule still applies after you have learned how collections work.
Unified Test Entry
Save as Oop2Lab.java. The anonymous class supplies a one-off bird implementation. The Runnable arguments carry test actions; the following Lambda lesson explains () -> .... The entry point checks that assertions are enabled so omitting -ea cannot report a false pass:
public class Oop2Lab {
public static void main(String[] args) {
boolean assertionsEnabled = false;
assert assertionsEnabled = true;
if (!assertionsEnabled) {
throw new IllegalStateException("run with -ea");
}
assert Zoo.introduce(new Dog("Blackie")).equals("Blackie: Woof");
assert Zoo.introduce(new Cat("Snow")).equals("Snow: Meow");
expectIllegal(() -> new Dog(" "));
expectNull(() -> Zoo.introduce(null));
Animal bird = new Animal("Little Gray") {
@Override String sound() { return "Chirp"; }
};
assert Zoo.introduce(bird).equals("Little Gray: Chirp");
TestWallet wallet = new TestWallet(5_000);
PaymentResult paid = Checkout.charge(wallet, 2_000);
assert paid.successful();
assert paid.reference().equals("test-2000");
assert !Checkout.charge(wallet, 4_000).successful();
expectIllegal(() -> Checkout.charge(wallet, 0));
expectIllegal(() -> Checkout.charge(wallet, -1));
expectNull(() -> Checkout.charge(null, 100));
assert wallet.balanceCents() == 3_000;
assert describe(null).equals("null");
assert describe(new Dog("Blackie")).equals("animal:Blackie");
assert describe("notice").equals("text:notice");
System.out.println("inheritance lab passed");
}
static String describe(Object value) {
if (value == null) return "null";
if (value instanceof Animal animal) {
return "animal:" + animal.name();
}
if (value instanceof String text) {
return "text:" + text;
}
return value.getClass().getSimpleName();
}
private static void expectIllegal(Runnable action) {
try {
action.run();
throw new AssertionError("expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
// Parameter contract rejects input as expected.
}
}
private static void expectNull(Runnable action) {
try {
action.run();
throw new AssertionError("expected NullPointerException");
} catch (NullPointerException expected) {
// null Boundary failed as expected.
}
}
}javac --release 17 -encoding UTF-8 -Xlint:all,-auxiliaryclass -d out \
Animal.java PaymentMethod.java TestWallet.java Oop2Lab.java
java -ea -cp out Oop2LabThese teaching documents consolidate several package-level auxiliary types to control size, so the command excludes only auxiliaryclass lint generated by file layout, while all other warnings remain enabled. Expected output is inheritance lab passed. After saving the experiment log, clean up:
cd
ls -ld -- "$chapter8_oop_root"
rm -r -- "$chapter8_oop_root"Before leaving this unit
When adding animals other than Dog or Cat, Zoo should not be modified; incorrect @Override and final overrides must be verified by the compiler. Payment tests should be separately recorded for success, insufficient balance, invalid amount, and null method, with amounts using integer smallest units. Polymorphism can replace implementations but does not automatically provide idempotency, transactional safety, or thread safety.
After completing the object unit, pass the short behavior as a value.