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.
Last updated: September 2026

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, and public), 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:

  1. 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.
  2. High Architectural Coupling: When client code references fields directly (such as account.balance), any subsequent refactoring of internal data structures—such as changing balance from a double to a BigDecimal or converting it to a distributed ledger call—immediately breaks every dependent class across the entire codebase.
  3. 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 private is 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 default access modifier keyword for members in Java! The keyword default is reserved exclusively for switch statement fallback labels and default method implementations in interfaces. Writing default int count = 0; inside a class causes a compile-time syntax error.

3. protected Access

  • A member declared protected is accessible to:
    1. All classes located in the same package (identical to package-private access).
    2. 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 (this or an instance of that subclass), not by attempting to dereference an arbitrary parent class reference variable.

4. public Access (Least Restrictive)

  • A member declared public is 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 ModifierEnclosing ClassSame PackageSubclass (Different Package)World (Any Package)
privateYesNoNoNo
Default (no keyword)YesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

[!IMPORTANT] Top-Level Class Visibility Rules: In Java, a top-level class (a class declared directly inside a .java file, not nested within another class) can have only two permissible access levels: public or package-private (default). Declaring a top-level class with private or protected causes an immediate compilation failure: modifier private not allowed here or modifier protected not allowed here. Furthermore, a single .java source file can contain at most one public top-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:

  1. Declare all instance variables with the private access modifier.
  2. Expose public accessor methods (getters) to permit safe read access to internal state.
  3. 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) or get, 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.

Loading diagram...
Encapsulation Architecture: Guarding State with Access Modifiers
Test Your Knowledge

Consider the following class definition:

public class Thermostat {
    private double temperature;

    public Thermostat(double initialTemp) {
        this.temperature = initialTemp;
    }

    public double getTemperature() {
        return this.temperature;
    }
}
Which statement accurately describes the architectural design of the Thermostat class?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Examine the following code snippet:

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;
    }
}
What happens when external client code executes account.setBalance(-100.0); on an active BankAccount instance?

A
B
C
D