Java 8 - Collect Stream into Unmodifiable List, Set or Map

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 advanced collectingAndThen(), 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:

Free eBook: Git Essentials

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.

Last Updated: December 19th, 2021
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

David LandupAuthor

Entrepreneur, Software and Machine Learning Engineer, with a deep fascination towards the application of Computation and Deep Learning in Life Sciences (Bioinformatics, Drug Discovery, Genomics), Neuroscience (Computational Neuroscience), robotics and BCIs.

Great passion for accessible education and promotion of reason, science, humanism, and progress.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms