CodeGym /課程 /JAVA 25 SELF /Gson — 序列化與反序列化、設定

Gson — 序列化與反序列化、設定

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

1. Gson 入門

我們已經接觸過 Jackson,並了解為何它被視為在 Java 中處理 JSON 的事實標準。不過還有另一個在 Android 世界尤其受歡迎的函式庫——GsonGson 由 Google 開發,作為將 Java 物件與 JSON 之間進行序列化/反序列化的輕量簡單解決方案。它的上手門檻很低:要開始使用幾乎不需設定——多數需求都能「開箱即用」解決。

Gson 的另一個優點是輕量。函式庫體積小,且不會引入一堆相依,因此常見於對應用程式大小很敏感的場景,例如行動裝置上。Gson 幾乎已成為 Android 專案的事實標準——在那裡,精簡與簡單至關重要。

順帶一提,Gson 這個名稱是 Google JSON 的縮寫。社群裡有時會幽默地解讀為 Genius’ Son(意為「天才之子」),當然這不是官方說法,只是雙關語的玩笑。

在專案中加入 Gson

若你使用 Maven 或 Gradle,只要加入依賴即可(版本可能不同):

Maven:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

Gradle:

implementation 'com.google.code.gson:gson:2.10.1'

我們暫時還沒學到專案的建置,因此一開始可以直接從 Gson 官方頁面 下載 jar 檔並加入專案。

2. 基本操作:序列化與反序列化

讓我們以一個簡單的類別為例,看看如何用 Gson 進行物件的序列化與反序列化。

範例:User 類別

// 範例用的類別
public class User {
    private String name;
    private int age;
    private boolean active;

    // 建構子
    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    // Getter 與 Setter(Gson 會在需要時使用)
    public String getName() { return name; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
}

序列化:物件 → JSON

import com.google.gson.Gson;

public class GsonExample {
    public static void main(String[] args) {
        User user = new User("Alice", 25, true);

        Gson gson = new Gson();
        String json = gson.toJson(user);

        System.out.println(json);
        // {"name":"Alice","age":25,"active":true}
    }
}

請注意:欄位會以其在類別中的名稱進行序列化!

反序列化:JSON → 物件

public class GsonExample {
    public static void main(String[] args) {
        String json = "{\"name\":\"Bob\",\"age\":30,\"active\":false}";

        Gson gson = new Gson();
        User user = gson.fromJson(json, User.class);

        System.out.println(user.getName()); // Bob
        System.out.println(user.getAge());  // 30
        System.out.println(user.isActive());// false
    }
}

處理物件清單

Gson 在集合處理上比 Jackson 稍微麻煩,但都有解法。

import java.util.List;
import java.util.Arrays;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;

public class GsonListExample {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Alice", 25, true),
            new User("Bob", 30, false)
        );

        Gson gson = new Gson();
        String json = gson.toJson(users);
        System.out.println(json);
        // [{"name":"Alice","age":25,"active":true},{"name":"Bob","age":30,"active":false}]

        // 反序列化清單
        Type userListType = new TypeToken<List<User>>(){}.getType();
        List<User> users2 = gson.fromJson(json, userListType);
        System.out.println(users2.get(0).getName()); // Alice
    }
}

重要細節:要反序列化集合請使用 TypeToken<>

3. 設定 Gson:GsonBuilder

Gson 透過 GsonBuilder 提供彈性設定。你可以啟用 pretty printing、序列化 null、自訂日期格式,等等。

範例:pretty printing 與序列化 null

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class GsonBuilderExample {
    public static void main(String[] args) {
        User user = new User("Charlie", 0, false);

        Gson gson = new GsonBuilder()
            .setPrettyPrinting()        // 美化輸出(縮排)
            .serializeNulls()           // 序列化為 null 的欄位
            .create();

        String json = gson.toJson(user);
        System.out.println(json);
        /*
        {
          "name": "Charlie",
          "age": 0,
          "active": false
        }
        */
    }
}

日期格式化

如果你有 Date 型別的欄位,預設 Gson 會以特定格式序列化。你可以指定自己的格式:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.Date;

public class DateExample {
    private String event;
    private Date date;

    public DateExample(String event, Date date) {
        this.event = event;
        this.date = date;
    }
}

public class Main {
    public static void main(String[] args) {
        DateExample meeting = new DateExample("Team Meeting", new Date());

        Gson gson = new GsonBuilder()
            .setDateFormat("yyyy-MM-dd HH:mm:ss")
            .create();

        String json = gson.toJson(meeting);
        System.out.println(json);
        // {"event":"Team Meeting","date":"2024-06-10 13:45:23"}
    }
}

4. Gson 註解:控制序列化

Gson 支援註解,以更精確地控制序列化與反序列化。

@SerializedName — 重新命名欄位

若你希望 JSON 中的欄位名稱不同,請使用 @SerializedName

import com.google.gson.annotations.SerializedName;

public class User {
    @SerializedName("full_name")
    private String name;
    private int age;
    private boolean active;

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

現在在序列化時,該欄位會命名為 full_name

User user = new User("Diana", 28, true);
String json = new Gson().toJson(user);
// {"full_name":"Diana","age":28,"active":true}

@Expose — 僅序列化被標註的欄位

如果你只想序列化特定欄位,請使用 @Expose,並據此設定 Gson

import com.google.gson.annotations.Expose;

public class User {
    @Expose
    private String name;

    @Expose
    private int age;

    private boolean active; // 不會被序列化

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

建立啟用 @Expose 支援的 Gson

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

Gson gson = new GsonBuilder()
    .excludeFieldsWithoutExposeAnnotation()
    .create();

User user = new User("Eve", 21, false);
String json = gson.toJson(user);
// {"name":"Eve","age":21}

@Since/@Until — 依版本條件式序列化
可以使用 @Since@Until 僅在特定版本序列化欄位(實務上不常用,但了解很有幫助)。

5. Gson 的特性與限制

處理巢狀物件

Gson 能輕鬆處理巢狀物件:

public class Profile {
    private User user;
    private String bio;

    public Profile(User user, String bio) {
        this.user = user;
        this.bio = bio;
    }
}

Profile profile = new Profile(new User("Frank", 27, true), "Java developer");
String json = new Gson().toJson(profile);
// {"user":{"name":"Frank","age":27,"active":true},"bio":"Java developer"}

處理集合

序列化集合(ListMap)沒有問題,但在反序列化時請使用 TypeToken(見上文)。

Gson 相較於 Jackson 的限制

  • 在較新的版本之前不支援 Java record
  • 對新的日期時間 API 支援有限(例如 LocalDateLocalDateTime——需要自訂配接器)
  • 無法使用 Jackson 的註解
  • 對複雜的多型結構沒有「開箱即用」支援
  • 沒有自動支援雙向引用(循環關係)

自訂配接器(TypeAdapter)

若標準功能不足,可以撰寫自己的配接器來處理複雜型別的序列化/反序列化。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;

public class BooleanAsIntAdapter extends TypeAdapter<Boolean> {
    @Override
    public void write(JsonWriter out, Boolean value) throws IOException {
        out.value(value ? 1 : 0);
    }

    @Override
    public Boolean read(JsonReader in) throws IOException {
        return in.nextInt() == 1;
    }
}

// 使用方式:
Gson gson = new GsonBuilder()
    .registerTypeAdapter(Boolean.class, new BooleanAsIntAdapter())
    .create();

6. 實作:帶設定的序列化與反序列化

讓我們擴充你的學習用應用程式,加入以 JSON 格式儲存與載入使用者清單的功能。

帶註解的 User 類別

import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;

public class User {
    @Expose
    @SerializedName("full_name")
    private String name;

    @Expose
    private int age;

    private boolean active; // 不會被序列化

    public User(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }

    // getters、setters...
}

將使用者清單儲存為 JSON

import java.util.List;
import java.util.Arrays;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class SaveUsers {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Ivan", 23, true),
            new User("Olga", 19, false)
        );

        Gson gson = new GsonBuilder()
            .excludeFieldsWithoutExposeAnnotation()
            .setPrettyPrinting()
            .create();

        String json = gson.toJson(users);
        System.out.println(json);
        /*
        [
          {
            "full_name": "Ivan",
            "age": 23
          },
          {
            "full_name": "Olga",
            "age": 19
          }
        ]
        */
    }
}

從 JSON 載入使用者清單

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;

public class LoadUsers {
    public static void main(String[] args) {
        String json = "[{\"full_name\":\"Ivan\",\"age\":23},{\"full_name\":\"Olga\",\"age\":19}]";

        Gson gson = new GsonBuilder()
            .excludeFieldsWithoutExposeAnnotation()
            .create();

        Type userListType = new TypeToken<List<User>>(){}.getType();
        List<User> users = gson.fromJson(json, userListType);

        for (User user : users) {
            System.out.println(user.getName() + " (" + user.getAge() + ")");
        }
        // Ivan (23)
        // Olga (19)
    }
}

7. Gson 與 Jackson 的比較

特性 Gson Jackson
易用性 +++++(非常簡單) +++(稍難)
程式庫大小 較大
速度 快,但略慢 非常快
彈性 中等 高(可調選項更多)
註解支援 自家(@SerializedName 自家(@JsonProperty 等)
新型別支援 受限 極佳(Java 8+、record
Android 支援 極佳 良好,但較笨重
日期處理 需透過配接器 開箱即用
多型 受限 可彈性設定

8. 使用 Gson 時的常見錯誤

錯誤 №1:未對集合使用 TypeToken
如果要反序列化清單或映射,務必使用 TypeToken<>;否則可能得到奇怪的錯誤或空集合。

錯誤 №2:缺少無參數建構子。
Gson 在沒有預設建構子的情況下也能運作,但對於複雜物件的反序列化有時會出錯。若預計要反序列化,建議準備一個無參數建構子。

錯誤 №3:欄位名稱不一致。
如果 JSON 中的欄位叫做 "full_name",而類別中是 "name",沒有加上 @SerializedName("full_name") 就無法繫結到欄位,值會是 null

錯誤 №4:private 欄位的問題。
Gson 可以序列化 private 欄位,但如果只有 private 欄位且沒有 getter/setter,反序列化有時會遇到問題。最好還是提供 getter 與 setter。

錯誤 №5:日期處理。
預設 Gson 會以不太方便的格式序列化 Date。對於 LocalDateLocalDateTime 等新型別,若沒有自訂配接器會發生序列化錯誤。

錯誤 №6:未使用 @Expose,卻啟用了 excludeFieldsWithoutExposeAnnotation()
如果你啟用了 excludeFieldsWithoutExposeAnnotation(),卻沒有用 @Expose 標註欄位,這些欄位就不會被序列化/反序列化——結果可能是空的 JSON,或物件中的值為 null

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