Why does Collections.UnmodifiableMap.UnmodifiableEntrySet override the stream() method?

Viewed 352

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);
}
2 Answers

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.)

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.

Related