8.1 Encapsulation and Access Modifiers
Key Takeaways
- Encapsulation bundles an object's state (fields) and behavior (methods) while hiding internal representation to prevent unauthorized direct external mutation.
- Java provides four access levels across three explicit keywords: private (defining class only), default/package-private (same package), protected (same package and subclasses in any package), and public (everywhere).
- The standard JavaBean encapsulation pattern mandates declaring all instance fields private while exposing public accessor (getter) and mutator (setter) methods.
- Mutator methods safeguard object state by embedding validation and business invariant logic to reject invalid inputs prior to modifying private fields.
- Top-level classes can only be declared public or default (package-private); declaring a top-level class private or protected causes a compile-time error.
8.1 Encapsulation and Access Modifiers
[!NOTE] Exam Focus: This section serves two of Oracle's published objectives directly: "use the private modifier" from the "Classes and Constructors" topic area, and "create and use accessor and mutator methods" from the "Java Methods" topic area. Candidates must demonstrate total mastery of data hiding principles, the four access levels (
private, default/package-private,protected, andpublic), getter and setter naming conventions, parameter validation within mutators, defensive copying, and top-level class access boundaries.
In object-oriented programming (OOP), encapsulation is the foundational software design principle of packaging an object's state (its instance fields) and its behavior (its instance methods) together into a single cohesive unit, while strictly regulating external access to internal data structures. Encapsulation is frequently referred to as data hiding or information hiding because it erects an architectural barrier between an object's internal implementation details and the external client code that interacts with that object.
1. The Principle of Encapsulation: Why Data Hiding Matters
To understand the necessity of encapsulation, consider the architectural hazards of writing an unencapsulated class where instance variables are directly exposed to the outside world using the public modifier.
The Hazards of Unencapsulated Code
When a class exposes its fields directly, any external class in the application can read, write, and manipulate those variables without restriction:
// POOR DESIGN: Completely unencapsulated class
public class BankAccount {
public String accountNumber;
public double balance;
}
// Client code in another class:
public class BankApp {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.accountNumber = null; // State corruption: null account number!
account.balance = -150000.0; // State corruption: negative balance violates business rules!
}
}
This unencapsulated design introduces three critical software vulnerabilities:
- State Corruption and Invariant Violations: External classes can assign invalid, out-of-range, or logically contradictory values to fields (for example, setting a bank balance to
-$150,000, an employee's age to-42, or an item quantity to-5). The class cannot protect its own internal integrity. - High Architectural Coupling: When client code references fields directly (such as
account.balance), any subsequent refactoring of internal data structures—such as changingbalancefrom adoubleto aBigDecimalor converting it to a distributed ledger call—immediately breaks every dependent class across the entire codebase. - Absence of Auditability and Access Control: Direct field mutation bypasses all opportunities to log transactions, trigger change events, verify user authorization, or synchronize state across threads.
The Encapsulated Alternative
By enforcing encapsulation, a class conceals its fields behind a protected boundary, exposing only well-defined, controlled public methods. The class itself becomes the sole arbiter of how its internal state is inspected, validated, and updated.
2. Java's Four Access Levels: The Modifier Hierarchy
Java regulates the visibility of classes, interfaces, constructors, variables, and methods through access modifiers. An access modifier precedes a declaration to specify precisely where that member can be referenced. Java provides four distinct access levels utilizing three explicit keywords plus an implicit default state:
1. private Access (Most Restrictive)
- A member marked
privateis accessible only within the exact body of the class in which it is declared. - It is completely inaccessible to all other classes, including classes residing in the exact same package, subclasses in any package, and unrelated external code.
- Exam Trap: Private members of a superclass are not directly inherited by subclasses. Although private fields occupy memory within the subclass object instance on the heap, they cannot be referenced directly by name in the subclass body.
2. Default / Package-Private Access (No Keyword)
- When a member or class declaration omits an access modifier keyword entirely, it receives package-private (often called default) access.
- A package-private member is accessible to any class that resides in the exact same package.
- It is completely inaccessible to any class outside that package, even if that external class extends the defining class via inheritance.
- Crucial Exam Rule: There is no
defaultaccess modifier keyword for members in Java! The keyworddefaultis reserved exclusively forswitchstatement fallback labels and default method implementations in interfaces. Writingdefault int count = 0;inside a class causes a compile-time syntax error.
3. protected Access
- A member declared
protectedis accessible to:- All classes located in the same package (identical to package-private access).
- Subclasses located in different packages, but strictly through inheritance.
- Subclass Access Nuance: When a subclass in a different package accesses a protected member, it must do so through an inherited reference (
thisor an instance of that subclass), not by attempting to dereference an arbitrary parent class reference variable.
4. public Access (Least Restrictive)
- A member declared
publicis accessible from any class in any package across the entire Java runtime environment, provided the enclosing class itself is accessible.
The Access Modifier Visibility Matrix
The following matrix summarizes the four access levels from most restrictive to least restrictive. Memorize this table for the 1Z0-811 examination:
| Access Modifier | Enclosing Class | Same Package | Subclass (Different Package) | World (Any Package) |
|---|---|---|---|---|
private | Yes | No | No | No |
| Default (no keyword) | Yes | Yes | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
[!IMPORTANT] Top-Level Class Visibility Rules: In Java, a top-level class (a class declared directly inside a
.javafile, not nested within another class) can have only two permissible access levels:publicor package-private (default). Declaring a top-level class withprivateorprotectedcauses an immediate compilation failure:modifier private not allowed hereormodifier protected not allowed here. Furthermore, a single.javasource file can contain at most onepublictop-level class, and the filename must match the name of that public class exactly.
3. Implementing the JavaBean Encapsulation Pattern
The standard Java industry convention for achieving clean encapsulation is the JavaBean design pattern. This design pattern dictates three fundamental practices:
- Declare all instance variables with the
privateaccess modifier. - Expose public accessor methods (getters) to permit safe read access to internal state.
- Expose public mutator methods (setters) to permit controlled write access to internal state.
package com.oracle.finance;
public class CustomerAccount {
// 1. Private fields: strictly isolated from external code
private String accountId;
private double balance;
private boolean active;
// Constructor establishes initial valid state
public CustomerAccount(String accountId, double initialDeposit) {
this.accountId = accountId;
if (initialDeposit >= 0.0) {
this.balance = initialDeposit;
} else {
this.balance = 0.0;
}
this.active = true;
}
// 2. Standard Accessor (Getter)
public String getAccountId() {
return this.accountId;
}
public double getBalance() {
return this.balance;
}
// Boolean Accessor: uses 'is' prefix convention
public boolean isActive() {
return this.active;
}
// 3. Mutator (Setter) with validation
public void setActive(boolean active) {
this.active = active;
}
public void deposit(double amount) {
if (amount > 0.0) {
this.balance += amount;
} else {
System.out.println("Deposit amount must be positive.");
}
}
public void withdraw(double amount) {
if (amount > 0.0 && amount <= this.balance) {
this.balance -= amount;
} else {
System.out.println("Invalid withdrawal amount or insufficient balance.");
}
}
}
Accessor and Mutator Naming Conventions
JavaBeans establish standardized naming conventions that IDEs, runtime frameworks, and the 1Z0-811 examination expect:
- Standard Getters: Begin with the prefix
get, followed by the field name with its first letter capitalized:getBalance(),getAccountId(),getLastName(). - Boolean Getters: Begin with the prefix
is(recommended) orget, followed by the capitalized property name:isActive(),isAvailable(),hasDiscount(). - Standard Setters: Begin with the prefix
set, followed by the capitalized field name, taking a single parameter whose data type matches the field type:setBalance(double balance),setActive(boolean active).
4. Mutator Validation, Invariants, and Defensive Copying
The primary practical benefit of encapsulation is the ability to enforce class invariants—rules and constraints that must remain true for every valid instance throughout its lifecycle.
Parameter Validation in Setters
By placing guard conditions inside mutator methods, a class can intercept and reject erroneous input before it can corrupt object state:
public class Employee {
private String name;
private int age;
private double hourlyRate;
public void setAge(int age) {
// Business invariant: legal employment age range
if (age >= 16 && age <= 75) {
this.age = age;
} else {
System.out.println("Validation Error: Age must be between 16 and 75.");
}
}
public void setHourlyRate(double hourlyRate) {
// Business invariant: minimum wage requirement
if (hourlyRate >= 15.0) {
this.hourlyRate = hourlyRate;
} else {
System.out.println("Validation Error: Hourly rate cannot fall below minimum wage ($15.00).");
}
}
public int getAge() {
return this.age;
}
public double getHourlyRate() {
return this.hourlyRate;
}
}
If client code executes emp.setAge(-10);, the guard clause evaluates to false, the assignment statement is bypassed, and the internal age field retains its prior valid value.
Read-Only and Write-Only Properties
Encapsulation gives developers fine-grained control over property mutability:
- Read-Only Property: Declare the instance field
private, provide a public getter, and provide no setter. The property can only be initialized during object construction (or via an immutable design). - Write-Only Property: Declare the instance field
private, provide a public setter, and provide no getter. This pattern is commonly used for sensitive credentials such as user passwords or API authorization keys, where external code can set or update the credential, but should never be able to read it back in plain text.
The Defensive Copying Trap
A subtle encapsulation defect occurs when a private field holds a reference to a mutable object (such as a primitive array, an ArrayList, or a java.util.Date), and the getter returns that direct reference to external callers:
public class Classroom {
private int[] testScores;
public Classroom(int[] scores) {
// Flaw 1: Storing direct reference to external array!
this.testScores = scores;
}
// Flaw 2: Returning direct reference to private array!
public int[] getTestScores() {
return this.testScores;
}
}
// Caller circumvents encapsulation:
int[] myScores = { 95, 88, 72 };
Classroom room = new Classroom(myScores);
// Mutation 1: External caller alters original array
myScores[0] = 0; // Directly mutates room's internal state!
// Mutation 2: External caller mutates array retrieved via getter
int[] leaked = room.getTestScores();
leaked[1] = 0; // Bypasses all encapsulation!
To prevent this reference leak, encapsulate mutable objects using defensive copying:
public class EncapsulatedClassroom {
private int[] testScores;
public EncapsulatedClassroom(int[] scores) {
// Store an independent copy
if (scores != null) {
this.testScores = scores.clone();
} else {
this.testScores = new int[0];
}
}
public int[] getTestScores() {
// Return an independent copy
return this.testScores.clone();
}
}
With defensive copying, mutations made by external code affect only their own local copies, leaving the internal state of EncapsulatedClassroom completely secure.
5. Common 1Z0-811 Exam Pitfalls
Pitfall 1: Direct Private Field Access from Another Class in the Same File
A classic 1Z0-811 trap presents two classes declared in the exact same .java source file, where one class attempts to access a private member of the other:
class Engine {
private int horsepower = 300;
}
public class Garage {
public static void main(String[] args) {
Engine e = new Engine();
System.out.println(e.horsepower); // COMPILE ERROR: horsepower has private access in Engine
}
}
Even though both classes live in the same source file and same package, private strictly restricts visibility to the enclosing class body Engine.
Pitfall 2: Assuming Default Access Means Public
If a class in package com.logistics declares a method void ship() without an access modifier, that method is package-private. If another class in package com.sales imports com.logistics.Order and calls order.ship(), compilation fails:
error: ship() is not public in Order; cannot be accessed from outside package
Importing a class does not grant access to its package-private members.
Pitfall 3: Top-Level Class Modifier Violations
Exam questions frequently display source code declaring private class Utility { } or protected class Config { } at the top level. Remember: top-level classes can never be marked private or protected.
Consider the following class definition:
Which statement accurately describes the architectural design of the Thermostat class?public class Thermostat {
private double temperature;
public Thermostat(double initialTemp) {
this.temperature = initialTemp;
}
public double getTemperature() {
return this.temperature;
}
}
A software engineer needs to declare a method so that it can be invoked by any class residing in the same package, and can also be invoked by subclasses located in different packages through inheritance, but remains strictly hidden from non-subclass classes in other packages. Which access modifier must the engineer choose?
Examine the following code snippet:
What happens when external client code executes public class BankAccount {
private double balance = 500.0;
public void setBalance(double balance) {
if (balance >= 0.0) {
this.balance = balance;
}
}
public double getBalance() {
return this.balance;
}
}
account.setBalance(-100.0); on an active BankAccount instance?