SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Applying Object-Oriented Principles in Java Programs

Java Inheritance, Polymorphism, Overriding and Sealed Classes (1Z0-831)

21 min read1Z0-831 · Applying Object-Oriented Principles in Java ProgramsUpdated

Inheritance in Java lets a class extend one superclass and reuse or override its instance methods, and polymorphism is the result: a call through a superclass-typed reference runs the method body of the object's actual class. Oracle 1Z0-831 objective 3.5 asks you to implement that in code — including abstract classes, sealed classes and interfaces, and records in a hierarchy — to override methods, including those of Object, and to tell the reference type from the object type. This lesson separates what the compiler decides from what the JVM decides, sets out the exact rules for a legal override, shows why fields, static and private methods are hidden rather than overridden, covers casts, constructor chaining and Java 25's flexible constructor bodies, and finishes with sealed hierarchies in exhaustive switches and the equals/hashCode contract.

What you’ll learn
  • Differentiate the reference type from the object type, and say which one decides member access, checked exceptions, overload choice and the overriding body that runs
  • Apply the override rules — signature, access, covariant return and checked exceptions — and recognise when a method is an overload instead
  • Explain why private, final and static methods and fields are not overridden, and trace code that relies on hiding
  • Predict whether a reference cast compiles, succeeds or throws ClassCastException
  • Trace constructor chaining, including implicit super(), overridable calls during construction and Java 25 constructor prologues
  • Declare abstract classes and sealed classes or interfaces with correctly modified permitted subtypes, including records
  • Override equals(Object), hashCode() and toString() correctly and predict their effect in hash-based collections

Reference type versus object type

Every reference expression has two types: the reference type (the declared type of the variable or expression, known to the compiler) and the object type (the class of the object actually created with new, known only at runtime). The reference type decides what code is allowed to compile; the object type decides which overriding method body runs.

QuestionDecided byWhen
Which methods and fields may be named through this reference?Reference typeCompile time
Which checked exceptions must the call handle?Reference type (the declaration the compiler found)Compile time
Which overload (signature) is chosen?Reference type and argument typesCompile time
Which body of an overridden instance method runs?Object typeRuntime
Which field, static method or private method is used?Reference type / the class that contains the codeCompile time
class Instrument {
    String tone() {
        return "plain";
    }
}

class Guitar extends Instrument {
    @Override
    String tone() {
        return "twang";
    }

    void strum() {
        System.out.println("strumming");
    }
}

public class Main {
    public static void main(String[] args) {
        Instrument a = new Guitar();
        System.out.println(a.tone());
        // a.strum();
        if (a instanceof Guitar g) {
            g.strum();
        }
    }
}

This prints twang then strumming. a.tone() compiles because Instrument declares tone(), and runs Guitar's override because the object is a Guitar. Uncommenting a.strum() fails with cannot find symbol: Instrument has no strum(), whatever the object really is. To reach subclass-only members you need a reference of the subclass type — a cast or, more safely, an instanceof pattern as shown.

The same compile-time view applies to checked exceptions. If Fetcher.read() declares throws IOException and an override in DiskFetcher drops the clause, a call through a Fetcher reference still has to catch or declare IOException — the compiler only sees Fetcher's declaration. A call through a DiskFetcher reference does not.

Polymorphism: overriding, overloading and dynamic dispatch

Polymorphism in Java means one call site can run different method bodies depending on the object's class; it works through overriding — a subclass instance method with the same signature (name and parameter types) as an inherited one. A method with the same name but different parameter types is an overload, a separate method that never replaces the inherited one.

A call is resolved in two steps:

  1. Compile time — overload resolution. The compiler searches the reference type for methods with that name, picks the most specific signature that fits the argument types, and records that signature.
  2. Runtime — dynamic dispatch. The JVM starts at the object's class and walks up the hierarchy to the first class that declares a method with the recorded signature — an override, or the original declaration if nothing overrides it.
class Logger {
    void log(Object o) {
        System.out.print("L-Object ");
    }
}

class ConsoleLogger extends Logger {
    void log(String s) {
        System.out.print("C-String ");
    }
}

public class Main {
    public static void main(String[] args) {
        Logger ref = new ConsoleLogger();
        ConsoleLogger c = new ConsoleLogger();
        ref.log("x");
        c.log("y");
    }
}

This prints L-Object C-String. For ref.log("x") the compiler only sees Logger, whose single log(Object) is chosen; ConsoleLogger does not override log(Object), so Logger's body runs even though the object is a ConsoleLogger and the argument is a String. Through c, both methods are visible and log(String) is more specific.

Put @Override on every intended override. It changes nothing at runtime, but the compiler rejects it when the method does not actually override anything — catching exactly this overload-instead-of-override mistake.

What is not overridden: private, final and static methods, and fields

Only accessible, non-final instance methods can be overridden. private methods are not inherited, final methods are locked, and static methods are hidden rather than overridden.

Superclass methodSame signature in the subclassResult
final void seal()void seal()Compilation fails — final methods cannot be overridden
static void count()static void count()Compiles — hiding; the call binds at compile time
static void count()void count() (instance)Compilation fails — an instance method cannot replace a static one
void run() (instance)static void run()Compilation fails — a static method cannot hide an instance one
private void helper()Any helper(), any return type or modifiersCompiles — a brand-new, unrelated method; override rules do not apply

The private case has a runtime consequence too. If Template.describe() calls its own private key(), that call is bound to Template.key() and is not dispatched, so a subclass that declares its own key() does not change what describe() returns. Calling the subclass method requires a subclass-typed reference.

For a static method, the class whose code contains the call — or the declared type of the reference in front of it — decides which version runs. Calling a static method through an instance reference (t.prefix()) is legal, but the object is ignored. An unqualified static prefix() inside an inherited method of Template always means Template.prefix(), even when that method runs on a subclass object.

Fields are hidden too

Fields are never polymorphic: a subclass field with the same name as an inherited one hides it, the object carries both, and which one an expression reads is fixed at compile time by the reference type (or, for an unqualified name, by the class whose code contains it).

class Shelf {
    String tag = "shelf";

    static String origin() {
        return "Shelf";
    }

    String tag() {
        return tag;
    }
}

class Bookshelf extends Shelf {
    String tag = "book";

    static String origin() {
        return "Bookshelf";
    }

    @Override
    String tag() {
        return tag;
    }
}

public class Main {
    public static void main(String[] args) {
        Shelf s = new Bookshelf();
        System.out.println(s.tag + " " + s.tag() + " " + s.origin());
    }
}

This prints shelf book Shelf. s.tag reads Shelf's field because s is declared as Shelf; s.tag() is an overridden instance method, so Bookshelf's body runs and reads Bookshelf's field; s.origin() is static, bound to Shelf.origin().

The trap to watch for is an inherited, not overridden method. If Bookshelf did not override tag(), a call to tag() on a Bookshelf object would run Shelf's body, whose tag was bound to Shelf.tag when Shelf was compiled — it returns "shelf", not the subclass value. Inside Bookshelf, super.tag reaches the hidden field explicitly.

Casting references: compile-time check, runtime check

A reference cast never changes the object; it changes the reference type, and it is checked twice. The compiler rejects a cast only when no object could ever satisfy it; the JVM then checks the actual object and throws ClassCastException if it is not an instance of the target type.

CastCompiles?At runtime
Upcast (subclass → superclass, e.g. (Instrument) guitar)Yes (the cast is optional)Always succeeds
Downcast (superclass → subclass)YesSucceeds only if the object really is that subclass
Between unrelated classes ((String) guitar)No — inconvertible types—
Non-final class → interface it does not implementYes — some subclass might implement itClassCastException unless the object does
final class → interface it does not implementNo — no instance can ever implement it—
public class Main {
    public static void main(String[] args) {
        Object o = "text";
        String s = (String) o;
        System.out.println(s);
        Integer i = (Integer) o;
    }
}

This prints text and then throws ClassCastException: Object could hold an Integer, so the second cast compiles, but this object is a String. Prefer a pattern such as if (o instanceof Integer n), which tests and binds in one step and never throws. Sealed hierarchies tighten the compile-time check further, because the compiler knows every permitted subtype.

Constructors in a hierarchy: super() and initialization order

Every constructor first runs a superclass constructor: either an explicit super(...) or this(...), or — if neither is written — an implicit no-argument super() inserted by the compiler. Superclass construction therefore completes before the subclass's field initializers and the rest of its constructor body run.

  • The compiler generates a default no-arg constructor only for a class that declares no constructors. If the superclass declares only Ledger(String code), a subclass constructor without an explicit super("...") fails to compile (constructor Ledger in class Ledger cannot be applied to given types).
  • Constructors are not inherited and cannot be overridden.
  • super.method() calls the direct superclass's implementation — the version that class has or inherits — and is how an override extends rather than replaces behaviour. Calls chain naturally (C calls super into B, which calls super into A); there is no super.super.

Because the object already has its final class while the superclass constructor runs, an overridable method called from that constructor dispatches to the subclass override — before the subclass's fields are initialized:

class Engine {
    Engine() {
        System.out.print("Engine ");
        start();
    }

    void start() {
        System.out.print("engine-start ");
    }
}

class Turbo extends Engine {
    int psi = 5;

    Turbo() {
        System.out.print("Turbo ");
        start();
    }

    @Override
    void start() {
        System.out.print("psi=" + psi + " ");
    }
}

public class Main {
    public static void main(String[] args) {
        new Turbo();
    }
}

This prints Engine psi=0 Turbo psi=5. During Engine(), start() runs Turbo's override and sees psi at its default value 0 (a reference field would be null). Only after Engine() returns does psi = 5 run, followed by the rest of Turbo(). This is why constructors should call only private, final or static methods.

Flexible constructor bodies (Java 25)

Since Java 25 (JEP 513, final), statements may appear before an explicit super(...) or this(...) call. That leading part is the constructor prologue; the code after the call is the epilogue. The prologue is for work that does not need the object to exist yet: validating or transforming arguments, and initializing the class's own fields.

In the prologue you mayIn the prologue you may not
Declare locals, use parameters, call static methodsRead any field of the object being built (this.x or plain x)
Throw an exception (the superclass constructor then never runs)Call instance methods, or use this/super as a value
Assign fields declared in this class that have no initializerAssign a field that has an initializer, or a superclass field
class Sensor {
    Sensor() {
        System.out.print("armed ");
        describe();
    }

    void describe() { }
}

class Thermostat extends Sensor {
    private final double rate;

    Thermostat(double rate) {
        if (rate < 0) {
            throw new IllegalArgumentException("negative");
        }
        this.rate = rate;
        super();
    }

    @Override
    void describe() {
        System.out.print("rate=" + rate);
    }
}

public class Main {
    public static void main(String[] args) {
        new Thermostat(0.5);
    }
}

This prints armed rate=0.5. rate is assigned in the prologue, so when Sensor() calls the overridable describe(), the subclass override already sees the real value — fixing the "default value seen during construction" problem from the previous section. Had this.rate = rate; come after super();, the output would be armed rate=0.0; a negative rate throws before Sensor() runs at all.

Violations are compile errors with specific messages, for example cannot reference a before supertype constructor has been called and cannot assign initialized field 'b' before supertype constructor has been called.

Abstract classes and abstract methods

An abstract class is a class declared abstract that cannot be instantiated directly and may declare abstract methods — methods with no body that every concrete subclass must implement. It can hold everything a normal class can: fields, constructors (run via super(...) from subclasses), concrete and final methods.

abstract class Report {
    private final String title;

    Report(String title) {
        this.title = title;
    }

    abstract String body();

    final String render() {
        return title + ": " + body();
    }
}

class Daily extends Report {
    Daily() {
        super("Daily");
    }

    @Override
    String body() {
        return "all good";
    }
}

public class Main {
    public static void main(String[] args) {
        Report r = new Daily();
        System.out.println(r.render());
    }
}

This prints Daily: all good: the abstract class fixes the algorithm in render() and leaves one step to the subclass.

  • An abstract method ends with a semicolon; giving it a body fails to compile.
  • An abstract method must be overridable, so abstract cannot be combined with private, static or final; any access other than private is fine.
  • A class with an abstract method must itself be abstract. An abstract class may also have no abstract methods at all.
  • A concrete (non-abstract) subclass must implement every inherited abstract method, or it fails: X is not abstract and does not override abstract method m() in Y. An abstract subclass may leave them for its own subclasses.
  • A class cannot be both abstract and final.
  • new Polygon() on an abstract class is a compile error, but Polygon p = new Hexagon(); is the normal way to use one. (An anonymous subclass, new Polygon() { ... }, also works.)

Sealed classes, records and exhaustive switches

A sealed class or interface lists exactly which types may directly extend or implement it, with sealed ... permits A, B. Each permitted subtype must directly extend it and must declare how the hierarchy continues:

Modifier on the permitted subtypeMeaning
finalClosed — no further subclasses. Records are implicitly final (and enums implicitly final or sealed), so they need no modifier.
sealedRestricted again — it must permit its own subclasses (a sealed class with none fails to compile).
non-sealedReopened — any class may extend it.
(none, on a class)Compilation fails: sealed, non-sealed or final modifiers expected. This holds for abstract classes too.

A permitted subtype of a sealed interface may also be an interface, which is then sealed or non-sealed (interfaces cannot be final). Permitted subtypes must live in the same module, or in the same package when the code is in the unnamed module; the permits clause may be omitted when they are declared in the same source file.

Records in hierarchies. A record's direct superclass is always java.lang.Record, so a record cannot use extends; it is implicitly final, so nothing can extend it and it cannot be abstract; it may implement any interfaces. That makes records the usual leaves of a sealed interface.

Exhaustiveness. Because the compiler knows every permitted subtype, a pattern switch over a sealed type needs no default when every subtype is covered:

sealed interface Payment permits Card, Cash, Voucher { }

record Card(String number) implements Payment { }

record Cash(int cents) implements Payment { }

non-sealed class Voucher implements Payment { }

class GiftVoucher extends Voucher { }

public class Main {
    static String label(Payment p) {
        return switch (p) {
            case Card c -> "card";
            case Cash(int cents) when cents > 1000 -> "big cash";
            case Cash c -> "cash";
            case Voucher v -> "voucher";
        };
    }

    public static void main(String[] args) {
        System.out.println(label(new GiftVoucher()) + " "
                + label(new Cash(5000)) + " " + label(new Cash(5)));
    }
}

This prints voucher big cash cash. GiftVoucher is matched by case Voucher v, which covers the whole non-sealed branch. Rules for these switches:

  • A guarded case (when ...) does not count towards exhaustiveness; an unguarded case for the same type is still needed (here case Cash c), otherwise the switch expression does not cover all possible input values.
  • Covering one subclass of a non-sealed subtype does not cover the subtype; only a case for the non-sealed type itself (or a supertype) does.
  • A case that can never match because an earlier case already covers it is a compile error (dominated), so specific cases go before general ones.
  • Record patterns such as Cash(int cents) deconstruct components and can nest, which makes recursive evaluation over a sealed tree concise.
  • If the hierarchy changes after the switch was compiled, an unmatched value throws MatchException at runtime rather than slipping through.

Overriding Object methods: equals, hashCode and toString

Every class inherits equals(Object), hashCode() and toString() from Object; the defaults compare identity, return an identity-based hash and print the class name with a hash. Value classes override them — and must override them with the exact Object signatures, all public.

import java.util.HashSet;
import java.util.List;
import java.util.Set;

class Isbn {
    private final String code;

    Isbn(String code) {
        this.code = code;
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof Isbn other && code.equals(other.code);
    }

    @Override
    public int hashCode() {
        return code.hashCode();
    }

    @Override
    public String toString() {
        return "Isbn[" + code + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Set<Isbn> set = new HashSet<>(List.of(new Isbn("978")));
        System.out.println(set.contains(new Isbn("978")) + " " + set);
    }
}

This prints true [Isbn[978]]: a different Isbn object with the same code is found in the set.

The contract. Objects that are equal by equals must return the same hashCode(). Hash-based collections (HashSet, HashMap) use the hash to pick a bucket and then equals(Object) to compare inside it, so both must be overridden consistently. Overriding only hashCode() leaves identity equality in place — two value-equal objects are both kept. Overriding only equals breaks lookups because equal objects usually land in different buckets.

The overload trap. public boolean equals(Coord other) does not override anything; it overloads equals(Object). It is chosen only when the compiler sees a Coord argument; a call with an Object-typed argument, and every collection method (contains, indexOf, remove), calls equals(Object) and gets identity comparison. Adding @Override turns this silent bug into a compile error.

Records generate all three methods from their components; you may still override them explicitly, using the same public signatures.

Tip. Objective 3.5 is examined through short, complete programs that ask what is printed, whether the code compiles, or whether it throws. Dispatch items call methods through a superclass reference and mix in overloads that look like overrides, inherited methods that read hidden fields, static methods called through instance references, and private methods redeclared in a subclass. Override-rule items offer several candidate methods to insert into a subclass — differing in access, return type, checked exceptions, or static/final/private status — and ask which compile. Cast items ask which statements compile and which throw ClassCastException. Constructor items cover a missing no-arg superclass constructor, an overridable method called during construction and Java 25 statements before super(). Sealed items ask which declarations of a permitted subtype compile and whether a pattern switch over a sealed hierarchy with records, guards or a non-sealed branch is exhaustive. Object-method items test equals and hashCode in hash collections and the equals overload trap.

Key takeaways
  • The reference type decides what compiles — callable members, overload choice and checked exceptions; the object type decides which overriding instance method body runs.
  • An override needs the same signature, the same or wider access, the same or a covariant return type, and no new or broader checked exceptions; different parameter types make an overload.
  • Use @Override on every intended override — it turns an accidental overload into a compile error.
  • private methods are not inherited, final methods cannot be overridden, static methods are hidden, and a method cannot switch between static and instance.
  • Fields are hidden, never overridden: field access resolves by reference type, and an inherited method reads its own class's field.
  • A cast compiles unless no object could satisfy it (unrelated classes, or a final class to an interface it lacks); otherwise a wrong object throws ClassCastException at runtime.
  • Every constructor runs a superclass constructor first; with no explicit call the compiler inserts super(), which fails if the superclass has no no-arg constructor.
  • An overridable method called from a superclass constructor runs the subclass override before subclass fields are initialized — unless Java 25's prologue assigned them before super().
  • Permitted subtypes of a sealed type must be final, sealed or non-sealed; records are implicitly final, extend Record and may implement interfaces.
  • equals must be overridden as equals(Object), together with a consistent hashCode(); an equals(Coord) overload is ignored by collections.

Frequently asked questions

What is the difference between reference type and object type in Java?

The reference type is the declared type of a variable or expression and is what the compiler uses to decide which members you can call, which overload is chosen and which checked exceptions must be handled. The object type is the class of the object actually created, and the JVM uses it at runtime to choose which overriding instance method body runs. Fields, static methods and private methods are resolved by the reference type, not the object type.

What are the rules for overriding a method in Java?

The overriding method must have the same name and parameter types, the same or wider access, the same return type or a subtype of it for reference types, and it may throw only the same checked exceptions, subclasses of them, or none. It cannot override a final, static or private method, and an instance method cannot replace a static one or the other way round. Unchecked exceptions are unrestricted.

Can you override a static or private method in Java?

No. A static method with the same signature in a subclass hides the superclass method, so which one runs is decided at compile time by the declared type or the class containing the call. A private method is not inherited at all, so a subclass method with the same name is a new, unrelated method, and calls inside the superclass still use the private version.

Why does an overridden method see null or zero when called from a constructor?

The superclass constructor runs before the subclass's field initializers and constructor body. If it calls an overridable method, dynamic dispatch runs the subclass override, which reads subclass fields that still hold default values such as null or 0. In Java 25 a constructor can assign its own uninitialized fields before calling super(), so the override sees the assigned value.

What is a constructor prologue in Java 25?

Java 25 (JEP 513) allows statements before an explicit super(...) or this(...) call; that part of the constructor is called the prologue. It can validate arguments, compute values, throw exceptions and assign fields declared in the same class that have no initializer, but it cannot read fields, call instance methods or otherwise use the object under construction.

What modifiers must a permitted subclass of a sealed class have?

Each class named in a sealed type's permits clause must be declared final, sealed (with its own permitted subclasses) or non-sealed (open to any subclass); a class without one of those modifiers fails to compile. Records are implicitly final, so a record can be a permitted subtype without any modifier. A permitted subinterface is sealed or non-sealed, since interfaces cannot be final.

Can a record extend a class or be extended in Java?

No. A record's direct superclass is always java.lang.Record, so it cannot declare extends, and it is implicitly final, so no class or record can extend it and it cannot be abstract. A record can implement any number of interfaces, which is why records are commonly used as the permitted subtypes of a sealed interface.

Why must equals and hashCode be overridden together?

Hash-based collections such as HashSet and HashMap first use hashCode() to find a bucket and then equals(Object) to compare the objects in it. If two objects are equal but return different hash codes, lookups miss them; if only hashCode() is overridden, identity equality still treats value-equal objects as different. The override must be equals(Object) — an equals method taking the class's own type is an overload that collections never call.

Source

This lesson covers the "Applying Object-Oriented Principles in Java Programs" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?