Search Tutorials


1Z0-830 Java SE 21 - OOP Concepts | JavaInUse

1Z0-830 OOP Concepts - Java SE 21 Certification Prep

Object-oriented programming is the foundation of the 1Z0-830 exam. This page covers the core OOP mechanics tested on the exam: class and object structure, constructor chaining rules, inheritance and method overriding, interface features through Java 9, polymorphism and casting, and the newer record and sealed class types introduced in Java 16 and 17. Access modifier rules and their interaction with inheritance are also a consistent source of exam questions.

Classes and Objects

A class is a blueprint that defines fields, constructors, and methods. An object is an instance of a class created with the new keyword. Understanding the distinction between instance members (one per object) and static members (shared across all objects of the class) is essential for the exam.
public class Person {
    // Instance fields - each Person object has its own copy
    private String name;
    private int age;

    // Static field - one copy shared by all Person objects
    private static int count = 0;

    // Canonical constructor - initializes all fields
    public Person(String name, int age) {
        this.name = name;  // this.name refers to the field; name refers to the parameter
        this.age = age;
        count++;
    }

    // No-arg constructor delegates to the other constructor via this()
    // this() must be the first statement in the constructor body
    public Person() {
        this("Unknown", 0);
    }

    // Instance method - operates on this object's fields
    public void introduce() {
        System.out.println("I am " + name + ", age " + age);
    }

    // Static method - cannot access instance fields or use this
    public static int getCount() {
        return count;
    }

    // Getters and setters - provide controlled access to private fields
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
}

// Creating objects with new
Person p1 = new Person("Alice", 30);
Person p2 = new Person();              // Uses the no-arg constructor
int total = Person.getCount();         // Static method called on the class, not an instance
Exam Tip: Static methods and fields belong to the class, not to any instance. A static method cannot refer to this, cannot call instance methods directly, and cannot access instance fields. Attempting any of these is a compile error. Calling a static method via an object reference (such as p1.getCount()) is allowed by the compiler but is misleading - it is equivalent to Person.getCount() and does not use p1 at all.

Constructors

Constructors initialize a new object. Java enforces strict rules about constructor chaining: a call to another constructor via this() or to the parent class via super() must be the very first statement in the constructor body, and only one of the two can appear in any single constructor.
public class Animal {
    private String name;

    // No-arg constructor delegates to the parameterized one
    public Animal() {
        this("Unknown");  // this() - must be the FIRST statement
    }

    // Parameterized constructor - the actual initialization logic
    public Animal(String name) {
        this.name = name;
    }
}

public class Dog extends Animal {
    private String breed;

    // Calls parent constructor first, then initializes breed
    public Dog(String name, String breed) {
        super(name);       // super() - must be the FIRST statement
        this.breed = breed;
    }

    // Delegates to the two-argument constructor via this()
    public Dog(String name) {
        this(name, "Unknown");  // this() delegates within Dog
    }
}

// Constructor rules to memorize for the exam:
// 1. If you define no constructor, the compiler adds a public no-arg constructor automatically.
//    If you define ANY constructor, the compiler does NOT add the no-arg constructor.
// 2. this() and super() must each be the first statement if used.
// 3. You cannot use both this() and super() in the same constructor.
//    (this() eventually leads to a super() call somewhere up the chain.)
// 4. If a constructor does not begin with this() or super(), the compiler
//    silently inserts super() with no arguments before the first statement.
// 5. This implicit super() causes a compile error if the parent class
//    has no accessible no-arg constructor.
Exam Tip: If a parent class defines only parameterized constructors and no no-arg constructor, every child class constructor must explicitly call super(args) as its first statement. Forgetting this causes a compile error because the compiler tries to insert super() automatically and finds no matching constructor. This is one of the most common constructor traps on the exam.

Inheritance

A subclass inherits all accessible fields and methods from its parent class and can override instance methods to provide specialized behavior. Java supports single inheritance for classes - a class can extend only one other class. Method overriding is resolved at runtime based on the actual type of the object, not the declared type of the reference.
// Parent (superclass)
public class Vehicle {
    protected String brand;  // protected - accessible in subclasses

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public void start() {
        System.out.println(brand + " vehicle starting");
    }

    public String getBrand() {
        return brand;
    }
}

// Child (subclass) - inherits everything accessible from Vehicle
public class Car extends Vehicle {
    private int doors;

    public Car(String brand, int doors) {
        super(brand);      // Initialize the parent part of the object
        this.doors = doors;
    }

    // Override - replaces Vehicle's start() for Car objects
    // @Override annotation is optional but strongly recommended - it causes a compile
    // error if the signature does not actually match a parent method
    @Override
    public void start() {
        System.out.println(brand + " car starting with key");
    }

    // New method - only exists in Car, not in Vehicle
    public void honk() {
        System.out.println("Beep!");
    }

    // Call parent method explicitly using super
    public void startWithParentBehavior() {
        super.start();  // Calls Vehicle's start()
        System.out.println("Car-specific startup complete");
    }
}

// Method overriding rules (all must be satisfied):
// 1. Method name and parameter list must be identical (same signature)
// 2. Return type must be the same or a covariant subtype (e.g., returning Dog where Animal is declared)
// 3. Access modifier must be the same or LESS restrictive (can widen, cannot narrow)
// 4. Cannot declare new checked exceptions not in the parent's throws clause
//    (can declare fewer, narrower, or none; can always add unchecked exceptions)
// 5. Cannot override methods marked final, static, or private
//    (static methods are hidden, not overridden; private methods are not inherited at all)
Exam Tip: Overriding and hiding are different. An instance method is overridden - the version called is determined by the runtime type of the object. A static method is hidden - the version called is determined by the declared type of the reference at compile time. If a subclass defines a static method with the same signature as a parent static method, it hides the parent method; it does not override it. Calling via a parent-typed reference always calls the parent static method, regardless of the actual object type.

Interfaces

An interface defines a contract that implementing classes must fulfill. Since Java 8, interfaces can also carry default implementations and static methods. Since Java 9, they can contain private methods to share logic between default methods without exposing it. All of these features are tested on the exam.
public interface Flyable {
    // Constant - implicitly public static final
    // Cannot be reassigned; always accessed as Flyable.MAX_ALTITUDE
    int MAX_ALTITUDE = 10000;

    // Abstract method - implicitly public abstract
    // Every non-abstract implementing class must provide a body for this
    void fly();

    // Default method (Java 8+) - provides a body that implementing classes inherit
    // Implementing classes can override it; if not overridden, the default is used
    default void land() {
        System.out.println("Landing...");
        prepareForLanding();  // Can call private helper methods
    }

    // Static method (Java 8+) - called on the interface, not on instances
    // NOT inherited by implementing classes or sub-interfaces
    static void checkWeather() {
        System.out.println("Checking weather conditions");
    }

    // Private method (Java 9+) - helper for default methods; not inherited or overridable
    private void prepareForLanding() {
        System.out.println("Preparing landing gear");
    }
}

// Implementing a single interface
public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird flapping wings");
    }
    // land() is inherited from Flyable and does not need to be overridden
}

// Implementing multiple interfaces - a class can implement any number of interfaces
public class Duck implements Flyable, Swimmable {
    @Override
    public void fly() { System.out.println("Duck flying"); }

    @Override
    public void swim() { System.out.println("Duck swimming"); }
}

// Default method conflict resolution:
// If two interfaces provide default methods with the same signature,
// the implementing class MUST override the method to resolve the ambiguity
interface A {
    default void greet() { System.out.println("Hello from A"); }
}

interface B {
    default void greet() { System.out.println("Hello from B"); }
}

class C implements A, B {
    @Override
    public void greet() {
        A.super.greet();  // Explicitly choose A's version if desired
    }
}

// An interface can extend multiple interfaces
public interface SuperFlyable extends Flyable, Controllable {
    void turbo();  // Additional abstract method
}
Exam Tip: A class can implement multiple interfaces but extend only one class. Interface fields are always public static final - they are constants, never instance variables. Interface abstract methods are always public abstract. Default methods are always public. Static interface methods are NOT inherited by implementing classes - you cannot call them via an instance or an implementing class name, only via the interface name itself (for example, Flyable.checkWeather()).

Polymorphism

Polymorphism means that a reference variable of a parent type can hold an object of any subtype. The method that is actually executed is determined at runtime by the object's actual type, not by the declared type of the reference. This is called dynamic dispatch or runtime binding, and it is the mechanism behind overriding. Overloading, by contrast, is resolved entirely at compile time based on the declared types of the arguments.
// Polymorphic reference - declared type is Animal, actual type is Dog
Animal animal = new Dog("Buddy", "Labrador");

// speak() is resolved at runtime - Dog's version is called even though
// the reference type is Animal. This is runtime polymorphism (dynamic dispatch).
animal.speak();

// You can only call methods declared in the reference type (Animal)
// animal.fetch() would be a compile error even if the object is a Dog

// Pattern matching instanceof (Java 16+) - check type and extract in one step
if (animal instanceof Dog dog) {
    dog.fetch();  // dog is in scope and already cast to Dog
}

// Traditional instanceof and cast (pre-Java 16 style)
if (animal instanceof Dog) {
    Dog d = (Dog) animal;    // Explicit cast - safe because instanceof passed
    d.fetch();
}

// ClassCastException - thrown at runtime if cast is invalid
Animal cat = new Cat("Whiskers");
Dog d = (Dog) cat;  // Compiles (compiler allows it) but throws ClassCastException at runtime

// Method overloading - compile-time polymorphism
// The compiler selects the method to call based on the declared types of the arguments
public class Calculator {
    public int    add(int a, int b)       { return a + b; }
    public double add(double a, double b) { return a + b; }
    public int    add(int a, int b, int c){ return a + b + c; }
}

// Overloading rules:
// - Methods must differ in the number, type, or order of parameters
// - Return type alone is NOT sufficient to distinguish overloads (compile error)
// - Access modifier may differ between overloads
// - Widening conversions are applied when no exact match exists
//   (e.g., add(1, 2) where 1 and 2 are int will match add(double, double)
//    if no add(int, int) exists, by widening int to double)

// Widening vs autoboxing: the compiler prefers widening over autoboxing
// add(1, 2) prefers add(long, long) over add(Integer, Integer) if both exist
Exam Tip: For overriding, the method called depends on the runtime type of the object - this is dynamic dispatch. For overloading, the method called depends on the compile-time (declared) types of the arguments - this is static dispatch. A common exam question is to give you a polymorphic reference and ask which overloaded version is selected: the answer is always based on the declared type of the argument, not its runtime type.

Records (Java 16+)

A record is a special kind of class designed to be a transparent, immutable data carrier. It eliminates the boilerplate of writing fields, a constructor, accessors, equals(), hashCode(), and toString() by generating all of them automatically from a compact component declaration.
// Record declaration - components listed in the header
public record Point(int x, int y) { }

// The compiler automatically generates:
// - private final int x and private final int y
// - A canonical constructor: Point(int x, int y)
// - Public accessor methods x() and y()  - NOT getX() / getY()
// - equals() that compares all components
// - hashCode() based on all components
// - toString() such as "Point[x=10, y=20]"

Point p = new Point(10, 20);
int x = p.x();   // 10 - note: no "get" prefix
int y = p.y();   // 20
System.out.println(p);  // Point[x=10, y=20]

// Compact constructor - validates or normalizes component values
// Parameters are implicit; you do not declare them again
// After the body, fields are assigned from whatever the parameters hold
public record Person(String name, int age) {
    public Person {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        name = name.strip();  // Reassigning the implicit parameter normalizes the stored value
    }
}

// What records CAN do:
// - Define static fields and static methods
// - Define additional instance methods
// - Implement interfaces
// - Override individual accessor methods or toString/equals/hashCode
// - Define custom constructors (must delegate to the canonical constructor)

// What records CANNOT do:
// - Extend any class (records implicitly extend java.lang.Record)
// - Be extended by other classes (records are implicitly final)
// - Declare additional instance fields beyond the components
// - Be abstract

public record Employee(String name, String dept)
    implements Comparable<Employee> {

    static int instanceCount = 0;  // Static field - allowed

    public String greeting() {
        return "Hello, " + name;   // Additional method - allowed
    }

    @Override
    public int compareTo(Employee other) {
        return name.compareTo(other.name);
    }
}
Exam Tip: Record accessor methods share the name of the component with no "get" prefix: point.x() not point.getX(). Records are implicitly final and cannot be subclassed. The compact constructor does not list parameters - they are implicit and available inside the body. Because fields are assigned after the compact constructor body exits, assigning to a parameter inside the body (such as name = name.strip()) changes the value that gets stored in the field.

Sealed Classes (Java 17+)

A sealed class or interface uses the permits clause to explicitly list every class that is allowed to directly extend or implement it. This gives the compiler a complete, closed picture of the type hierarchy, which allows switch expressions over sealed types to be exhaustive without a default case. Sealed classes work particularly well in combination with records and pattern matching.
// Sealed class - only the listed classes may directly extend Shape
public sealed class Shape
    permits Circle, Rectangle, Triangle { }

// Each permitted subclass must be exactly one of: final, sealed, or non-sealed
// A subclass that is none of these three is a compile error

public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) { this.radius = radius; }
    public double radius()       { return radius; }
}

// A permitted subclass can itself be sealed, further restricting its own hierarchy
public sealed class Rectangle extends Shape
    permits Square {
    public double width()  { return 0; }
    public double height() { return 0; }
}

public final class Square extends Rectangle { }

// non-sealed reopens the hierarchy - any class may extend Triangle
public non-sealed class Triangle extends Shape {
    public double base()   { return 0; }
    public double height() { return 0; }
}

// Sealed interfaces - same rules apply to implementing classes
public sealed interface Vehicle
    permits Car, Truck, Motorcycle { }

public final class Car implements Vehicle { }

// Records are implicitly final, so they satisfy the final requirement
public record Truck(int capacity) implements Vehicle { }

// Exhaustive pattern matching - no default case needed because the compiler
// knows that Circle, Rectangle, and Triangle are the only Shape subtypes
double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
    };
}
Exam Tip: The three valid modifiers for a permitted subclass are final, sealed, and non-sealed. A subclass that is none of these three does not compile. The permits clause is optional only when all permitted subclasses are defined in the same source file as the sealed class - if they are in separate files, the permits clause is required. Permitted subclasses must be in the same module as the sealed class, or in the same package if no module is involved.

Access Modifiers

Access modifiers control which code can see a class, field, method, or constructor. The four levels in Java form a hierarchy from most restrictive to least restrictive. Understanding exactly which combinations are legal when overriding is one of the most reliably tested OOP topics on the exam.
// The four access levels, from most to least restrictive:

// private - accessible only within the same class
// Use for implementation details that no other code should see
private int secret;

// package-private (no modifier) - accessible within the same package
// Use when you want package-internal cooperation without exposing publicly
int packageField;

// protected - accessible within the same package AND in subclasses (even in other packages)
// Use for fields and methods that subclasses need but external code should not access
protected int inherited;

// public - accessible from anywhere
public int visible;

// Top-level class access:
// A top-level class can only be public or package-private
// public class Foo { }        - visible everywhere
// class Foo { }               - visible only within its package

// Nested class access:
// Nested (inner and static nested) classes may use all four access modifiers

// Method access and overriding:
// When overriding, the overriding method can make the method MORE visible, never LESS
class Parent {
    protected void method() { }
}

class Child extends Parent {
    @Override
    public void method() { }  // OK - widening from protected to public is allowed

    // @Override
    // private void method() { }  // Compile error - narrowing from protected to private
}

// Access modifier summary table (who can see the member):
// Modifier      | Same class | Same package | Subclass | Everywhere
// private       |    YES     |      NO      |    NO    |    NO
// (none)        |    YES     |      YES     |    NO    |    NO
// protected     |    YES     |      YES     |    YES   |    NO
// public        |    YES     |      YES     |    YES   |    YES

// Note on protected and subclasses in different packages:
// A subclass in a DIFFERENT package can access a protected member only through
// a reference of its own type or a subtype - not through a reference of the parent type
class Sub extends Parent {
    void demo() {
        Sub s = new Sub();
        s.inherited = 10;     // OK - accessing via subclass reference
        Parent p = new Parent();
        // p.inherited = 10;  // Compile error - different package, parent reference
    }
}
Exam Tip: When overriding, access can only stay the same or become wider. The order from narrowest to widest is: private, package-private, protected, public. A common exam trap is a child class trying to override a public method with a protected method - this is a compile error. Also note that private methods are not inherited at all; defining a method with the same signature in a subclass is not overriding, it is simply a new method that happens to share a name.

1Z0-830 Java SE 21 Certification - Table of Contents

Master all exam topics with comprehensive study guides and practice examples.


Popular Posts