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.
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 Level | Declaring Class | Same Package | Subclass (Different Package) | World (Different Package & Non-Subclass) |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes (via inheritance only) | No |
| (default / package-private) | Yes | Yes | No | No |
private | Yes | No | No | No |
Member vs. Top-Level Visibility Rules
- Top-Level Types: Top-level classes, records, and interfaces may only be declared
publicor package-private (default). Specifyingprivateorprotectedon 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 implicitlypublic, and private helper methods are explicitlyprivate. Specifyingprotectedinside 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:
- Any class located within the exact same package.
- 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) ornullfor 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:
volatileguarantees visibility, but does not guarantee atomicity (compound operations likecount++still require explicit synchronization orAtomicInteger).
Summary Comparison of Non-Access Modifiers
| Modifier | Permitted On | Primary Architectural Effect | Common Exam Trap |
|---|---|---|---|
final | Classes, Methods, Variables, Parameters | Prevents subclassing, overriding, or variable reassignment. | Final reference prevents pointer reassignment, not internal state mutation. |
static | Methods, Fields, Nested Classes, Blocks | Associates member with class type; eliminates instance requirement. | Static methods cannot invoke instance methods or access this. |
transient | Fields only | Excludes field from default serialization stream. | Deserialization resets transient fields to default zero/null values without running constructors. |
volatile | Fields only | Enforces 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> listprevents reassigninglist = new ArrayList<>(), but does NOT prevent callinglist.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 MyClassorprivate class MyClassat the top level of a file produces immediate compiler errors.
Given two classes in different packages:
package pkg.origin;
public class Engine {
protected int rpm = 3000;
protected void tune() {}
}
Which lines will cause a compile-time error?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();
}
}
A developer writes the following class intending to make it immutable:
Why is this class NOT truly 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; }
}
What memory visibility and ordering guarantee does the volatile modifier provide for a shared class field in Java?
Consider the following class in package net.service:
If class ClientApp resides in package net.client, what happens if ClientApp attempts to instantiate ConnectionPool?package net.service;
class ConnectionPool {
int poolSize = 10;
public void connect() {}
}