The following code shows two ways to declare a generic type with @Nullable. In the first case (`NullableDeclaration`), I am declaring the generic type as Nullable. In the second case (`NonNullDeclaration`), I am declaring the generic type as `@NonNull`, but its usages are `@Nullable`.
In some sense, `NullableDeclaration` is "right", correct? But are the benefits of doing it this way?
A benefit of `NullableDeclaration` is that subclasses/implementations can declare the generic type as `@Nullable` or `@NonNull`, and everywhere that "PROPERTY" is used in the `NullableDeclaration` interface and subclasses automatically pick up the `@Nullable` or `@NonNull` declaration.
A downside of `NullableDeclaration` is that all users have to declare it with a duplicated `@Nullable`. If we have one class/interface that we use in 1,000 places, having to write an extra `@Nullable` 1,000 times is going to get pushback from the developers.
As I gain experience with Jspecify, I am wishing for more granularity in declaring "subsets/categories" of defaults. `@NullMarked` and `@NullUnmarked` are great. But here I would like to see something that can target `NullableDeclaration<String> field1` to remove that category of warnings:
"If the parent class is declared with a Null/NonNull generic type, usages inherit that annotation without having to declare the annotation again."
If we have a declared type `NullableDeclaration<@Nullable PROPERTY>`, then we should be able to declare a usage as `NullableDeclaration<PROPERTY>` and have it be interpreted as `NullableDeclaration<@Nullable PROPERTY>`.
Does this make sense? What other pros and cons am I missing?
Rich MacDonald
public class NullVsNonNull_GenericType_Example {
/**
* Declare generic type as Nullable.
* Pro: All usages of PROPERTY are automatically @Nullable
* Con: Every declaration has to include @Nullable in the generic type
*/
interface NullableDeclaration<@Nullable PROPERTY>{
PROPERTY get();
void set(PROPERTY value);
}
/**
* Declare the methods/params as Nullable.
* Pro: Users can declare this without @Nullable in the generic type.
* Con: Methods and parameters in this class need to explicitly declare @Nullable/@NonNull property
*/
interface NonNullDeclaration<PROPERTY>{
@Nullable PROPERTY get();
void set(@Nullable PROPERTY value);
}
public static abstract class MyDomain {
NullableDeclaration<String> field1; // Compiler warning: Null constraint mismatch: The type 'String' is not a valid substitute for the type parameter '@Nullable PROPERTY'
NullableDeclaration<@Nullable String> field1a; // no warnings, but verbose declaration.
NonNullDeclaration<String> field2; // No compiler warnings and less text. Developers are happier?
void test() {
@Nullable String str1 = field1.get();
field1.set(str1);
@Nullable String str2 = field2.get();
field1.set(str2);
// str1 is nullable but I don't explicitly declare this.
str1.toString(); //I DO get a compiler warning here, so everything is working correctly
}
}
}
Still, on that front, I'd say that we're doing our best to maintain a principle that you can understand the nullness of a type usage from just the annotations on the type usage itself and any @NullMarked/@NullUnmarked annotations on enclosing scopes. ...a principle that's come up in a variety of discussions, both about automatic inference of type arguments and about other possible features (inheritance of nullness information from supertypes, special cases for well-known types, etc.).
There aren't "two different kinds" of Optionals, "those that might contain null" and "those that can't." As a result, we don't want it to be possible to have both an Optional<String> and an Optional<@Nullable String>: If that were possible, then you might find yourself with one of kind when an API requires the other. This is in contrast to types like List, where List<String> (can't put nulls in, won't get nulls out) and List<@Nullable String> (can put nulls in, might get nulls out) are useful to distinguish.
This approach sometimes requires users to write Optional<@NonNull V> (a "non-null projection") when using a type parameter V that allows nullable types.