Explore Java and more with Jeff 'JavaJeff' Friesen

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 via JPPC 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.


Download code.zip

Note: Application created with Java SE 6u16.


LEARN JAVA FROM APRESS