3

While looking at the source code, I could see that the stream() method has been overridden in Collections.UnmodifiableMap.UnmodifiableEntrySet. But the code seems to be identical to Collection.stream() except the return type in Collections.UnmodifiableMap.UnmodifiableEntrySet.stream() is more specific to be Stream<Entry<K,V>> rather than just Stream<E> as in Collection.stream().

The spliterator() method is different in both classes, but even if stream is not overriden I think that the UnmodifiableEntrySet.spliterator() would be invoked from Collection.stream() if the object is of type UnmodifiableEntrySet.

So, is there any reason why the stream method was overriden?

Collection.java

@Override
default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}
 
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Collections.UnmodifiableMap.UnmodifiableEntrySet.java

@SuppressWarnings("unchecked")
public Spliterator<Entry<K,V>> spliterator() {
    return new UnmodifiableEntrySetSpliterator<>(
        (Spliterator<Map.Entry<K, V>>) c.spliterator());
}

@Override
public Stream<Entry<K,V>> stream() {
    return StreamSupport.stream(spliterator(), false);
}
Gautham M
  • 4,816
  • 3
  • 15
  • 37
  • 1
    Simply because `UnmodifiableEntrySet` is a set of `Entry` while `Collection` is generic. I am not sure what's confusing you. – Aniket Sahrawat Feb 14 '21 at 16:35
  • @AniketSahrawat Yes. My question was why should `UnmodifiableEntrySet` override `stream()` method if the the method body is exactly identical to `Collection.stream()` – Gautham M Feb 14 '21 at 16:56
  • @AniketSahrawat I was able to somewhat find a solution after some debugging. – Gautham M Feb 14 '21 at 17:26
  • 1
    @GauthamM - `Stream>` is not a `Stream`. Another example is `List` is not a `List`. – Arvind Kumar Avinash Feb 14 '21 at 17:46
  • @ArvindKumarAvinash https://stackoverflow.com/a/66198058/7804477 these are my findings. I had tried removing the overriden `stream` from `UnmodifiableEntrySet` and `UnmodifiableCollection`, such that the `stream` call would go to `Collection` and `spliterator` call would invoke the overriden version in `UnmodifiableEntrySet`. What I understand from your point is that the returned stream would be a generic one and it would not be a stream of `Entry`. If that was true, then I should not be able to invoke `map(Entry::value)` on that stream. But I was able to do it. Please review my answer as well – Gautham M Feb 15 '21 at 03:10
  • 2
    I misread your question yesterday. If you take a look at the implementations of `stream`, you will notice that it has been overridden in 7 places. The relevant overrides are in `UnmodifiableCollection` and then again in `UnmodifiableEntrySet`. `UnmodifiableCollection#stream` is basically delegating the call to whatever `Collection` was passed. If there is no override in `UnmodifiableEntrySet`, the call would be delegated to the original `Collection` (which is mutable). – Aniket Sahrawat Feb 15 '21 at 16:44

2 Answers2

4

Below Java Doc / program comes from openjdk 14 2020-03-17.

The main reason to override spliterator and stream is to ensure the entry of UnmodifiableEntrySet is unmodified.

From comment of UnmodifiableEntrySet:

We need this class in addition to UnmodifiableSet as Map.Entries themselves permit modification of the backing Map via their setValue operation. This class is subtle: there are many possible attacks that must be thwarted.

To begin, UnmodifiableEntrySet extends UnmodifiableSet which extends UnmodifiableCollection. In UnmodifiableCollection, proxy pattern is used to avoid modifying the backing Collection c, most method just call the backing Collection method, like spliterator and stream:

    @Override
    public Spliterator<E> spliterator() {
        return (Spliterator<E>)c.spliterator();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> stream() {
        return (Stream<E>)c.stream();
    }

So if UnmodifiableEntrySet does not override those methods, the behavior will follows UnmodifiableCollection implementation, and the backing entries will be exposed and can be modified through Entry#setValue.

Hence spliterator and stream methods are overridden and UnmodifiableEntrySetSpliterator is introduced to wrap all access to the backing entry with UnmodifiableEntry, ensuring the entry can not be modified.

Why UnmodifiableCollection override stream?

It seems that there is no need to override stream in UnmodifiableCollection, as we can just use default implementation in Collection (just create stream by the spliterator). But the author decided to override stream using backing Collection c stream method, one of the possible reason is the backing Collection may override stream method for performance reason, e.g. Collections.CopiesList, or it's spliterator method does not fulfill the requirement according to Collection#stream

This method should be overridden when the spliterator() method cannot return a spliterator that is IMMUTABLE, CONCURRENT, or late-binding. (See spliterator() for details.)

samabcde
  • 6,988
  • 2
  • 25
  • 41
  • This explains most of it. But I have few doubts. https://stackoverflow.com/a/66198058/7804477 - This is what I tried. In my `CollectionsCopy` class I had removed the overriden `stream` from both `UnmodifiableEntrySet` & `UnmodifiableCollection`. Then I tried `unmodifiableMap.entrySet().stream().findFirst().orElse(null)`. This actually returned an immutable entry. if I hadn't removed the overriden `stream` from `UnmodifiableCollection`, then mutable entry was returned as expected. Was overriding `stream` really required in `UnmodifiableCollection`. Isn't overriding `spliterator` enough? – Gautham M Feb 16 '21 at 16:35
  • 1
    @Gautham M, I agree that is not really required. But the author decided to use backing `Collection c` `stream` method by default, I think one of the reason is `c` may override `stream` method for performance reason, e.g. `Collections.CopiesList`. – samabcde Feb 17 '21 at 03:19
  • 1
    @GauthamM I believe samabcde's answer is correct, and in fact the override of UnmodifiableEntrySet.stream() is required. I'm not sure why your example didn't work. I modified my build of the JDK to remove that override, and the stream contained mutable entries, thus allowing the unmodifiable map to be modified. – Stuart Marks Feb 17 '21 at 05:07
  • @StuartMarks Yes, I was able to get mutable entries if I hadn't removed the overridden `stream` in `UnmodifiableCollection`. If I had removed that as well, then the entry would be immutable. – Gautham M Feb 17 '21 at 05:14
  • 3
    @GauthamM Oh yes, I understand your question now. It might have been possible to get away without the override of UnmodifiableCollection.stream, but it would have potentially made the code brittle under maintenance. The general rule for the wrapper collections is that they should override _everything_ so that they can be verified to protect their invariants without inspecting the entire class hierarchy. – Stuart Marks Feb 17 '21 at 05:18
  • @samabcde That is right. Could you please add the details in your second comment to the answer as well. – Gautham M Feb 17 '21 at 05:20
  • @StuartMarks Along with that, the overriding of `stream` for performance, in other classes mentioned by samabcde makes it necessary to override it in `UnmodifiableCollection` – Gautham M Feb 17 '21 at 05:33
0

After copying the contents of Collections.java to a new class CollectionsCopy.java, I tried removing the stream method from UnmodifiableEntrySet then I did some debugging and was able to get to an answer.

UnmodifiableEntrySet extends UnmodifiableSet which extends UnmodifiableCollection which in turn implements Collection. Since UnmodifiableCollection has a field final Collection<? extends E> c;, it had to override the stream() as below

public Stream<E> stream() {
    return (Stream<E>)c.stream();
}

In the constructor of UnmodifiableEntrySet the Set<? extends Map.Entry<? extends K, ? extends V>> object is cast to raw Set type and passed to the super constructor.

super((Set)s);

The comment attached to the code was :

Need to cast to raw in order to work around a limitation in the type system

So when I removed the stream() from UnmodifiableEntrySet (inside my CollectionsCopy.java), due to the raw type conversion, the spliterator method being invoked was that in the Set instead of the one in UnmodifiableEntrySet. This was because the field c in UnmodifiableCollection would be Set instead of UnmodifiableEntrySet and when c.stream() was invoked from UnmodifiableCollection.stream(), it would invoke Set.spliterator(). So, it was necessary for UnmodifiableEntrySet to override the implementation of stream() of UnmodifiableCollection.

Also, I tried removing the overriden stream() from UnmodifiableCollection. This time, it worked as expected as the spliterator was invoked from the UnmodifiableEntrySet.

EDIT : Based on a few comments that suggested that the returned stream from Collection would not be a stream of Entry instead it would be just a Stream. But in such a case, I shouldn't be able to invoke any methods from the Entry class on the stream. But I was able to invoke map(Entry::getValue) on that stream. So , the stream returned was indeed a stream of type Entry.

Gautham M
  • 4,816
  • 3
  • 15
  • 37