Elvis Comes to Java
In December 2008, Joseph D. Darcy blogged about leading Sun's efforts to develop a set of small language changes for JDK 7. This announcement led Sun to create Project Coin, an official conduit for receiving language enhancement proposals from the Java community.
By the time Project Coin wrapped up, approximately 70 proposals
had been received. One of the proposals focused on
introducing Elvis (?:) and other null-safe operators
to Java. However,
none of these operators made the final cut.
Although Darcy's explanation as to why these operators aren't being included in JDK 7 revealed that this next Java generation will use type annotations "to ease the pain of null-handling", I think many developers still desire to see Java include the Elvis operator. For that reason, this article presents a tool that introduces Elvis to Java.
Elvis Overview
Before I reveal this tool, let's review the purpose for the Elvis operator and examine its syntax. Let's begin by examining the following Java code/pseudocode fragment:
if (someobjectexpr != null)
result = someobjectexpr;
else
result = defaultobjectexpr;
This fragment tests some kind of object expression to see if it
evaluates to the null reference. If not, this expression is
reevaluated and its value is assigned to the result
variable.
However, the object expression might evaluate to null. In this
case, a default object expression (of the same type, and which
doesn't evaluate to null) is evaluated and its value is assigned
to result.
The preceding fragment can be more compactly expressed via Java's ternary operator:
result = (someobjectexpr != null) ? someobjectexpr : defaultobjectexpr;
However, there's still too much boilerplate: We have to respecify
someobjectexpr. Elvis allows us to avoid
this second specification, reducing the amount of code to that
shown below:
result = (someobjectexpr != null) ?: defaultobjectexpr;
This fragment is equivalent to the previous fragment, returning
someobjectexpr's value if this value isn't
null, and otherwise returning
defaultobjectexpr's value.
Implementing Elvis
I initially thought about implementing Elvis via an annotation processor, after reading the Roman Numerals, in your Java blog post. However, this approach proved to be problematic.The problem with this approach is that the Java compiler first checks for valid syntax (and Elvis isn't valid) prior to invoking annotation processors. The only reason the aforementioned blog post's processor works is that Roman numerals are also valid Java identifiers.
I next considered modifying the compiler. However this approach is limited by the fact that it's not portable to all compilers -- each Java compiler would need to be modified separately.
I finally settled on using a preprocessor. Prior to compilation, the preprocessor reads the source file, translating each line containing Elvis into equivalent valid syntax. Following this preprocessing step, the source file is compiled.
Should the preprocessor read a file with a .java
extension and overwrite the original file, or should it read a
file with some other extension, storing the result in an
equivalent .java file? I chose to use a custom file
extension because I wanted to preserve the original source code.
What would be an appropriate file extension to use? Because I
consider Java plus the Elvis language enhancement (really just
syntactic sugar) to be one step beyond ordinary Java, I decided to
refer to the new language as Java++, and to the file extension as
.jpp.
After making these decisions, I created a JPPDemo.jpp
source file that demonstrates what I'm trying to achieve with the
preprocessor. Listing 1 presents this source file's contents.
// JPPDemo.jpp
public class JPPDemo
{
static class User
{
String name;
}
public static void main (String [] args)
{
String s = null;
System.out.println (`s ?: "is null"`);
s = "Hello";
System.out.println (`s ?: "is null"`);
User user = null;
System.out.println (`user ?: new User ()`.name ?: "unknown"`);
user = new User ();
System.out.println (`user ?: new User ()`.name ?: "unknown"`);
user.name = "Duke";
System.out.println (`user ?: new User ()`.name ?: "unknown"`);
}
}
Listing 1: JPPDemo.jpp
To make it easier to write the preprocessor, each Elvis expression
must be delimited with the backtick symbol (`). If
Elvis expressions are chained together, the previous expression's
final backtick is the next expression's starting backtick.
The `s ?: "is null"` expression returns
s's value if this variable doesn't contain null.
Otherwise, this expression returns is null. In either
case, the returned value is output.
More interestingly, `user ?: new User ()`.name ?:
"unknown"` first evaluates user, returning the
reference in this variable, or the result of new User
() if this variable contains null.
The expression then uses this non-null reference to access the
name member. If name doesn't contain
null, the string referenced from this variable is output.
Otherwise, unknown is returned and outputs.
An Elvis expression must not include == null. This is
why I wrote `s ?: "is null"` instead of `s ==
null ?: "is null"`, and `user ?: new User ()`.name ?:
"unknown"` instead of `user == null ?: new User
()`.name == null ?: "unknown"`.
After figuring out how the preprocessor should handle Elvis, I
created a JPPC application that first preprocesses
its solitary .jpp argument, storing the result in a
.java file, and then compiles this latter file.
Listing 2 presents the source code.
// JPPC.java
// Java PreProcessor and Compiler
// Must place tools.jar on CLASSPATH before invoking JPPC.
import java.io.*;
import javax.tools.*;
public class JPPC
{
public static void main (String [] args)
{
if (args.length != 1)
{
System.err.println ("usage: java JPPC filespec[.jpp]");
return;
}
String inFile = args [0];
if (!inFile.toLowerCase ().endsWith (".jpp"))
inFile = inFile+".jpp";
String outFile = inFile.substring (0, inFile.indexOf (".jpp"))
.concat (".java");
BufferedReader br = null;
PrintWriter pw = null;
try
{
FileReader fr = new FileReader (inFile);
br = new BufferedReader (fr);
FileWriter fw = new FileWriter (outFile);
pw = new PrintWriter (fw);
preprocess (br, pw);
}
catch (IOException ioe)
{
System.out.println (ioe.getMessage ());
}
finally
{
if (br != null);
try { br.close (); } catch (IOException ioe) {}
if (pw != null);
pw.close ();
}
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler ();
if (compiler == null)
{
System.err.println ("compiler not available");
return;
}
compiler.run (null, null, null, new String [] { outFile });
}
static void preprocess (BufferedReader br, PrintWriter pw)
throws IOException
{
String line;
while ((line = br.readLine ()) != null)
{
line = processElvis (line);
pw.println (line);
}
}
static String processElvis (String line)
{
StringBuilder builder = new StringBuilder ();
/*
Algorithm:
1) Create output buffer and copy line to output buffer.
2) Create temp buffer.
3) Search output buffer (left-to-right) for next ?:
4) If ?: not seen then
4.1) Replace each ` with a space in output buffer.
4.2) Return output buffer as String.
5) Record location of ?
6) Scan backwards from ? to preceding `, copying each character
(except for ? and `) to temp buffer.
7) If ` not found then error and end.
8) Reverse temp buffer.
9) Replace characters from ` location+1 through ? location-1 with (
followed by temp buffer followed by == null
10) Search output buffer (left-to-right) for next ?:
11) Record location of ?
12) Replace : following ? with a space.
13) Scan forwards from space until ` found.
14) If ` not found then error and end.
15) Replace ` with : followed by temp buffer followed by )
16) Reset temp buffer.
17) Goto 3.
*/
StringBuilder output = new StringBuilder (line);
StringBuilder temp = new StringBuilder ();
do
{
int indexElvis = output.indexOf ("?:");
if (indexElvis == -1)
break;
int index = indexElvis-1;
while (index >= 0 && output.charAt (index) != '`')
temp.append (output.charAt (index--));
if (index == -1)
{
System.err.println ("missing ` prefix");
System.exit (1);
}
temp.reverse ();
builder.append ("(");
builder.append (temp);
builder.append ("==null");
output.replace (index+1, indexElvis-1, builder.toString ());
builder.setLength (0);
indexElvis = output.indexOf ("?:");
output.setCharAt (indexElvis+1, ' ');
index = indexElvis+2;
while (index < output.length () && output.charAt (index) != '`')
index++;
if (index == output.length ())
{
System.err.println ("missing ` suffix");
System.exit (1);
}
builder.append (":");
builder.append (temp);
builder.append (")");
output.replace (index, index+1, builder.toString ());
builder.setLength (0);
temp.setLength (0);
}
while (true);
for (int i = 0; i < output.length (); i++)
if (output.charAt (i) == '`')
output.setCharAt (i, ' ');
return output.toString ();
}
}
Listing 2: JPPC.java
Although Listing 2's algorithm and source code for processing the
Elvis operator is fairly straightforward, the following example,
which translates `user ?: new User ()`.name ?:
"unknown"`, gives you more insight into how this all works:
// Translation Pass 1: Translate the first ?: operator. `(user == null ? new User () : user).name ?: "unknown"` // Translation Pass 2: Translate the second ?: operator. `((user == null ? new User () : user).name == null ? "unknown" : (user == null ? new User () : user).name) // Translation Pass 3: Remove initial backtick symbol. ((user == null ? new User () : user).name == null ? "unknown" : (user == null ? new User () : user).name)
While reviewing Listing 2, you'll notice a generic
preprocess() method. I included this method to
simplify processElvis() so that this latter method
doesn't also have to read lines. More importantly,
preprocess() makes it easier to add new Java++
features to JPPC.
The following Windows command line shows you how to run
JPPC with the earlier JPPDemo.jpp source
file as an argument. Notice that the JDK's tools.jar
file must be included in the CLASSPATH:
java -cp %JAVA_HOME%\lib\tools.jar;. JPPC JPPDemo.jpp
Of course, the .jpp extension is optional. After you
invoke this command line, and if all goes well, you should notice
JPPDemo.java and JPPDemo.class files in
the current directory.
Specify the following command line to run JPPDemo:
java JPPDemo
In response, you should observe the following output:
is null Hello unknown unknown Duke
To simplify working with JPPC, you might want to
create a shell script (a batch file if you're using Windows). For
example, using Listing 3's batch file, you would specify
jppc JPPDemo (or jppc JPPDemo.jpp) to
preprocess and compile Listing 1.
java -cp %JAVA_HOME%\lib\tools.jar;. JPPC %1
Listing 3: jppc.bat
Conclusion
Although bringing Elvis to Java viaJPPC instead of
compiler changes might not seem to be that interesting, at least
JPPC offers a portable solution that you wouldn't get
if you only modified Sun's implementation of the Java compiler. As
an exercise, implement a more efficient version of the
processElvis() method.
Note: Application created with Java SE 6u16.









