I can haz lambda on Java 7?

April 29, 2013

Superficially, lambda expressions in Java 8 are just syntactic sugar for instances anonymous inner classes. Syntactic sugar should not require classfile changes, so one might expect that code compiled with Java 8 lambdas could run on a Java 7 JRE. It turns out that you can’t do this, and not just because of an automatic “we always increment the classfile version”. To see why, it helps to start at the beginning.

We’re big fans of functional programming, but not quite willing to scrap our years of perfectly good working Java code, so our compromise is a functional Java style, which means lots of anonymous inner classes. As you can imagine, we’re looking forward to Java 8 with Project Lambdaand were a bit disappointed to see the latest timelinedoesn’t have a GA release until a year from now.

What could we do until then? There are early access versions available, but “let’s upgrade production to this early access build” and “automatically managing hundreds of millions of dollars” don’t seem to be a good match. What if we could compile with Java 8, but run on a well-tested Java 7 environment?

First, let’s create two similar classes, one using an anonymous inner class, and the other using lambdas.

Look at all those braces I didn’t have to type. Beautiful. Unsurprisingly, if we compile with Java 8, we can run with Java 8 with no problem.

kevin$ $JAVA8/bin/javac *.java
kevin$ $JAVA8/bin/java WithLambda
and it works...
kevin$ $JAVA8/bin/java WithoutLambda
and it works...

If we try to run those classes under Java 7, we have a problem.

kevin$ $JAVA7/bin/java WithoutLambda
Exception in thread "main" java.lang.UnsupportedClassVersionError: WithoutLambda : Unsupported major.minor version 52.0
kevin$ $JAVA7/bin/java WithLambda
Exception in thread "main" java.lang.UnsupportedClassVersionError: WithLambda : Unsupported major.minor version 52.0

Well, can we target a different classfile version? There’s no combination of source and target that will actually make javac happy with this.

kevin$ $JAVA8/bin/javac -target 1.7 *.java
javac: target release 1.7 conflicts with default source release 1.8
kevin$ $JAVA8/bin/javac -source 1.8 -target 1.7 *.java
javac: source release 1.8 requires target release 1.8
kevin$ $JAVA8/bin/javac -source 1.7 -target 1.7 *.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
WithLambda.java:3: error: lambda expressions are not supported in -source 1.7
        Runnable r = () -> System.out.println("and it works...");
                        ^
  (use -source 8 or higher to enable lambda expressions)
1 error
1 warning

So there’s simply no way to compile Java source with lambdas into a pre-Java 8 classfile version. It turns out this isn’t just a failure to expose the possibilities as options. Although lambda could have been implemented as pure syntactic sugar with compile time replacement by an anonymous inner class, it isn’t. You can see something is up when you look at what these two files compile to.

kevin$ $JAVA8/bin/javac *.java
kevin$ ls -l *.class
-rw-r--r--  1 kevin  staff  1041 Apr 26 13:41 WithLambda.class
-rw-r--r--  1 kevin  staff   555 Apr 26 13:41 WithoutLambda$1.class
-rw-r--r--  1 kevin  staff   393 Apr 26 13:41 WithoutLambda.class

The inner class version compiles as expected into two classes, but the lambda version compiles into a single class, which means that instance of Runnable goes… where? Turns out, it doesn’t exist, at least not at the bytecode level.

Brian Goetz explains the details in Translation of Lambda Expressions. First, the body of the lambda is converted into an internal private method, which you can see in the class file.

kevin$ javap -private WithLambda
Compiled from "WithLambda.java"
class WithLambda {
  WithLambda();
  public static void main(java.lang.String[]);
  private static void lambda$0();
}

Next, instead of creating an instance of the inner class (which was never created), it calls the lambda metafactory, a new platform method which dynamically creates an instance of the right type with the body of the abstract method consisting of a call to the lambda method. This particular call to the lambda metafactory is a special form call the lambda factoryand it uses the invokedynamic instruction to make it possible to specialize the metafactory. Specifically, there are many cases where the JVM will have no need to actually generate a class implementing the interface, but instead return some simpler internal structure.

So as anyone familiar with Betteridge’s law knew from the start, you cannot have lambdas in Java 7. With Java 8 still a year away, and Scala tool support improving, migrating is looking attractive.