Advanced Class Design & Design Patterns practice questions

From OCP Java SE 21 (1Z0-830) · 27 questions on this topic

Advanced Class Design & Design Patterns practice questions from OCP Java SE 21 (1Z0-830). This pack has 27 questions tagged Advanced Class Design & Design Patterns, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Advanced Class Design & Design Patterns

  1. Question 1

    What is the output of the following program? ```java public class Main { enum Direction { NORTH, SOUTH, EAST, WEST; @Override public String toString() { return name().charAt(0) + name().substring(1).toLowerCase(); } } public static void main(String[] args) { System.out.println(Direction.NORTH + " " + Direction.SOUTH.name()); } } ```

    1. A. North SOUTHCorrect answer

      Direction.NORTH appears in a string-concatenation expression, so its toString() override is called: name().charAt(0) yields the char 'N', name().substring(1).toLowerCase() yields "orth", and char + String concatenation produces "North". Direction.SOUTH.name() is a final Enum method that bypasses toString() entirely and returns the declared identifier "SOUTH". The two parts join as "North SOUTH".

    2. B. NORTH SOUTH

      String concatenation invokes toString() on each non-String operand (JLS §15.18.1). Direction.NORTH.toString() returns "North" through the override, not the default "NORTH"; a candidate who overlooks the @Override arrives at this wrong output.

    3. C. North South

      Enum.name() is declared final in java.lang.Enum and always returns the constant's source-text identifier — "SOUTH" for SOUTH — regardless of any toString() override in the subclass. Treating name() as though it delegates to toString() is the error here.

    4. D. Compilation fails

      toString() is not final in java.lang.Enum, so an enum body may override it freely. The @Override annotation correctly identifies an existing inherited method and the code compiles without error.

    Explanation

    String concatenation converts each non-String operand by calling its toString() method (JLS §15.18.1), so Direction.NORTH in the expression uses the overridden toString() that title-cases the declared name, yielding "North". Enum.name() is a separate, final method that returns the constant's source identifier unchanged — no override in the enum body can affect it — so Direction.SOUTH.name() is always "SOUTH". The two values concatenate to produce "North SOUTH".

  2. Question 2

    An Animal reference holding a plain Animal is downcast to Dog. What is the result of compiling and running this program? ```java public class Main { static class Animal { void speak() { System.out.println("generic"); } } static class Dog extends Animal { @Override void speak() { System.out.println("woof"); } } public static void main(String[] args) { Animal a = new Animal(); Dog d = (Dog) a; d.speak(); } } ```

    1. A. woof

      Assumes the cast succeeds and the subclass override runs; the object is a plain Animal, so the cast to Dog fails before speak() is ever called.

    2. B. generic

      Assumes the failed cast is ignored and the base method runs; the cast throws ClassCastException, so no speak() ever executes.

    3. C. Throws ClassCastExceptionCorrect answer

      The compiler permits the downcast because an Animal reference could hold a Dog, but at run time the object is a plain Animal, so the cast fails with ClassCastException.

    4. D. Compilation fails

      Confuses a downcast with an impossible cast between unrelated types; a supertype-to-subtype cast is legal at compile time and only checked at run time.

    Explanation

    A cast from a supertype to a subtype is a downcast: the compiler allows it because a variable of type Animal COULD hold a Dog, so the check is deferred to run time. Here the object really is an Animal, so the cast fails with ClassCastException and speak() is never called. 'Compilation fails' is the trap for candidates who confuse a downcast with an impossible cast between unrelated types (which javac does reject); guarding with 'a instanceof Dog d' is the fix.

  3. Question 3

    This program looks a constant up by name and then compares two constants. What does it print? ```java public class Main { enum Level { LOW, MEDIUM, HIGH } public static void main(String[] args) { Level chosen = Level.valueOf("HIGH"); System.out.println(chosen.ordinal() + " " + Level.MEDIUM.compareTo(chosen) + " " + chosen.name().length()); } } ```

    1. A. 3 -1 4

      Treats ordinal() as one-based; ordinal is zero-based, so the third constant is 2, not 3.

    2. B. 2 -1 4Correct answer

      The looked-up constant's zero-based ordinal is 2, MEDIUM.compareTo(HIGH) is the ordinal difference 1 - 2 = -1, and the constant name 'HIGH' has length 4.

    3. C. 2 1 4

      Reverses the compareTo operands; MEDIUM.compareTo(HIGH) is the ordinal difference in declaration order, 1 - 2 = -1, not +1.

    4. D. 2 -1 6

      Miscounts the name length; name() returns exactly the constant identifier 'HIGH', whose length is 4, not 6.

    Explanation

    ordinal() is zero-based, so HIGH (the third constant) is 2. Enum's compareTo is defined as the difference of ordinals in declaration order, so MEDIUM.compareTo(HIGH) is 1 - 2 = -1 (a negative value, not the sign of an alphabetic comparison). name() returns the exact constant identifier 'HIGH', whose length is 4. The '2 1 4' distractor reverses the compareTo operands; the '3 -1 4' one treats ordinal as one-based.

  4. Question 4

    The name 'label' appears three times: as a field of Main, as a field of the inner class, and as a method parameter. What does this program print? ```java public class Main { private String label = "outer"; class Inner { private String label = "inner"; String render(String label) { return label + "/" + this.label + "/" + Main.this.label; } } public static void main(String[] args) { System.out.println(new Main().new Inner().render("param")); } } ```

    1. A. outer/inner/outer

      Assumes the bare simple name resolves to the outer field rather than the method parameter, and misorders the remaining two; for an unqualified name the innermost declaration, the parameter, wins.

    2. B. param/outer/inner

      Swaps the last two components, treating this.label as the enclosing field and the qualified form as the inner field; in fact this.label is the inner class's own field and only the qualified enclosing-this reaches the outer instance's field.

    3. C. Compilation fails

      Reusing the same name as a parameter, an inner field and an outer field is legal; shadowing plus qualified this make each reference resolvable, so it compiles.

    4. D. param/inner/outerCorrect answer

      The unqualified label is the parameter, this.label is the inner class's field ("inner"), and the qualified enclosing-this reaches the outer instance's field ("outer"), giving param/inner/outer.

    Explanation

    Name resolution works from the innermost declaration outward: an unqualified simple name binds to the nearest enclosing declaration, which here is the method parameter. A plain this refers to the immediately enclosing instance, so this.field reads the inner class's own field, while the qualified EnclosingType.this form is the only way to reach a field of the outer instance that the inner field shadows.

  5. Question 5

    The class below uses constructor chaining: `Main()` delegates to `Main(int)` via `this(5)`, and an instance initializer block sets `x = 100`. What does the program print? ```java public class Main { int x; { x = 100; } Main() { this(5); x = x + 1; } Main(int n) { x = x + n; } public static void main(String[] args) { Main m = new Main(); System.out.println(m.x); } } ```

    1. A. 106Correct answer

      Main() delegates to Main(int n) via this(5). Because Main(int n) calls super() implicitly, the compiler places the instance-initializer block there: x=100 before the body runs, then x+=5 gives 105. Main(int n) returns, and Main() executes the remaining x=x+1, producing 106.

    2. B. 6

      Assumes the instance initializer never runs, leaving x at its default of 0. In reality Main(int n) calls super() implicitly, and the compiler inserts the instance-initializer code there; x is set to 100 before x += n executes.

    3. C. 101

      Mislocates the instance initializer in Main() — the this() caller — rather than in Main(int n). When a constructor begins with this(...), the compiler does NOT insert instance-initializer code into it; that code is placed only in the constructor that (directly or transitively) calls super(), which here is Main(int n). With this misconception: Main(int n) sees x=0, x becomes 0+5=5; back in Main() the init then fires setting x=100, then x+1=101.

    4. D. 105

      Correctly traces the instance initializer inside Main(int n) — x=100, then x+=5=105 — but ignores the statement `x = x + 1` that follows the this(5) call in Main(). After this(5) returns, execution continues with the remaining statements in the caller constructor, advancing x to 106.

    Explanation

    When a constructor begins with this(...), the Java compiler does not insert instance-initializer code into that constructor; it inserts it only into the constructor that calls super() — explicitly or implicitly (JLS §8.8.7.1, §12.5). Here Main(int n) is the super()-calling constructor, so x=100 executes before its body, yielding x=105 after x+=n. Control then returns to Main() which carries out the statement following this(5), producing the final printed value.

  6. Question 6

    What is the output of the following program? ```java public class Main { static class Base { String name() { return "Base"; } } static class Derived extends Base { @Override String name() { return "Derived"; } } static void print(Base b) { System.out.println("Base:" + b.name()); } static void print(Derived d) { System.out.println("Derived:" + d.name()); } public static void main(String[] args) { Base b = new Derived(); print(b); } } ```

    1. A. Derived:Derived

      Assumes overload resolution also uses the runtime type of the argument (Derived), selecting `print(Derived d)`. Overloading is a compile-time operation: the compiler chooses the overload from the declared type of the argument. Because `b` is declared `Base`, the compiler picks `print(Base b)` regardless of what object `b` holds at runtime.

    2. B. Base:Base

      Correctly predicts the compile-time overload choice but then assumes `b.name()` also resolves at compile time to `Base.name()`. Instance method calls in Java use virtual dispatch: the JVM looks up the method on the actual runtime class of the receiver. Because the object is a `Derived`, `Derived.name()` runs, not `Base.name()`.

    3. C. Base:DerivedCorrect answer

      Overload resolution is performed at compile time using the declared (static) type of the argument. Because `b` is declared as `Base`, the compiler binds the call to `print(Base b)`, writing that binding into the bytecode. Inside that method, `b.name()` is an instance call resolved at runtime via virtual dispatch: the actual object is a `Derived`, so `Derived.name()` executes and returns "Derived". The prefix "Base:" comes from the compile-time overload choice; the suffix "Derived" from the runtime override (JLS §15.12.2 and §15.12.4.4).

    4. D. Derived:Base

      Inverts both mechanisms simultaneously: it assumes overload resolution uses the runtime type (selecting `print(Derived d)`) while `b.name()` uses the compile-time type (selecting `Base.name()`). In reality overloading is resolved at compile time and overriding is dispatched at runtime — precisely the opposite of what this option assumes.

    Explanation

    Java selects which overloaded method to call at compile time, based solely on the declared types of the arguments; the runtime types of those arguments have no influence on overload selection. Instance method calls, by contrast, are dispatched at runtime against the actual type of the receiver object. These two mechanisms are entirely independent: the overload binding is frozen when the class is compiled, while virtual dispatch occurs fresh at each invocation, so a single call chain can exhibit both behaviors simultaneously.

  7. Question 7

    What is the output of the following program? ```java public class Main { static class Parent { static String kind() { return "Parent"; } String describe() { return "parent-instance"; } } static class Child extends Parent { static String kind() { return "Child"; } @Override String describe() { return "child-instance"; } } public static void main(String[] args) { Parent p = new Child(); System.out.println(p.kind() + " " + p.describe()); } } ```

    1. A. Child child-instance

      Assumes `p.kind()` uses the same runtime virtual dispatch as instance methods. JLS §8.4.8.2 explicitly differentiates the two: class (static) methods are hidden, not overridden. A hidden static method is resolved from the compile-time type of the qualifier (`Parent`), not the runtime type, so `Parent.kind()` executes even though `p` holds a `Child` at runtime.

    2. B. Parent child-instanceCorrect answer

      Static methods are not overridden — they are hidden. When `p.kind()` is called through a reference declared as `Parent`, the compiler resolves the method at compile time from the declared type and emits a direct invokestatic to `Parent.kind()`, returning "Parent" (JLS §8.4.8.2). The runtime type of the object has no effect. `p.describe()` is a genuine instance override: the JVM dispatches it at runtime to `Child.describe()` because the actual object is a `Child`, returning "child-instance". Adjacent calls on the same reference trigger two opposite resolution rules.

    3. C. Parent parent-instance

      Correctly identifies that `p.kind()` resolves to `Parent.kind()` at compile time, but then wrongly applies the same compile-time logic to the instance method `p.describe()`. Instance methods use virtual dispatch: the JVM selects the implementation from the actual runtime class of the object, which is `Child`, and `Child.describe()` overrides `Parent.describe()`.

    4. D. Child parent-instance

      Inverts both rules: it assumes the static `kind()` is resolved at runtime (giving "Child") and the instance `describe()` is resolved at compile time (giving "parent-instance"). The actual rules are the exact opposite — static methods are resolved at compile time by declared type, instance methods at runtime by actual type.

    Explanation

    Java draws a hard line between class (static) methods and instance methods with respect to inheritance. Instance methods are overridden: the JVM selects the implementation at runtime from the actual class of the receiver object. Static methods are hidden: the compiler selects the implementation at compile time from the declared type of the qualifier variable, and the runtime type of the object is never consulted. Invoking a static method through an instance reference compiles without error, but the runtime type of that object has no influence whatsoever on which static method body executes.

  8. Question 8

    What is the output of the following program? ```java public class Main { interface Describable { default String describe() { return "interface"; } } static class Base { public String describe() { return "class"; } } static class Child extends Base implements Describable { } public static void main(String[] args) { Describable d = new Child(); System.out.println(d.describe()); } } ```

    1. A. interface

      Assumes the interface default method wins when both a default and an inherited class method share the same signature. JLS §9.4.1.3 is the opposite: a concrete class method always takes priority over any matching interface default, so "interface" is never printed.

    2. B. classCorrect answer

      Child inherits describe() from Base. JLS §9.4.1.3 states that a concrete method inherited from a superclass always takes priority over a matching interface default method, regardless of the reference's declared type. The call dispatches to Base.describe(), which returns "class".

    3. C. Compilation fails

      A compile error would arise only if no class-level implementation existed to satisfy the interface contract. Because Base provides a concrete describe(), Child satisfies Describable through inheritance and the compiler raises no error.

    4. D. Throws `AbstractMethodError` at runtime

      AbstractMethodError occurs when the runtime encounters a method that has no concrete backing implementation. Child inherits a concrete describe() from Base, so the virtual dispatch table is fully populated and no such error can occur.

    Explanation

    When an implementing class inherits a concrete method from a superclass and the interface also declares a default method with the same signature, the class method wins unconditionally (JLS §9.4.1.3). A default method is a fallback intended for classes that supply no implementation of their own; when a superclass already provides one, the default is silently bypassed. The declared type of the reference does not affect dispatch — the runtime type is Child, whose method table entry for describe() is populated by Base. Believing the interface default takes precedence, that the code fails to compile, or that a runtime error is thrown all reflect misreadings of this priority rule.

Practise all 27 Advanced Class Design & Design Patterns questions

OCP Java SE 21 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 21

Other topics in this pack