Actually, maybe we don’t need NonNull at all for interop*:
This is the worst case I could come up with:
public interface JBox {
<T extends @Nullable Object> void put(
@NonNull T notNullable,
T maybeNullable,
@Nullable T nullable
);
}
And to my understanding, we can perfectly preserve which arguments are valid/invalid at the cost of a more complicated type clause:
def put[Base <: AnyRef, Aug <: Base | Null](
notNullable: Base,
maybeNullable: Aug,
nullable: Base | Null
)
Example: Scastie - An interactive playground for Scala.
In the case where there is only two of the three parameters, we can instead use a single type parameter.
Otherwise we have to either under- or over-correct:
Under-correct:
def put[T <: AnyRef | Null](
notNullable: T,
maybeNullable: T,
nullable: T | Null
)
jbox.put[Foo ](Foo(), Foo(), null) // allowed, should be
jbox.put[Foo ](Foo(), null, null) // forbidden, should be
jbox.put[Foo | Null](Foo(), null, null) // allowed, should be
jbox.put[Foo | Null](null, null, null) // allowed, should be forbidden !
(equivalent to, but more further from the source: T <: AnyRef, maybeNullable: T | Null)
Over-correct:
def put[T <: AnyRef](
notNullable: T,
maybeNullable: T,
nullable: T | Null
)
jbox.put[Foo ](Foo(), Foo(), null) // allowed, should be
jbox.put[Foo ](Foo(), null, null) // forbidden, should be allowed !
jbox.put[Foo | Null](Foo(), null, null) // allowed, should be
jbox.put[Foo | Null](null, null, null) // forbidden, should be
Note that we can use asInstanceOf to still be able to do the forbidden case:
jbox.put[Foo](Foo(), null.asInstanceOf, null)
*Since we change the type parameter clause depending on the signature, this might cause issues when the java source is updated: Adding parameters/changing nullability