At Java One, I overheard someone mentioning an outrageous trick to abuse the arrow ->
of the lambda syntax. Of course I had to investigate. Here are the gory details...and please, kids, don't try this at home.
Unfortunately, Java doesn't have a convenient syntax for initializing lists and maps. In Scala, you can write
val scores = Map("Harry" -> 1, "Fred" -> 2)
but in Java, it is
Map<String, Integer> scores = new HashMap<>(); scores.put("Harry", 1); scores.put("Fred", 2);
Years ago, I discovered with morbid fascination that one can use anonymous subclasses and an obscure initializer syntax to make this a bit less painful:
Map<String, Integer> scores = new HashMap<>() {{ put("Harry", 1); put("Fred", 2); }};
This is called double brace initialization, and it is in general not a good idea. A new class is created, and the instance may or may not test correctly with equals
, depending on how the equals
method is implemented.
Now that we have actual arrows in Java, can we abuse them for defining maps? Our friends in the world of C# are doing it.
The idea is to use a lambda expression such as
Harry -> 1
As with any lambda expression, there is just one thing one can do with it: turn it into an instance of a functional interface. Like this:
Function<String, Integer> f = Harry -> 1;
Now f
is a function that yields 1 for any string input.
Given f
, it is easy enough to recover the value: just call f.apply("")
. But what about Harry
? Note that it's not a string. It is the name of the parameter.
As it turns out, since JDK 8, one can compile a Java program with
javac -parameters Program.java
Then the names of the parameters are included in class files and can be retrieved through reflection.
As an aside, this is potentially useful because it can reduce annotation boilerplate. Consider a typical JAX-RS method
Person getEmployee(@PathParam("dept") Long dept, @QueryParam("id") Long id)
In almost all cases, the parameter names are the same as the annotation arguments, or they can be made to be the same. If the annotation processor could read the parameter names, then one could simply write
Person getEmployee(@PathParam Long dept, @QueryParam Long id)
Let’s hope annotation writers will enthusiastically embrace this mechanism, so there will be momentum to drop that compiler flag in the future.
Back to our little puzzle. We could get to the parameter name if we had a java.lang.reflect.Method
instance for the method of the lambda expression. To do that, we need to know a bit about how lambda expressions are compiled. The Java compiler generates a static method such as
private static java.lang.Integer lambda$main$7796d112$1(java.lang.String)
in the class file of the class containing our lambda expression. That method is ultimately called when the lambda expression is invoked. And it has the desired parameter name, as you can verify by calling
javap -c -p -v Program
Here is the relevant part of the output:
... private static java.lang.Integer lambda$main$7796d112$1(java.lang.String); descriptor: (Ljava/lang/String;)Ljava/lang/Integer; flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: iconst_2 1: invokestatic #37 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 4: areturn LineNumberTable: line 49: 0 MethodParameters: Name Flags Harry synthetic ...
Of course, if there are multiple lambda expressions, one needs to find the correct method if one wants to recover the parameter name. Benji Weber's blog shows how to do that.
Lambda expressions can be serialized into instances of the class java.lang.invoke.SerializedLambda
, from which one can retrieve the name of the class containing the implementation method, as well as its name.
You get the names of the class and method as
String className = serializedLambda.getImplClass().replaceAll("/", "."); // in our example, "Program" String methodName = serializedLambda.getImplMethodName() // in our example, "lambda$main$7796d112$1"
Now we just need to find the matching Method
instance and get its parameter name:
Class<?> containingClass = Class.forName(className); Method m = containingClass.getDeclaredMethod(methodName, String.class); return m.getParameters()[0].getName();
But how do we get the SerializedLambda
from the functional interface instance? You need to call the writeReplace
method on the instance. That's a private method, so you need to use reflection:
Method replaceMethod = f.getClass().getDeclaredMethod("writeReplace"); replaceMethod.setAccessible(true); SerializedLambda serializedLambda = (SerializedLambda) replaceMethod.invoke(this);
Here is a complete program with all the pieces. The plumbing is placed into a functional interface Arrow<T>
. You can then call
Arrow<Integer> f = Harry -> 1; System.out.println(f.getName() + " " + f.getValue());
More usefully, you can define a method for constructing maps whose keys are strings:
@SafeVarargs static <T> Map<String, T> map(Arrow<T> ... arrows) { Map<String, T> result = new HashMap<>(); for (Arrow<T> arrow : arrows) result.put(arrow.getName(), arrow.getValue()); return result; }
To make a map, call
Map<String, Integer> m = Arrow.map(Harry -> 1, Fred -> 2);
Of course, that only works if the map keys are valid identifiers.
Benji Weber has another example for HTML builders.
It's great fun that you can do that, but it also feels quite repulsive. It just doesn't seem to be in the spirit of Java to bend the rules like this. Don't try this at home!