Introduction
One of the major goals of Java 9 was to make everyday programming simpler.
While Java 8 introduced Streams, Lambdas, and the Date & Time API, developers still found themselves writing verbose code for common tasks such as:
- Creating small collections
- Building immutable data
- Processing streams until a condition was met
- Handling nullable values in streams
Java 9 addressed these issues by introducing several practical improvements that quickly became part of everyday Java programming.
These enhancements include:
- Collection Factory Methods
- Immutable Collections
takeWhile()dropWhile()- Enhanced
iterate() Optional.stream()Stream.ofNullable()
Although individually small, together they significantly reduce boilerplate code and improve readability.
Learning Objectives
By the end of this article, you will be able to:
- Create immutable collections using factory methods.
- Understand the difference between immutable and unmodifiable collections.
- Use
takeWhile()anddropWhile(). - Generate finite streams using the new
iterate()method. - Convert
Optionalinto Streams. - Handle nullable objects in Stream pipelines.
- Apply these features in enterprise applications.
Collection Factory Methods
Before Java 9
Creating a simple list required multiple statements.
List<String> countries = new ArrayList<>();
countries.add("India");
countries.add("USA");
countries.add("Japan");
If immutability was required:
List<String> countries =
Collections.unmodifiableList(
Arrays.asList(
"India",
"USA",
"Japan"));
This is verbose and still leaves room for accidental modification of the underlying collection if it is shared.
Java 9 Solution
List<String> countries =
List.of(
"India",
"USA",
"Japan");
Simple.
Readable.
Immutable.
Characteristics of List.of()
List<String> list =
List.of("A", "B", "C");
Properties:
- Immutable
- Preserves insertion order
- Does not allow
null - Optimized for small collections
Attempting to modify it:
list.add("D");
Results in:
UnsupportedOperationException
Set.of()
Set<String> roles =
Set.of(
"ADMIN",
"USER",
"AUDITOR");
Properties:
- Immutable
- Duplicate elements are not allowed
nullvalues are not allowed
Example:
Set.of("A", "A");
throws:
IllegalArgumentException
This is an important difference from HashSet, which silently ignores duplicate insertions.
Map.of()
Before Java 9:
Map<String,Integer> scores =
new HashMap<>();
scores.put("Math",90);
scores.put("Physics",95);
Java 9:
Map<String,Integer> scores =
Map.of(
"Math",90,
"Physics",95);
Much cleaner.
Map.ofEntries()
For larger maps:
Map<String,Integer> marks =
Map.ofEntries(
Map.entry("Math",90),
Map.entry("Science",95),
Map.entry("English",88));
This avoids the limitation of Map.of() overloads and improves readability for many entries.
Immutable vs Unmodifiable
This distinction is often misunderstood.
Consider:
List<String> list =
new ArrayList<>();
Then:
List<String> view =
Collections.unmodifiableList(list);
view cannot be modified directly.
However, modifying the original list changes what view exposes.
list.add("Java");
The unmodifiable view reflects the change.
By contrast:
List<String> list =
List.of("Java");
creates an immutable collection whose contents cannot be changed through any reference.
Enterprise Use Cases
Collection factory methods are useful for:
- Application configuration
- Constants
- Role definitions
- Country lists
- HTTP headers
- Supported currencies
- Feature flags
They make intent explicit: the collection is not expected to change.
Stream takeWhile()
Suppose we process transaction amounts.
100
200
300
450
600
700
Requirement:
Process values while they are less than 500.
Java 9:
List<Integer> values =
numbers.stream()
.takeWhile(n -> n < 500)
.toList();
Result:
100
200
300
450
Processing stops as soon as the predicate becomes false.
Stream dropWhile()
Requirement:
Ignore values until reaching 500.
List<Integer> values =
numbers.stream()
.dropWhile(n -> n < 500)
.toList();
Result:
600
700
The stream skips the initial matching elements, then processes the remainder.
iterate() Enhancement
Java 8
Infinite stream:
Stream.iterate(
1,
n -> n + 1);
Limiting required:
.limit(10)
Java 9
Finite stream:
Stream.iterate(
1,
n -> n <= 10,
n -> n + 1);
The termination condition is built into the stream definition, improving readability.
Optional.stream()
Suppose:
Optional<Customer> customer
Java 8 required extra work to integrate with Streams.
Java 9:
customer.stream()
.map(Customer::getName)
.forEach(System.out::println);
If the Optional is empty, the resulting stream is empty.
This is particularly useful when composing Stream pipelines.
Stream.ofNullable()
Before Java 9:
if(customer != null){
process(customer);
}
Java 9:
Stream.ofNullable(customer)
.forEach(this::process);
This creates:
- A stream with one element if the value is non-null.
- An empty stream if the value is
null.
It eliminates many small null checks.
Enterprise Examples
Feature Flags
Set<String> enabledFeatures =
Set.of(
"PAYMENTS",
"REPORTS",
"NOTIFICATIONS");
Supported Countries
List<String> countries =
List.of(
"India",
"USA",
"Japan");
Processing Transactions
transactions.stream()
.takeWhile(Transaction::isValid)
.forEach(processor::process);
Configuration Maps
Map<String,String> config =
Map.of(
"ENV","PROD",
"CACHE","REDIS");
Small configuration maps become concise and self-documenting.
Performance Considerations
The factory methods create compact immutable collection implementations optimized for small collections.
Benefits include:
- Lower memory overhead
- Fewer objects
- Better cache locality
- Clearer intent
For large mutable datasets, traditional collection implementations remain appropriate.
Common Mistakes
Expecting Mutability
List<String> list =
List.of("A","B");
list.add("C");
Results in:
UnsupportedOperationException
Using null
List.of("A", null);
Throws:
NullPointerException
Unlike many legacy collections, the factory methods reject null values.
Confusing takeWhile() with filter()
takeWhile() stops processing once the predicate fails.
filter() evaluates every element independently.
This distinction is especially important for ordered streams.
Best Practices
✔ Use List.of(), Set.of(), and Map.of() for immutable constants.
✔ Prefer Map.ofEntries() for larger immutable maps.
✔ Use takeWhile() and dropWhile() only when stream encounter order matters.
✔ Use Stream.ofNullable() to simplify nullable pipelines.
✔ Use Optional.stream() when integrating Optional with Streams.
Interview Questions
What is the difference between Collections.unmodifiableList() and List.of()?
Collections.unmodifiableList() returns an unmodifiable view of another collection. Changes to the underlying collection are still visible.
List.of() creates a truly immutable collection.
Does List.of() allow null elements?
No. Attempting to include a null element results in a NullPointerException.
When should takeWhile() be preferred over filter()?
When processing should stop as soon as the predicate becomes false on an ordered stream.
What does Optional.stream() return?
A stream containing one element if the Optional has a value, or an empty stream if it does not.
Summary
Java 9’s Collection and Stream enhancements refined many of the APIs introduced in Java 8. Factory methods made immutable collections concise and expressive, while Stream improvements reduced boilerplate and improved readability.
Although these features are relatively small compared to JPMS, they have a significant impact on everyday development and are widely used in modern Java codebases.
Coming Up Next
Part 25 – Java 9 Platform Enhancements: JShell, Private Interface Methods, Process API Improvements, Try-with-Resources Enhancements, Multi-Release JARs, and jlink
We’ll explore the remaining Java 9 features that improved developer productivity, platform capabilities, deployment, and runtime customization.