Java strange warning "suspicious toArray() call"

Viewed 166

When implementing a Stream-class using a delegate stream, I get strange warning by IntelliJ:

Array of type java.lang.Object[] expected, A[] found

The code which triggers the warning:

public class MyStream<T> implements Stream<T> {
    private final Stream<T> delegate;

    public MyStream(Stream<T> delegate) {
        this.delegate = delegate;
    }

    *snip*

    @Override
    public <A> A[] toArray(IntFunction<A[]> generator) {
        return delegate.toArray(generator);
    }

    *snip*
}

The full warning:

Array of type java.lang.Object[] expected, A[] found

Inspection info: Reports two types of suspicious calls to Collection.toArray(). The first type is any calls where the type of the specified array argument is not of the same type as the array type to which the result is casted. Example:

void m(List list) {
  Number[] ns = (Number[])
      list.toArray(new String[0]);
}

The second type is any calls where the type of the specified array argument does not match the type parameter of the collection declaration. Example:

void m(List<Number> list) {
  Number[] ns =
      list.toArray(new String[0]);
}

I somewhat assume this is a false positive, caused by type erasure, mostly because I cannot find any sane reason why this would cause a problem, and also because the examples from the warning do not match very well with my code.

However, when thinking about it, I wondered: If this is a false positive triggered because of type erasure, why does it know about A at all? Also, Java does usually cast a A[] to a Object[] implicitly, so why doesn't it here?

So: What is happening here?

2 Answers

Here's some different code that can serve to illustrate the problem your IDE is detecting:

Stream<Integer> intStream = Stream.of(1, 2, 3);
String[] stringArray = intStream.toArray(i -> new String[3]);

Or, using your own class (I didn't test this):

new MyStream<>(Stream.of(1, 2, 3)).toArray(i -> new String[3]);

That code throws a java.lang.ArrayStoreException on the second line (the second one would be expected to, as well). But it compiles.

That is exactly the problem with delegate.toArray(generator);. You're effectively calling

Stream<T>.toArray(IntFunction<A[]>)

Where A is not guaranteed to be the same as or compatible with T. To relate this to the example code above, T has the place of Integer and A has the place of String. See the problem?

The JavaDocs of Stream.toArray warns about this:

Throws: ArrayStoreException - if the runtime type of any element of this stream is not assignable to the runtime component type of the generated array


That is the problem that IntelliJ is detecting. You cannot assume that <A> and the type-wide parameter <T> are going to coincide.

In other words, it's not a false positive and you can use the example above to reproduce it.

IntelliJ thinks that this is suspicious because your stream is a stream of Ts, but you are producing an array of As. Note that A is the type parameter of the toArray method, which is independent of the type parameter T for the class. Not much to do with type erasure.

This is indeed a false positive. You are implementing the method for Stream, which requires an A[] to be returned after all. The method in Stream is indeed designed rather unsafely like this, allowing you to pass in anything for A, no matter what type of stream it is. It is documented that it will throw an ArrayStoreException if A is the wrong type.

Throws

ArrayStoreException - if the runtime type of the array returned from the array generator is not a supertype of the runtime type of every element in this stream

By delegating to delegate, you will be throwing an ArrayStoreException, which you should, (according to the docs) if someone passed a wrong A to toArray too, assuming the instance stored in delegate is correctly implemented. There's nothing wrong here

You can suppress the inspection for this one line by writing this comment above it:

//noinspection SuspiciousToArrayCall
Related