Creating Maps With Named Lambdas
Learn how you can create a Java Map like this: map = mapOf(one -> 1, two -> 2) using a trick to get the lambda parameter name.
Join the DZone community and get the full member experience.
Join For FreeThe Magical Map
Wouldn't it be great if we could create Java maps like this?
Map<String, Integer> map = mapOf(
one -> 1,
two -> 2
);
Map<String, String> map2 = mapOf(
one -> "eins",
two -> "zwei"
);
Well, we can! [UPDATE: Check out the last clause in this article before you use it in your own code] Read this post and learn more about lambdas and how we can get the name of their parameters.
The Solution
By introducing the following interface, we get the functionality above.
public interface KeyValueStringFunction<T> extends Function<String, T>, Serializable {
default String key() {
return functionalMethod().getParameters()[0].getName();
}
default T value() {
return apply(key());
}
default Method functionalMethod() {
final SerializedLambda serialzedLabmda = serializedLambda();
final Class<?> implementationClass = implementationClass(serialzedLabmda);
return Stream.of(implementationClass.getDeclaredMethods())
.filter(m -> Objects.equals(m.getName(), serialzedLabmda.getImplMethodName()))
.findFirst()
.orElseThrow(RuntimeException::new);
}
default Class<?> implementationClass(SerializedLambda serializedLambda) {
try {
final String className = serializedLambda.getImplClass().replaceAll("/", ".");
return Class.forName(className);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
default SerializedLambda serializedLambda() {
try {
final Method replaceMethod = getClass().getDeclaredMethod("writeReplace");
replaceMethod.setAccessible(true);
return (SerializedLambda) replaceMethod.invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SafeVarargs
static <V> Map<String, V> mapOf(KeyValueStringFunction<V>... mappings) {
return Stream.of(mappings)
.collect(
toMap(
KeyValueStringFunction::key,
KeyValueStringFunction::value
)
);
}
}
Limitations
We can only create maps with keys that are of type String (or anything super String like CharSequence, Serializable, or Comparable<String>) because, obviously, lambda names are strings.
We must use a Java version that is higher than Java 8u80 because it was at that time that lambda names could be retrieved at runtime.
The most annoying limitation is that we have to compile (i.e. "javac") our code with the "-parameter" flag, or else the parameter names will not be included in the run time package (e.g. JAR or WAR). We can do this automatically by modifying our POM file like this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<!-- Add this line to your POM -->
<arg>-parameters</arg>
</compilerArgs>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
If you forget to add the "-parameter" flag, the Java runtime will always report a default name of "arg0" as the name of the parameter. This leads to that the maps will (at most) contain one key "arg0", which is not what we want.
Opportunities
The values, can be of any type. In particular they can be other maps, enabling us to construct more advanced map hierarchies. We could, for example, create a map with different countries with values that are also maps containing the largest towns and how many inhabitants each city has like this:
Map<String, Map<String, Integer>> map3 = mapOf(
usa -> mapOf(
new_york -> 8_550_405,
los_angeles -> 3_971_883,
chicago -> 2_720_546
),
canada -> mapOf(
toronto -> 2_615_060,
montreal -> 1_649_519,
calgary -> 1_096_833
)
);
Update: Important Notes
A number of people have pointed out that SerializedLambda is "thin ice" that we should not rely on in production code. Tagir Valeev (@tagir_valeev) made a performance test of the scheme in this post and compared it to the old fashioned way of just creating a Map and using a regular put() to enter data. The findings were that the old way is orders of magnitudes faster. You can find the entire benchmark here. Thanks Tagir for making this test available.
You should view this way of entering data in a Map as academic an as an inspiration of what can be done in Java. Do not use it in production code.
Keep on mapping!
Published at DZone with permission of Per-Åke Minborg, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments