3.2 Encapsulation and Access Modifiers

Key Takeaways

  • Java provides four access levels: public, protected, package-private (default), and private, with visibility strictly bounded by packages and inheritance hierarchies.
  • The protected modifier grants access within the same package and to subclasses in different packages, but cross-package subclass access requires reference variables of that subclass type or its subtypes.
  • Defensive copying is essential on both constructor input and getter output to prevent external callers from mutating private collection or array state.
  • The volatile modifier guarantees cross-thread memory visibility and happens-before ordering, while transient excludes fields from standard serialization streams.
Last updated: September 2026

Encapsulation and Access Modifiers

Encapsulation is the core object-oriented discipline of bundling internal state with authorized behavior while restricting unauthorized external access to implementation details. In Java SE 21, encapsulation is enforced through access modifiers, defensive copying strategies, robust class invariants, and non-access modifiers.


The Four Java Access Levels and Visibility Boundaries

Java provides four distinct visibility levels governed by three explicit keywords (public, protected, private) and one implicit default level (package-private):

Access LevelDeclaring ClassSame PackageSubclass (Different Package)World (Different Package & Non-Subclass)
publicYesYesYesYes
protectedYesYesYes (via inheritance only)No
(default / package-private)YesYesNoNo
privateYesNoNoNo

Member vs. Top-Level Visibility Rules

  • Top-Level Types: Top-level classes, records, and interfaces may only be declared public or package-private (default). Specifying private or protected on top-level type declarations causes a compile-time error.
  • Class Members: Fields, methods, constructors, and nested types declared within a class body can utilize all four access modifiers (public, protected, package-private, private).
  • Interface Members: In interfaces, constant fields are implicitly public static final, abstract methods and default methods are implicitly public, and private helper methods are explicitly private. Specifying protected inside an interface causes a compile-time error.

JavaBeans Standards and Accessor Conventions

The JavaBeans specification provides standardized patterns for encapsulating class properties:

public class UserProfile {
    private String username;
    private boolean active;       // Primitive boolean
    private Boolean verified;     // Wrapper Boolean
    private int loginAttempts;

    // Standard getter and setter for reference / non-boolean types
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    // JavaBeans convention for primitive boolean: isActive()
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }

    // EXAM TRAP: Wrapper Boolean MUST use getVerified(), NOT isVerified()
    public Boolean getVerified() { return verified; }
    public void setVerified(Boolean verified) { this.verified = verified; }

    // Validation-backed mutator protecting invariants
    public void setLoginAttempts(int loginAttempts) {
        if (loginAttempts < 0) {
            throw new IllegalArgumentException("Login attempts cannot be negative");
        }
        this.loginAttempts = loginAttempts;
    }
}

Defensive Copying: Mutable Objects and Collections

A frequent vulnerability in object-oriented architecture is reference aliasing, where external callers retain references to mutable objects encapsulated inside a class.

import java.util.*;

public final class SecurityToken {
    private final String tokenId;
    private final Date expirationDate;
    private final List<String> scopes;
    private final int[] rawKeys;

    public SecurityToken(String tokenId, Date expirationDate, List<String> scopes, int[] rawKeys) {
        this.tokenId = Objects.requireNonNull(tokenId, "tokenId required");
        
        // 1. Defensive copy of mutable Date object on input
        this.expirationDate = expirationDate != null ? new Date(expirationDate.getTime()) : null;
        
        // 2. Defensive copy of Collection input (protects against caller mutating the list)
        this.scopes = scopes != null ? new ArrayList<>(scopes) : new ArrayList<>();
        
        // 3. Defensive copy of primitive array on input
        this.rawKeys = rawKeys != null ? rawKeys.clone() : new int[0];
    }

    public String getTokenId() {
        return tokenId; // String is immutable, safe to return directly
    }

    // Defensive copy on getter output
    public Date getExpirationDate() {
        return expirationDate != null ? new Date(expirationDate.getTime()) : null;
    }

    // Return unmodifiable view or defensive snapshot
    public List<String> getScopes() {
        return Collections.unmodifiableList(scopes);
        // Alternatively in Java 10+: return List.copyOf(scopes);
    }

    // Defensive copy of internal array on output
    public int[] getRawKeys() {
        return rawKeys.clone();
    }
}

Shallow vs. Deep Immutability

  • Collections.unmodifiableList() & List.copyOf(): Provide shallow immutability. The collection container cannot be resized, replaced, or sorted, but if the elements stored inside the collection are mutable objects (e.g., List<Customer>), callers can still invoke mutating methods on those individual elements!
  • Deep Immutability: Requires that every nested object and contained element is also strictly immutable or defensively cloned upon access.

Deep Dive: Cross-Package protected Access Rules

The protected modifier is heavily tested on the 1Z0-830 exam. It permits access to:

  1. Any class located within the exact same package.
  2. Any subclass located in a different package, but strictly through inheritance or through an explicit reference variable typed to that subclass (or its descendants).

JLS §6.6.2 Subclass Access Rule

When code in a subclass in package pkg.child accesses a protected member declared in a superclass in package pkg.parent, the access is only legal if the object reference expression is of the subclass type or one of its subtypes. Accessing the protected member on a reference of the superclass type or a sibling subclass type fails compilation!

// File: pkg/parent/Vehicle.java
package pkg.parent;

public class Vehicle {
    protected int speed = 60;
    protected void honk() {
        System.out.println("Beep!");
    }
}
// File: pkg/child/Car.java
package pkg.child;

import pkg.parent.Vehicle;

public class Car extends Vehicle {
    public void testProtectedAccess() {
        // CASE 1: Direct access via inherited 'this' -> VALID
        System.out.println(this.speed);
        this.honk();

        // CASE 2: Access via Car reference (Subclass reference) -> VALID
        Car myCar = new Car();
        System.out.println(myCar.speed);
        myCar.honk();

        // CASE 3: Access via SportsCar reference (Subtype of Car) -> VALID
        SportsCar sportsCar = new SportsCar();
        System.out.println(sportsCar.speed);
        sportsCar.honk();

        // CASE 4: Access via Vehicle reference (Superclass reference) -> COMPILE ERROR!
        Vehicle rawVehicle = new Vehicle();
        // System.out.println(rawVehicle.speed); // ERROR: speed has protected access in Vehicle
        // rawVehicle.honk();                   // ERROR: honk() has protected access in Vehicle

        // CASE 5: Access via Sibling reference (Boat extends Vehicle) -> COMPILE ERROR!
        // Boat boat = new Boat();
        // System.out.println(boat.speed);      // ERROR: speed has protected access in Vehicle
    }
}

class SportsCar extends Car {}

Non-Access Modifiers on Fields, Methods, and Classes

Non-access modifiers qualify runtime characteristics, threading behavior, and immutability invariants:

The final Modifier

  • On a Class: Forbids extension (public final class SecurityEngine).
  • On a Method: Forbids overriding or hiding in subclasses (public final void validate()).
  • On a Variable / Field: Guarantees single assignment. A blank final instance field must be assigned exactly once in every constructor path or instance initializer block; failure to initialize causes a compile-time error.

The static Modifier

  • Binds the member to the class namespace rather than individual object instances.
  • Static fields are shared across all instances loaded by the same ClassLoader.
  • Static methods cannot access this, super, or instance fields without an explicit object reference.

The transient Modifier

  • Signals to the standard Java Object Serialization mechanism (java.io.Serializable) that the field should not be serialized into the byte stream.
  • Upon deserialization, transient fields are reset to their default primitive values (0, false, 0.0) or null for reference types.

The volatile Modifier

  • Ensures that reads and writes to a field go directly to main memory rather than being cached in thread-local CPU caches.
  • Establishes a happens-before relationship, guaranteeing memory visibility across threads.
  • Exam Distinction: volatile guarantees visibility, but does not guarantee atomicity (compound operations like count++ still require explicit synchronization or AtomicInteger).

Summary Comparison of Non-Access Modifiers

ModifierPermitted OnPrimary Architectural EffectCommon Exam Trap
finalClasses, Methods, Variables, ParametersPrevents subclassing, overriding, or variable reassignment.Final reference prevents pointer reassignment, not internal state mutation.
staticMethods, Fields, Nested Classes, BlocksAssociates member with class type; eliminates instance requirement.Static methods cannot invoke instance methods or access this.
transientFields onlyExcludes field from default serialization stream.Deserialization resets transient fields to default zero/null values without running constructors.
volatileFields onlyEnforces cross-thread memory visibility and prevents instruction reordering.Does not make compound operations (like x++) thread-safe.

Common 1Z0-830 Exam Traps

  • Cross-Package Protected Access: Remember that a subclass in another package CANNOT access a protected member through a parent reference (Parent p = new Parent(); p.protectedField; -> ERROR).
  • Mutable References in Final Fields: Declaring final List<String> list prevents reassigning list = new ArrayList<>(), but does NOT prevent calling list.add("new item").
  • Package-Private Class Invisibility: A public method inside a package-private class cannot be called from another package because the enclosing class itself is completely invisible.
  • Top-Level Modifier Violations: Attempting to declare protected class MyClass or private class MyClass at the top level of a file produces immediate compiler errors.
Loading diagram...
Java Access Modifier Boundary Matrix
Test Your Knowledge

Given two classes in different packages:

package pkg.origin;
public class Engine {
    protected int rpm = 3000;
    protected void tune() {}
}
package pkg.custom;
import pkg.origin.Engine;

public class TurboEngine extends Engine {
    public void test() {
        Engine e = new Engine();
        TurboEngine te = new TurboEngine();
        
        // Line 1: System.out.println(this.rpm);
        // Line 2: te.tune();
        // Line 3: System.out.println(e.rpm);
        // Line 4: e.tune();
    }
}
Which lines will cause a compile-time error?

A
B
C
D
Test Your Knowledge

A developer writes the following class intending to make it immutable:

public final class DataSet {
    private final String label;
    private final int[] values;

    public DataSet(String label, int[] values) {
        this.label = label;
        this.values = values;
    }

    public String getLabel() { return label; }
    public int[] getValues() { return values; }
}
Why is this class NOT truly immutable?

A
B
C
D
Test Your Knowledge

What memory visibility and ordering guarantee does the volatile modifier provide for a shared class field in Java?

A
B
C
D
Test Your Knowledge

Consider the following class in package net.service:

package net.service;

class ConnectionPool {
    int poolSize = 10;
    public void connect() {}
}
If class ClientApp resides in package net.client, what happens if ClientApp attempts to instantiate ConnectionPool?

A
B
C
D