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).
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.JSpecifyJDKModelsdefaults toJSpecifyExperimental, soJSpecifyExperimental=trueturns 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 againstObject[], taken from javac. The two disagree, so a class that both implementsListand delegates to another list'stoArray()has no signature that satisfies both.The override check does not consult the JDK model at all: with
-XepOpt:NullAway:JSpecifyJDKModels=falsethe call-site error disappears and the override error stays.Reproducer
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
Object[]is the only return type the override check accepts, and the delegated call produces@Nullable Object[].What does and does not report
Object[] call(List<String> l) { return l.toArray(); }@Nullable Object[] call(List<String> l) { return l.toArray(); }Object[] call(List<String> l) { return l.toArray(new String[0]); }Object[] toArray()@Nullable Object[] toArray()<T2> T2[] toArray(T2[] a)T2has a non-null upper bound)<T2 extends @Nullable Object> T2[] toArray(T2[] a)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 wherejava.util.List.toArrayhas a@Nullableupper 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, whileObject[]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 implementsListand delegates to another list'stoArray, while replacing the Checker Framework with NullAway in Apache Calcite (apache/calcite#5213).