Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Pros/Cons of Nullable Generic Type Declaration and ways to reduce verbiage

20 views
Skip to first unread message

Rich MacDonald

unread,
Sep 25, 2024, 5:56:26 PM9/25/24
to jspecify...@googlegroups.com

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

        }
    }
}

Rich MacDonald

unread,
Sep 25, 2024, 6:01:57 PM9/25/24
to jspecify-discuss
Pardon the poor formatting. And a poor example in the test code. Replace the last code with:

String str3 = field1.get(); // str3 is nullable but I don't explicitly declare it as nullable. str3.toString(); //I DO get a compiler warning here, so everything is working correctly

Chris Povirk

unread,
Sep 26, 2024, 4:56:24 PM9/26/24
to Rich MacDonald, jspecify-discuss
Hi, Rich,

Sorry that we haven't gotten to your prior thread at all. I think it may require some Eclipse-specific knowledge, which few of us have. I imagine that one of us could still manage a partial answer, but you can see that I haven't given it one... :\

In this thread, I think you are likewise seeing some Eclipse-specific bits of behavior, but you're also asking about something we've discussed a fair amount for JSpecify.

Our answer in 1.0 is that we don't support "definitely nullable" type parameters. If you want to see way too much background, you can see one meandering thread about that possible feature, which was split off from a thread about what `class Foo<@Nullable T>` would mean, since different tools give it different meanings, and you could imagine that we might someday consider annotating type parameters to achieve a different purpose.

As you've seen, Eclipse does have support for that, as it did for years before the JSpecify group started. In an ideal world, Eclipse would offer a setting that ignores or warns about such declarations for compatibility reasons, but I'd be hard pressed to argue that it should be a priority for them when they have users who rely on the current support.

Given that, we'd recommend the "definitely non-null" approach. Incidentally, we also recommend that over an approach that lets users pass either a nullable type argument or a non-nullable one.

That would sidestep your question about field-local defaults or automatic inference of nullable type arguments. 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. That wouldn't rule out a field-scoped annotation like the one you suggested (though that would be a fairly special-purpose annotation), but I mention it as 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.).

Thanks for the question. Let me know if I missed anything!

--Chris

Chris Povirk

unread,
Sep 26, 2024, 4:58:54 PM9/26/24
to Rich MacDonald, jspecify-discuss
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.).

You can see summaries of a couple such discussions in successive FAQ entries:

Rich MacDonald

unread,
Sep 27, 2024, 10:30:50 AM9/27/24
to jspecify-discuss
I received an email reply from Werner Dietl that didn't make it to the board, so sharing it here:

----------
  Hi Rich,

You should declare NullableDeclaration like this:

interface NullableDeclaration<PROPERTY extends @Nullable Object>

This makes the class parametric in the nullness of PROPERTY. Now one can use the class either with a non-null or a nullable type argument.

The meaning of interface NullableDeclaration<@Nullable PROPERTY> is actually not specified by JSpecify - the annotation on the type parameter declaration has no defined meaning.
You do not specify what tool you're using. If you're using the Checker Framework or EISOP, interface NullableDeclaration<@Nullable PROPERTY> is interpreted as meaning the type argument has to be nullable - which would explain the warning for the type of field1.

For further examples, please see

Best,

----------------------

My comments:

Thanks for the `NullableDeclaration<PROPERTY extends @Nullable Object>` correction. That makes sense and helps.

Interesting that the eclipse null checker did try to do something with `NullableDeclaration<@Nullable PROPERTY>`. 


Chris Povirk

unread,
Sep 27, 2024, 10:37:38 AM9/27/24
to Rich MacDonald, jspecify-discuss, Werner Dietl
Thanks, Werner and I may have different readings of what you're going for, or I may have been overstating the level of consensus on the "definitely non-null" approach.

For the question of what you're going for: If so, it's a question of whether your scenario benefits from "two different kinds" of the class or not:

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.

I have been reading you as saying that you don't have "two different kinds" here. Werner may disagree. Or, again, he may be advocating for using <T extends @Nullable Object> even in that case, since that can sometimes save users from having to write "@NonNull V":

This approach sometimes requires users to write Optional<@NonNull V> (a "non-null projection") when using a type parameter V that allows nullable types.

Obviously Werner can speak for himself, so that's all just to set the stage :) 

Rich MacDonald

unread,
Sep 27, 2024, 12:01:46 PM9/27/24
to jspecify-discuss
I am going for "Let's make this work for java!" Which means people like me have to make our refactoring experiences public, so YOU can get a feel for how and where noobs will run into problems. Where should the documentation be enhanced? What kinds of examples should it have? Where are the gotchas and tradeoffs for developers and how to present that? 

This particular code example was looking at two choices of declaring nullables. Where the "problem" is: "OMG, do I HAVE to declare @Nullable in a thousand places?", can I avoid it, and what are the repercussions for doing so?

So the new/corrected code looks like (Warnings from the eclipse ecj compiler in comments):

public class NullVsNonNull_GenericType_Example_Fix { interface NullableTypeDeclaration<PROPERTY extends @Nullable Object>{ PROPERTY get(); void set(PROPERTY value); } //equivalent to NullableParamsDeclaration<PROPERTY extends Object> //no need for the developer to declare @Nullable, but cannot be used for @NonNull situations. interface NullableParamsDeclaration<PROPERTY>{ @Nullable PROPERTY get(); void set(@Nullable PROPERTY value); } public static abstract class MyDomain { NullableTypeDeclaration<String> fieldNonNull; // no annotation means the NonNull String type NullableTypeDeclaration<@NonNull String> fieldNonNull2; //explicit @NonNull declaration is redundant NullableTypeDeclaration<@Nullable String> fieldNullable; // Have to declare the Nullable case. However, means it can be used in either Nullable or NonNull cases. NullableParamsDeclaration<String> fieldNullParams; // Functions as Nullable and can be declared without the Nullable annotation void test() { String str1 = fieldNonNull.get(); str1.toString(); //known to be nonnull String str2 = fieldNullable.get(); str2.toString(); // DO get a compiler warning here, so everything is working correctly String str3 = fieldNonNull2.get(); str3.toString(); //known to be nonnull String str4 = fieldNullParams.get(); str4.toString(); // DO get a compiler warning here, so everything is working correctly } } }


In the above code, NullableTypeDeclaration is the powerful approach. It lets the developer use the same code for either Nullable or NonNull situations. But it comes at a "price" of requiring explicit declaration wherever it is used. Fine for small cases, but I have many thousands of lines of code that will need to be refactored to add this declaration (regex find and replace, here we come). Honestly, it is also hard on the developer's eyes.

NullableParamsDeclaration has the downside that it can only be used for Nullable situations, but the developer can use it without having to declare it as Nullable.

Frankly, I am considering declaring two interfaces:
interface NullableParamsDeclaration<PROPERTY>{ @Nullable PROPERTY get(); void set(@Nullable PROPERTY value); } interface NonNullParamsDeclaration<PROPERTY>{ @NonNull PROPERTY get(); void set(@NonNull PROPERTY value); }

This scales horribly for the developer writing the interfaces, but the users are happy.
Reply all
Reply to author
Forward
0 new messages