Home > Software design >  How to deal with warning 'unsafe null type convertion' when using java.lang.Optional
How to deal with warning 'unsafe null type convertion' when using java.lang.Optional

Time:06-07

I would like to offer some accessors to a value-container (that is a java.lang.Map under the hood).

When implementing accessors like this

public Optional<Integer> getSomeValueThatMayBeNullOrAnInteger() {
    Integer nullable = this.get("keyToANullableInteger", Integer.class); // is casting to given Class
    return Optional.ofNullable(nullable);
}

... 'ofNullable' is marked with 'unsafe null type convertion'.

Why is this? The parameter ofNullable is not marked as @NonNull. Is it, because empty() or of() is used and of() is checking for NonNull? Is this a Eclipse-bug, because empty() is ignored, when checking the possibilities?

Can someone please explain this to me and maybe tell me, if there is another way to solve this warning as using @SuppressWarnings("null"). Thanks

CodePudding user response:

Missing nullity annotations in java core

Every type has a nullity status. Thus, in Optional<Integer>, both types mentioned have a nullity status - is it @NonNull Optional<@NonNull Integer>? Presumably the answer is trivially: Yes.

There is no point in having a 'nullable optional' (if you have those, stop and fix that first), and the typearg to optional is trivially @NonNull (you can't have an Optional.SOME whose value is null).

However, eclipse doesn't know that. Furthermore, the java core classes are not nullity annotated.

Presumably, you've marked your class as 'default to -everything is nonnull-' otherwise you need to slap a @NonNull on everything which is unwieldy. Thus, eclipse thinks your method's return type is @NonNull Optional<@NonNull Integer>.

However, the Optional.ofNullable method (in class java.util.Optional), simply returns @WhoKnows Optional<@WhoKnows T> - the j.u.Optional class is not marked as 'default to everything being non null' nor is the method itself annotated with @NonNull because the java core classes do not have such annotations at all. Therefore, eclipse is warning you, telling you: Hey, Optional.ofNullable might return null (we know it won't, but how would eclipse know that?), and you're just returning it verbatim, but your method promises to never return null so this is a potential problem.

How to use nullity annotations

You need a way to have your nullity checker (eclipse, in your case) know the nullity status of everything. Including libraries you are using, so, at least including java.*. That means you need a concept called 'external annotations' - a file that 'fills in the blanks', so to speak, on java.* and everything else. For starters, it would list that the ofNullable static method in java.util.Optional should be deemed to be annotated with @NonNull.

Without such a system it's a miserable, miserable experience - going halfway on nullity annotations means you get an endless cavalcade of the very warnings you are getting now. Leading you to remove the warnings (which makes the feature pointless) or annotate most of your methods with @SuppressWarnings("null") which would also make the feature pointless.

The last time I checked which is admittedly a very long time ago, there was no such file or system available, but I believe eclipse has at least added the concept of 'external annotations'. I'd search the web and try to find an EA file that includes all/most of the java.* API at least and use it.

Pick a side!

Optional is a way to lift nullity status into the type system.

Annotations are also a way to do that.

Do not use both at the same time.

Useful facts about nullity systems

  • Optional is not backwards compatible. Take java.util.Map's .get() method. It currently returns V. If you feel Optional is the correct answer, then this method is incorrect as it should be returning Optional<V> instead. However, changing it is backwards incompatible, and java is exceedingly unlikely to ever do that. Thus, Optional will never get to their perfect world, where Optional is used as de-facto mechanism everywhere.
  • Annotations can be backwards compatible and therefore clearly superior as an answer. Unfortunately, there are at least 9 competing annotation-based nullity systems, and they all work quite differently. Some are type-use based, some are not, for example.
  • Most annotation-based frameworks don't have a @PolyNull concept (only checkerframework's does). That means they are all incapable of representing the nullity status of certain existing methods.
  • Optional doesn't properly compose and doesn't have the poly null concept and cannot be made to have it, so that is another reason Optional will never be able to fully replace null in java.

Where does that leave you

Nullity annotations are unwieldy (not standardized, lacking external annotation files). Optional is a pipe dream (existing APIs cannot be modified to use it). So where does that leave you?

Mostly, just accept that null is fine. Turn off the annotation-based checks, stop using Optional for stuff like this.

There is something you can do right now, and something that the java API is already doing, as it's entirely backwards compatible:

_Work on nicer APIs.

Have a look at the javadoc of java.util.Map, for example. There's computeIfAbsent, getOrDefault, and a few other methods that mostly mean you don't need any type-based nullity at all to work with Map without having to bother with null. For example, you don't have to write this code:

Map<String, List<String>> example = ...;

List<String> list = example.get(key);
if (list == null) {
  list = new ArrayList<>();
  example.put(key, list);
}
list.add("Test");

Instead you can just write:

Map<String, List<String>> example = ...;

example.computeIfAbsent(key, k -> new ArrayList<>()).add("Test");

and you don't have to write:

Map<String, Integer> example = ...;

int v = 0;
// 2 calls - ugly
if (example.containsKey(key)) v = example.get(key);
// or:
Integer vRaw = example.get(key);
if (vRaw != null) v = vRaw.intValue();

You can instead just write:

Map<String, Integer> example = ...;

int v = example.getOrDefault(key, 0);

You can apply the same principles to the code you write. For example:

public int getSomeValue(int defaultValue) {
  Integer nullable = this.get("keyToANullableInteger", Integer.class);
  return nullable == null ? defaultValue : nullable.intValue();
}

public Integer getSomeValueOrNull() {
  return this.get("keyToANullableInteger", Integer.class);
}

public int getSomeValue() {
  Integer n = this.get("keyToANullableInteger", Integer.class);
  if (n == null) throw new NullPointerException("key not present in data store");
  return n.intValue();
}

No chance a caller is going to be confused about what can and cannot be null here. (You probably don't need all 3, think a bit about your API design).

  • Related