📜 ⬆️ ⬇️

List.of () and everything, everything, everything ...

Hello, habrozhiteli. Finally got around to write something on Habr. The first article was a bit boring and highly specialized. Therefore, I write in the sandbox a second time. (UPD but for some reason it didn’t hit the wow in the sandbox)

This time it will be about Java innovations. Namely, about ImmutableCollections. You probably have already used List.of () somewhere. Most likely in tests, because I don’t see any practical value in these methods. But even in the tests you can stumble on banal pitfalls. They are banal due to the fact that once having read their code, everything immediately falls into place, but there are still very, very many questions why it was done this way and not differently.

Let's start with the List interface, in which there are static functions of.
')
As many as eleven !!!


List<E> List<E>.<E>of(E e1); List<E> List<E>.<E>of(E e1, E e2); List<E> List<E>.<E>of(E e1, E e2, E e3); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9); List<E> List<E>.<E>of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10); List<E> List<E>.<E>of(E... elements) 


Why so many methods in Java ??? For a long time I asked this question, but did not reach into the source code and google. Today I did find an answer that completely dissatisfied me.

In the case of vararg calls.

It would seem, nevertheless, quite logical, less objects, less memory, less work of the garbage collector. Although, what's the difference if we use it by and large in tests. But the fact is that I think this answer is incorrect.

If we look at the code of these methods, we will see the following:

 static <E> List<E> of() { return ImmutableCollections.List0.instance(); } static <E> List<E> of(E e1) { return new ImmutableCollections.List1<>(e1); } static <E> List<E> of(E e1, E e2) { return new ImmutableCollections.List2<>(e1, e2); } static <E> List<E> of(E e1, E e2, E e3) { return new ImmutableCollections.ListN<>(e1, e2, e3); } static <E> List<E> of(E e1, E e2, E e3, E e4) { return new ImmutableCollections.ListN<>(e1, e2, e3, e4); } /* ...      ... */ static <E> List<E> of(E... elements) { switch (elements.length) { // implicit null check of elements case 0: return ImmutableCollections.List0.instance(); case 1: return new ImmutableCollections.List1<>(elements[0]); case 2: return new ImmutableCollections.List2<>(elements[0], elements[1]); default: return new ImmutableCollections.ListN<>(elements); } } 

The first 3 methods return us strange objects of the classes List0, List1, List2. All others will return ListN. And the last method in which varargs are used can return one of the listed lists. Beginning from 3 elements to 10, we actually send arguments to the method, and in no array will this data be wrapped.

It would seem that everything is fine, but let's dig further. Let's see the implementation of the ListN <> constructor

 ListN(E... input) { @SuppressWarnings("unchecked") E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input for (int i = 0; i < input.length; i++) { tmp[i] = Objects.requireNonNull(input[i]); } this.elements = tmp; } 

As you can see, the varargs syntax is already present. And this means that even if it was possible to avoid wrapping in an array during the first call, this happened anyway if the constructor was called. And why was such an implementation needed? This question is still open to me. I would be glad if someone in the comments will tell.

Now about the pitfalls of these collections. Let's look at the implementation of these collections from the inside.

At the head of all Immutable lists is:

 abstract static class AbstractImmutableList<E> extends AbstractList<E> implements RandomAccess, Serializable 

All methods of this collection abstract throw UnsupportedOperationException. Not all abstract methods from AbstractList have proven in this class. Therefore, I attach the code:

  @Override public boolean add(E e) { throw uoe(); } @Override public boolean addAll(Collection<? extends E> c) { throw uoe(); } @Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); } @Override public void clear() { throw uoe(); } @Override public boolean remove(Object o) { throw uoe(); } @Override public boolean removeAll(Collection<?> c) { throw uoe(); } @Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); } @Override public void replaceAll(UnaryOperator<E> operator) { throw uoe(); } @Override public boolean retainAll(Collection<?> c) { throw uoe(); } @Override public void sort(Comparator<? super E> c) { throw uoe(); } 

That is, for example, the method containsAll will have the same implementation as all the other collections that we have successfully used, and not always. But now is not about that.
The classes List0, List1, List2, and ListN are inherited from the AbstractImmutableList class. Each class implements some of the methods.

Take for example the class List0. The contains method can throw a NullPointerException.

 @Override public boolean contains(Object o) { Objects.requireNonNull(o); return false; } 

This is very unexpected. Why it was impossible to just always return false? Why is there a check for null. This remains to me incomprehensible.

The behavior of the containsAll method is the same as in regular lists.

  public boolean containsAll(Collection<?> o) { return o.isEmpty(); // implicit nullcheck of o } 

True NPE will pop up because of the call to the isEmpty () method, and not the for each loop as in regular lists.
In the source code, I noticed one comment that raised my spirits and reminded me how much the machine (more about the compiler) is smarter than a person.

 @Override public E get(int index) { Objects.checkIndex(index, 0); // always throws IndexOutOfBoundsException return null; // but the compiler doesn't know this } 

Let's go further to List1. There are more questions. Let's start with the constructor.

 List1(E e0) { this.e0 = Objects.requireNonNull(e0); } 

Why can't I have null stored in the list? What is the logic? Let's go further. The contains method still throws out the NPE.

 @Override public boolean contains(Object o) { return o.equals(e0); // implicit nullcheck of o } 

Although there is already more logical. If the constructor does not create lists with null, then this is expected. But what prevented to write:

 return o != null && o.equals(e0); 

or much more beautiful:

 return e0.equals(o); 

Again, relying on the fact that e0 cannot be null. The implementation of the containsAll method lies in the AbstractCollection class:

 public boolean containsAll(Collection<?> c) { for (Object e : c) if (!contains(e)) return false; return true; } 

If the collection is null, then we get the same NPE, as is the case with regular collections. But we also get NPE if the collection of parameters is null, since there is a dependency on the contains method, which will give us this NPE.

At what NPE is quite dangerous here.

 List<String> list = new ArrayList<>(); list.add("FOO"); list.add(null); List<String> immutableList = List.of("foo", "bar"); immutableList.containsAll(list); 

In this case, we will not say NPE, since our list does not contain the string "FOO". We will immediately receive a false response. If in our ArrayList the first element would be “foo”, then we would immediately catch the NPE. Therefore, be extremely careful in such situations.

List2 and ListN are sinners the same.

In summary, I still have a few questions. Why these collections do not behave the same as ordinary ArrayList, LinkedList? Why collections cannot contain null. Why was it necessary to create so many methods? Is this code really in a hurry and nobody wants to do it? But, since I cannot give answers to these questions, it remains to use what is, knowing about the pitfalls that are present in these new handy features.

P.S. I assume that Set and Map also have their own similar pitfalls. But I’ll reach them sometime later, when a few more minutes of free time appear.

Source: https://habr.com/ru/post/349944/


All Articles