8

It seems widely accepted that assert statements should be reserved for testing and disabled in production because errors should have been resolved by then, and enabling assertions affects performance. However surely the same could be said of null checks with if statements. Why is this code considered good for production

if(x != null) {
    x.setId(idx);
    if (y != null) {
        if (y.id == x.id) {
            x.doSth();
        }
    } else {
        //handle error
    }
} else {
    //handle error
}

but this code isn't? (Assuming assertions are enabled)

try {
    assert(x != null); 
    x.setId(idx);
    assert(y != null);
    if (y.id == x.id) {
        x.doSth();
    }
} catch (AssertionError e) {
    //handle error
}

I understand using an if statement when it's expected that a variable may not be initialized. However when it's for defensive coding, assert seems more elegant and readable.

I also tested performance for each method with this:

public class AssertTest {

    static final int LOOPS = 10000000;

    public static void main(String[] args) {

            String testStr = "";

            long startNotEqualsTest = System.currentTimeMillis();
            for (int i = 0; i < LOOPS; i++) {
                if (testStr != null) {
                    testStr = System.currentTimeMillis() + "";
                } else {
                    throw new RuntimeException("We're doomed");
                }
            }
            long notEqualsTestDuration = System.currentTimeMillis() - startNotEqualsTest;

            testStr = "";

            long startAssertTest = System.currentTimeMillis();      
            for (int i = 0; i < LOOPS; i++) {
                try {
                    assert(testStr != null);
                    testStr = System.currentTimeMillis() + "";
                } catch (AssertionError e) {
                    throw new RuntimeException("We're doomed");
                }
            }
            long assertTestDuration = System.currentTimeMillis() - startAssertTest;

            System.out.println("Duration with if : " + notEqualsTestDuration + "ms");
            System.out.println("Duration with assert : " + assertTestDuration + "ms");  
    }
}

Timing is about the same for both methods:

Duration with if : 1234ms
Duration with assert : 1313ms

Disabling assertions makes very little difference:

Duration with if : 1391ms
Duration with assert : 1375ms

Have I missed any compelling reasons why null checks with if conditions are preferable to assert?

ACHC
  • 312
  • 1
  • 2
  • 10
  • Your assumptions are incorrect. A lot of professionals do not like null checks. [See this](https://stackoverflow.com/q/271526/738746). – Bhesh Gurung May 30 '18 at 22:03
  • I don't like them either. I think it's overly defensive coding. But on some projects they're mandated by management or a rule is set in the code inspector. Assuming null checks are required for whatever reason, can assert be used instead of putting them in if statements? – ACHC May 30 '18 at 22:13

3 Answers3

6

Ask yourself how you would handle the null cases? What do you do if an object is null that should not be. In most of the cases it absolutely ok to not handle it, at all. Just let it produce a NullPointerException sometime later in the execution. It is very likely to be a programming error, anyway, so it's ok to be caught by an ExceptionHandler eventually writing it to the logs.

Of course, there are cases when you need to react on null objects. if-else is made for such cases. It's easy, self-explaining, every programmer knows that construct, so why not using it. assert in production is discouraged, anyway. See this post on SO, for instance. And, for my taste, it's quite cumbersome with the try/catch block.

There are other options, as well. For instance, DavidW suggested to use annotations, which is perfectly fine, as long as you make sure that these annotations are interpreted (can be an issue when migrating code, for example).

Another approach are Validator classes. For instance, the Apache Commons library has a Validate class that checks for certain conditions and would throw an appropriate exception if the condition is not fullfilled. Of course, you can write your own Validators that will throw your custom exceptions, as well. You'll end up with a short concise one-liner like

Validate.notNull(myObj) 
Validate.hasEmailCorrectSyntax("foo@bar.com");

Also take a look at Java's Object.requireNotNull

Jan B.
  • 6,030
  • 5
  • 32
  • 53
  • I _almost_ agree with you :), except for the part of just letting a `NullPointerException` happen. I believe that if you know when you're writing code that a `null` value is an error, and you can't control all the callers, that you should (however you do it) check for valid parameters immediately. That way when the code is refactored and someone changes your `HashMap` to a `HashTable` it doesn't suddenly start breaking. – DavidW May 30 '18 at 22:39
  • I _fully_ agree with you :). That's why I wrote "most of the cases". In the second paragraph I'm arguing for using if-else when necessary. – Jan B. May 30 '18 at 22:54
  • In some cases you could let the NPE happen, but I'm assuming as DavidW says that you're writing code where null is known to be an unexpected value. What I like about assert is that it throws an error if the condition isn't met, so it is clear that it has to be handled, and the expected conditions are stated in the code. You could argue that throwing an AssertionError is not much different to throwing a NPE - both have to be explicitly caught - but with NPE it's not clear from the code where or under what conditions it may be thrown; with assert it is. – ACHC May 30 '18 at 23:05
  • I agree with both of you, sometimes you shouldn't wait for the NullPointer to be raised. Anyway, reconsider using the Validator construct. In the end it's a one-liner and it can do the same as handling the AssertionError. The benefit is, it doesn't clutter your code and it's customizable and reusable. Not to forget, if you have many _asserts_ but only one try/catch you can't tell which assert raised the error. – Jan B. May 30 '18 at 23:12
  • Thanks. [Validate](https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/Validate.html) looks very useful especially [noNullElements](https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/Validate.html#noNullElements(T,%20java.lang.String,%20java.lang.Object...)), as in the project I am working on there are countless iterators doing a null check on each element. – ACHC May 31 '18 at 00:29
2

Using assert at all has to presume that the -ea flag is enabled, and in many instances...it isn't. The assert will accomplish the same thing as the null checks, with the clear side-effect that if one doesn't have their environment configured in a particular way, the code will not function as they expect.

For that reason, it makes sense to eschew assert and leverage the null checks where necessary. More concisely, if one can, it's preferable to wrap everything that could be null in an Optional instance and operate on it using ifPresent (or orElseThrow since you seem to want to indicate an error if those values are null) instead.

Makoto
  • 104,088
  • 27
  • 192
  • 230
  • 1
    Hmmm, "if one can, it's preferable to wrap everything that could be null in an Optional". Can you elaborate on that, as I think it might be misleading. Brian Goetz once held a lecture where avoiding null by Optional is discouraged: https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf – Jan B. May 30 '18 at 22:44
  • @Matt: It depends on what you're doing with the reference with respect to the null check. In this context, using `ifPresent` on `x` would eliminate one null check. Using `ifPresent` on the second block would also eliminate a null check but could understandably look clunky. – Makoto May 30 '18 at 22:56
  • But how are you going to handle the _else block_ with ``// handle error``? You can't do that with ``ifPresent``. – Jan B. May 30 '18 at 23:05
  • @Matt: In actuality there's no real error in those blocks, just a condition which wasn't met. If you wanted to be really proactive about this, you could use the `orThrow` method to capture the fact that those values are expected *not* to be null. – Makoto May 30 '18 at 23:18
  • ``ifPresent`` returns void, you can't chain ``orElseThrow`` to it. – Jan B. May 30 '18 at 23:31
  • @Makoto you mean isPresent? Makes sense in context as it returns boolean, please edit if you agree. Good point about Optional - had only used it in lambdas before. Yes you're right I can see someone removing the -ea flag and every flight is cancelled :) – ACHC May 31 '18 at 00:40
  • @ACHC: No, `ifPresent` allows you to perform a consuming operation if you pass a void method (like `x.doSth()`). It reads like `x.ifPresent(YourClass::doSth)`. It gets you out of the habit of `if` checks since you don't need them if you can just perform the operation instead. – Makoto May 31 '18 at 00:52
  • Ok nice. I'd definitely favour that for null checks in new code with good test coverage that won't throw unexpected NPEs - currently looking for a replacement for null checks (sometimes handled but mostly not) in existing code with very few unit tests. – ACHC May 31 '18 at 02:32
  • I agree as long as you are not forced to wrap the object in an _Optional_ beforehand. If you initially don't have an Optional, then``if (obj != null) { doSth };`` is prefered over ``Optional.ofNullable(obj).ifPresent(o -> {doSth});``. That's not my sole opinion, Oracle themselves discourages to use Optional as a replacement for ``if-else`` pattern to check for ``null`` and even calls it a code smell (please see the pdf in the first comment). – Jan B. May 31 '18 at 13:06
0

In my experience, if x or y being null is, in fact, an error then (instead of your first example) what will be coded is:

void someFunction(AnObject x, AnObject y) {
    if (x == null || y == null) {
        throw new IllegalStateException("Contract violation: x and y must be non-null");
    }
    // rest of method can be run without null checks.
}

As I've noted in some of the discussion below, if you know up front that certain parameter values are breaking (like null or NaN in a float/double) then it makes a lot of sense to fail fast. It helps callers isolate the problem better than if a null value gets put in a Map and the NPE gets thrown on another thread, for instance.

(Note: edited to remove a contentious suggestion to use annotations.)

DavidW
  • 1,413
  • 10
  • 17
  • Noooo !!!! `@NottNull` doesn't give such guarantee in general case. https://stackoverflow.com/questions/34094039/using-notnull-annotation-in-method-argument – Jacek Cz May 30 '18 at 22:17
  • Obviously said mature code was running on a properly configured appserver. One that also properly limited non-local code to publicly accessible API classes... – DavidW May 30 '18 at 22:21
  • Also, it's not part of the standard JDK like `assert` is. – Vasan May 30 '18 at 22:24
  • @Vasan That's fair; I suppose I could have specified `@Nonnull` instead (standard Java), but that's not what we used. Please note that I lead with what I expect the most widely applicable way of handling this case would be, and noted the annotation usage as an afterthought. In our experience, use of annotations - combined with decent static analysis tools - lead to a noticeable improvement in the codebase. Both in stability and in clean code from removing redundant null checks. – DavidW May 30 '18 at 22:30
  • @DavidW I get your point, perhaps my example is a bit off as it handles the null case. More often I see code with null checks scattered like pepper just to prevent NPEs, but never considering how null should be handled. With assert an error is thrown - you're not forced to handle it either as it's an unchecked exception, but at least it's clear from the code that null is an unexpected state. – ACHC May 30 '18 at 22:34
  • @DavidW Your first point is still incorrect - there are [no standard @NonNull annotations](https://stackoverflow.com/questions/35892063/which-nonnull-java-annotation-to-use) in Java. As for the rest of your comment, fair enough. Although I'd still be cautious of willy-nilly replacing defensive checks with the annotation - the `if`/`assert` way gives an opportunity to catch and send back a meaningful error message for code that "slips through" with bad calls. – Vasan May 30 '18 at 22:38