Repository navigation
JSpecify JDK models: Collection.toArray() cannot be both called and overridden #1728
Description
Version
NullAway 0.14.0, and current master (21f02dd). Error Prone 2.50.0, JDK 21.
Flags: -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:JSpecifyJDKModels=true -XDaddTypeAnnotationsToSymbol=true. JSpecifyJDKModels defaults to JSpecifyExperimental, so JSpecifyExperimental=true turns it on too.
Summary
A call to Collection.toArray() yields @Nullable Object[], taken from the JSpecify JDK model. An override of the same method is checked against Object[], taken from javac. The two disagree, so a class that both implements List and delegates to another list's toArray() has no signature that satisfies both.
The override check does not consult the JDK model at all: with -XepOpt:NullAway:JSpecifyJDKModels=false the call-site error disappears and the override error stays.
Reproducer
package foo;
import java.util.AbstractList;
import java.util.List;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static Object[] call(List<String> list) {
// BUG: Diagnostic contains: incompatible types: @Nullable Object [] cannot be converted to Object []
return list.toArray();
}
static class MyList extends AbstractList<String> {
@Override public String get(int index) { return ""; }
@Override public int size() { return 0; }
// BUG: Diagnostic contains: Method returns @Nullable Object [], but overridden method returns Object []
@Override public @Nullable Object[] toArray() { return new Object[0]; }
}
}Test.java:11: warning: [NullAway] incompatible types: @Nullable Object [] cannot be converted to Object []
return list.toArray();
^
Test.java:19: warning: [NullAway] Method returns @Nullable Object [], but overridden method returns Object [], which has mismatched type parameter nullability
@Override public @Nullable Object[] toArray() { return new Object[0]; }
^
The second one is the clearer defect on its own: the override spells exactly the signature the model gives List.toArray(), and it is rejected.
The combination that has no answer
static class MyList extends AbstractList<String> {
private final List<String> delegate;
MyList(List<String> delegate) { this.delegate = delegate; }
@Override public String get(int index) { return delegate.get(index); }
@Override public int size() { return delegate.size(); }
// BUG: Diagnostic contains: incompatible types: @Nullable Object [] cannot be converted to Object []
@Override public Object[] toArray() { return delegate.toArray(); }
}Object[] is the only return type the override check accepts, and the delegated call produces @Nullable Object[].
What does and does not report
| Form | Reported |
|---|---|
Object[] call(List<String> l) { return l.toArray(); } |
yes |
@Nullable Object[] call(List<String> l) { return l.toArray(); } |
no |
Object[] call(List<String> l) { return l.toArray(new String[0]); } |
no |
override Object[] toArray() |
no |
override @Nullable Object[] toArray() |
yes |
override <T2> T2[] toArray(T2[] a) |
yes (T2 has a non-null upper bound) |
override <T2 extends @Nullable Object> T2[] toArray(T2[] a) |
no |
The last two rows are worth contrasting with the rest: for toArray(T[]) the override check does see the model, and rejects an override whose type variable is non-null bounded where java.util.List.toArray has a @Nullable upper bound. So models reach the override check for type-variable bounds, and the array-element annotation is what goes missing.
Expected
The call site and the override should agree on one signature for the same method. Either the override check reads the model's @Nullable Object[] — which makes the identical-signature override above legal, while Object[] stays legal as a covariant narrowing — or the model's return type follows the collection's element type, which would also settle the call site.
Workaround
Cast the call's result. Calcite has a helper for it, org.apache.calcite.linq4j.Nullness.castNonNullArray.
Where we hit it
FlatLists, which both implements List and delegates to another list's toArray, while replacing the Checker Framework with NullAway in Apache Calcite (apache/calcite#5213).
Activity
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Match the JDK signature in the ConsList toArray overrides
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Match the JDK signature in the ConsList toArray overrides
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Match the JDK signature in the ConsList toArray overrides
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Match the JDK signature in the ConsList toArray overrides
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
msridhar commentedon Aug 26, 2026
I believe #1722 fixed this one; adding some new regression tests around overriding
Collection.toArrayspecifically in #17577 remaining items
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2
[CALCITE-7736] Let SqlFunctions carry the nulls that SQL values have
[CALCITE-7736] Upgrade NullAway to 0.14.2