The 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! 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 lambda names could be retrieved run-time.
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>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.
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
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!