CodeGym /課程 /JAVA 25 SELF /使用動態結構:Map、List、JsonNode

使用動態結構:Map、List、JsonNode

JAVA 25 SELF
等級 46 , 課堂 3
開放

1. 將 JSON 讀入 Map 與 List

通常在處理 JSON 時,我們會預先知道其結構,並能以 Java 類別描述它。但實務上常常不那麼可預期:欄位可能出現或消失,巢狀層級也會改變。在這些情況下,使用通用資料結構 — MapList — 或 JSON 樹會更方便。

為每種變體都建立 Java 模型既耗時又脆弱。通用的集合與樹狀結構讓你能彈性地只擷取需要的部分,而不受固定綱要束縛。

將 JSON 物件反序列化為 Map

JSON 範例:

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "active": true
}

反序列化為 Map:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"email\":\"alice@example.com\",\"active\":true}";

        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> data = mapper.readValue(json, Map.class);

        System.out.println(data);
        // 輸出:{id=123, name=Alice, email=alice@example.com, active=true}
    }
}

現在你可以像存取字典一樣取用任意欄位:

System.out.println(data.get("name")); // Alice

將物件陣列反序列化為 List

JSON 陣列:

[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

反序列化:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;

public class UsersReadExample {
    public static void main(String[] args) throws Exception {
        String json = "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":\"Bob\"}]";

        ObjectMapper mapper = new ObjectMapper();
        List<Map<String, Object>> users = mapper.readValue(json, List.class);

        for (Map<String, Object> user : users) {
            System.out.println(user.get("name"));
        }
        // 輸出:
        // Alice
        // Bob
    }
}

重要細節:在讀取為通用結構時,所有巢狀物件會變成 Map,而陣列會變成 List。對於複雜結構,必須小心進行型別轉換與檢查:

Object items = data.get("items");
if (items instanceof List) {
    List<?> itemList = (List<?>) items;
    // ...
}

2. JsonNode:Jackson 中的 JSON 樹

使用 Map/List 雖然方便,但不夠安全:容易在型別上出錯,或忽略巢狀層級。Jackson 提供了更強大的工具 — 類別 JsonNode。它是一棵通用的樹,每個節點都可能是物件、陣列、值,或是 null

取得 JsonNode

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class JsonNodeStart {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
        ObjectMapper mapper = new ObjectMapper();

        JsonNode root = mapper.readTree(json);
        // root 是 JSON 樹的根節點
    }
}

存取欄位

int id = root.get("id").asInt();         // 123
String name = root.get("name").asText();  // Alice
JsonNode tags = root.get("tags");         // 陣列

System.out.println("姓名: " + name);

遍歷陣列

for (JsonNode tag : tags) {
    System.out.println(tag.asText());
}
// 輸出:
// java
// json

巢狀物件

較複雜的 JSON 範例:

{
  "user": {
    "id": 1,
    "profile": {
      "nickname": "java_guru",
      "age": 25
    }
  }
}

擷取巢狀值:

JsonNode profile = root.get("user").get("profile");
String nickname = profile.get("nickname").asText();
System.out.println(nickname); // java_guru

安全的存取:get vs path

- get("key") — 若鍵不存在,會回傳 null。在 null 上呼叫如 asText() 之類的方法會導致 NullPointerException
- path("key") — 若鍵不存在,會回傳「空」節點,asText() 會得到 ""asInt() 會得到 0

String phone = root.path("phone").asText(); // "" 如果沒有該欄位則為空字串

檢查節點型別

if (root.has("tags") && root.get("tags").isArray()) {
    for (JsonNode tag : root.get("tags")) {
        // ...
    }
}

修改 JsonNode

JsonNode 是不可變的。要建立/修改樹,請使用 ObjectNode/ArrayNode

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

ObjectMapper mapper = new ObjectMapper();

// 建立新物件
ObjectNode obj = mapper.createObjectNode();
obj.put("id", 10);
obj.put("name", "Bob");

// 新增陣列
ArrayNode arr = mapper.createArrayNode();
arr.add("Java").add("JSON");
obj.set("tags", arr);

System.out.println(obj.toPrettyString());
/*
{
  "id" : 10,
  "name" : "Bob",
  "tags" : [ "Java", "JSON" ]
}
*/

3. 使用 Gson: JsonElement, JsonObject, JsonArray

Jackson 並非唯一選擇。Gson 也提供了便於處理動態 JSON 的 API:JsonElementJsonObjectJsonArray

解析為 JsonElement

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
JsonElement root = JsonParser.parseString(json);

存取欄位

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

JsonObject obj = root.getAsJsonObject();
int id = obj.get("id").getAsInt();
String name = obj.get("name").getAsString();
JsonArray tags = obj.getAsJsonArray("tags");

for (JsonElement tag : tags) {
    System.out.println(tag.getAsString());
}

巢狀與安全性

if (obj.has("email")) {
    String email = obj.get("email").getAsString();
}

處理陣列

String arrJson = "[{\"id\":1},{\"id\":2}]";
JsonArray arr = JsonParser.parseString(arrJson).getAsJsonArray();

for (JsonElement el : arr) {
    JsonObject item = el.getAsJsonObject();
    System.out.println(item.get("id").getAsInt());
}

修改

Gson 中,物件是可變的 — 可以新增與刪除欄位:

import com.google.gson.JsonObject;

JsonObject newObj = new JsonObject();
newObj.addProperty("id", 42);
newObj.add("tags", tags);
System.out.println(newObj.toString());

4. 實作:從未知結構的 JSON 抽取資料

任務: 假設我們有一個 JSON 設定檔,其結構可能會變動:

{
  "service": "mail",
  "enabled": true,
  "params": {
    "host": "smtp.example.com",
    "port": 587
  }
}

使用 hostport 以 Jackson 擷取:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
JsonNode params = root.path("params");
String host = params.path("host").asText();
int port = params.path("port").asInt();

System.out.println(host + ":" + port); // smtp.example.com:587

使用 Gson 完成相同操作:

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

JsonObject rootObj = JsonParser.parseString(json).getAsJsonObject();
JsonObject params = rootObj.getAsJsonObject("params");
String host = params.get("host").getAsString();
int port = params.get("port").getAsInt();

System.out.println(host + ":" + port);

5. 常見錯誤與細節

錯誤 1:錯誤的型別轉換。 如果你預期是字串,但欄位實際是數字或 null,呼叫 asText()getAsString() 可能得到意料之外的結果或例外。例如,在欄位不存在時,root.get("foo").asText() 會導致 NullPointerException

錯誤 2:未檢查 null。 尤其在巢狀存取時:root.get("params").get("host")。如果 params 不存在,會造成 NPE。Jackson 中請使用 path();在 Gson 中請透過 has 檢查存在性,並以 isJsonObject()/isJsonArray() 檢查型別。

錯誤 3:混淆節點型別。 如果欄位是陣列,卻以物件方式存取,將會出錯。請檢查型別:Jackson — isArray()/isObject(),Gson — isJsonArray()/isJsonObject()

錯誤 4:使用 Map/List 時喪失型別資訊。 反序列化為 Map<String, Object> 時,巢狀結構會變成層層相依的 Map/List,這使得導航變得複雜,並導致大量的型別轉換與 instanceof 檢查。

錯誤 5:誤解樹的(不)可變性。 在 Jackson 中,JsonNode 是不可變的,修改必須透過 ObjectNodeArrayNode。在 Gson 中,物件是可變的;請記住,修改節點會影響目前樹中對它的所有參照。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION