1. 为什么 Gson 值得在 Android 入门阶段就认真学
刚接触 Android 网络编程的人,几乎都会经历一个相同的阶段:接口调通了,数据也拿到了,但拿到手的是一坨字符串,想把它变成 Java 对象,于是开始手写JSONObject一层层getString、getInt、getJSONArray。写三五个字段还行,一旦遇到嵌套对象、对象数组、对象里再套对象数组,代码立刻变成一坨意大利面,改一个字段名要翻遍整个文件。
Gson 就是来解决这个问题的。它是 Google 开源的一个 Java 库,核心能力只有一句话:在 Java 对象和 JSON 字符串之间做双向转换。序列化是把对象变成 JSON 字符串,反序列化是把 JSON 字符串变成对象。听起来简单,但它真正省事的地方在于——你不需要手动去解析每一个字段,只要 Java 类的结构和 JSON 的结构对得上,一行代码就能完成整棵数据树的映射。
这篇内容适合两类人:一类是刚学完 Android 基础、准备接触网络请求的入门者;另一类是已经用过 Gson 但只会fromJson一个方法、遇到嵌套结构就发懵的开发者。我会从最基础的依赖引入讲起,把 JSON 对象、对象嵌套、数组、对象数组、对象里嵌套对象数组这五种典型结构全部拆开讲透,每一种都给出 JSON 原文、对应的 Java 类定义、解析代码和实际输出结果。中间会穿插我自己踩过的坑,比如字段名不一致怎么办、泛型丢失为什么会导致解析失败、null值怎么处理这些文档里一笔带过但实际很要命的问题。
需要提前说明的是,Gson 不是唯一选择,Moshi、Jackson、Fastjson 都能干这件事。但在 Android 入门阶段,Gson 的 API 最简单、文档最全、社区示例最多,遇到问题最容易搜到答案。等你把 Gson 的映射逻辑吃透了,再换其他库基本是半天的事。所以我的建议是:第一遍学解析,就用 Gson 打底。
2. 引入依赖与 Gson 实例的创建方式
2.1 依赖引入的两种写法与选择依据
在 Android 项目里引入 Gson,最直接的方式是在模块级build.gradle的dependencies块里加一行:
dependencies { implementation 'com.google.code.gson:gson:2.10.1' }版本号这里写的是 2.10.1,实际使用时建议去官方仓库看一眼最新稳定版。为什么不建议随便写个老版本?因为 Gson 在 2.8.x 到 2.10.x 之间对反射性能、Record类型支持、null安全处理都做过优化,老版本在某些嵌套场景下会有意料之外的行为。入门阶段没必要追最新,但也不要用五六年前的版本。
如果你用的是 Kotlin 项目,还有另一种选择——Gson 的 Kotlin 扩展或者直接配合kotlinx.serialization。但既然这篇讲的是 Gson,我建议先用纯 Gson 把逻辑跑通,不要一上来就混用多个序列化方案,否则出错时你根本分不清是哪一层的问题。
提示:引入依赖后记得同步 Gradle。如果同步失败提示找不到包,先检查是不是把
implementation写成了compile(老写法已废弃),再检查网络仓库配置是否正常。
2.2 new Gson() 还是 GsonBuilder
很多人入门时直接new Gson()就开干,这没错,但你要知道这个默认实例有几个"默认行为":
- 字段名为
null时,默认不会输出到 JSON 里; - 解析时遇到 JSON 里有但 Java 类里没有的字段,默认忽略,不报错;
- 日期默认会被转成一串奇怪的格式,而不是你想要的
yyyy-MM-dd。
如果你只是做简单的对象映射,new Gson()完全够用。但一旦涉及日期格式化、null值保留、字段命名策略(比如后端返回下划线风格user_name,你的 Java 字段是驼峰userName),就必须用GsonBuilder来定制:
Gson gson = new GsonBuilder() .serializeNulls() // 保留 null 字段 .setDateFormat("yyyy-MM-dd HH:mm:ss") // 统一日期格式 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create();这里解释一下setFieldNamingPolicy的意图。后端接口经常用下划线命名,而 Java 习惯驼峰命名。你有两个选择:要么给每个字段加@SerializedName("user_name")注解,要么全局设置命名策略。字段少的时候用注解更精确,字段多且命名规律统一时用全局策略更省事。我个人的习惯是:只要超过三个字段需要转换命名,就直接上全局策略,否则满屏注解看着累。
还有一个容易被忽略的点:Gson实例是线程安全的,可以做成全局单例复用。每次解析都new Gson()虽然不会出错,但会反复创建反射相关的缓存,在列表滚动频繁解析的场景下是白白浪费性能。所以实际项目里我一般写一个工具类持有单例。
3. 从最简单的 JSON 对象开始建立映射直觉
3.1 一个用户对象的最小案例
先看最简单的 JSON 对象:
{ "name": "张三", "age": 25, "isStudent": false }对应的 Java 类:
public class User { private String name; private int age; private boolean isStudent; }解析代码只有一行:
User user = gson.fromJson(jsonString, User.class);这里有个新手最容易困惑的点:为什么我的类里没有写 getter/setter,Gson 也能解析?因为 Gson 默认使用反射直接读写字段,不依赖 getter/setter。这一点和某些框架不一样,所以你可以写很"干净"的 POJO。但反过来说,如果你把字段声明成private final,Gson 在某些版本下会解析失败,因为反射无法给 final 字段赋值。入门阶段,字段就用普通的private即可。
3.2 字段名对不上时的两种解法
真实接口里,JSON 的 key 和 Java 字段名经常对不上。比如后端返回:
{ "user_name": "张三", "user_age": 25 }而你的类写的是userName、userAge。这时候直接解析,两个字段都会是默认值(null和0),而且不会报错——这是最坑的地方,程序不崩,但数据是空的,你得排查半天。
解法一,用注解精确指定:
public class User { @SerializedName("user_name") private String userName; @SerializedName("user_age") private int userAge; }解法二,全局命名策略(前面提过):
Gson gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create();注意:全局策略是"双向"的,序列化时也会把
userName输出成user_name。如果你只希望解析时兼容、序列化时保持驼峰,那就只能用注解,别用全局策略。
我踩过的一个坑是:同一个类里既有需要下划线转换的字段,又有本来就带下划线的字段,全局策略会把后者也再处理一遍,导致名字错乱。所以命名不统一时,老老实实用注解,别图省事。
4. 对象嵌套:一层套一层怎么映射
4.1 嵌套结构的 JSON 与类设计
实际接口返回的数据很少是扁平的,最常见的就是对象里套对象:
{ "code": 200, "message": "success", "data": { "id": 1001, "title": "Android 入门", "author": { "name": "李四", "email": "lisi@example.com" } } }这种结构对应三个类:外层响应类、中间的数据类、内层的作者类。关键在于类的嵌套关系要和 JSON 的嵌套关系一一对应:
public class ApiResponse { private int code; private String message; private Article data; } public class Article { private int id; private String title; private Author author; } public class Author { private String name; private String email; }解析依然是那一行:
ApiResponse response = gson.fromJson(jsonString, ApiResponse.class);Gson 会自动递归地把data映射成Article,把author映射成Author。你不需要手动getJSONObject("data").getJSONObject("author")。
4.2 嵌套层级过深时的取舍
我见过一些接口,嵌套能到五六层,比如data.list.item.detail.info。这种情况下,如果你老老实实建五六个类,代码会非常臃肿。我的经验是:
- 如果中间层字段你根本用不到,可以只建你需要的层级,把中间层直接映射成目标类型。但前提是 JSON 结构允许——如果中间层是数组,就不能跳。
- 如果嵌套确实深且字段多,考虑用
Map<String, Object>或者JsonObject先接住,再按需取。但这样会失去类型安全,不推荐作为常规方案。
入门阶段,我建议老老实实建类,把嵌套关系理清楚。这个过程本身就是在帮你理解数据结构,等熟练了再考虑优化。
5. 数组与对象数组的解析差异
5.1 纯数组:List 与数组类型的等价写法
先看最简单的字符串数组:
["Java", "Kotlin", "Dart"]解析成List<String>:
Type type = new TypeToken<List<String>>(){}.getType(); List<String> languages = gson.fromJson(jsonString, type);这里出现了本篇第一个关键知识点:TypeToken。为什么不能直接写List<String>.class?因为 Java 的泛型在编译后会被擦除,List<String>和List<Integer>在运行时都是List,Gson 无法知道你要转成哪种元素类型。TypeToken的作用就是在运行时把这个泛型信息"捞"回来。
注意:
new TypeToken<List<String>>(){}.getType()结尾那对花括号不能省,它创建的是一个匿名子类,正是这个子类保留了泛型信息。写成new TypeToken<List<String>>().getType()会编译报错。
5.2 对象数组:最容易写错泛型的地方
对象数组是实际开发中出现频率最高的结构:
[ {"id": 1, "name": "苹果"}, {"id": 2, "name": "香蕉"}, {"id": 3, "name": "橙子"} ]对应类:
public class Fruit { private int id; private String name; }解析成列表:
Type type = new TypeToken<List<Fruit>>(){}.getType(); List<Fruit> fruits = gson.fromJson(jsonString, type);如果你偷懒写成gson.fromJson(jsonString, Fruit[].class),也能解析,得到的是Fruit[]数组。两种写法都行,但列表更灵活,因为后续增删元素方便。我个人的习惯是:解析结果用List,除非有明确的性能或内存要求才用数组。
这里有个我踩过的坑:早期我写成了gson.fromJson(jsonString, List.class),编译通过,运行也不报错,但拿到的List里装的其实是LinkedTreeMap而不是Fruit,一取字段就抛ClassCastException。原因就是泛型被擦除了,Gson 不知道元素类型,只能退化成 Map。凡是带泛型的解析,必须用 TypeToken,这是铁律。
6. 对象里嵌套对象数组的完整实战
6.1 一个贴近真实接口的复合结构
这是本篇最核心、也最能体现 Gson 价值的部分。看下面这个结构:
{ "code": 200, "message": "success", "data": { "className": "三年二班", "teacher": { "name": "王老师", "subject": "数学" }, "students": [ { "id": 1, "name": "小明", "scores": [ {"subject": "语文", "score": 90}, {"subject": "数学", "score": 95} ] }, { "id": 2, "name": "小红", "scores": [ {"subject": "语文", "score": 88}, {"subject": "数学", "score": 92} ] } ] } }这个结构包含了:外层对象、对象嵌套(teacher)、对象数组(students)、对象数组里再嵌套对象数组(每个学生的scores)。可以说把前面讲的几种情况全用上了。
6.2 类结构设计与逐层拆解
对应的类设计如下,注意每一层的对应关系:
public class ClassResponse { private int code; private String message; private ClassData data; } public class ClassData { private String className; private Teacher teacher; private List<Student> students; } public class Teacher { private String name; private String subject; } public class Student { private int id; private String name; private List<Score> scores; } public class Score { private String subject; private int score; }解析代码:
ClassResponse response = gson.fromJson(jsonString, ClassResponse.class); ClassData data = response.getData(); List<Student> students = data.getStudents(); for (Student s : students) { System.out.println(s.getName()); for (Score sc : s.getScores()) { System.out.println(sc.getSubject() + ": " + sc.getScore()); } }你会发现,只要类结构设计对了,解析代码永远只有一行。Gson 会自动处理所有层级的递归映射。这也是我一直强调"先把类设计对"的原因——类设计对了,解析就是白送的;类设计错了,你会花大量时间在调试为什么某个字段是 null。
6.3 类结构设计的三个实用原则
第一,字段名和 JSON key 保持一致,能省掉大量注解。如果后端命名不规范,优先推动后端改,改不动再用注解兜底。
第二,集合类型统一用List,不要一会儿ArrayList一会儿List,保持一致性。Gson 解析出来默认是ArrayList,声明成List接口更灵活。
第三,不要给 POJO 加构造函数。Gson 通过反射创建实例,如果类里只有带参构造函数而没有无参构造函数,某些版本会报错。入门阶段,POJO 就写成只有字段的纯数据类,最省心。
7. 那些文档不写但实际会遇到的坑
7.1 null 值、空数组与默认值
JSON 里某个字段是null,或者整个数组是空的[],Gson 的行为是这样的:
- 对象字段为
null:Java 字段被赋值为null(引用类型)或默认值(基本类型,如int为 0)。 - 数组为空:解析成空的
List,不是null。 - JSON 里缺少某个字段:Java 字段保持默认值,不报错。
这里有个隐患:int类型的字段如果 JSON 里是null,Gson 会赋值为 0,你无法区分"真的是 0"还是"数据缺失"。所以对于可能缺失的数值字段,建议用包装类型Integer,这样缺失时是null,能明确判断。
7.2 解析失败时的异常处理
gson.fromJson在遇到格式错误时会抛JsonSyntaxException。实际开发中,接口返回的可能是错误页面的 HTML,而不是预期的 JSON,这时候直接解析就会崩。所以解析代码一定要包在 try-catch 里:
try { ApiResponse response = gson.fromJson(jsonString, ApiResponse.class); if (response == null) { // 处理空结果 } } catch (JsonSyntaxException e) { // 记录日志,提示数据格式异常 }提示:
fromJson传入空字符串或null时返回null,不会抛异常。所以拿到结果后要先判空,再取字段。
7.3 混淆配置不能忘
如果项目开启了代码混淆,Gson 依赖反射读写字段,字段名被混淆后就和 JSON key 对不上了,解析结果全是 null。解决办法是在混淆规则里保留数据类:
-keep class com.example.app.model.** { *; } -keepattributes Signature -keepattributes *Annotation*Signature属性是给泛型用的,不加的话TypeToken相关的解析在混淆后可能失效。这个坑我在一个上线项目里踩过,测试环境一切正常,打包后数据全空,排查了大半天才定位到混淆。只要用了 Gson,混淆规则就必须配。
8. 序列化:把对象写回 JSON 字符串
前面讲的都是解析(反序列化),反过来把对象转成 JSON 字符串同样常用,比如本地缓存、日志打印、请求体构造。
User user = new User(); user.setName("张三"); user.setAge(25); String json = gson.toJson(user); // 输出:{"name":"张三","age":25}默认情况下,值为null的字段不会出现在输出里。如果你希望保留,就用前面提到的serializeNulls()。另外,toJson对集合同样适用:
List<Fruit> fruits = new ArrayList<>(); // ... 添加元素 String json = gson.toJson(fruits);序列化时有个细节值得注意:循环引用会导致栈溢出。如果两个对象互相持有对方引用,toJson会无限递归直到崩溃。入门阶段一般遇不到,但设计数据模型时要避免双向强引用。
9. 性能与选型上的几点个人经验
Gson 基于反射,解析大量数据时性能不如手写解析或者基于代码生成的方案。但在绝大多数 App 场景下,这个差距可以忽略。真正影响性能的往往是这几个点:
- 反复创建 Gson 实例:前面说过,做成单例。
- 在列表滚动时同步解析大 JSON:解析应该放在子线程,解析完再回主线程更新 UI。
- 解析不需要的字段:如果接口返回一大堆字段你只用三个,可以考虑只建三个字段的类,Gson 会忽略多余的,反而更快。
至于选型,我的看法是:入门阶段用 Gson 建立"对象与 JSON 一一对应"的思维模型,这个模型是通用的。等你对映射逻辑烂熟于心,再去看 Moshi 的@Json、kotlinx.serialization 的@Serializable,会发现概念都是相通的,迁移成本很低。真正难的不是 API,而是把接口返回的数据结构准确地翻译成类结构,这个能力只能靠多练。
最后分享一个我自己的习惯:拿到一个新接口,先不写代码,把返回的 JSON 格式化好,对着它一层层画括号,标出哪些是对象、哪些是数组、哪些是嵌套。画完再动手建类,基本一次就能过。这个笨办法帮我省下的调试时间,远比它花掉的多。