Java Collections Are Evolving
Collections have seen a lot of changes in the past couple of JDKs. Let's look at how they've improved in Java 9 and Java 10.
Join the DZone community and get the full member experience.
Join For FreeIt's very easy as a developer to get into the routine of using the same tools the same way as always, particularly if the language we use has seen lengthy periods of extreme stability. Java is no longer in that mode: with releases every six months, we can expect to see updates to it very frequently.
These updates may be large structural changes (like the Java Module System or API updates (for example, the Process API). Each release is likely to add something that's useful to us, so staying up-to-date is going to help us to be more productive developers. In this article, we're going to specifically look at the updates to Collections in Java 9 (released September 2017) and Java 10 (released March 2018). Java Collections are a core part of the language that we probably touch in one way or another every day, and anything that makes it easier to work with Collections makes our job easier.
Java 9: Convenience Factory Methods for Collections
Java 9 introduced new ways to create immutable collections. At some point, we've all written code that looks something like this:
List<String> moods = Arrays.asList("HAPPY", "SAD");
As of Java 9, you can now write the following instead:
List<String> moods = List.of("HAPPY", "SAD");
While saving six characters may be exciting to those who prefer very terse code, this might not seem like a huge improvement.
What's important to realize, though, is that this second case creates an immutable list. We may have fallen into the trap of thinking Arrays.asList returns an immutable list because it's not possible to append to it:
jshell> List<String> moods = Arrays.asList("HAPPY", "SAD");
moods ==> [HAPPY, SAD]
jshell> moods.add("ANGRY")
| java.lang.UnsupportedOperationException thrown
| at AbstractList.add (AbstractList.java:153)
| at AbstractList.add (AbstractList.java:111)
| at (#2:1)
However, changes are allowed within the limits of the list:
jshell> moods.set(0, "ANGRY")
$3 ==> "HAPPY"
jshell> System.out.println(moods)
[ANGRY, SAD]
It's likely this was not intended when we wrote this code. Instead, what we really wanted was:
List<String> moods = Collections.unmodifiableList(Arrays asList("HAPPY", "SAD"));
Now you can see that:
List<String> moods = List.of("HAPPY", "SAD");
...which does return an immutable list, is not only much shorter than the pre-Java-9 equivalent but also more correct than simply using Arrays.asList.
This is useful for Lists, but what's even more useful is that similar methods have been added for Sets and Maps. To create an immutable Set before Java 9, you could use something like:
Set<String> moods = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("HAPPY", "SAD")));
This really was unpleasant, even when using static imports to reduce some of the noise. In Java 9, it's simply:
Set<String> moods = Set.of("HAPPY", "SAD");
There are also factory methods for creating Maps. Before Java 9, if we wanted to create a Map with a fixed set of values, we'd have to do something a little long-winded:
Map<String, Mood> wordToMood = new HashMap<>();
wordToMood.put("happy", HAPPY);
wordToMood.put("good", HAPPY);
wordToMood.put("great", HAPPY);
//… more values
wordToMood.put("horrible", SAD);
wordToMood.put("bad", SAD);
wordToMood.put("awful", SAD);
This was even worse if we wanted to initialize this for a constant (e.g. a static field), as it would have to be put somewhere in a static block, and wrapping this in an unmodifiableMap simply adds to the noise. In Java 9, this can be:
Map<String, Mood> wordToMood
= Map.ofEntries(Map.entry("happy", HAPPY),
Map.entry("good", HAPPY),
Map.entry("great", HAPPY)
//...more values
Map.entry("horrible", SAD),
Map.entry("bad", SAD),
Map.entry("awful", SAD));
Static imports can make this more succinct, too. This Map.ofEntries method works for any arbitrary number of key/value pairs, as each pair is wrapped in a Map.entry and the ofEntries method takes a vararg. If the Map has fewer than ten values, we might want to use the convenience Map.of method, which takes up to ten key/value parameters:
Map<String, Mood> wordToMood = Map.of("happy", HAPPY,
"good", HAPPY,
"great", HAPPY,
"horrible", SAD,
"bad", SAD,
"awful", SAD);
Java 10: Creating Immutable Copies of Collections
Java 9 introduced these factory methods to make it easier to create new immutable collections from known values. Java 10 recognizes that this is not the only way we create collections, and introduces more ways of creating immutable collections from existing collections or operations.
There's now an easy way to create an immutable Collection that's a copy of an existing Collection. Prior to Java 10, you could create a new list that was a copy of an existing collection using a copy constructor:
List<String> newCopyOfCollection = new ArrayList<>(moods);
In this case, even if the original collection moods is immutable/ unmodifiable, the new collection is not:
jshell> List<String> newCopyOfCollection = new ArrayList<>(moods);
newCopyOfCollection ==> [HAPPY, SAD]
jshell> newCopyOfCollection.add("ANGRY")
$5 ==> true
jshell> System.out.println(newCopyOfCollection)
[HAPPY, SAD, ANGRY]
To create a list that cannot be changed, it would have to be wrapped in an unmodifiable list:
jshell> List<String> newCopyOfCollection = Collections.unmodifiableList(new ArrayList<>(moods));
newCopyOfCollection ==> [HAPPY, SAD]
jshell> newCopyOfCollection.add("ANGRY")
| java.lang.UnsupportedOperationException thrown
| at Collections$UnmodifiableCollection.add
(Collections.java:1056)
| at (#8:1)
For Lists specifically, you can also use Collections.copy, but the syntax is a bit clunky and it's easy to get runtime errors if your destination list isn't set up correctly.
In Java 10, it's much easier to create a new immutable list from an existing Collection:
List<String> newCopyOfCollection = List.copyOf(moods);
As you'd expect, you can't add or remove elements or alter the items in the list:
jshell> List<String> newCopyOfCollection = List.copyOf(moods);
newCopyOfCollection ==> [HAPPY, SAD]
jshell> newCopyOfCollection.add("ANGRY")
| java.lang.UnsupportedOperationException thrown
| at ImmutableCollections.uoe
(ImmutableCollections.java:71)
| at
ImmutableCollections$AbstractImmutableList.add
(ImmutableCollections.java:77)
| at (#21:1)
Similar methods exist for Set:
Set<String> setCopyOfCollection = Set.copyOf(moods);
...and Map. The Map version takes another Map to copy, not a Collection:
Map<String, Mood> copyOfMoodMap = Map.copyOf(wordToMood);
Java 10: Creating Immutable Collections From Streams
Java 10 makes it easy to create immutable collections from Stream operations, with the addition of the toUnmodifiableList, toUnmodifiableSet, and toUnmodifiableMap methods on Collectors. This means that from Java 10, you can create immutable Collections not only from some known values or by copying an existing Collection or Map, but also from Stream operations.
For example, let's assume we have a Stream operation that takes a sentence and turns it into a list of unique words:
List<String> uniqueWords =
Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message)
.map(String::toLowerCase)
.distinct()
.collect(Collectors.toList());
This list is a simple ArrayList containing the results, and can be changed:
jshell> String message = "I am so so happy today, and I am not happy every day";
message ==> "I am so so happy today, and I am not happy every day"
jshell> List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message).
...>
map(String::toLowerCase).
...> distinct().
...>
collect(Collectors.toList());
uniqueWords ==> [i, am, so, happy, today, and, not, every, day]
jshell> uniqueWords.getClass()
$35 ==> class java.util.ArrayList
jshell> uniqueWords.add("SAD")
$36 ==> true
jshell> System.out.println(uniqueWords)
[i, am, so, happy, today, and, not, every, day, SAD]
If the aim of this stream operation was to return some fixed set of results, we probably didn't want to return an ArrayList but some sort of immutable list. In Java 10, we can do this:
List<String> uniqueWords
= Patternc ompile("\\s*[^\\p{IsAlphabetic}]+\\s*"
.splitAsStream(message)
.map(String::toLowerCase)
.distinct()
.collect(Collectors.toUnmodifiableList());
This returns a List that cannot be changed:
jshell> List<String> uniqueWords = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").splitAsStream(message).
...>
map(String::toLowerCase).
...>
distinct().
...>
collect(Collectors.toUnmodifiableList());
uniqueWords ==> [i, am, so, happy, today, and, not, every, day]
jshell> uniqueWords.getClass()
$39 ==> class java.util.ImmutableCollections$ListN
jshell> uniqueWords.add("SAD")
| java.lang.UnsupportedOperationException thrown
| at ImmutableCollections.uoe
(ImmutableCollections.java:71)
| at
ImmutableCollections$AbstractImmutableList.add
(ImmutableCollections.java:77)
| at (#40:1)
There's also Collectors.toUnmodifiableSet, which may be more appropriate in this scenario since the Collection contains only unique values.
Collectors.toUnmodifiableMap is for creating immutable Maps, and, like toMap, is a little trickier, as it means we need to give functions to define what the keys and the values are. If we change our example to use a Map to calculate the number of times each word is in the sentence, we can demonstrate how to collect into an immutable Map:
Map<String, Long> wordCount = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").
splitAsStream(message).
map(String::toLowerCase).
collect(Collectors.toUnmodifiableMap(Function.identity(), word -> 1L, (oldCount, newVal) -> oldCount + newVal));
As before, we can see that values can't be added to or removed from this Map:
jshell> Map<String, Long> wordCount = Pattern.compile("\\s*[^\\p{IsAlphabetic}]+\\s*").
splitAsStream(message).
...>
map(String::toLowerCase).
...>
collect(Collectors.toUnmodifiableMap(Function.identity(),
...>
word -> 1L,
...>
(oldCount, newVal) -> oldCount + newVal));
wordCount ==> {and=1, i=2, am=2, day=1, so=2, every=1, today=1, not=1, happy=2}
jshell> wordCount.getClass()
$49 ==> class java.util.ImmutableCollections$MapN
jshell> wordCount.put("WORD", 1000L)
| java.lang.UnsupportedOperationException thrown
| at ImmutableCollections.uoe
(ImmutableCollections.java:71)
| at
ImmutableCollections$AbstractImmutableMap.put
(ImmutableCollections.java:558)
| at (#50:1)
In conclusion, we can see that Java is evolving to make it easier for us developers to write convenient and correct code. Some of these changes are just small additions to existing APIs, so it's easy to miss them in the fuss of bigger language changes. Collections have evolved in the most recent versions of Java, and if we stay up-to-date and use these changes, we'll find our lives a little bit easier.
If you enjoyed this article and want to learn more about Java Collections, check out this collection of tutorials and articles on all things Java Collections.
Opinions expressed by DZone contributors are their own.
Comments