可能这也不算踩坑,但对自己来说是一个新的知识点。 有一份多层嵌套的 json 格式的数据(geojson格式):
{
"type": "FeatureCollection",
"name": "json",
"features": [
{
"type": "Feature",
"properties": {
"name": "毛天区间",
"delstatus": 0,
"keyValue": "7.0mm",
......
},
"geometry": {
"type": "Point",
"coordinates": ""
}
},
....
]
}
我需要做的是从这份 json 数据中,从其他系统的接口中获取数据,给 features 下的 每一个元素的 properties 属性赋予新的字段。 那这么简单的需求,想当然的就开始用 fastjson 开干,我写的代码是这样的:
public JSONObject test(){
// 获取geojson
JSONObject geojson = restTemplate.getForEntity(url, JSONObject.class).getBody();
JSONArray features = geojson.getJSONArray("features")
for (int j = 0; j < features.size(); j++) {
// 避免引用丢失, 不要直接使用getJsonArray、getJsonObject方法
JSONObject feature = yuliangJSON.getJSONObject(j);
JSONObject properties = feature.getJSONObject("properties");
// 伪代码,赋新值
properties.put("a",1);
properties.put("b",1);
}
return geojson;
}
这么简单明确的代码,想来不会有什么问题,谁知执行完返回的 geojson 中没有新增的字段,跟赋值前的一样,通过getJSONObject
获取的不应该是引用类型吗,跟 map 类型实例中中 get(key)
方法返回的一样? 带着这个疑问,我们 写一个测试用例看看:
public void testMapJson() {
Map<String, Object> A = new LinkedHashMap<>();
A.put("name", "张三");
Map<String, Object> B = new LinkedHashMap<>();
B.put("map", A);
JSONObject jsonObject = new JSONObject(B);
// 修改A
A.put("name", "李四");
// 第一个断言:
Assertions.assertEquals("李四", jsonObject.getJSONObject("map").getString("name"));
// 第二个断言: jsonobject获取map返回的对象应该与 A 是同一个对象
Assertions.assertSame(jsonObject.getJSONObject("map"), A);
}
我们构造了一个嵌套的 Map,并写了两个断言测试,哪一个会通过呢?答案是第一个通过,第二个没通过。 这就有些与我们直觉相悖,第一个断言成功,应该就代表jsonobject
获取"map"返回的对象与 A 是同一个对象,我们来调试进入源码看下: 源码中可以看到我们 map
键对应的值是Map
类型,最终由交由new JSONObject()
方法执行。 JSONObject 是 LinkHash Map 的子类,new JSONObject()
的执行效果是浅复制,效果类似于:
Map originMap = new HashMap();
Map newMap = new HashMap(originMap);
// newMap 与 originMap不是同一个对象
这个方法最终会执行浅复制,从原始 Map 实例中复制到新的 Map 实例中,如果此时 Map 中的键所对应的值为引用类型,则他们会共同持有该引用类型实例。 也就是说 JSONObject.getJSONObject
方法返回的其实是一个新的对象,直接修改这个对象的键所对应的值并不会影响新的对象, 我们用一张图来表示:
- 当我们使用 new JSONObject(b)后来创建 JSONObject 对象后,创建的对象是由一个
map
键,对应的值是指向 Map A 的引用; - 调用
JSONObject.getJSONObject("map")
返回的结果,等价于执行new HashMap(A)
,执行的结果是一个新的 Map 对象,与原始的 A 对象没有关系(💡如果此时 A 中有值为引引用类型的属性,新的 Map 对象会与 A 共享同一个引用)
注:getJSONArray 方法同样,也会丢失引用
那了解了这些,想要实现我们的目的:不丢失引用,应该怎么做呢? 我们可以通过 jsonobject 的 get()方法获取,并用 Map 进行强制类型转换,此时会直接返回原始对象,这样就不会丢失引用:
JSONObject shuiwenGEOJSON = restTemplate.getForEntity(shuiwenURL, JSONObject.class).getBody();
ArrayList shuiwenFeatures = (ArrayList) shuiwenGEOJSON.get("features");
Map feature = (Map)shuiwenFeatures.get(j);
Map properties = (Map)feature.get("properties");