2
votes

I have a class with a hasField function that checks if a field is present and not null, and a getField function that returns the value of the field (or null if not present).

In my code when I call getField right after checking hasField, I know that getField is not going to return null, but the IDE Inspection (Constant Conditions and Exceptions) doesn't know that. I get a bunch of method method name may produce a NullPointerException

I'm trying to find a clean way to make this warning go away.

Workarounds

Here are some workarounds I could do but I find all of these hacky:

  1. Surround getField with Objects.requireNotnull, the code would be no-op. Would prefer not doing that as it makes the code slightly less readable.
  2. Suppress warnings where I know this is safe. Again not preferred as this is going to happen at a bunch of places in our code.
  3. Ignore warnings. In this case we might miss legit warnings just because warnings section will be too noisy.

Ideal solution

Would I be able to somehow set up the warnings in such a way that if hasField is true, then getField will return a non-null? I looked into JetBrains Contract Annotations but doing what I want here seems to be beyond what is supported with @Contract

Code Sample

Here's a minimum working code sample that demonstrates the issue:

import javax.annotation.Nullable;

public class Hello {

  private Hello(){}
  public static void main(String[] args) {
    TestClass test1 = new TestClass(null);
    if (test1.hasSample()) {
      System.out.println(test1.getSample().equals("abc"));
    }
 }
}

class TestClass {
  private final String sample;

  TestClass(String field) { this.sample = field; }

  boolean hasSample() { return sample != null; }

  @Nullable public String getSample() { return sample; }
}

I get the following warning

Method invocation equals may produce NullPointerException

I'd ideally want to be able to tell IDE that getSample is not null when hasSample is true.

1

1 Answers

2
votes

Disclosure I'm IntelliJ IDEA developer responsible for this subsystem


No, it's not possible now. There's no better solution than possible workarounds you already listed, assuming that you cannot change the API. The closest thing we have is the inlining of very trivial methods. However, it works only if:

  • The methods like hasSample() and getSample() are called from the same class
  • The called methods cannot be overridden (private/static/final/declared in final class)

E.g. this feature works in the following code:

final class TestClass { // if final is removed, the warning will appear again
  private final String sample;

  TestClass(String field) { this.sample = field; }

  boolean hasSample() { return sample != null; }

  @Nullable
  public String getSample() { return sample; }

  @Override
  public String toString() {
    if (hasSample()) {
      return "TestClass: "+getSample().trim(); // no warning on trim() invocation here
    }
    return "TestClass";
  }
}

For now, I can only suggest refactoring your APIs to Optionals like this:

import java.util.Optional;

public class Hello {

  private Hello(){}
  public static void main(String[] args) {
    TestClass test1 = new TestClass(null);
    test1.getSample().ifPresent(s -> System.out.println(s.equals("abc")));
    // or fancier: test1.getSample().map("abc"::equals).ifPresent(System.out::println);
  }
}

final class TestClass {
  private final String sample;

  TestClass(String field) { this.sample = field; }

  public Optional<String> getSample() { return Optional.ofNullable(sample); }
}