Java Generics and Collections • Maurice Naftalin & Stuart Marks
Maurice Naftalin and Stuart Marks discuss the second edition of "Java Generics and Collections," 19 years after the first. They explore the evolution of Java from version 5 to 25, focusing on major shifts like the move toward immutability, the introduction of sequenced collections and streams, and the critical design principles of collection encapsulation. The conversation delves into the anemic domain model anti-pattern, the controversial `UnsupportedOperationException`, and the framework's inconsistent null handling, reflecting on decades of experience with the Java Collections Framework.
In the 19 years since the first edition of “Java Generics and Collections” was published for Java 5, the language and its programming style have evolved significantly. The release of Java 21, which introduced sequenced collections, served as a key catalyst for the second edition. However, the changes run deeper, reflecting a stylistic shift that makes code from the Java 5 era feel “antiquated.” Modern Java development has been reshaped by features like streams, records, unmodifiable collection factory methods, and the deprecation of wrapper class constructors (e.g., new Integer()).
The Shift Towards Immutability
A central theme in Java’s evolution is the increasing emphasis on immutability, a trend influenced by functional programming paradigms. While Java is not and will not become a purely functional language, the benefits of immutability are now widely recognized, especially with the advent of concurrent collections in Java 5.
This shift is evident in modern coding practices:
- Unmodifiable Collections: The introduction of factory methods like
List.of()andMap.of()in Java 9 made it easier to create unmodifiable collections, which are now common in examples. - Streams: The Streams API encourages a transformational approach to data processing. Instead of modifying a collection in place, developers are more likely to stream its elements, apply transformations, and collect the results into a new collection. This pattern pairs naturally with unmodifiability.
Guidance on Collection Usage and Encapsulation
A new chapter on “Usage Guidance” addresses critical design principles that have been learned over decades. The most important of these is the ownership and encapsulation of collections.
An object should own its state entirely.
This means preventing clients from obtaining a reference to an internal collection and modifying it, either accidentally or maliciously. To achieve this, developers must go beyond simple private fields and consider:
- Defensive Copying: When accepting a collection in a constructor or method, make a copy to store internally.
- Unmodifiable Wrappers: When returning an internal collection, wrap it using methods like
Collections.unmodifiableList()or return a defensive copy to prevent external modification.
A crucial distinction is made between unmodifiability (shallow immutability) and true immutability (deep immutability). An unmodifiable collection prevents changes to the collection itself, but if it contains mutable elements, the object’s state can still be altered. Proper encapsulation requires ensuring that no part of the state graph exposed to a client can be mutated.
The Anemic Domain Model Anti-Pattern
A common mistake is using bare collection types where a dedicated domain object is needed. For example, representing a list of projects as a List<Set<Task>>. This is an instance of the anemic domain model anti-pattern, where objects are merely data containers with no associated logic.
A better approach is to define a proper Project class that contains the Set<Task>. This allows the Project object to encapsulate its own state and behavior, such as calculating the total duration of its tasks. This avoids the need to request highly specialized collection classes with features like callbacks, as the custom logic can be implemented directly in the domain object’s methods.
A Retrospective on the Collections Framework’s Design
Another new chapter offers a design retrospective on the Collections Framework, acknowledging its remarkable longevity and thoughtful design by Joshua Bloch.
Extending the Framework Correctly
The book highlights the fragile base class problem, where subclassing a concrete implementation (e.g., ArrayList) can lead to broken code if the superclass’s implementation details change in a future JDK version. The framework was designed for extension through its abstract base classes (e.g., AbstractList), which provide a stable and safe way to create custom collection implementations.
The UnsupportedOperationException Controversy
A long-standing point of contention is the use of UnsupportedOperationException for unmodifiable collections, which seemingly violates the Interface Segregation Principle. The book defends this design choice as a pragmatic trade-off between conflicting forces, such as keeping the framework’s API surface small.
The issue is often a symptom of poor encapsulation and unclear ownership.
You should only be modifying things that you own. And so if you own it, you know that it’s modifiable.
The type system is just one way to enforce constraints; well-documented contracts and coding conventions are equally powerful.
The Challenge of Null Handling
The framework’s handling of null is described as its “least proudest moment” and a “mess.” The policy has been inconsistent throughout its history:
- JDK 1.0: Legacy collections like
VectorandHashtabledisallowednull. - JDK 1.2: The new Collections Framework generally allowed
null(e.g.,HashMap). - Java 5: Concurrent collections disallowed
nullagain.
This inconsistency has led to contradictions in specifications and behavior, creating a challenge that persists today. Despite these issues, the Collections Framework has proven to be a durable and effective part of the Java platform for nearly 30 years.