Thanks to everyone working on JSpecify. I think you're doing something very valuable and I will do my best to incorporate your work into my system.
I have seen requests for "real-world" cases. Here is one: I have an existing "domain model framework" to handle various "side-effects and capabilities". For example:
1) All property access must hook into an "access control framework", as not all users will have access to all the field values.
2) Changing the value of a field "dirties" the owner and queues the owners to be persisted at the next database commit.
The solution was to delegate all "get/set" methods to a "Property Accessor" framework "behind" the domain objects. (Smalltalk developers will be familiar.)
So here is a simple java object model. A single nullable field:
public enum MyEnum{
ONE,
TWO
}
public static class MyDomain{
@Nullable MyEnum myEnum;
}
I implement the PropertyAccessor in two parts:
1) A single instance on a static variable, reachable by the domain instances.
2) Every call to the domain "get/set" methods instantiates an instance that exists for the duration of the call.
(Aside: The latter instantiation is obviously hell on the garbage-collector if it ran that way in practice. Luckily, the JIT compiler can inline all this to prevent any new instances being created, for the cost of a few stack variables.)
The complete example code is shown below:
package test.jspecify;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
/**
* MyDomain has a field myEnum with a getter and setter. The field is Nullable.
* Getters and setters need to execute "side-effect" administrative tasks, such as access-control checking, validation, listeners, etc.
* Framework solution is to delegate to a "PropertyAccessor" layer.
* Domain class provides getter and setter to the PropertyAccessor. This provides decoupling between the layers.
*
* Implementation note, irrelevant to the @Nullability issue.
* A single TestGetSetNullableEnum instance is implemented at startup via a static method. It handles all instances of MyDomain.
*
* Every call to "get" or "set" the field value of a MyDomain instantiates an EnumNullablePropertyAccessor instance.
* However, final declarations will allow the JIT to inline that without causing heap allocation. (Stack variables are used instead.)
*/
public class ExamplePropertyAccessorNullability {
// Domain classes
public enum MyEnum{
ONE,
TWO
}
public static class MyDomain{
@Nullable MyEnum myEnum;
@NonNull EnumNullablePropertyAccessor<MyEnum> myEnum() {
return getSetEnum.on(
this,
()->myEnum,
value -> this.myEnum = value
);
}
public @Nullable MyEnum getEnum() {
return myEnum().get();
}
public void setEnum(@Nullable MyEnum value) {
myEnum().set(value);
}
}
// Property Accessor classes
static final class TestGetSetNullableEnum<PROPERTY extends Enum<PROPERTY>>{
final String fieldName;
final Class<PROPERTY> klass;
public TestGetSetNullableEnum(String fieldName, Class<PROPERTY> klass) {
this.fieldName = fieldName;
this.klass = klass;
}
public @NonNull EnumNullablePropertyAccessor<PROPERTY> on(Object source, Supplier<@Nullable PROPERTY> getter, Consumer<@Nullable PROPERTY> setter) {
return new EnumNullablePropertyAccessor<PROPERTY>(this, source, getter, setter);
}
}
public static final class EnumNullablePropertyAccessor<PROPERTY extends Enum<PROPERTY>> {
final TestGetSetNullableEnum<PROPERTY> getSetEnum;
final Object source;
final Supplier<@Nullable PROPERTY> getter;
final Consumer<@Nullable PROPERTY> setter;
EnumNullablePropertyAccessor(TestGetSetNullableEnum<PROPERTY> getSetEnum, Object source, Supplier<@Nullable PROPERTY> getter, Consumer<@Nullable PROPERTY> setter) {
this.getSetEnum = getSetEnum;
this.source = source;
this.getter = getter;
this.setter = setter;
}
@Nullable PROPERTY get() {
//access control checking, etc
return getter.get();
}
void set(@Nullable PROPERTY value) {
//access control checking, dirtying the source instance, two-way association updates, validation, listeners, etc
setter.accept(value);
}
}
/**
* Following line causes the warning:
* Unsafe null type conversion (type annotations): The value of type 'ExamplePropertyAccessorNullability.@NonNull TestGetSetNullableEnum<ExamplePropertyAccessorNullability.@NonNull MyEnum>'
* is made accessible using the less-annotated type 'ExamplePropertyAccessorNullability.TestGetSetNullableEnum<ExamplePropertyAccessorNullability.MyEnum>'
*/
static final TestGetSetNullableEnum<MyEnum> getSetEnum = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);
/**
* Alternative following line has no warnings but is hard to understand:
* Why would a "Nullable PropertyAccessor need to declare a @NonNull?
* Who is causing this to be declared as @NonNull? (Not using package-info.java defaults)
*/
static final TestGetSetNullableEnum<@NonNull MyEnum> getSetEnum2 = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);
@NullUnmarked
static class GetSetDeclarations{
//still getting the warning
static final TestGetSetNullableEnum<MyEnum> getSetEnum3 = new TestGetSetNullableEnum<>("myEnum", MyEnum.class);
}
}
The problem I am having is the unwanted warning:
Unsafe null type conversion (type annotations): The value of type 'ExamplePropertyAccessorNullability.@NonNull TestGetSetNullableEnum<ExamplePropertyAccessorNullability.@NonNull MyEnum>'
is made accessible using the less-annotated type 'ExamplePropertyAccessorNullability.TestGetSetNullableEnum<ExamplePropertyAccessorNullability.MyEnum>'Part of me "gets it": I have defined a class with an unspecified generic, and all the explicit uses of that generic provide the @NonNull/@Nullable. But I don't understand what is causing the warning.
I have tried the desperate/clueless alternatives, including moving the @Nullable annotations to the other classes. No success yet.
I moved all the field declarations to another static class and added @NullUnmarked to the class, i.e. GetSetDeclarations in the above code. This did not work, which surprised me.
My eclipse -> compiler -> errors/warnings -> null analysis settings are all set at the "warning level". I suppose I could live with turning some of them into "info level" but that is a last resort. (And which one corresponds to the 'less-annotated type'?)
As an aside: If anyone is working with the eclipse team, the "Missing '@NonNullByDefault' annotation on package" is unfortunate because it prevents the developer from setting the default to 'NullUnmarked' to remove the warning.
Any help is appreciated.
Rich MacDonald
macdona...@gmail.com