In the context of the comments and answers given at List.of() or Collections.emptyList() and List.of(...) or Collections.unmodifiableList() I came up with two following rul
Generally, use of the new factories is safe for new code, where there is no existing code that depends on behaviors of the existing collections.
There are several reasons the new collection factories aren't drop-in replacements for code that initializes collections using the existing APIs. Obviously immutability is one of the most prominent reasons; if you need to modify the collection later, it obviously can't be immutable! But there are other reasons as well, some of them quite subtle.
For an example of replacement of existing APIs with the new APIs, see JDK-8134373. The review threads are here: Part1 Part2.
Here's a rundown of the issues.
Array Wrapping vs Copying. Sometimes you have an array, e.g. a varargs parameter, and you want to process it as a list. Sometimes Arrays.asList
is the most appropriate thing here, as it's just a wrapper. By contrast, List.of
creates a copy, which might be wasteful. On the other hand, the caller still has a handle to the wrapped array and can modify it, which might be a problem, so sometimes you want to pay the expense of copying it, for example, if you want to keep a reference to the list in an instance variable.
Hashed Collection Iteration Order. The new Set.of
and Map.of
structures randomize their iteration order. The iteration order of HashSet
and HashMap
is undefined, but in practice it turns out to be relatively stable. Code can develop inadvertent dependencies on iteration order. Switching to the new collection factories may expose old code to iteration order dependencies, surfacing latent bugs.
Prohibition of Nulls. The new collections prohibit nulls entirely, whereas the common non-concurrent collections (ArrayList
, HashMap
) allow them.
Serialization Format. The new collections have a different serialization format from the old ones. If the collection is serialized, or it's stored in some other class that's serialized, the serialized output will differ. This might or might not be an issue. But if you expect to interoperate with other systems, this could be a problem. In particular, if you transmit the serialized form of the new collections to a Java 8 JVM, it will fail to deserialize, because the new classes don't exist on Java 8.
Strict Mutator Method Behavior. The new collections are immutable, so of course they throw UnsupportedOperationException
when mutator methods are called. There are some edge cases, however, where behavior is not consistent across all the collections. For example,
Collections.singletonList("").addAll(Collections.emptyList())
does nothing, whereas
List.of("").addAll(Collections.emptyList())
will throw UOE. In general, the new collections and the unmodifiable wrappers are consistently strict in throwing UOE on any call to a mutator method, even if no actual mutation would occur. Other immutable collections, such as those from Collections.empty*
and Collections.singleton*
, will throw UOE only if an actual mutation would occur.
Duplicates. The new Set
and Map
factories reject duplicate elements and keys. This is usually not a problem if you're initializing a collection with a list of constants. Indeed, if a list of constants has a duplicate, it's probably a bug. Where this is potentially an issue is when a caller is allowed to pass in a collection or array (e.g., varags) of elements. If the caller passes in duplicates, the existing APIs would silently omit the duplicates, whereas the new factories will throw IllegalArgumentException
. This is a behavioral change that might impact callers.
None of these issues are fatal problems, but they are behavioral differences that you should be aware of when retrofitting existing code. Unfortunately this means that doing a mass replacement of existing calls with the new collection factories is probably ill-advised. It's probably necessary to do some inspection at each site to assess any potential impact of the behavioral changes.
First of all, it is important to note that the collection factories return immutable variants. Unfortunately, this does not show in the type system so you have to track that manually / mentally. This already forbids some replacements that might otherwise be worthwile, so it must become 0. in your list of rules. :)
For example, creating a collection of seed elements that are later modified by other code might look like this:
private final Set<String> commonLetters = initialCommonLetters()
private static Set<String> initialCommonLetters() {
Set<String> letters = new HashSet<>();
letters.add("a");
letters.add("e");
return letters;
}
Would be great to simply write commonLetters = Set.of("a", "e");
but this will likely break other code as the returned set is immutable.
The (im)mutability discussion immediately leads to constants. This was a major reason to introduce them! Gone are the days where you need a static initializer block to create that COMMON_LETTERS
constant. This would hence be the place where I would look first for use cases.
As you say, there seems to be no reason to start replacing calls to Collections::empty...
, Collections::singleton...
, or Arrays::asList
just for the fun of it. What I would do, though, as soon as I start using the new methods in a class I would replace the old variants as well to have the code rely on fewer concepts, making understanding it easier.
The last argument is also one that could apply to the of()
variants in general. While Collections::empty...
and Collections::singleton...
are somewhat clearer about their intent, I slightly tend towards saying that always using of
, no matter how many arguments you have, offsets that advantage by writing code that, as a whole, uses less concepts.
I see no reason to continue using Arrays::asList
.