Skip to content

VOL 01 / CH 07 / LESSON 04

7.4 Encapsulation and Access Boundaries ​

Prerequisites: Fields, Methods, Constructors, and Object Invariants Estimated time: 30 minutes You are ready to continue when: You can hide representation details and only expose queries and business operations necessary to maintain invariants.

1. private fields prevent callers from bypassing the rules ​

java
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class Book {
    private final String isbn;
    private String title;
    private long priceCents;
    private boolean borrowed;
    private final List<String> tags = new ArrayList<>();

    public Book(String isbn, String title, long priceCents) {
        this.isbn = requireText(isbn, "isbn");
        rename(title);
        changePrice(priceCents);
    }

    public String isbn() {
        return isbn;
    }

    public String title() {
        return title;
    }

    public long priceCents() {
        return priceCents;
    }

    public boolean isBorrowed() {
        return borrowed;
    }

    public void rename(String newTitle) {
        title = requireText(newTitle, "title");
    }

    public void changePrice(long newPriceCents) {
        if (newPriceCents < 0) {
            throw new IllegalArgumentException("negative price");
        }
        priceCents = newPriceCents;
    }

    public boolean borrow() {
        if (borrowed) return false;
        borrowed = true;
        return true;
    }

    public boolean returnBook() {
        if (!borrowed) return false;
        borrowed = false;
        return true;
    }

    public void addTag(String tag) {
        tags.add(requireText(tag, "tag"));
    }

    public List<String> tags() {
        return List.copyOf(tags);
    }

    private static String requireText(String value, String name) {
        Objects.requireNonNull(value, name);
        if (value.isBlank()) {
            throw new IllegalArgumentException("blank " + name);
        }
        return value;
    }
}

The complete Book example is final, so subclasses cannot replace rename or changePrice while its constructor runs. The next chapter will address inheritance design. This class checks nonblank text, not ISBN format or uniqueness.

The class does not generate a setter for every field:

  • ISBN is immutable after creation, so it only has query methods.
  • rename and changePrice express business intent and centralize validation.
  • The checkout status can only be transitioned via borrow and returnBook.
  • Price is now stored as a long to represent the smallest currency unit, avoiding binary floating-point errors in monetary calculations; currency and rounding still require business agreements.

Encapsulation isn't "private fields + a set of public setters that can write back any value." The core is to let the object maintain its valid state and retain the freedom to change its internal representation.

2. Prevent External Modification When Returning Mutable Fields ​

If tags() directly returns the internal ArrayList, the caller can bypass addTag and perform operations like clearing or adding invalid values. List.copyOf(tags) returns an unmodifiable snapshot of the current elements, so subsequent internal changes will not affect the old snapshot.

This is a shallow snapshot: if the list elements themselves are mutable objects, those objects may still be shared. List.copyOf also rejects null elements. Defensive strategies should be designed in conjunction with the element type and API contract.

Another option is to return a read-only view. The view cannot be modified by the caller, but it may change as the internal list changes. Snapshots and views do not have an absolute advantage or disadvantage; the difference lies in temporal semantics.

3. Information Hiding Allows Internal Implementation Evolution ​

As long as the public contract remains unchanged, a class can:

  • Change the price from a long to a value object;
  • Add caching for normalized book titles;
  • Change the internal tag list to an insertion-ordered set when the public contract already requires unique tags;
  • Add audit information or a state machine.

Clients depend on the method contract, not the field layout. At the same time, changing the return type, exception strategy, or thread safety guarantees may still break compatibility and cannot be disguised away with "encapsulation."

4. Four Member Access Levels ​

ModifierSame Top-Level Class Nested ScopeSame PackageSubclasses in Other PackagesOther Code
privateYesNoNoNo
No modifier (package-level)YesYesNoNo
protectedYesYesConditionalNo
publicYesYesYesYes

Access to protected instance members of subclasses in other packages is also restricted by the "through which receiver" rule. This is further explained with code in the inheritance chapter. Public members may also be subject to Java module export rules.

Top-level classes can only be declared as public or package-level; private and protected can be used for member classes, but cannot directly modify top-level classes.

5. Packages Handle Naming and Access Boundaries ​

java
package com.atlas.library.model;

public final class Book {
    // ...
}

Package declarations appear after comments and whitespace, and before imports and type declarations. Common build tools require that directories correspond to package names, for example:

text
src/main/java/com/atlas/library/model/Book.java

Users can import types:

java
import com.atlas.library.model.Book;

They can also use the fully qualified name. Imports only allow source code to use short names; they do not copy the class, and they are not runtime "load commands." The unnamed package is a poor fit for regular projects: types in it cannot be imported into named packages.

6. JavaBeans Is a Convention, Not a Definition ​

getName(), setName(...), and primitive-boolean isActive() are common JavaBeans property naming conventions. Some frameworks use these conventions, but the specific support for constructors, fields, record classes, or annotations depends on the framework version and configuration.

Domain objects do not need to expose parameterless constructors and all setters just to look like beans. First, ensure the object's invariants are met. Then, configure the serialization, ORM, or UI binding tools the project actually uses.

Professional Quick Reference ​

Design IssuePriority Answer
Is the field public?Default: private
Do we need a setter?Only provide if business logic allows arbitrary replacement
Identity fieldConstructor injection + final + read-only query
Mutable collection getterSnapshot, read-only view, or domain query
Package collaborationPackage-level members, carefully control package size
Extension pointProtected requires inheritance of contract, not "half-public"

Before Moving On ​

Determine query and modification entry points for ISBN, book title, price, borrowing status, and tags; if the answer is simply "each item adds a getter/setter," the object's invariant can still be bypassed. For the tags collection, clearly specify whether a snapshot or a read-only view is returned, and explain why it remains a shallow boundary even when elements are mutable. Finally, distinguish what each of access modifiers, package declarations, and import statements is responsible for.

→ Next: Static Members and Class-Level State

Specification: JLS §6.6: Access Control.

Built with VitePress | Software Systems Atlas