Java 21 Object-Oriented Programming: Classes, Records and Sealed Types
Introduction
Object-oriented programming in Java 21 connects class design with code behavior. This 1Z0-830 lesson explains constructor chains, records, nested classes, overriding, interfaces and sealed hierarchies. You will trace initialization and distinguish the member selected at compile time from the method dispatched at run time.
Examples use Java SE 21 rules. An explicit constructor invocation must be the first statement in this version.
Learning objectives
After this lesson you will be able to:
- Declare classes, records and the four kinds of nested classes, and instantiate each correctly.
- Predict the order of static initializers, instance initializers, field initializers and constructors.
- Explain the object lifecycle: creation, reachability, eligibility for garbage collection, and why
finalizeis not a tool. - Distinguish overloading, overriding, hiding and shadowing, and apply the overriding rules (access, return type, exceptions).
- Use abstract classes, interfaces (default, static, private methods) and resolve diamond conflicts.
- Declare sealed classes and interfaces and reason about their
permitsrules. - Write enums with fields, constructors and constant-specific bodies.
- Combine
instanceofpattern matching with inheritance without compile errors.
1. Classes, fields, methods and constructors
A class declares state (fields) and behavior (methods). Fields have default values (0, false, null) while local variables have none and must be definitely assigned before use.
public class Account {
private static int created; // one copy per class
private final String id; // one copy per object, assigned once
private long balanceCents; // defaults to 0
public Account(String id) {
this.id = id;
created++;
}
public void deposit(long cents) {
if (cents <= 0) throw new IllegalArgumentException("amount must be positive");
balanceCents += cents;
}
}Key rules:
- For an ordinary named class with no constructor declaration, the compiler adds a no-argument constructor with the same access as the class. As soon as you declare any constructor, the default one disappears.
- In Java SE 21, an explicit constructor invocation,
this(...)orsuper(...), must be the first statement. If you write neither, the compiler insertssuper();. If the superclass has no accessible no-argument constructor, the subclass constructor does not compile. - A
finalinstance field must be assigned exactly once, by its initializer, an instance initializer block or every constructor. Astatic finalfield must be assigned by its initializer or a static initializer. - Access modifiers, from widest to narrowest:
public,protected(package access plus restricted subclass access across packages), package-private (no keyword),private.
Overloading means methods with the same name and different parameter lists. Resolution first considers fixed-arity calls without boxing or unboxing, then fixed-arity calls allowing those conversions, and finally variable-arity invocation. Reference widening also participates. Within a phase, the compiler selects the most specific applicable method or reports ambiguity. A single print(long) call with an int argument chooses print(long) over print(Integer) because widening beats boxing.
2. Initialization order
Class initialization is triggered by active use, such as creating an instance or invoking a declared static method. Reading an inlined constant does not trigger it. Superclasses initialize before subclasses; each class then runs its static field initializers and blocks in textual order. For each new, the order is:
- The superclass constructor chain runs first (recursively), which itself runs the superclass instance initializers.
- Instance field initializers and instance initializer blocks of the current class, in textual order.
- The body of the constructor.
class Base {
static { System.out.print("1 "); }
{ System.out.print("2 "); }
Base() { System.out.print("3 "); }
}
class Derived extends Base {
static { System.out.print("4 "); }
{ System.out.print("5 "); }
Derived() { System.out.print("6 "); }
}
// new Derived(); prints: 1 4 2 3 5 6
// A second new Derived() prints only: 2 3 5 6A classic trap: if a Base constructor calls an overridable method, the overriding method in Derived runs before Derived's field initializers, so it sees default values (null, 0).
3. Records
A record is a concise, shallowly immutable data carrier. The header record Point(int x, int y) {} generates private final fields, a canonical constructor, accessors named x() and y() (not getX), and equals, hashCode and toString.
public record Price(String currency, long cents) {
public Price { // compact canonical constructor
if (cents < 0) throw new IllegalArgumentException("negative price");
currency = currency.toUpperCase(java.util.Locale.ROOT); // may reassign the parameters
}
public Price(long cents) { this("EUR", cents); } // other constructors must delegate
public static Price zero() { return new Price(0); }
}Rules to memorize:
- A record is implicitly
finaland cannot extend a class (it extendsjava.lang.Record) but can implement interfaces. - You cannot declare instance fields other than the components; you can declare static fields, static methods and instance methods.
- In a compact constructor you do not assign
this.field; the fields are assigned automatically at the end, after your validation and any parameter reassignment. - A non-canonical constructor must start with
this(...). - You may explicitly declare an accessor, but it must be
publicand return the component type. - Shallow immutability: a final component reference can point to mutable data. List.copyOf prevents structural changes through the stored list and separates it from later source-list edits; mutable elements remain shared.
4. Nested classes
| Kind | Declared | Can access | Needs outer instance | Notes |
|---|---|---|---|---|
| Static nested | static class N inside a class |
outer members, including private ones; instance members require an explicit outer reference | No | Created with new Outer.N() |
| Inner (member) | class I inside a class |
all outer members incl. private | Yes | outer.new I(); since Java 16 may declare static members |
| Local | inside a method | outer members and effectively final locals | Yes in instance method | Not visible outside the method |
| Anonymous | new Type() { ... } |
same as local | Yes in instance context | No constructor; extends a class or implements one interface |
Local records, local enums and local interfaces are allowed and are implicitly static, so they cannot capture local variables or instance members of the enclosing method.
public class Shop {
private final String name = "Main Street";
class Receipt { // inner class
String header() { return "Receipt from " + name; }
}
static class Catalog { } // static nested class
}
Shop shop = new Shop();
Shop.Receipt r = shop.new Receipt(); // needs the outer instance
Shop.Catalog c = new Shop.Catalog(); // does notVariables captured by a lambda, local class or anonymous class must be effectively final: never reassigned after initialization.
5. Object lifecycle and garbage collection
An object is created by new, lives while it is reachable through a chain of references from a live thread, a static field or a local variable, and becomes eligible for garbage collection when no reachable reference remains. The exam asks "after which line is the object eligible?", and the usual patterns are:
Widget a = new Widget(); // object W1
Widget b = a; // W1 has two references
a = null; // still reachable through b
b = new Widget(); // W2 created; W1 now eligibleFacts to remember:
System.gc()is only a suggestion; the JVM may ignore it. No output order from the collector is guaranteed.finalize()is deprecated for removal since Java 18 (JEP 421). Use explicit close and try-with-resources for timely cleanup. A Cleaner can provide fallback cleanup, with no guarantee of prompt execution.- Islands of isolation (objects referencing each other, but unreachable from outside) are all eligible.
- A reference passed to a method is copied. Reassigning the parameter inside the method does not change the caller's variable, but mutating the object is visible.
6. Inheritance, overriding and polymorphism
Inheritance depends on accessibility: public and protected members can be inherited across packages; package-private members require the same package. Private members and constructors are not inherited. Overriding rules for an instance method:
- An override-compatible signature; for the non-generic examples here, the same name and parameter types.
- The return type is the same or a covariant subtype (for reference types).
- The access modifier is the same or wider.
- The
throwsclause may not add new or broader checked exceptions (it may drop them or narrow them). - The overridden method must not be
final,staticorprivate.
Hiding applies to static methods and to fields: they are resolved by the declared (compile-time) type of the reference, not by the runtime object. Instance methods are resolved by the runtime type (dynamic dispatch). Overloads are chosen at compile time using the declared types of the arguments.
class Animal {
String name = "animal";
String sound() { return "..."; }
static String kind() { return "Animal"; }
}
class Dog extends Animal {
String name = "dog";
@Override String sound() { return "woof"; }
static String kind() { return "Dog"; }
}
Animal a = new Dog();
System.out.println(a.name); // animal (field: declared type)
System.out.println(a.sound()); // woof (instance method: runtime type)
System.out.println(a.kind()); // Animal (static: declared type)Always annotate with @Override: it turns a typo or a mismatched signature into a compile error. Calling super.sound() invokes the parent version; super cannot be used in a static context.
7. Abstract classes and interfaces
An abstract class cannot be instantiated and may mix abstract methods (no body) with concrete ones, fields and constructors. A concrete subclass must implement every inherited abstract method. An abstract method cannot be private, static or final.
An interface declares a contract. Since Java 8 and 9 it may contain:
- abstract methods (implicitly
public abstract), defaultmethods (a body, inherited by implementers,public),staticmethods (called asInterface.method(), not inherited by implementers),privateinstance helpers for default methods andprivate statichelpers usable by static or default methods,- constants (implicitly
public static final).
interface Notifier {
void send(String to, String body);
default void sendAll(List<String> recipients, String body) {
recipients.forEach(r -> send(r, decorate(body)));
}
private String decorate(String body) { return "[OnLevelUp] " + body; }
static Notifier noop() { return (to, body) -> { }; }
}Diamond conflicts. Unrelated interface defaults with override-equivalent signatures need resolution if no class method supplies the implementation. A more specific subinterface default can win over its parent default. Inside the override it can choose a parent with InterfaceName.super.method(). A concrete method inherited from a class always wins over an interface default ("class wins" rule).
| Feature | Abstract class | Interface |
|---|---|---|
| Instance state (fields) | Yes | No (only constants) |
| Constructors | Yes | No |
| Multiple inheritance | One superclass | Many interfaces |
| Method bodies | Any visibility | default, static, private only |
| Functional interface (lambda target) | No | Yes, if not declared sealed and it has one effective abstract function; public Object method signatures do not count |
8. Sealed classes and interfaces
Java 17 finalized sealed types, which restrict who may extend or implement them. This is essential for exhaustive pattern matching.
sealed interface Shape permits Circle, Rectangle, Square { }
record Circle(double r) implements Shape { }
record Rectangle(double w, double h) implements Shape { }
non-sealed class Square implements Shape { }Rules:
- A permitted class must directly extend or implement the sealed type and be final, sealed or non-sealed. A permitted subinterface must be sealed or non-sealed. Records are implicitly final. Enums are implicitly final or sealed depending on their constant bodies.
- If the permitted subclasses are in the same source file as the sealed type,
permitscan be omitted and they are inferred. - Permitted subclasses must be in the same package (or the same module, if the sealed type is in a named module).
- A
non-sealedclass reopens the hierarchy: anyone can extend it.
9. Enums
An enum defines a fixed set of constants and extends Enum. It is implicitly final when no constant has a class body, otherwise implicitly sealed with those anonymous constant classes as permitted subclasses. It can implement interfaces. Its constructor is private if no access modifier is written; public and protected constructors are forbidden.
public enum Plan {
FREE(0), PRO(999) {
@Override String badge() { return "best value"; }
}, TEAM(2999);
private final int priceCents;
Plan(int priceCents) { this.priceCents = priceCents; }
String badge() { return name().toLowerCase(); }
int priceCents() { return priceCents; }
}Useful API: values(), valueOf(String) (throws IllegalArgumentException if the name does not exist), name(), ordinal(), and compareTo (by ordinal). The constants must come first in the body, ended by a semicolon when other members follow. Enums work in switch (no qualified names in classic case labels, in Java 21 qualified names are allowed) and EnumMap/EnumSet are efficient collections keyed by them.
10. instanceof pattern matching with inheritance
obj instanceof Type t tests and binds in one step. Compile-time rules: the cast from the expression's static type to Type must be legal, so testing a String expression against Integer is always a compile error, while Object permits many reference tests. An interface-to-class test can still fail at compile time when the types are provably incompatible, for example an unrelated interface and a final class. The binding variable is in scope only where the test is definitely true, and the pattern variable may not redeclare a name already in scope.
if (!(shape instanceof Circle c)) {
return 0;
}
return Math.PI * c.r() * c.r(); // c is in scope hereWorked example: predict the output
Statement. What does this program print?
class Vehicle {
String describe() { return "vehicle " + wheels(); }
int wheels() { return 4; }
}
class Bike extends Vehicle {
int wheels = 2;
@Override int wheels() { return wheels; }
}
public class Main {
public static void main(String[] args) {
Vehicle v = new Bike();
System.out.println(v.describe());
System.out.println(v.wheels);
}
}Step 1. v.wheels does not compile: Vehicle has no field wheels. Fields are resolved by declared type, and Vehicle declares only a method with that name; a method and a field may share a name, but the field must exist in the declared type.
Step 2. Remove that line. describe() is inherited and calls wheels(). Dynamic dispatch selects Bike.wheels(), which returns the field wheels, which is 2 once the constructor has run.
Solution. After removing the faulty line the output is vehicle 2; as written, compilation fails at v.wheels.
Why the alternatives are worse. Answering vehicle 4 forgets dynamic dispatch. Answering 2 for the second line forgets that fields use the declared type. Answering "prints vehicle 2 then 2" misses the compile error entirely.
Common pitfalls and exam tips
- Exam tip: read the declared type and runtime type separately on every line. Methods follow the runtime type, fields and static methods follow the declared type.
- A constructor named like the class but with a return type (even
void) is an ordinary method, not a constructor. this()andsuper()cannot both appear, and neither can appear after another statement.- A subclass cannot reduce visibility: overriding a
publicmethod with a package-private one fails to compile. Review checked exceptions in overriding methods for the related throws restriction. - Static methods cannot be overridden, and
@Overrideon one is a compile error. - Records: forgetting that the accessor is
name()rather thangetName()is a frequent distractor. - A sealed type with a permitted subclass that is not
final,sealedornon-sealeddoes not compile. Plan.valueOf("pro")fails: matching is case-sensitive. The generic Enum.valueOf form also requires the enum Class argument.- Interface constants are implicitly
static final; assigning to one is a compile error.
Mini case study: modelling subscriptions
OnLevelUp sells two kinds of access: a monthly subscription and a lifetime purchase of one course. A junior developer proposes class Access with a String type field and if chains everywhere. Redesign it using the features of this lesson.
Solution. Use a sealed interface with record implementations, and an enum for billing periods:
enum Period { MONTHLY, LIFETIME }
sealed interface Access permits Subscription, CoursePurchase {
boolean grants(String courseId);
}
record Subscription(String userId, Period period) implements Access {
public Subscription { if (period != Period.MONTHLY) throw new IllegalArgumentException(); }
public boolean grants(String courseId) { return true; }
}
record CoursePurchase(String userId, String courseId) implements Access {
public boolean grants(String id) { return courseId.equals(id); }
}The sealed interface lets a switch over Access be exhaustive without default. See Java 21 switch patterns and exhaustiveness for the matching rules. Adding a third subtype requires recompiling exhaustive switches that no longer cover the hierarchy. Records give equality and toString for free, and the compact constructor validates data at creation time. An enum prevents invalid period strings. The String type design would silently accept typos and push errors to runtime.
Key terms
- Overriding: a subclass instance method replacing an inherited one with the same signature.
- Hiding: a static method or field in a subclass masking the parent member, resolved by declared type.
- Compact constructor: record constructor without a parameter list, run before fields are assigned.
- Sealed type: a class or interface that lists its permitted direct subtypes.
- Effectively final: a variable never reassigned after initialization.
- Eligible for GC: no reachable reference to the object remains.
- Default method: an interface method with a body, inherited by implementers.
- Covariant return: an overriding method returning a subtype of the original return type.
Summary checklist
- I can trace static, instance and constructor initialization across a two-level hierarchy.
- I know what a record generates and what a compact constructor can and cannot do.
- I can choose between static nested, inner, local and anonymous classes and know their access rules.
- I can say after which line an object becomes eligible for garbage collection.
- I can apply the overriding rules for access, return type and checked exceptions.
- I can resolve default-method conflicts with
Interface.super.method(). - I can declare sealed hierarchies with
final,sealedandnon-sealedsubtypes. - I can write an enum with fields, a constructor and a constant-specific body.
Official sources
-
Oracle exam page for 1Z0-830 (objectives): https://education.oracle.com/java-se-21-developer-professional/pexam_1Z0-830 (opens in a new tab)
-
Java Language Specification, Java SE 21, chapter 8 (Classes): https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html (opens in a new tab)
-
JEP 395: Records: https://openjdk.org/jeps/395 (opens in a new tab)
-
JEP 409: Sealed Classes: https://openjdk.org/jeps/409 (opens in a new tab)
-
JEP 421: Deprecate Finalization for Removal: https://openjdk.org/jeps/421 (opens in a new tab)
-
The Java Tutorials, Classes and Objects: https://docs.oracle.com/javase/tutorial/java/javaOO/index.html (opens in a new tab)
-
Java SE 21 interfaces: https://docs.oracle.com/javase/specs/jls/se21/html/jls-9.html (opens in a new tab)
-
Java SE 21 initialization: https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html (opens in a new tab)
-
Java SE 21 overload resolution and instanceof: https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html (opens in a new tab)
-
Java SE 21 Enum API: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Enum.html (opens in a new tab)
-
Java SE 21 Cleaner API: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ref/Cleaner.html (opens in a new tab)
