使用字面值初始化Map

在Java中使用字面值初始化一个Map很困难, 正如Java的一贯作风, 代码比较恶心, 比如

myMap = new HashMap<String, Object>();
myMap.put("id", 5);
myMap.put("name", "xuqi");
myMap.put("age", 18);

这令强迫症实在难以忍受, 所以我决定研究一下看看有什么”优雅”的解决方案

其他语言的做法

  • Javascript

    var myMap={id:5, name: "xuqi", age: 18};
  • Python

    myMap = {'id': 5, name: 'xuqi', 'age': 18}
  • C#

    var myMap = new Dictionary<string, Object> {{"id", 5}, {"name", "xuqi"}, {"age", 18}};
  • C++ (就连C++ 都…)

    std::map<int, string> int_to_string = {{1, "java"}, {2, "is"}, {3, "pretty"}};

Java中的一些解决办法

双括号初始化法

//这个方法多了一些多余的put很令人不爽
Map<String , Object> map = new HashMap<String , Object>(){{
put("id", 5);
put("name", "xuqi");
put("age", 18);
}};

二维数组封装法

定义如下函数:

static Map<String,String> createStringMap(String[][] entries){
return Stream.of(entries).collect(Collectors.toMap(data -> data[0], data -> data[1]));
}

static Map<String,Integer> createIntegerMap(Object[][] entries){
return Stream.of(entries).collect(Collectors.toMap(data -> (String) data[0], data -> (Integer) data[1]));
}

则可以用如下方式创建Map

//创建值为String的Map
Map<String,String> stringMap =
createStringMap(new String[][] {{ "key1", "value1" },{ "key2", "value2" },{ "key3", "value3" }});

//创建值为Integer的Map
Map<String,Integer> intMap =
createIntegerMap(new Object[][] {{ "key1", 1 },{ "key2", 2 },{ "key3", 3 }});

遗憾的是, 因为Java强大的类型擦除特性, 通过这个思路暂时无法写出下面的方法, 除非再加个恶心的Class<T>参数

//该方法因为类型擦除无法实现
static <T> Map<String,T> createMap(Object[][] entries)

另外这个方式也没有编译时检查功能, 不太安全。

参数列表封装法

有很多类库都是这个办法,比如Java 9的

Map<String, String> unmodifiableMap = Map.of("key1", "value1", "key2", "value2");

自己实现的话,大概可以这样

public static <A> Map<String, A> asMap(Object... keysAndValues) {
return new LinkedHashMap<String, A>() {{
for (int i = 0; i < keysAndValues.length - 1; i++) {
put(keysAndValues[i].toString(), (A) keysAndValues[++i]);
}
}};
}

可以这样调用

Map<String, String> one = asMap("1stKey", "1stVal", "2ndKey", "2ndVal");
Map<String, Object> two = asMap("1stKey", Boolean.TRUE, "2ndKey", new Integer(2));

上面的asMap没有编译期类型安全检查, 而Java9的版本似乎是安全的,但好像参数数量有限制,而且限制的数量还很少(这点我没去确认)

另外这个方式语义也不太好, Map应该是一对对的,不是吗?

目前最优雅的办法-lambda

查了N多资料后,我终于找到了一个优雅的办法, 那就是使用lambda表达式, 客户代码很简单, 而且类型安全

//混合型Map
Map<String,Object> map = hashMap(id -> 5,name -> "xuqi",age -> 18);

//String
Map<String,String> stringMap = hashMap(id -> "5",name -> "xuqi",age -> "18");

//Integer
Map<String,Integer> intMap = hashMap(id -> 5,name -> 6666,age -> 18);

实现方法至少需要Java 8u60, 而且在编译时需要加上 -parameters 参数, 在gradle可以加上下面的代码

tasks.withType(JavaCompile) {
configure(options) {
options.compilerArgs << '-parameters' << '-Xlint:unchecked'
}
}

这个实现的关键是SerializedLambda

实现代码

下面是代码中最主要的部分, 获取lambda表达式的参数名

Method replaceMethod = getClass().getDeclaredMethod("writeReplace");
replaceMethod.setAccessible(true);
SerializedLambda lambda= (SerializedLambda) replaceMethod.invoke(this);
Class<?> containingClass = Class.forName(lambda.getImplClass().replaceAll("/","."));
Method method = asList(containingClass.getDeclaredMethods())
.stream()
.filter(method0 -> Objects.equals(method0.getName(), lambda.getImplMethodName()))
.findFirst()
.orElseThrow(UnableToGuessMethodException::new);
return method.getParameters()[0].getName();

参考资料