checker-framework
checker-framework copied to clipboard
Nullness Checker warning about non-null field in top-level lambda
Running the Nullness Checker on this code:
public class LambdaInit {
String f1;
Object o =
new Object() {
@Override
public String toString() {
f1.toString();
return "";
}
};
}
yields
error: (dereference.of.nullable)
at f1.toString(). It should not.
There is a test case in checker/tests/nullness/java8/lambda/LambdaInit.java.
CF is treating this in top-level lambda as underinitialized, which is not always true and there is currently no way to specify if it's underinitialized or initialized.
In addition, it's also unsoundly (incompletely?) treating inner class access to outer this as initialized.
Example 1 - inner class accessing underinitialized field passing check
public class Test {
String a;
Runnable b = new Runnable();
final class Runnable {
void run() {
System.out.println(a.toString());
}
}
public Test(String a) {
b.run();
this.a = a;
}
public static void main(String[] args) {
new Test("hi");
}
}
Example 2 - top-level lambda which is safe failing the check (dereference.of.nullable)
public class Test2 {
String a;
Runnable b = () -> {
System.out.println(a.toString());
};
public Test2(String a) {
this.a = a;
}
public static void main(String[] args) {
new Test2("hi");
}
}
Example 3 - top-level method reference which is safe failing check (methodref.receiver.bound.invalid)
public class Test3 {
String a;
Runnable b = this::bMethod;
private void bMethod() {
System.out.println(a.toString());
}
public Test3(String a) {
this.a = a;
}
public static void main(String[] args) {
new Test3("hi");
}
interface Runnable {
void run();
}
}
The inner class / lambda has an implicit field for outer this, which is underinitialized when the outer object is still being constructed. It becomes a circular reference when writing code as above and currently the framework does not handle it.
I would rather skip all initialization checks and treat all nonnull fields as initialized - is there a way in the checker framework?
The error in LambdaInit is correct. Try compiling and running this code:
public class LambdaInit {
String f1;
Object o =
new Object() {
@Override
public String toString() {
f1.toString();
return "";
}
};
String s = o.toString();
public LambdaInit() {
this.f1 = "";
}
public static void main(String[] args) {
new LambdaInit();
}
}
Note if you initialize the fields in the constructor, the errors go away and there is not null pointer exception.
public class LambdaInit {
String f1;
Object o;
String s;
public LambdaInit() {
this.f1 = "";
this.o =
new Object() {
@Override
public String toString() {
return f1;
}
};
this.s = o.toString();
}
public static void main(String[] args) {
new LambdaInit();
}
}
@smillst This code is same as your first example (NPE) but passes check:
public class LambdaInit {
String f1;
class InnerClass {
@Override
public String toString() {
f1.toString();
return "";
}
}
Object o = new InnerClass();
String s = o.toString();
public LambdaInit() {
this.f1 = "";
}
public static void main(String[] args) {
new LambdaInit();
}
}
The real offence is Object o = new InnerClass(); not the f1.toString(); - the error is reported on the incorrect line technically.