A rule that must remain true
In the previous chapter we created a Book with a title and a counter. The field loans is private and changes through recordLoan(). This prevents another part of the program from directly writing a negative number into the field, but does not yet demonstrate that the object is well designed: if the method itself subtracted one without checking, the rule would still be violated. Encapsulating means drawing a boundary around state and the operations governing it, so that from outside only actions consistent with the object's meaning can be performed.
A rule that must be true throughout an object's useful life is called an invariant. In a grade register, for example, we want at least one grade, and each grade must fall in the range from zero to thirty. If averaging is a register operation, it cannot make sense to ask it to divide by zero because somebody secretly emptied the array. The concrete problem, then, is not “how do I hide the fields?” but “how do I prevent an impossible state from being created or becoming observable?”.
We can explain information hiding with a car's gear lever. The analogy is useful as long as it reminds us that the driver uses controls rather than directly modifying gears. A lever alone, however, does not guarantee the gearbox cannot break: the implementation must also respect the rules. I therefore prefer to show those rules in code, where we can verify them.
Think of the difference between asking the car to engage a gear and directly writing the gear number into a variable. In the first case an operation can check whether the gearbox permits that request in its current condition; in the second the car's user must know and respect rules that belong to the car itself. The analogy has a limit: software does not prevent a programmer from writing a wrong implementation of the public method. This is why, after choosing a boundary, we test it with permitted and forbidden calls. The quality of encapsulation is measured in permitted actions and observable guarantees, not the number of private words in the file.
Further reading – Representation and contract. Representation is the way the class chooses to store data:
GradeRegisteruses an array,IntStackan array and a counter. The contract instead concerns what the class's user can expect: which values are permitted, which results are obtained and what happens in abnormal cases. If one day we replace the array with another structure, callers' code should continue to work as long as we maintain the contract. This is the practical advantage of the boundary.
A register that safeguards its own data
The following program is also available as GradeRegister.java. Save it with this name, compile it with javac --release 25 -Xlint:all -d build GradeRegister.java and run it with java -cp build GradeRegister from the file's folder.
public final class GradeRegister {
private final String student;
private final int[] grades;
public GradeRegister(String student, int[] grades) {
if (student == null || student.isBlank()) {
throw new IllegalArgumentException("missing student");
}
if (grades == null || grades.length == 0) {
throw new IllegalArgumentException("at least one grade is required");
}
for (int grade : grades) {
if (grade < 0 || grade > 30) {
throw new IllegalArgumentException("grade out of range");
}
}
this.student = student;
this.grades = grades.clone();
}
public GradeRegister(String student, int grade) {
if (grade < 0 || grade > 30) {
throw new IllegalArgumentException("grade out of range");
}
this(student, new int[] {grade});
}
public String student() {
return student;
}
public int[] grades() {
return grades.clone();
}
public double average() {
int sum = 0;
for (int grade : grades) {
sum += grade;
}
return (double) sum / grades.length;
}
public static void main(String[] args) {
int[] originals = {24, 27, 30};
GradeRegister register = new GradeRegister("Ada", originals);
originals[0] = 0;
int[] received = register.grades();
received[1] = 0;
System.out.println(register.student() + ": " + register.average());
}
}
The output is Ada: 27.0. In main we modify two arrays outside the register: first the one given to the constructor, then the one obtained from grades(). If the register had kept or directly returned its own array, the average could change without passing through any operation that checks the grades. The two calls to clone() instead create copies of the array; for an int array, copying the elements means copying primitive values. Figure 8.1 shows the boundary's two directions.
Important note –
finaldoes not freeze an array.private final int[] gradesprevents the class's code from reassigning the field to another array after initialization. It does not prevent changing the array elements. The invariant therefore depends on validation and copies as well as field visibility. If the elements were mutable objects,clone()would copy references and might not suffice: we would need to consider copying the elements or using a different model.
Checking the register's two doors
Let us follow construction one line at a time. originals contains three valid values. The constructor first checks the name, then the existence of at least one grade, then each one's range. Only afterward does it assign the fields and copy the array. If it found 31 during the check, it would interrupt construction: the caller would not receive a seemingly usable GradeRegister with an illegitimate datum hidden inside. This distinction matters because a check performed only by average() would come too late and leave other methods free to observe an incorrect state.
The first door is entry. If we replaced this.grades = grades.clone() with this.grades = grades, the array owned by the caller and the one kept by the register would be the same object. The line originals[0] = 0 would change the average from 27.0 to 19.0, although no register method had approved the change. The second door is exit. Even keeping the entry copy, if grades() simply returned grades, the line received[1] = 0 would change the average from 27.0 to 18.0. Both copies are needed because the two access paths are independent. The numbers make the violation visible: we are not making a copy “just to be safe” without knowing what problem it solves.
An int array is a good first example because its elements are primitive values. If the register kept an array of mutable Assessment objects, copying only the array would create a new container but leave the instances inside shared. A caller could still modify a grade through a reference they possess. The solution depends on the model: create copies of the elements, choose immutable objects or expose only views and operations that maintain the contract. Defensive copying means truly protecting the boundary, not automatically applying clone() to every structure.
Customer records
Consider a Customer class for a shop, with private first name, last name and telephone fields and getters and setters for every field. This form makes visible that a private field cannot be modified directly by external code, but it is not enough to protect a valid state. A newly created customer might exist without a first or last name; a setter might accept empty text. Even adding a null check requires care: if the last-name setter mistakenly checked the first name, validation would concern the wrong data. We must give the object a coherent rule from construction onward and verify it in every operation that could change it.
We return to the same domain in the Customer.java source. A customer is created with first name, last name and telephone; the full name can be read, while the telephone can be updated through an operation validating the new data. In this small version first and last name do not change after construction. It is a model choice, not a getter-and-setter limitation. If the shop had a procedure for correcting a last name, we could add a method with that procedure's rules.
public final class Customer {
private final String firstName;
private final String lastName;
private String telephoneNumber;
public Customer(String firstName, String lastName, String telephoneNumber) {
this.firstName = requiredText(firstName, "first name");
this.lastName = requiredText(lastName, "last name");
updateTelephone(telephoneNumber);
}
private static String requiredText(String text, String field) {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("missing " + field);
}
return text.strip();
}
public String fullName() {
return firstName + " " + lastName;
}
public String telephoneNumber() {
return telephoneNumber;
}
public void updateTelephone(String newNumber) {
this.telephoneNumber = requiredText(newNumber, "telephone");
}
public static void main(String[] args) {
Customer customer = new Customer(" Ada ", " Rossi ", " 123 ");
System.out.println(customer.fullName());
customer.updateTelephone(" 456 ");
System.out.println(customer.telephoneNumber());
}
}
The program prints Ada Rossi and 456. The private method requiredText centralizes the common check: it rejects null and text made only of whitespace, then removes leading and trailing whitespace. The public method updateTelephone expresses a modification allowed by the domain; it does not give the caller unrestricted field access. The class is final, so its constructor can call that method without the risk of a subclass executing an override on an incomplete object. When we study inheritance we will see why calling an overridable method from a constructor is often dangerous.
A real telephone number requires more careful rules than “nonempty text”: prefixes, spaces, extensions and international conventions depend on the context. Here the check is deliberately minimal and declared. Inventing a rule accepting only digits would reject valid numbers in many cases. The encapsulation lesson is not knowing how to validate every number in the world, but placing validation where the object protects its own meaning. If we change the rule, callers of updateTelephone continue to use the same operation, while its documented contract tells us which new inputs are allowed.
Returning to the stack, with verifiable rules
The stack encountered in chapter 7 stores elements in LIFO order: the last number pushed is the first popped. The complete IntStack class uses a private array and a size counter. The invariant is 0 ≤ size ≤ elements.length: the counter can neither be negative nor exceed capacity. The constructor rejects a nonpositive capacity; push checks that there is space; pop checks that an element exists. When one of these operations cannot be performed, it reports the reason with an exception, which we will read in chapter 12.
public void push(int value) {
if (size == elements.length) {
throw new IllegalStateException("full stack");
}
elements[size] = value;
size++;
}
public int pop() {
if (size == 0) {
throw new IllegalStateException("empty stack");
}
size--;
return elements[size];
}
Figure 8.2 freezes the stack after two pushes into a capacity of three. The DemoStack program prints 2, then 12 and 7: first it observes the element count, then pops in the reverse order from insertion.
Key concept – A contract, not just a private array. The user of
IntStackknows the operations and expected errors, not the internal index of the next free slot. The private array prevents direct modifications, while the checks maintain a valid relationship between capacity and size. We do not return the array: a caller must not be able to bypasspushandpop.
Following the stack without looking inside the array
Start with a new stack of capacity three. Its size() method returns zero. After push(7), the size is one; after push(12), it is two. The first pop() returns 12 and the size returns to one; the second returns 7 and the size returns to zero. This sequence teaches LIFO order without asking the object's user to know the internal indices. The invariant 0 ≤ size ≤ capacity holds after every successfully completed operation. It is useful to stop after each line and check both the returned value and the observable state.
What happens if we call pop() again? The stack is empty. Returning zero would be ambiguous, because zero is a value we could legitimately have pushed. The class chooses to report empty stack, without decreasing the counter below zero. What happens if we push a fourth element without popping any? The stack of capacity three is full: the method rejects the operation before writing beyond the array bounds. These two checks do more than avoid exceptions produced by the array; they express the data structure's contract with messages speaking of a full or empty stack.
An IntStack can be written with different names and behaviors. To avoid confusing contracts, we follow a growing story: in chapter 7 we named the operations, in this chapter we fix the rules, in chapter 12 we observe what happens when a call does not respect them. This way the reader does not have to learn that pop() returns zero on one page and that another pop() throws an exception on another as if they were the same API. A version returning zero when the stack is empty remains useful as a counterexample: it shows us why the new contract is better.
The exception IllegalArgumentException tells the constructor's caller that the argument does not respect the contract. A minimal explanation is needed here: throw interrupts construction and reports the problem, avoiding the delivery of an invalid register. In chapter 12 we will carefully distinguish exceptions of this kind, errors and cases where a program can recover. A message such as grade out of range is more useful than an exception without context, but a real application could also include the rejected value when it contains no sensitive data.
Visibility and responsibility
Java offers four relevant levels for class members. private limits access to the class declaring the member; without a modifier, access is within the package; protected includes the package and, with further rules, subclasses in other packages; public permits access where the class and its module are accessible. The access-control specification gives details, especially for protected. A subpackage is not the same package as the name preceding it, as we saw in C06.
| Member declaration | Within the class | Same package | Subclass outside package | Other packages |
|---|---|---|---|---|
private |
yes | no | no | no |
| no modifier | yes | yes | no | no |
protected |
yes | yes | yes, according to subclass access rules | no |
public |
yes | yes | yes | yes, if class and package are accessible |
The table helps us orient ourselves, but does not authorize declaring a field public merely because someone needs to read it. student() offers the name, average() offers a calculated result, and grades() offers a copy: each method declares what the register permits us to observe. A generic setter accepting any array would bypass the check unless it applied the whole contract again. Offering a domain operation, such as recordGrade, that checks the individual grade before updating state designed to change is often clearer.
What a class's user really sees
Take private final int[] grades. Another class cannot write register.grades[0], because the field name is private. If we made the field public, the expression would become accessible wherever the class is accessible, and anyone could assign -5 to a position: the constructor check would no longer maintain the invariant over time. Making a method public is different. register.average() exposes a calculation result, leaving representation control to the register. register.grades() is even more delicate: it returns data, but returns a copy precisely to avoid opening an indirect path to the private field.
Within a package, a declaration without an access modifier is reachable by other classes in that package. But the names library.catalog and library.catalog.internal indicate two distinct packages; the second does not automatically acquire access to the first's members. protected adds possible use in subclasses, with precise rules even when the subclass is in another package. Do not read it as “public for all subclasses and anyone using them”: in the inheritance chapter we will see why the expression through which access is attempted also matters.
For top-level classes the possibilities are not identical to those for members: a class can be public or have package access, but cannot be declared private as a top-level class. A nested class, however, is an outer class member and can be private. This detail lets us hide the support type too, not just its fields. Module accessibility remains a further boundary: a public class in an unexported package does not become accessible from every other module.
The delicate case of protected
Suppose library.base.Register declares a field protected int revisions and library.special.HistoricalRegister extends Register. In the subclass code it is legal to use the current instance's field, for example this.revisions. But being inside the subclass is not enough to read revisions through any reference to Register from the other package. The language protects the access the subclass needs to build its behavior; it does not open the field as if it were public. Within the base class's own package, however, protected access is also available to classes that do not extend it. This is why the phrase “visible to children” is too short and can lead to incorrect compilation predictions.
The design decision comes before the table. If a subclass needs the revision count, we can offer it a protected method exposing the data or, better, an operation respecting the base class's rule. Making a field mutable to help a subclass ties the two types' implementations together: changing the field's meaning can break code we no longer control. private is often a good starting point for fields, but choosing public and protected for methods requires knowing the contract's real users.
To verify – Packages and subpackages. Draw three files:
catalog.Book,catalog.Testandcatalog.test.Test. IfBookhas a method without a modifier, onlycatalog.Testcan call it thanks to the shared package. The wordcatalogin the third package's name grants it no privilege. Then change the method topublicand observe that visibility of theBooktype and any package export remain conditions to consider.
Constructors and delegation
GradeRegister has two constructors with different parameters: one receives an array, the other a single grade. This is a form of overloading: the name stays the same, but the parameter list lets the compiler choose which to call. The second uses this(student, new int[] {grade}) to delegate complete construction to the first. We thereby avoid two independent procedures that could diverge in validation. Methods can be overloaded in the same way; selection depends on declared parameters, not just return type.
Before delegation, the second constructor checks grade. This arrangement is permitted by flexible constructor bodies, which became stable in Java 25: statements can be executed before this(...) or super(...) while respecting constraints on referencing the instance under construction. In our prologue we read only the parameter; we do not call methods on the still-incomplete object. The official guide also illustrates more advanced cases, including assigning a field early before super(...). We do not need this here: introducing a new possibility does not make every complicated construction good.
Java version – Flexible constructors. The stable form arrives in Java 25. If you compile our example's second constructor for an earlier release, the check before
this(...)is invalid. You can use the traditional form by moving validation to the delegated constructor; the behavior required of the register stays the same. The official change summary distinguishes this stable form from its preview versions.
Following new without inventing a memory model
When we write new GradeRegister("Ada", originals), Java creates a new instance and performs its initialization. Before the constructor completes successfully, the caller does not receive a ready-to-use register. If validation throws IllegalArgumentException, the new expression does not return the instance to the caller. This is why the initial check is so important: we do not want to deliver an object with an empty array and hope that average() notices the problem later.
Comparing new with malloc suggests a concrete sequence of loading, allocation and assignment of this. This may help us intuit that a new object needs space, but hides decisive differences. new invokes Java creation and initialization rules; malloc reserves raw memory and knows nothing of Java constructors. Physical data placement and JVM optimizations are not part of the contract our program must use. What interests us instead is a verifiable property: two new GradeRegister(...) expressions produce two distinct instances, each with its own state.
If you declare no constructor, the compiler supplies a no-parameter one according to the class rules. But as soon as you declare one yourself, you should not expect a no-parameter constructor to still exist automatically. Our register has two explicit constructors and new GradeRegister() does not compile: the data needed to respect the invariant are missing. A constructor has no return type, not even void. You cannot call it again later as register.GradeRegister(...): changing an already-created object requires an ordinary method declaring the permitted change.
Two ways to initialize, one contract
The single-grade constructor could copy the three assignments of the array constructor. It seems little work, but at the first new rule we would need to remember to change both. Delegation with this(student, new int[] {grade}) makes both forms go through the same check. This is why delegation improves reliability, not just file brevity. The constructor called by delegation does not create a second instance: it completes construction of the same one.
In traditional Java, the call to this(...) or super(...) had to precede every other constructor statement. Java 25 permits a prologue within precise limits, as in our example's check of the grade parameter. The constructor cannot freely use the incomplete instance in that prologue. If you are reading a project compiled for Java 17 or 21, the form with a check before delegation is not available as stable syntax: move the check to the delegated constructor or to a validation expression compatible with the project's release.
Why a constructor is not just any method
A method can be called several times on an already-created object; a constructor participates in that object's birth. Its name matches the class name and no return type appears. If we wrote void GradeRegister(...), we would declare an ordinary method with an unusual name, not a constructor. The distinction also explains why the object cannot be “reconstructed” by calling its constructor again after obtaining it: a later change requires a method with its own contract.
When two constructors are overloaded, the parameter list must allow the signature to be selected. GradeRegister(String, int[]) and GradeRegister(String, int) are distinguished by the second parameter. Changing only the parameter's local name, or adding a different return type, does not create a new valid signature. The caller chooses a form based on the data they have; the class must enforce the same invariant in both. Delegation with this(...) is one way to guarantee this unity, but we must avoid a circular chain in which two constructors call each other without ever initializing the object.
Constructor access is part of the contract. If it is public, callers that see the class can use it; if it is private, only the class's code can call it directly. A static factory can choose which instance to return or validate data before constructing it, but the name “factory” does not force us to use a Singleton. If a domain requires a register for each student, sharing a single instance would be a modeling mistake even if private-constructor syntax permits it. Keywords govern access; the object's meaning decides the choice.
Nested objects, blocks and unique instances
Inner classes, static nested classes, local classes, initialization blocks and Singletons require attention even outside the register example. They are real tools, but are not mandatory steps for protecting the register's array. A nested class is declared inside another class; a nonstatic inner class is associated with an instance of the outer class, while a static nested class does not require that instance. The distinction becomes useful when a support type truly belongs to the outer object's implementation. Local and anonymous classes will return in the interfaces part, where they will have a concrete problem to solve.
An initialization block executes statements when the class or one of its instances is initialized. If a constructor expresses the work better, the block adds only another place to look. Construction order between base class and subclass will be tested in chapter 9; static and instance blocks also require distinguishing class initialization from object creation.
An experiment on initialization order
Print a message from a static block, one from an instance block and one from the constructor. Before running it, we can predict the result. When the class is initialized, its static initializers run in source order; in a simple launch with main, this happens before constructing instances. Every time we create an object, its class's instance initializers participate in construction before the constructor body. With two objects of the same class we will therefore see the static message once, and the instance and constructor messages twice each.
We do not say the static block runs “every time the class is loaded” as though loading and initialization were the same event. The JVM distinguishes these phases, and a class can be loaded before being initialized. For the teaching program we only need to observe that static code is not tied to an individual instance. If you can initialize a field with a simple expression or in the constructor, prefer the form making the value easiest to follow; a separate block is justified when it has recognizable work to do.
The DemoBlocks program offers a test with two static blocks, two instance blocks and two creations. Before running it, separate on paper what belongs to the class from what belongs to each object.
public class DemoBlocks {
static {
System.out.println("static 1");
}
static {
System.out.println("static 2");
}
{
System.out.println("instance 1");
}
{
System.out.println("instance 2");
}
public DemoBlocks() {
System.out.println("constructor");
}
public static void main(String[] args) {
new DemoBlocks();
new DemoBlocks();
}
}
The output begins with static 1 and static 2, only once in the launch shown. Then comes instance 1, instance 2, constructor for the first object; the same trio returns for the second. We do not simply read the file from top to bottom as a list of statements executed once. We distinguish class initialization, which precedes active use in our case, from repeated instance construction. Blocks of the same kind follow source order together with the relevant field initializations. If one class extends another, the base's initialization sequence also intervenes; chapter 9 will make it observable with a separate example.
Initialization blocks are not a Java 11 innovation: they were already available before that release. Knowing this matters because someone maintaining pre-Java-11 programs might otherwise think they cannot use them. Their purpose matters more than the date, however. If initialization is a simple assignment, writing it next to the field makes the code easier to follow. A block with complex effects requires a reason that the file's reader can recognize.
A private constructor can prevent other code from directly creating instances. It is one ingredient of the Singleton pattern, which offers one shared instance. In the grade register this would be wrong: each student must have their own register. A single instance does not automatically make global state safe or easy to test; we will use this solution only when the problem requires it.
The historical Singleton and the sharing question
A Singleton can use a static field initially set to null and a getInstance() method that constructs the object on the first call. In a single-threaded program the sequence seems clear: the first call creates the instance, later ones return it. With several threads, however, two calls can both observe the still-null field and construct two instances. Making only the constructor private does not solve the problem: that word limits who can invoke it, it does not coordinate concurrent executions. The concurrency chapter will explain tools for reasoning about ordering between threads; here we avoid printing the naive implementation as a safe model to copy.
Further reading – What atomic means. An operation is atomic with respect to other threads when its effect is observed as one indivisible step. In the naive Singleton, “check whether the field is
null” and “assign the newly constructed instance” are two separate steps: another thread can intervene between them. A private constructor does not make the pair atomic. Designing concurrent access requires synchronization rules or an initialization form already offering the necessary guarantee; reading the source sequence alone is not enough to prove it.
If the domain really requires a unique instance of a service without configuration parameters for each use, a one-constant enum can represent that choice simply. Even then, the service can have mutable state, and uniqueness of the instance does not automatically make concurrent changes correct. Another possibility is to explicitly create an object at the application's startup point and pass it to the components needing it. Readers of constructor signatures then see the dependency, and a test can supply a different instance. This practice is often called dependency injection: here the term indicates explicitly passing a collaborator, without requiring any framework.
Key concept – A single instance is not always an invariant. A customer's register, a calculation's stack and the book whose pages we turned in the previous chapter must be able to exist several times. Before choosing a Singleton, formulate the domain rule requiring exactly one instance and ask whether it must hold for the process, a session, a request or a user. If the answer changes with context, a global variable hidden behind
getInstance()makes the model less clear.
A protected constructor is yet another choice: it permits use in the package and subclasses according to access rules, without indiscriminately opening construction. It is useful when the type is designed for extension and the subclass must initialize the inherited part. We do not choose the modifier to imitate a Singleton; we choose it after deciding who can create or extend the type. In the next chapter we will see this decision in the Vehicle base class.
Three nested-class forms, three different relationships
A nonstatic member inner class is associated with an outer-class instance. Imagine a Library defining Scan inside itself: each Scan object could refer to the library it belongs to. Inside Scan, this indicates the current scan, while Library.this indicates the associated outer instance. This distinction between the two this references is essential for reading self-references. If two libraries each create a scan, the outer references differ; there is no single implicit global library.
An inner instance's relationship with its outer instance does not forbid declaring static members in the inner type. The prohibition on declaring static methods in an inner class belonged to versions before Java 16, not Java 25. A static method declared in the inner class belongs to the type and cannot implicitly use Library.this as an instance method would. JLS §8.1.3 precisely distinguishes the two. In our example Detail uses the outer instance; Format is instead a static nested type because it does not need that relationship.
Figure 8.3 redraws that relationship with two outer instances. Each detail knows the library that created it; there is no single library shared by all details.
Detail instance is associated with its own outer instance. A static nested class does not keep this implicit relationship.A static nested class belongs to the source structure of the outer class. Its instances do not receive an implicit reference to an outer object. In the following program, Format is declared inside the library class but works only on the data it receives. The relationship between the two types is visible in the code, without forcing every Format to remember a particular library. We can create several objects of that type: static here does not mean “unique instance”.
A local class is declared inside a block, for example a method body. Its name is available in that block's scope. If it uses a local variable of its enclosing method, that variable must be final or effectively final: that is, it can be assigned once and not changed afterward. Java accepts an unchanged variable even without the final modifier: what matters is that it is effectively final. Anonymous classes are another local form we will use when we encounter interfaces and have an example motivating their use.
The selection criterion is not “nesting always makes the file shorter”. A small private support class used only by the outer class can make the boundary clearer. A class that grows, has its own contract or is used by several parts of the program may deserve its own file. Nested classes also participate in access rules and can access the outer class's private members according to their form; this does not authorize dependencies that are hard to follow.
To see the three relationships in execution, DemoNestedClasses.java contains a complete case. The names are deliberately simple: here we observe relationships between instances, we are not yet designing a real library.
public class DemoNestedClasses {
private final int id;
public DemoNestedClasses(int id) {
this.id = id;
}
public class Detail {
public String description() {
return "library: " + DemoNestedClasses.this.id;
}
}
public static class Format {
public String name() {
return "catalog";
}
}
public String localLabel() {
String prefix = "id=";
class Label {
String text() {
return prefix + id;
}
}
return new Label().text();
}
public static void main(String[] args) {
DemoNestedClasses library = new DemoNestedClasses(7);
Detail detail = library.new Detail();
Format format = new Format();
System.out.println(detail.description());
System.out.println(format.name());
System.out.println(library.localLabel());
}
}
The call library.new Detail() associates the new Detail with that particular library. Inside the inner class, DemoNestedClasses.this.id unambiguously indicates the outer instance's field. new Format() does not need the library: Format is statically nested and its method does not read a DemoNestedClasses's state. The local class Label is visible only in the localLabel method; it reads prefix, which is not reassigned and is therefore effectively final. The program prints, in order, library: 7, catalog and id=7.
The historical drawing of inner classes showed two outer-class instances, each with its own inner object. We can reconstruct the situation without imagining a hidden global variable: if we create first = new DemoNestedClasses(7) and second = new DemoNestedClasses(9), then first.new Detail() produces a detail that reads 7 through DemoNestedClasses.this; second.new Detail() produces one that reads 9. Detail is a single declaration in the source, but each of its inner instances is associated with a particular outer instance. Precisely this association justifies the nonstatic form when behavior must use outer state.
If Format only needs to return the word catalog, it has no reason to carry an implicit relationship with a library. The static nested form expresses this independence better. But do not confuse the modifier with the static fields studied in chapter 7: we can create many Format objects, each with its own possible instance state. static here describes the nested declaration's relationship to the outer class, it does not limit the object count to one.
The local class Label meets a third need: it is useful only while executing localLabel(), so its name stays in the method block. The value prefix used inside it is a local variable of the surrounding method. Java requires that it not be reassigned after initialization; otherwise we could not read that value as a stable capture. Writing the word final is unnecessary if the code already respects the rule. A very long local class, or one reused in several methods, however, makes the method hard to read: in that case it is worth considering a member type or a separate file. The innermost form is not automatically the most encapsulated in the sense useful to the reader.
A small test changes only prefix after its declaration, adding for example prefix = "number="; before the local class. The program no longer compiles, because the local class captures a variable that is not effectively final. If instead you change the identifier passed to the library constructor, the lines reading outer-instance state change, but not catalog. This difference is the reason to choose carefully between an inner class and a static nested class.
To verify
First return to DemoNestedClasses: create two libraries with different identifiers and a Detail for each. Predict which lines change and explain why Format continues to print catalog. Then reassign prefix before the local-class declaration and verify that the compiler rejects the capture. Here the test must distinguish the relationship with the outer instance from the one with a local variable.
Then return to defensive copying of arrays: remove one call to clone() at a time and predict the average printed after changes to external arrays. Which copying direction is missing in each case? Activity C08 provides the commands and results with which to check the answer. Both experiments require the same reading method: identify which object or value remains reachable, then verify the prediction by running the program.