Skip to content

可能这也不算踩坑,但对自己来说是一个新的知识点。 有一份多层嵌套的 json 格式的数据(geojson格式):

json
{
  "type": "FeatureCollection",
  "name": "json",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "毛天区间",
        "delstatus": 0,
        "keyValue": "7.0mm"
        ......
      },
      "geometry": {
        "type": "Point",
        "coordinates": ""
      }
    },
    ....
  ]
}

我需要做的是从这份 json 数据中,从其他系统的接口中获取数据,给 features 下的 每一个元素的 properties 属性赋予新的字段。 那这么简单的需求,想当然的就开始用 fastjson 开干,我写的代码是这样的:

java

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)方法返回的一样? 带着这个疑问,我们 写一个测试用例看看:

java
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 是同一个对象,我们来调试进入源码看下: image.png 源码中可以看到我们 map键对应的值是Map类型,最终由交由new JSONObject()方法执行。 JSONObject 是 LinkHash Map 的子类,new JSONObject()的执行效果是浅复制,效果类似于:

java
Map originMap = new HashMap();
Map newMap = new HashMap(originMap);
// newMap 与 originMap不是同一个对象

这个方法最终会执行浅复制,从原始 Map 实例中复制到新的 Map 实例中,如果此时 Map 中的键所对应的值为引用类型,则他们会共同持有该引用类型实例。 也就是说 JSONObject.getJSONObject方法返回的其实是一个新的对象,直接修改这个对象的键所对应的值并不会影响新的对象, 我们用一张图来表示:

  1. 当我们使用 new JSONObject(b)后来创建 JSONObject 对象后,创建的对象是由一个 map键,对应的值是指向 Map A 的引用;
  2. 调用 JSONObject.getJSONObject("map")返回的结果,等价于执行new HashMap(A),执行的结果是一个新的 Map 对象,与原始的 A 对象没有关系(💡如果此时 A 中有值为引引用类型的属性,新的 Map 对象会与 A 共享同一个引用)

注:getJSONArray 方法同样,也会丢失引用

那了解了这些,想要实现我们的目的:不丢失引用,应该怎么做呢? 我们可以通过 jsonobject 的 get()方法获取,并用 Map 进行强制类型转换,此时会直接返回原始对象,这样就不会丢失引用:

java
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");