Skip to content

Is rejecting String.class for a Class<@Nullable String> parameter intended? #1595

Description

@Artur-

Summary

When a constructor (or method) takes a Class<T> type token and T is inferred as a @Nullable type via the diamond operator, NullAway rejects passing a plain class literal such as String.class:

[NullAway] incompatible types: Class<String> cannot be converted to Class<@Nullable String>

Since there is no way to write a Class<@Nullable String> literal in Java (String.class is always Class<String>, and nullness is not reified on a class token), I'd like to confirm whether this is intended behavior or a false positive that should be special-cased for Class<T> type tokens.

Minimal reproducer

package com.example;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class ClassTokenRepro {

    static class Signal<T extends @Nullable Object> {
        private final Class<T> valueType;

        Signal(Class<T> valueType) {
            this.valueType = valueType;
        }
    }

    void caller() {
        // T = String -> param Class<String>, arg Class<String>: OK
        Signal<String> nonNull = new Signal<>(String.class);

        // T = @Nullable String -> param Class<@Nullable String>, arg Class<String>.
        // ERROR: [NullAway] incompatible types:
        //        Class<String> cannot be converted to Class<@Nullable String>
        Signal<@Nullable String> nullable = new Signal<>(String.class);
    }
}

An explicit type witness does not change the result (the parameter type is still Class<@Nullable String>):

Signal<@Nullable String> nullable = new Signal<@Nullable String>(String.class); // same error

The only way we found to satisfy the checker is an explicit type-use cast, which is erased at runtime to plain String.class:

Signal<@Nullable String> nullable = new Signal<>((Class<@Nullable String>) String.class); // OK

Why this looks questionable

  • Class<T> is invariant, so strictly Class<String> is not a Class<@Nullable String>.
  • However, a Class instance is a runtime type token. Nullness is not part of the runtime class, and there is no Class<@Nullable String> literal to produce. So for a Class<T> parameter, the nullness of T arguably carries no meaning and the only available argument is a Class<NonNullVersionOfT>.
  • This makes any API of the shape Foo(Class<T> type) with T extends @Nullable Object effectively unusable with a nullable T without a cast.

When it changed

The change first appears in 0.13.2, which lines up with the "Initial handling of constructor diamond operators" work (PR #1464): before that the diamond construction was not analyzed, so the nullable case compiled.

Question

Is this the intended behavior for Class<T> (and similar runtime type-token) or just how it happens to work right now?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions