Enhanced null handling - Invocation and defaulting

Version 0.2

Stephen Colebourne

I. Problem

The concept of null is fundamental to Java - any variable, except primitives, can be null. Unfortunately, null is also remarkably badly supported by the basic language syntax, with the only real way to handle it being checking if the variable == null. This leads to two key problems:

NullPointerExceptions are the one of the most, if not the most, common exceptions encountered in Java programs. They occur when a developer has failed to check a variable for null before accessing a method or field. They are particularly insidious because they often happen late in the development cycle, in testing or in production, a time when they are most expensive to fix. The key cause is the inability of Java to check for nulls at compile time. For example:

    public String getPostcode(Person person) {

      return person.getAddress().getPostcode();  // may throw NPE if person or address is null

    }

The most frequently used, and only reliable, way to avoid NullPointerExceptions is to check each variable reference for null. Unfortunately, this is rarely done as developers are more interested in getting the primary business logic implemented and working. When they do code the null-checks, maybe due to unit tests, UAT or a production issue, the code suddenly loses all readability, and the real intent of the method is lost:

    public String getPostcode(Person person) {

      if (person != null) {

        Address address = person.getAddress();

            if (address != null) {

          return address.getPostcode();

        }

      }

      return null;

    }

An alternative to skipping processing when a null value is found is to provide a default value instead:

    String str = getStringMayBeNull();

    str = (str == null ? "" : str);

The defaulting of a null value to a non-null value is verbose.

This proposal aims to tackle these problems by adding simple compiler syntax sugar to handle these common issues with null.

II. Proposal

The proposal is in two related parts.

IIa. Null-default

This proposal adds a new operator ?: which is a short form of the standard ternary conditional operator.

    String str = getStringMayBeNull();

    str = str ?: "";

which would probably be written as:

    String str = getStringMayBeNull() ?: "";

This operator is also useful in conjunction with auto-unboxing:

    Integer value = getIntegerMayBeNull();

    int val = value ?: -1;

Another examples might be defaulting the value of an enum in the switch statement to avoid an NPE.

The following rules apply:

JLS chapters needing alteration:

The Buckley Complexity Quotient woud appear to be 2/15 x degree(15,16).

IIb. Null-safe invocation

This proposal adds a new operator ?. which can be used for invoking fields or methods. If the developer uses the standard dot operator then a NullPointerException will be thrown if the LHS is null. If the developer uses the question-dot operator when the LHS is null then the result of the expression is null:

    public String getPostcode(Person person) {

      return person?.getAddress()?.getPostcode();

    }

This new form is merely syntax sugar. When compiled it will yield something similar to:

    public String getPostcode(Person person) {

      String v$0 = null;

      if (person != null) {

        Address v$1 = person.getAddress();

            if (v$1 != null) {

          v$0 = address.getPostcode();

        }

      }

      return v$0;

    }

The syntax sugar generated code is fully compatible with the existing Java language, and as such this proposal has no impact on the JVM or legacy code.

It should be noted that whilst this avoids NullPointerExceptions caused by invoking a method it does not affect in any way any NullPointerException that is raised by the method itself.

The operator follows the same rules as field and method invocaton with the following additional rules:

JLS chapters needing alteration:

The Buckley Complexity Quotient woud appear to be 2/15 x degree(15,16).

Primitives

The situation with primitives is complex as the null operators interact with boxing and unboxing. For example, the following can throw a NullPointerException:

    int count = datastore?.getCountReturnsInt();

This occurs when the null-safe expression results in a null (because dataStore is null) and that result is auto-unboxed to an int. This can be mitigated however by utilising the null-default operator:

    int count = datastore?.getCountReturnsInt() ?: -1;

Whilst the language could mandate that a null-safe invocation with a primitive result is always followed by a null-default operator this is probably excessive. This is because it would prevent the normal boxing that would occur were the expression to be assigned to an object type, or passed to a method accepting an object type.

The best approach is to allow tools (IDEs and static analysis) to locate all cases where unboxing should be protected by a null-default operator. This provides a more generally useful solution rather than mandating it for this one special case.

III. Other language implementations

The syntax and implementation is based on Groovy. Other languages, including Fan have similar constructs. The null-default operator in C# is ?? instead of ?:.

IV. Issues

There are three main points of contention with the proposal - primitives, 'letting null slip' and annotations.

The first concern, primitives, is addressed by combining the two operators, null-safe and null-default, in one proposal. The null-safe invocation operator should not be implemented without the null-default operator. While enforcing the use of the null-default operator where the null-default operator returns a primitive is just about possible, it would be hard to specify and interact poorly with boxing, unboxing, assignment and method resolution. The better approach is to provide a simple coding technique for defaulting the value (null-defaults) and to allow tools (IDEs and static analysis) to identify all locations where null-default should be used.

The second is a concern that proposals like this encourage poor programming style. This is typically accompanied by advice to use the null-object pattern. Unfortunately, the null-object pattern is not widely used, nor is it likely to be. It is complex and verbose in source and byte code and often not as clear as using the built in value - null.

This proposal contends that nulls are a fact of life for most Java programs and most Java developers, and that using them is not necessarily bad design. As such, the curent support for managing them in the language is insufficient for writing clean and clear code.

The third argument is that we should wait until the implementation of JSR-305/308 which aims to introduce nullable/not-null annotations for types. This proposal contends that defining a variable as nullable/not-null is an orthogonal concern to actually working with the values. The Fan language fully supports nullable/not-null variables, yet it still has the null-safe and null-default operators. Put simply, the addition of nullable/not-null variables allows nulls to be more closely controlled, but certainly not removed. And when actually handling them, the null-safe and null-default operators come into their own. In addition, it should be noted that the null-default operator is extremely useful for handling auto-unboxing separate to the main debate on null-handling.

(It should be noted that the addition of annotations for null-handling is a poor-quality solution to that particular problem. This is because the annotation gets processed separately to the language and so cannot influence the validity of other operators. In Fan, where the nullable/not-null definition is integrated at the language level, it allows various combinations of nullable/not-null variables and the null-safe or null-default operators to be made compile errors.)

Finally, it should be noted that the null-default operator could be implemented before null-safe invocation if that was deemed desirable.

V. Summary

Nulls are a pain, yet they can also be a useful programming tool when they represent unknown information. They are unavoidable, as many libraries return null, thus not even a rigorous application of the NullObject design pattern will avoid exceptions. Manually handling nulls is tedious, error-prone and seriously obscures the meaning of the actual program logic.

This proposal provides a simple, readable and practical means to handle issues with null in a style that is appropriate to Java, while remaining fully backwards compatible.

Finally, this is the most popular language change request from ordinary developers.