XTend null safe throws NullPointerException

1.2k views Asked by At

I am porting my template code to XTend. At some point I have this type of condition handling in a test case:

@Test
def xtendIfTest() {
    val obj = new FD
    if (true && obj?.property?.isNotNull) {
        return
    }
    fail("Not passed")
}

def boolean isNotNull(Object o) {
    return o != null
}
class FD {
 @Accessors
 String property
}

This works as expected as the property is null and the test will fail with "Not passed" message. But a simple change in the return type of isNotNull method to Boolean (wrapper):

def Boolean isNotNull(Object o) {
    return o != null
}

fails with a NullPointerException. Examining the generated java code for this I can see that XTend is using an intermediate Boolean object expression and that is the cause of NPE. Am I missing the point of the XTend null safe operator (?.) or I can't use a method like this after the operator?

Thanks.

2

There are 2 answers

0
Franz Becker On BEST ANSWER

The operator behaves properly. The exception is thrown because of the usage of a Boolean in an if-expression, which requires auto-unboxing.

If you try the following:

@Test
def xtendIfTest() {
    val Boolean obj = null
    if (obj) {
        return
    }
    fail("Not passed")
}

You will also run into a NullPointerException.

This is consistent with the Java Language Specification (https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.8) - when auto-unboxing is required this can yield a NullPointerException:

@Test
public void test() {
    Boolean value = null;
    if (value) { // warning: Null pointer access: This expression of type Boolean is null but requires auto-unboxing
        // dead code
    }
}

Hope that helps.

0
Kelvin On

Short answer: Change the second null-safe call to a regular call.

I.e. change

obj?.property?.isNotNull

to this:

obj?.property.isNotNull

Long answer:

The docs describe the null-safe operator thusly:

In many situations it is ok for an expression to return null if a receiver was null

That means the second call in your example, property?. won't even call isNotNull if the left side of the call was null. Instead, it will return null. So the conditional "effectively" evaluates to:

if (true && null) {  // causes NPE when java tries to unbox the Boolean 

(By the way - the true is superfluous in this context, but I'm keeping it just in case you had another condition to check - I'm assuming you're just simplifying it to true for this example.)

If you make the change I'm suggesting, obj?.property will be evaluated, then the result will be passed to isNotNull, evaluating to this:

if (true && isNotNull(null)) {

which returns the proper Boolean object that will be auto-unboxed as expected.

A Word of Caution

In your first form of isNotNull, i.e. the one returning primitive boolean, you should actually get a warning like "Null-safe call of primitive-valued feature isNotNull, default value false will be used".

This is because you're stretching the intent of the null-safe call, which is to return null without invoking the right side method if the left side of the operator was null. But if your isNotNull returns a primitive boolean, the whole expression obviously can't evaluate to null, so Xtend uses a default instead, which is false for booleans.

To emphasize the problem in a different way - it evaluates to false without calling isNotNull - that means even if you used a method isNull after the operator, it would still return false!

The docs also mention this behavior (albeit in general terms):

For primitive types the default value is returned (e.g. 0 for int). This may not be what you want in some cases, so a warning will be raised by default

So I recommend always using a non-primitive return value on the right-hand side of a null-safe call. But if you're going to convert the isNotNull to a regular call as I suggested, this rule doesn't apply, and either return type is fine.