Proposal: Elvis and Other Null-Safe Operators
Stephen Colebourne
scolebourne at joda.org
Sun Mar 1 08:45:22 PST 2009
Elvis and Other Null-Safe Operators for Java
AUTHOR(S):
Stephen Colebourne
primarily written up by Neal Gafter
(Neal Gafter is responsible for the formal write-up[6] of the proposal
detailed below. However, in private communication he indicated that he
did not intend to submit it to Project Coin, as indicated in his
write-up: "[I do] not specifically advocate adding these features to the
Java programming language. Rather, this document is offered as an
example of a language change proposal in a form suitable for
consideration in the JDK7 small language changes JSR. Specifically, it
is more like a specification than a tutorial or sales job.".
As such, this proposal is submitted by myself, thanks to Neal's
willingness to allow me to reuse his write-up. For the submission, I
have reworded the advantages/benefits/disadvantages/alternatives
sections from Neal's original document and added detail to the examples.
Please see the original[6] to compare Neal's version to mine.)
*OVERVIEW*
FEATURE SUMMARY:
The ?: binary "Elvis" operator results in the value of the
left-hand-side if it is not null, avoiding evaluation of the
right-hand-side. If the left-hand-side is null, the right-hand-side is
evaluated and is the result.
The ?. null-safe member selection operator provides the same meaning as
. (member selection), except when the left-hand-side evaluates to null,
in which case any subexpressions on the right-hand-side are not
evaluated and the ?. expression yields null.
The ?[] indexing operator operates on a left-hand-side that is an array
of object type. If the value of the left-hand operand is null, that is
the result. If the left-hand-operand is not null, the index
subexpression is evaluated and used to index the array, yielding the result.
These three operators always result in a value, not a variable.
NOTE: The Elvis operator could be added on its own without the other two
operators if deemed necessary/desirable (Elvis is generally considered
less controversial as far as I can tell).
MAJOR ADVANTAGE:
It is a common occurance in most large systems to find code that checks
for null. Common cases are to provide a default value instead of null,
to obtain the result from a nested JavaBean where any of the accessors
might be null, or to handle auto-unboxing properly. This proposal
captures these common coding patterns, and as a result makes the code
clearer and more expresive. Less boilerplate. Clearer business logic.
The result of /not/ handling null properly is a NullPointerException.
This is such a common mistake amongst developers, that many regular
internet users (non-developers) are aware of the term
"NullPointerException", because of its prevalence in production systems.
MAJOR BENEFIT:
Two common coding patterns are simplified - defaulting the value of
null, and avoiding a NPE on access. The remaining code is focussed more
tightly on the business logic rather than on the details of coding.
There are also significant benefits in the handling of auto-unboxing, as
well as switching on enums and the for-each loop. In all three cases,
the language change added an ability to create a NPE without providing
an easy and obvious means to avoid it (you have to add an if statement
and another level of block which entirely defeats the purpose of the
'convenience' unboxing).
Finally, the proposed code will generally be slightly more performant
than the code written by hand. This is because each part of the
expression will only be evaluated once with the proposed change, whereas
a developer will normally call each part multiple times while checking
for null.
MAJOR DISADVANTAGE:
Associated costs in documentation, tutorials and overall language size.
The principle perceived disadvantage, however, is that it encourages,
rather than discourages, the use of null values in APIs. No one is
disputing that empty arrays or empty collections should be returned from
APIs rather than nulls, however that is only a small proportion of the
returned types in any large system. Many large systems consist of large
numbers of JavaBean type objects which may have null values for many of
their fields (representing an absence of information, invalid data,
etc.). In these cases, null is a suitable and valuable value to hold in
those fields, and is widely used as such. Accessing the resulting data
for use often requires defaulting the values or handling nulls, and that
is where this proposal comes in.
To put that another way, if you write low-level APIs, such as the JDK,
Apache Commons or Google Collections, then this proposal is of little
value. If your day job involves integrating code from 50 different
libraries using hundreds of JavaBean style data structures where fields
can all be null, then this proposal will have a huge impact. Its a
matter of perspective.
ALTERNATIVES:
It is possible to solve some of the issues using libraries[1]. These
solutions are not terribly appealing however and would be unlikely to
make it into the JDK.
The other alternative is, as with any proposal, to make no change. This
leaves developers to continue to obscure business logic with
null-handling clutter despite indicating this as their most-wanted
change in Java[5].
*EXAMPLES*
SIMPLE EXAMPLE:
Standard example:
String s = mayBeNull?.toString() ?: "null";
Auto-unboxing example:
Integer ival = ...; // may be null
int i = ival ?: -1; // no NPE from unboxing
ADVANCED EXAMPLE:
Given a Java class
class Group {
Person[] members; // null if no members
}
class Person {
String name; // may be null
}
Group g = ...; // may be null
we can compute the name of a member if the group is non-null and
non-empty and the first member has a known (non-null) name, otherwise
the string "nobody":
final String aMember = g?.members?[0]?.name ?: "nobody";
Without this feature, a developer would currently write:
String aMember = null;
if (g != null && g.members != null && g.members[0].name != null) {
aMember = g.members[0].name;
} else {
aMember = "nobody";
}
The proposed version is a lot shorter, clearer, and can even be
assigneed to a final variable.
*DETAILS*
SPECIFICATION:
Lexical:
We do not add any tokens to the language. Rather, we introduce new
operators that are composed of a sequence of existing tokens.
Syntax:
The folllowing new grammar rules are added to the syntax
PrimaryNoNewArray:
NullSafeFieldAccess
NullSafeMethodInvocation
NullSafeClassInstanceCreationExpression
NullSafeArrayAccess
NullSafeFieldAccess:
PrimaryNoNewArray ? . Identifier
NullSafeMethodInvocation:
PrimaryNoNewArray ? . NonWildTypeArgumentsopt Identifier ( ArgumentListopt )
NullSafeClassInstanceCreationExpression:
PrimaryNoNewArray ? . new TypeArgumentsopt Identifier TypeArgumentsopt (
ArgumentListopt ) ClassBodyopt
NullSafeArrayAccess:
PrimaryNoNewArray ? [ Expression ]
ConditionalExpression:
ElvisExpression
ElvisExpression:
ConditionalOrExpression ? : ConditionalExpression
Semantics:
A null-safe field access expression e1?.name first evaluates the
expression e1. If the result is null, then the null-safe field access
expression's result is null. Otherwise, the result is the same as the
result of the expression e1.name. In either case, the type of the
result is the same as the type of e1.name. It is an error if this is
not a reference type.
A null-safe method invocation expression e1?.name(args) first evaluates
the expression e1. If the result is null, then the null-safe method
invocation expression's result is null. Otherwise the arguments are
evaluated and the result is the same as the result of the invocation
expression e1.name(args). In either case, the type of the result is the
same as the type of e1.name(args). It is an error if this is not a
reference type.
A null-safe class instance creation expression e1?.new name(args) first
evaluates the expression e1. If the result is null, then the null-safe
class instance creation expression's result is null. Otherwise, the
arguments are evaluated and the result is the same as the result of the
class instance creation expression e1.new name(args). In either case,
the type of the result is the same as the type of e1.new name(args).
A null-safe array access expression e1?[e2] first evaluates the
expression e1. If the result is null, then the null-safe array access
expression's result is null. Otherwise, e2 is evaluated and the result
is the same as the result of e1[e2]. In either case, the type of the
result is the same as the type of e1[e2]. It is an error if this is not
a reference type.
An Elvis expression e1?:e2 first evaluates the expression e1. It is an
error if this is not a reference type. If the result is non-null, then
that is the Elvis expression's result. Otherwise, e2 is evaluated and
is the result of the Elvis expression. In either case, the type of the
result is the same as the type of (e1!=null)?e1:e2. [Note: this section
must mention bringing the operands to a common type, for example by
unboxing when e2 is a primitive, using the same rules as the ternary
operator]
Exception Analysis:
JLS section 12.2.1 (exception analysis of expressions) is modified to
read as follows. Additions are shown in bold.
A method invocation expression or null-safe method invocation expression
can throw an exception type E iff either:
* The method to be invoked is of the form Primary.Identifier or
Primary?.Identifier and the Primary expression can throw E; or
* Some expression of the argument list can throw E; or
* E is listed in the throws clause of the type of method that is
invoked.
A class instance creation expression or null-safe class instance
creation expression can throw an exception type E iff either:
* The expression is a qualified class instance creation expression
or a null-safe class instance creation expression and the qualifying
expression can throw E; or
* Some expression of the argument list can throw E; or
* E is listed in the throws clause of the type of the constructor
that is invoked; or
* The class instance creation expression or null-safe class
instance creation expression includes a ClassBody, and some instnance
initializer block or instance variable initializer expression in the
ClassBody can throw E.
For every other kind of expression, the expression can throw type E iff
one of its immediate subexpressions can throw E.
Definite Assignment:
JLS section 16.1 (definite assignment and expressions) is augmented with
the following new subsections
16.1.x Null-safe Method Invocation
* v is definitely assigned after e1?.name(args) iff v is definitely
assigned after e1.
* v is definitely unassigned after e1?.name(args) iff v is
definitely unassigned after args.
* in an expression of the form e1?.name(args), v is [un]assigned
before args iff v is [un]assigned after e1.
16.1.x Null-safe Class Instance Creation Expression
* v is definitely assigned after e1?.new name(args) iff v is
definitely assigned after e1.
* v is definitely unassigned after e1?.new name(args) iff v is
definitely unassigned after args.
* in an expression of the form e1?.new name(args), v is
[un]assigned before args iff v is [un]assigned after e1.
16.1.x Null-safe Array Access
* v is definitely assigned after e1?[e2] iff v is definitely
assigned after e1.
* v is definitely unassigned after e1?[e2] iff v is definitely
unassigned after e2.
* in an expression of the form e1?[e2], v is [un]assigned before e2
iff v is [un]assigned after e1.
16.1.x Elvis Operator
* v is definitely assigned after e1?:e2 iff v is definitely
assigned after e1.
* v is definitely unassigned after e1?:e2 iff v is definitely
unassigned after e2.
* in an expression of the form e1?:e2, v is [un]assigned before e2
iff v is [un]assigned after e1.
COMPILATION:
These new expression forms can be desugared as follows:
* e1?.name is rewritten as (t != null ? t.name : null)
* e1?.name(args) is rewriten as (t != null ? t.name(args) : null)
* e1?.new name(args) is rewritten as (t != null ? t.new name(args)
: null)
* e1?[e2] is rewritten as (t != null ? t[e2] : null)
* e1?:e2 is rewritten as (t != null ? t : e2)
where t is a new temporary that holds the computed value of the
expression e1.
TESTING:
This feature can be tested by exercising the various new expression
forms, and verifying their correct behavior in erroneous and
non-erroneous situations, with or without null as the value of the
left-hand operand, and with respect to definite assignment and exception
analysis.
LIBRARY SUPPORT:
No library support is required.
REFLECTIVE APIS:
No reflective APIs require any changes. However, the not-yet-public
javac Tree APIs, which describe the syntactic structure of Java
statements and expressions, should be augmented with new tree forms for
these new expression types.
OTHER CHANGES:
No other platform changes are required.
MIGRATION:
No migration of existing code is recommended. These new language
features are mainly to be used in new code. However, IDEs should
provide refactoring advice for taking advantage of these new operators
when existing code uses the corresponding idiom.
*COMPATIBILITY*
BREAKING CHANGES:
No breaking changes are caused by this proposal.
EXISTING PROGRAMS:
Because the changes are purely the introduction of new expression forms,
there is no impact on the meaning of existing code.
*REFERENCES*
EXISTING BUGS:
4151957: Proposal: null-safe field access operator
URL FOR PROTOTYPE:
No Java prototype exists at this time. However, Groovy[2] and Fan[3]
(among others) have the Elvis and null-safe member operators.
OTHER REFERENCES
[1] Stephan Schmidt's discussion of Better Strategies for Null Handling
in Java -
http://www.slideshare.net/Stephan.Schmidt/better-strategies-for-null-handling-in-java
[2] Groovy Operators - http://groovy.codehaus.org/Operators
[3] Fan operators -
http://fandev.org/doc/docLang/Expressions.html#nullConvenience
[4] Stephen Colebourne's brief on null-safe operators -
http://docs.google.com/View?docid=dfn5297z_3c73gwb
[5] Summary of three recent language change polls showing better null
handling as a key developer request -
http://www.jroller.com/scolebourne/entry/jdk_7_language_changes_everyone
[6] The version of this proposal written by Neal Gafter -
http://docs.google.com/Doc?docid=ddb3zt39_78frdf87dc&hl=en
More information about the coin-dev
mailing list