Skip to content

Inheritance — Practice Tasks

Twelve exercises covering hierarchy design, override correctness, LSP, sealed types, and dispatch behavior.


Task 1 — Predict the dispatch

class A { void m() { System.out.println("A"); } }
class B extends A { @Override void m() { System.out.println("B"); } }
class C extends B { /* no override */ }
class D extends C { @Override void m() { System.out.println("D"); } }

A a = new D();
a.m();    // ?

Predict the output, then run.


Task 2 — Field hiding vs method override

class Animal {
    int legs = 4;
    int legs() { return legs; }
}
class Spider extends Animal {
    int legs = 8;
    @Override int legs() { return legs; }
}

Animal a = new Spider();
System.out.println(a.legs);   // ?
System.out.println(a.legs()); // ?

Predict, then explain why one is 4 and the other is 8.


Task 3 — LSP violation

Write a Bird class with method fly(). Add a subclass Penguin extends Bird. Discuss: how does this violate LSP? Refactor the hierarchy so it doesn't.

(Hint: not all birds fly. Maybe Bird shouldn't have fly(). Maybe there's a Flyer capability.)


Task 4 — Sealed expression hierarchy

Define a sealed interface Expr with permitted record subtypes Num(int v), Add(Expr l, Expr r), Mul(Expr l, Expr r). Implement int eval(Expr e) using exhaustive pattern-matching switch.

Then add Sub(Expr l, Expr r) to permits. Run eval — what error do you get? Fix the switch.


Task 5 — Covariant return

class Cell {
    Object get() { return null; }
}
class IntCell extends Cell {
    @Override ??? get() { return 42; }
}

Fill in the return type so the override is covariant. Verify with javap that a bridge method was generated.


Task 6 — Override or overload?

Predict which of the following are overrides, which are overloads, which are compile errors.

class A {
    void m(int x) { }
    void m(long x) { }
    int n(int x) { return 0; }
}
class B extends A {
    @Override void m(int x) { }     // a
    void m(int x, int y) { }        // b
    @Override double n(int x) { }   // c
    int n(long x) { return 0; }     // d
}

Identify each as override / overload / error and explain why.


Task 7 — Constructor chaining puzzle

class A {
    A() { System.out.println("A()"); }
    A(int x) { System.out.println("A(int)"); }
}
class B extends A {
    B() { this(0); System.out.println("B()"); }
    B(int x) { super(x); System.out.println("B(int)"); }
}

new B();

Predict the output. Then change super(x) to super(). Predict again.


Task 8 — Composition via delegation

You have a Stack<E> that incorrectly extends ArrayList<E>. Refactor it to contain an ArrayList<E> instead. Ensure: - Only push, pop, peek, size, isEmpty are public. - ArrayList mutators are not exposed. - Iteration still works.


Task 9 — protected access surprise

This compiles in package alpha, but fails in package beta. Why?

// alpha/A.java
package alpha;
public class A {
    protected void m() { }
}

// beta/B.java
package beta;
import alpha.A;
public class B extends A {
    void test(A other) {
        other.m();   // ERROR
    }
}

What is the rule? Find a way to make it work.


Task 10 — Diamond with default methods

Define interfaces:

interface X { default String hello() { return "X"; } }
interface Y { default String hello() { return "Y"; } }

Make a class Z implements X, Y. What does new Z().hello() return? (Hint: it doesn't compile. Fix it.)


Task 11 — Visitor refactored to sealed

A toy AST has the visitor pattern:

interface Expr { <R> R accept(ExprVisitor<R> v); }
interface ExprVisitor<R> {
    R num(NumExpr n);
    R add(AddExpr a);
}
class NumExpr implements Expr { /* ... */ }
class AddExpr implements Expr { /* ... */ }

Refactor to sealed types and pattern-matching switch. Compare lines of code, readability, and whether you've gained or lost extensibility.


Task 12 — Equals & hashCode contract in a hierarchy

Implement Point with x, y and proper equals/hashCode. Then define ColoredPoint extends Point with an additional color. Try to write equals for ColoredPoint that satisfies all of: - Reflexive - Symmetric - Transitive - Consistent

Spoiler: you'll hit Effective Java Item 10. Discuss why this is impossible without breaking symmetry, and what alternatives exist (composition, final, getClass() checks vs instanceof checks).


Validation

Task How
1, 2, 7 Compile and run; compare predicted output
3 Code review your refactor; can Bird b = new Penguin(); b.fly(); even compile after refactor?
4 Add Sub then run; observe compile error pinpointing the missing case
5 javap -p -c IntCell.class should show two get() methods
6 Compile each in isolation; let @Override annotations and javac do the labeling
8 Try new MyStack<>().add("x") — should not compile
9 b.test(new B()) works; b.test(new A()) doesn't (in package beta)
10 After fix, hello() should call your chosen interface's default explicitly
11 Lines of code: count before/after
12 Test reflexivity / symmetry / transitivity with all four cases (Point eq Point, Point eq CP, CP eq Point, CP eq CP)

Solutions sketch

Task 1 answer: D. The hierarchy A→B→C→D; D overrides; receiver is D.

Task 2 answer: 4 (field, static dispatch via Animal); 8 (method, dynamic dispatch).

Task 3: introduce a Flyer interface that Sparrow implements but Penguin does not. Bird becomes a class with shared bird state but no fly().

Task 4: the switch must include case Sub s ->. Pattern matching exhaustiveness gives you a compile error pointing exactly at this.

Task 5: Integer get(). javap will show both Integer get() and a synthetic Object get() bridge.

Task 6 (a) override (b) overload (c) error — different return type, not covariant; void→double not allowed (d) overload; and depending on context (a) is fine, but (c) breaks override compatibility.

Task 7 first run:

A(int)
B(int)
B()
Second run (super() instead of super(x)): A() / A(int) ... actually, super() has implicit argument resolution — compile would fail without an A() with no-args; if A has both, then the chain is A() / B(int) / B(). Use javap to verify.

Task 9: protected from outside the package only allows access via this-typed references. other.m() is not this.m(). To work around, call ((B) other).m() if it actually IS a B.

Task 10: override hello and call X.super.hello() or Y.super.hello() explicitly.

Task 12: the canonical answer is "use composition, not inheritance" or "use getClass() instead of instanceof" (which breaks LSP). Effective Java Item 10 has the full discussion.


Memorize this: Inheritance puzzles usually trace back to (a) static vs dynamic dispatch, (b) override vs overload vs hide, or (c) constructor ordering. Always reach for @Override, prefer composition, use sealed types for closed unions.