Object-Oriented Concepts in Java
Introduction
This lesson covers the third objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Using Object-Oriented Concepts in Java. Oracle does not publish a weighting per objective, but this is the widest topic of the exam and the one that feeds the most code-analysis questions: classes, records, nested classes, initialization order, inheritance, overriding versus hiding, abstract classes, interfaces, sealed hierarchies, enums and pattern matching with instanceof all appear here, and they reappear inside the questions of every other objective.
Exam questions in this area are rarely about definitions. They show two or three small types and a main method, then ask whether the code compiles, which line fails, or what is printed. To answer reliably you need the exact rules, not an intuition. This lesson gives you those rules, each with a realistic example, and a method for tracing initialization and dispatch by hand.
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:
- If you declare no constructor, the compiler adds a no-argument one with the same access as the class. As soon as you declare any constructor, the default one disappears.
- The first statement of a constructor is
this(...)orsuper(...). 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 plus subclasses), package-private (no keyword),private.
Overloading means several methods with the same name and different parameter lists. The compiler picks the overload at compile time using, in order: exact match or widening primitive conversion, then boxing, then varargs. A single print(long) call with an int argument chooses print(long) over print(Integer) because widening beats boxing.
2. Initialization order
This is a favorite exam topic. When a class is first used, the JVM runs, in textual order, the static field initializers and static blocks, once. 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 6
A 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(); // 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 record holding a
Listcomponent is not deeply immutable unless you copy it withList.copyOfin the constructor.
4. Nested classes
| Kind | Declared | Can access | Needs outer instance | Notes |
|---|---|---|---|---|
| Static nested | static class N inside a class |
outer static members | 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 not
Variables 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 eligible
Facts 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). Never rely on it. Use try-with-resources orjava.lang.ref.Cleaner.- 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
A subclass inherits non-private members. Overriding rules for an instance method:
- Same name and same parameter types (the signature).
- 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),privateandprivate staticmethods (helpers shared by 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. If a class inherits two default methods with the same signature from two interfaces, it does not compile unless the class overrides the method. 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, with exactly one abstract method |
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.
public sealed interface Shape permits Circle, Rectangle, Square { }
public record Circle(double r) implements Shape { }
public record Rectangle(double w, double h) implements Shape { }
public non-sealed class Square implements Shape { }
Rules:
- Every permitted subtype must directly extend or implement the sealed type and must itself be declared
final,sealedornon-sealed. Records and enums are implicitly final. - 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. It is implicitly final (unless a constant has a body), cannot extend a class, can implement interfaces, and its constructor is implicitly private.
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 testing an Object or an interface-typed expression against a class is fine. 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 here
Worked 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. - 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. Enum.valueOf("pro")fails: matching is case-sensitive.- 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, so adding a third kind later produces compile errors at each switch that needs updating. 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) and chapter 9 (Interfaces): 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)