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
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.
renameandchangePriceexpress business intent and centralize validation.- The checkout status can only be transitioned via
borrowandreturnBook. - 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
| Modifier | Same Top-Level Class Nested Scope | Same Package | Subclasses in Other Packages | Other Code |
|---|---|---|---|---|
private | Yes | No | No | No |
| No modifier (package-level) | Yes | Yes | No | No |
protected | Yes | Yes | Conditional | No |
public | Yes | Yes | Yes | Yes |
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
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:
src/main/java/com/atlas/library/model/Book.javaUsers can import types:
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 Issue | Priority Answer |
|---|---|
| Is the field public? | Default: private |
| Do we need a setter? | Only provide if business logic allows arbitrary replacement |
| Identity field | Constructor injection + final + read-only query |
| Mutable collection getter | Snapshot, read-only view, or domain query |
| Package collaboration | Package-level members, carefully control package size |
| Extension point | Protected 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.