Reputation: 265131
I wanted to extend a bit of code that processes annotations to also support the Nullable
annotation from the Checker Framework. To my surprise, this didn't work. Upon further inspection, it seems like the Checker Framework annotation is never picked by by reflection, even though meta-annotated with @Retention(RUNTIME)
.
class AnnotationTest {
@javax.annotation.Nullable
Object javax;
@jakarta.annotation.Nullable
Object jakarta;
@org.checkerframework.checker.nullness.qual.Nullable
Object checker;
@Test
void annotations() {
final Field[] fields = getClass().getDeclaredFields();
for (final Field field : fields) {
System.out.println(field.getName() + " annotated with " + Arrays.toString(field.getDeclaredAnnotations()));
}
}
}
Output:
javax annotated with [@javax.annotation.Nullable()]
jakarta annotated with [@jakarta.annotation.Nullable()]
checker annotated with []
It is a Spring Boot project and it doesn't matter if the test is executed with IntelliJ or Gradle. Why are the other annotations picked up, but not the Checker Framework annotation?
For completeness sake, here's the definition of the annotation:
@SubtypeOf({})
@ImplicitFor(literals = LiteralKind.NULL, typeNames = java.lang.Void.class)
@DefaultInUncheckedCodeFor({TypeUseLocation.RETURN, TypeUseLocation.UPPER_BOUND})
@Documented
@Retention(RetentionPolicy.RUNTIME) // <-- retained at runtime!
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
public @interface Nullable {}
Upvotes: 4
Views: 190
Reputation: 102775
It's there. You're asking the wrong node.
javax.annotation.Nullable
and checker's nullable are completely different. This is a general trend: There are ~15 frameworks with nullity annotations and none of them are the same. It's a big mess.
For what it is worth, checkerframework is more correct and most others are less correct. The reason they are wrong is possibly incompetence (hey, this stuff is way more complicated than it seems like), but more likely: Backwards compatibility.
Java 1.5 started with the annotations; the target TYPE_USE
wasn't a thing back then. Later on (1.8?), TYPE_USE was introduced. Checkerframework either was started after TYPE_USE was available, or made a backwards incompatible update to move to it. javax/jakarta's nonnull predates it (or was designed by someone who didn't know what they were doing / has a wildly different idea of what non-null even means conceptually - but I think it predates).
These are the 2 completely different notions of nullity here:
TYPE_USE
The key difference is that checkerframework correctly states that it is targeted solely at types: @Target(ElementType.TYPE_USE)
. This is 'the new one'. That means given:
@Nullable private String foo;
The node that @Nullable
applies to is just the String
part. The field has no annotations here. None at all.. The field has a type and that has an annotation.
This conceptual notion of null is one that expands the type system itself. Any type has a nullity status.
The downside of this view is that its complicated in terms of generics. Just like there are 4 different ways to express 'list of numbers' in terms of generics:
List<? extends Number>
List<? super Number>
List<Number>
List
(legacy / raw mode)You need at least 4 levels of nullity (think of a method that accepts a List<String>
and we are discussing the nullity of String)
List<@NonNull String>
may be passed to this method.null
is fine, strings are fine). You can pass a List<@Nullable String>
to this method.List<@Nullable String>
as well as a List<@NonNull String>
. Which can be very useful, and is not expressible by any nullity framework... except.... checker-framework! Which allows this with @PolyNull
, more or less.Just like generics, this stuff is intrinsically complicated. It just is, so while this view of nullity is all sorts of difficult, it is an accurate representation of how things work, and thus, 'correct'.
ElementType.FIELD
In contrast, an annotation can state that it applies to fields, methods, and method parameters. In that case, if you write:
@Nullable private String foo;
The field has an annotation and its type doesn't.
This is a completely different view of nullity. You're just annotating whether a field/method's return value/parameter allows null or does not. It is not possible to get into details. You can't convey the concept of a list of non-nullable sets containing nullable strings. Because you can't carry this nullity concept into generics, it isn't composable and therefore you cannot possibly get to a world where every type at every location has known nullity.
But it is simple.
More generally type use is just 'correct', in that you can of course have a nullable list of non-nullable sets containing nullable strings. Which you can express with TYPE_USE
but cannot express without it.
The javax/jakarta nullility annotations don't have a @Target
annotation. That means their target is 'everything except type parameters' (because the JLS says that's what 'no @Target
annotation' means).
Therefore jakarta's annotations don't actually explain what that annotation even means 'this cannot be null', sure, but, the 'this' in that sentence is ambiguous.
In the case where an annotation can apply to both types and fields/methods/parameters (such as jakarta's), this code becomes ambiguous:
public @Nullable String field;
Given that the annotation can apply both to the field as a whole as well as the the field's type specifically, how is one to interpret this code? As per the compiler's specs, in such a case, the annotation applies to both. Both field.getAnnotations()
as well as field.getAnnotatedType().getAnnotations()
will find it, and indeed it's as if you wrote @Nullable public {@Nullable String} field
(two separate things both annotated), except of course that's not legal java syntax.
This seems great, except, it isn't: You now have 2 mostly unrelated completely different ways to analyse nullity. You don't want that.
Sure!
field.getAnnotatedType().getDeclaredAnnotations()
is what you're looking for.
Upvotes: 5
Reputation: 8117
The reason is that the Checker Framework's Nullable
is a type qualifier:
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
It is a bug that the javax version (illegally placed in the javax namespace by the abandoned JSR 305 project) and the jakarta version are declaration qualifiers, since nullness is a quality of a type, not a declaration.
You can verify that the @Nullable
annotation appears on the type by running the following code:
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.util.Arrays;
class AnnotationTest {
@org.checkerframework.checker.nullness.qual.Nullable Object checker;
public static void main(String[] args) {
final Field[] fields = AnnotationTest.class.getDeclaredFields();
for (final Field field : fields) {
System.out.println("field " + field.getName() + " annotated with "
+ Arrays.toString(field.getDeclaredAnnotations()));
AnnotatedType atype = field.getAnnotatedType();
System.out.println(
"type " + atype.getType() + " annotated with "
+ Arrays.toString(atype.getDeclaredAnnotations()));
}
}
}
whose output is
field checker annotated with []
type class java.lang.Object annotated with [@org.checkerframework.checker.nullness.qual.Nullable()]
Upvotes: 2
Reputation: 31
Try the following in the annotations method:
@Test
void annotations() {
final Field[] fields = getClass().getDeclaredFields();
for (final Field field : fields) {
// direct annotations
System.out.println(field.getName() + " annotated with " + Arrays.toString(field.getDeclaredAnnotations()));
// type-use annotations
AnnotatedType annotatedType = field.getAnnotatedType();
System.out.println("Type-use annotations on " + field.getName() + ": " + Arrays.toString(annotatedType.getDeclaredAnnotations()));
}
}
Upvotes: 0