Practice: OOP
Five exercises covering what the slides in this module introduced — overload resolution order with widening/boxing/varargs, flexible constructor bodies (JEP 513), static/field hiding vs. instance method overriding, exhaustive
switchover sealed hierarchies, and the write-only nature of unnamed variables (_). Try to answer before opening each explanation.
Exercise 1 — Overload resolution: widening beats boxing beats varargs
javapublic class Resolver {
static void test(long x) { System.out.println("long"); }
static void test(Integer x) { System.out.println("Integer"); }
static void test(int... x) { System.out.println("varargs"); }
public static void main(String[] args) {
test(5);
}
}What's printed?
Answer
long
The compiler resolves overloads in strict phases, and it stops at the first phase where exactly one applicable method exists:
- Phase 1 — exact match or widening only (no boxing, no varargs).
5is anintliteral.intwidens tolongvia a widening primitive conversion, sotest(long)is applicable in this phase.test(Integer)is not applicable here — going frominttoIntegerrequires autoboxing, which phase 1 disallows.test(int...)is not applicable here either — varargs is excluded from phase 1.
Since phase 1 already finds exactly one applicable method
(test(long)), the compiler picks it and never even considers phase 2
(boxing) or phase 3 (varargs). This is exactly the ordering the slide
lays out: exact match → widening → autoboxing → varargs, each phase
tried only if the previous one found nothing applicable.
Exercise 2 — Flexible Constructor Bodies (JEP 513): what can precede super()?
javapublic class Sensor {
private final int id;
public Sensor(int id) {
if (id < 0) {
throw new IllegalArgumentException("id must be >= 0");
}
super();
this.id = id;
}
}
public class TypedSensor extends Sensor {
private final String type;
public TypedSensor(int id, String type) {
if (type == null) {
throw new IllegalArgumentException("type is required");
}
this.type = type; // line A
super(id); // line B
}
}Does this compile? If not, which line is the problem?
Answer
It does not compile — line A, this.type = type;, is the
problem, because it executes before super(id) on line B.
JEP 513 (Flexible Constructor Bodies, finalized in Java 25) relaxed
the old rule that super()/this() must be the textually first
statement in a constructor. Statements now can precede the explicit
constructor invocation — but only if they don't touch the instance
being constructed. Sensor's constructor is a correct example: the
if (id < 0) throw ... check only reads the parameter id, never
this, so it's allowed to sit before super().
TypedSensor's constructor breaks that rule. this.type = type;
assigns to an instance field — it reads/writes state that belongs to
this — and it does so before super(id) has run, i.e. before the
Sensor portion of the object even exists yet. Flexible constructor
bodies loosen where you can put validation/pre-computation logic,
but the core safety invariant is unchanged: nothing that touches this
(field reads, field writes, instance method calls) may run until after
the superclass is fully initialized. Swapping the two lines — calling
super(id) first, then this.type = type; — fixes the error.
Exercise 3 — Static hiding, field hiding, and instance overriding, side by side
javaclass Vehicle {
String category = "generic";
static String kind() { return "Vehicle"; }
String describe() { return "a " + category; }
}
class Car extends Vehicle {
String category = "car";
static String kind() { return "Car"; }
@Override
String describe() { return "a " + category; }
}
Vehicle v = new Car();
System.out.println(v.kind());
System.out.println(v.category);
System.out.println(v.describe());What's printed on each of the three lines?
Answer
plaintextVehicle generic a car
All three lines involve a variable v whose reference type is
Vehicle but whose runtime type is Car, and each line resolves
differently depending on what kind of member is being accessed:
v.kind()—kind()isstatic. Static methods are never overridden, only hidden, and a call through an instance reference is resolved entirely at compile time using the reference's declared type (Vehicle), not the object's actual type. So it prints"Vehicle", even thoughvactually points to aCarat runtime.v.category— fields are not polymorphic at all. Field access is always resolved by the compile-time type of the reference, sov.categoryreadsVehicle's field,"generic"—Car'scategoryfield merely hidesVehicle's, it doesn't replace it.v.describe()—describe()is a genuinely overridden instance method (nostatic, notprivate, notfinal), so it uses dynamic dispatch: the JVM looks atv's actual runtime type (Car) and invokesCar.describe(). Inside that method body, the unqualified referencecategoryis resolved at compile time withinCar's own source, so it meansCar'scategoryfield ("car") — giving"a car".
The takeaway matches the slide's hiding-vs-overriding table exactly: only instance methods get true runtime polymorphism; static methods and fields are always resolved by the reference type.
Exercise 4 — Exhaustive switch over a sealed hierarchy
javapublic sealed interface Notification permits Email, Sms, PushNotification { }
public record Email(String address) implements Notification { }
public record Sms(String number) implements Notification { }
public non-sealed class PushNotification implements Notification { }
class SilentPush extends PushNotification { }
class Router {
String describe(Notification n) {
return switch (n) {
case Email e -> "email to " + e.address();
case Sms s -> "sms to " + s.number();
};
}
}Does Router compile?
Answer
No — compile error. The switch expression is not exhaustive.
Notification is sealed with exactly three permitted direct
subtypes: Email, Sms, and PushNotification. For a pattern-matching
switch over a sealed type to be exhaustive without a default branch,
it must cover every permitted direct subtype — here, only Email
and Sms are covered; PushNotification has no case at all, so the
compiler cannot prove the switch handles every possible Notification.
Note that PushNotification being declared non-sealed (which legally
reopens it — any class, like SilentPush, may extend it freely) is a
bit of a red herring: it doesn't change what the switch needs. A single
case PushNotification p -> ... would cover PushNotification and
every future subclass of it (like SilentPush) through ordinary type-pattern
matching — you don't need to enumerate SilentPush separately. The fix
is either adding case PushNotification p -> "push to device" or a
default -> branch; either one restores exhaustiveness and the code
compiles.
Exercise 5 — Unnamed variables (_): write-only by design
javarecord Point(int x, int y) { }
Object obj = new Point(3, 4);
if (obj instanceof Point(int x, int _)) {
System.out.println(x + ", " + _); // line A
}
List<String> items = List.of("a", "b", "c");
int count = 0;
for (var _ : items) {
count++; // line B
}
try {
int[] data = {1, 0, 2};
int result = 10 / data[1];
} catch (ArithmeticException _) {
System.out.println("division failed"); // line C
}
System.out.println(count);Which line fails to compile, and why do the other uses of _ work fine?
Answer
Line A fails to compile — System.out.println(x + ", " + _) tries
to read _. Per JEP 456, an unnamed variable declared with _ is
strictly write-only: it exists to satisfy a syntax position that
requires a variable (a record-pattern component, a loop variable, a
catch parameter) when the value itself is never needed. The compiler
deliberately gives _ no readable binding, so any expression that tries
to reference it — as + _ does here — is a compile error, not merely a
warning.
Lines B and C compile without issue, and so would the record pattern on
line A's if if the println didn't try to read _:
int _insidePoint(int x, int _)discards theycomponent — you only care aboutx.for (var _ : items)discards the loop element — only the fact that an iteration happened (count++) matters.catch (ArithmeticException _)discards the exception object — only the fact that a division by zero occurred matters.
All three are separate, unrelated unnamed variables in the same method,
and that's fine: _ is specifically exempted from Java's normal rule
that forbids redeclaring a local variable name already in scope, so any
number of _s can coexist. Fixing line A to simply
System.out.println(x) would make the whole snippet compile; running it
would print 3, then division failed (since data[1] is 0), then
3 for count (three elements iterated).