Skip to content

VOL 01 / CH 08 / LESSON 05

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

bash
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:

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:

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:

bash
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.log

Make 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:

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:

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 ​

java
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:

java
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.
        }
    }
}
bash
javac --release 17 -encoding UTF-8 -Xlint:all,-auxiliaryclass -d out \
  Animal.java PaymentMethod.java TestWallet.java Oop2Lab.java
java -ea -cp out Oop2Lab

These 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:

bash
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.

→ Next Step: Lambda Basics

Built with VitePress | Software Systems Atlas