Introduction
A stream represents a sequence of elements and supports different kinds of operations that lead to the desired result. The source of a stream is usually a Collection or an Array, from which data is streamed from.
Streams differ from collections in several ways; most notably in that the streams are not a data structure that stores elements. They're functional in nature, and it's worth noting that operations on a stream produce a result and typically return another stream, but do not modify its source.
To "solidify" the changes, you collect the elements of a stream back into a Collection
.
In this guide, we'll take a look at how to collect a stream into an unmodifiable collections.
Collect Stream into Unmodifiable Collections
It's worth noting that there's a difference between an immutable and unmodifiable collection.
You can't change the contents of an unmodifiable collection. But, if the source collection changes, the unmodifiable collection changes too. An immutable collection is one that is a result of copying a source collection to create a new one. This new one should be unmodifiable too.
In the proceeding sections, we'll take a look at how you can collect a stream into an unmodifiable list, set or map. For this purpose, the regular collect()
and collectingAndThen()
methods do the trick. The former allows you to directly convert a stream into a collection, while the latter allows us to collect a stream into a regular collection, and then convert it into its unmodifiable counterpart through a separate function.
Instead, you could introduce other functions or chain the collectingAndThen()
method to introduce new changes in the pipeline before collecting into an unmodifiable collection.
If you'd like to read more about the regular
collect()
and advancedcollectingAndThen()
, read our Java 8 Streams: Convert a Stream to List and Guide to Java 8 Collectors: collectingAndThen()!
Collect Stream into Unmodifiable List
Let's start out with a list. We'll use the standard Collectors.toList()
collector, followed by a call to unmodifiableList()
of the Collections
class. Alternatively, you can supply a toUnmodifiableList()
collector to the collect()
method:
Stream<Integer> intStream = Stream.of(1, 2, 3);
List<Integer> unmodifiableIntegerList1 = intStream.collect(Collectors.toUnmodifiableList());
List<Integer> unmodifiableIntegerList2 = intStream.collect(
Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
)
);
If we try to modify these lists, an UnsupportedOperationException
should be thrown. Their simpleName
should be UnmodifiableRandomAccessList
and they should contain the exact same elements as seen in the stream:
@Test
public void listShouldBeImmutable() {
// Should contain elements 1, 2, and 3
assertEquals(
"[1, 2, 3]",
unmodifiableIntegerList1 .toString()
);
// Should be of type UnmodifiableList
assertEquals(
"UnmodifiableRandomAccessList",
unmodifiableIntegerList1 .getClass().getSimpleName()
);
// Should throw an exception when you attempt to modify it
assertThrows(
UnsupportedOperationException.class,
() -> unmodifiableIntegerList1 .add(4)
);
}
Collect Stream into Unmodifiable Set
If a stream you're dealing with has duplicates, and you'd like to get rid of them - the easiest way isn't to filter the list or keep track of the encountered elements in another list. The easiest solution to remove duplicates from a list is to box the list into a set, which doesn't allow for duplicates!
Again, the collectingAndThen()
collector works wonders here, as you can collect the stream into a Set
and convert it into an unmodifiable set in the downstream function:
Stream<Integer> intStream = Stream.of(1, 1, 3, 2, 3);
Set<Integer> integerSet1 = intStream.collect(Collectors.toUnmodifiableSet());
Set<Integer> integerSet2 = intStream.collect(
Collectors.collectingAndThen(
Collectors.toSet(),
Collections::unmodifiableSet
)
);
Then, the Set
should be unmodifiable. Any change attempt should throw an UnsupportedOperationException
:
@Test
public void setShouldBeImmutable() {
// Set shouldn't contain duplicates
assertEquals(
"[1, 2, 3]",
integerSet1.toString()
);
// Set should be of type UnmodifiableSet
assertEquals(
"UnmodifiableSet",
integerSet1.getClass().getSimpleName()
);
// Set should not be modifiable
assertThrows(
UnsupportedOperationException.class,
() -> integerSet1.add(3)
);
}
Collect Stream into Unmodifiable Map
Collecting to an unmodifiable map works in much the same way as the previous two, so let's try to spice it up a bit. Say you have a case where you want to store numbers and their square-value equivalents:
Key | Value |
2 | 4 |
3 | 9 |
4 | 16 |
But, when you receive duplicate keys, you don't want to repeat entries:
Key | Value | Passes? |
2 | 4 | YES |
3 | 9 | YES |
4 | 16 | YES |
4 | 16 | NO |
Yet, the method, when converting into a map using the same approach we used before, there's no place to check for duplicate entries:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 4);
Map<Integer, Integer> map1 = stream.collect(
Collectors.toUnmodifiableMap(
Function.identity(),
i -> (int)Math.pow(i, 2)
)
);
Map<Integer, Integer> map2 = stream.collect(
Collectors.collectingAndThen(
Collectors.toMap(
// Key
Function.identity(),
// Value
i -> (int) Math.pow(i, 2)
),
Collections::unmodifiableMap
)
);
Note the use of Function.identity()
in the key mapper of the Collectors.toMap()
method. The method identity()
makes the mapper use the Integer
element itself as the key of the map entry.
Thus, when you call it with duplicate entries, it always throws an IllegalStateException
:
Exception in thread "main" java.lang.IllegalStateException:
Duplicate key 4 (attempted merging values 16 and 16)
It's easy to remedy this problem with stream operations themselves, so the client doesn't have to worry about providing a clean list! Just by adding an intermediate distinct()
operation to the stream, we can filter out duplicate values before collecting:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Map<Integer, Integer> map1 = stream.distinct().collect(
Collectors.toUnmodifiableMap(
Function.identity(),
i -> (int)Math.pow(i, 2)
)
);
Map<Integer, Integer> map2 = stream.distinct().collect(
Collectors.collectingAndThen(
Collectors.toMap(
// Key
Function.identity(),
// Value
i -> (int) Math.pow(i, 2)
),
Collections::unmodifiableMap
)
);
Let's test the result:
@Test
public void mapShouldBeImmutable() {
assertEquals(
"{1=1, 2=4, 3=9, 4=16}",
map1.toString()
);
assertEquals(
"UnmodifiableMap",
map1.getClass().getSimpleName()
);
assertThrows(
UnsupportedOperationException.class,
() -> map1.put(5, 25)
);
}
Conclusion
In this short guide, we've taken a look at how to collect streams into unmodifiable collections - a list, set and map!
We've also taken a quick look at how to handle duplicate values, which can in some data structures, raise exceptions, and in others, cause silent failure.