本期是Java开发教程系列的第二十三期,本期介绍 JSON 数据处理:互联网上最通用的数据格式,也是第 22 期那个"一言"接口返回的东西.学完本期,任何接口返回的 JSON 都能变成 Java 对象.若你已经熟悉 Gson 或 Jackson 的基本用法,可以跳过本期内容.
天外音:想要的图在主贴最后了.
版权声明:图片部分来自于网络,如有侵权,可联系删除.
每期叠甲:
时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.
一、JSON 是什么:给数据"塑形"的文本
JSON 的写法,你在第 10 期就见过了:它长得很像 Java 的 Map:
{
"name": "小明",
"score": 92.0,
"passed": true,
"tags": ["勤奋", "细心"]
}
规则只有几条:
- 花括号
{} 包着一个对象,里面是一串 "键": 值
- 键必须是双引号字符串
- 值可以是:字符串、数字、布尔、null、数组
[]、另一个对象 {}
- 数组
[] 和 Java 的 List 对应
为什么全世界接口都用 JSON?三个理由:文本(任何人能看、任何语言能解析)、结构化(嵌套关系天然表达)、约定简单(就上面四条规则).第 11 期我们自己发明了 姓名,电话 的格式:那就是一个"自制 JSON",可惜只有自己的程序能懂.
二、引入 Gson:一个库搞定互转
Java 没有内置 JSON 处理(标准库只提供基础能力),业界最常用的是 Gson(Google 出品)和 Jackson.本期用 Gson,因为它最小最直白.
怎么引入? 如果你的项目是 Maven 或 Gradle 管理,加一行依赖(Gradle):
implementation 'com.google.code.gson:gson:2.11.0'
没配构建工具的话,去 Maven 中央仓库下载 gson-2.x.jar,放进项目的 lib 目录,在 IDEA 里右键 jar → “Add as Library”.后续教程默认你已经会用 Gradle/Maven 加依赖.
对象 → JSON(序列化,toJson):
import com.google.gson.Gson;
public class Player {
public String name;
public int level;
public double hp;
}
Player p = new Player();
p.name = "Steve";
p.level = 10;
p.hp = 20.5;
Gson gson = new Gson();
String json = gson.toJson(p);
System.out.println(json);
// {"name":"Steve","level":10,"hp":20.5}
JSON → 对象(反序列化,fromJson):
String json = "{\"name\":\"Steve\",\"level\":10,\"hp\":20.5}";
Player p = gson.fromJson(json, Player.class);
System.out.println(p.name + " 等级 " + p.level); // Steve 等级 10
注意:类要有无参构造器(Gson 反射创建对象时要用,第 24 期讲反射),字段名和 JSON 键名一一对应才转得进来;JSON 里没有的字段保持默认值.
三、List / Map 互转:TypeToken 登场
fromJson(json, Player.class) 能转单个对象,那转 List 呢?直觉是:
// 错误写法!编译能过(只会给个 unchecked 警告),但运行期转出来是 List<Object>,
// 取出元素强转成 Player 时抛 ClassCastException
List<Player> list = gson.fromJson(json, List.class);
问题在第 14 期的类型擦除:运行期 List<Player> 就是 List,JVM 根本不知道元素该转成 Player.而 Gson 需要"元素类型"才能正确转换.解法:Gson 提供 TypeToken 包装,把泛型类型保真传进去:
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
Type playerListType = new TypeToken<List<Player>>() { }.getType();
List<Player> list = gson.fromJson(json, playerListType);
new TypeToken<List<Player>>() { } 这个写法利用了"匿名子类可以携带泛型信息":你创建一个 TypeToken 的匿名子类,Java 的反射就能从父类的泛型参数里取出 List<Player>.这是一个固定的套路,背下来:
- 单个对象:
fromJson(json, Player.class)
- 集合对象:
new TypeToken<List<Player>>() { }.getType()
Map 同理:
Type mapType = new TypeToken<Map<String, Object>>() { }.getType();
Map<String, Object> map = gson.fromJson(json, mapType);
System.out.println(map.get("name")); // Steve
从 Map 转回对象:gson.fromJson(gson.toJson(map), Player.class).先转 JSON 字符串,再转对象,Gson 里常见的"绕一下".
四、字段改名与忽略: @SerializedName 等
有时 JSON 的键名和 Java 字段名对不上(比如接口叫 player_name,你字段叫 playerName).要么字段跟着接口改名(难看),要么用 @SerializedName 指定:
import com.google.gson.annotations.SerializedName;
public class Player {
@SerializedName("player_name") // JSON 里叫这个,Java 里叫下边那个
public String playerName;
@SerializedName("lv")
public int level;
}
toJson 时也会用注解里的名字输出:双向生效.
想忽略某个字段,用 @Expose 配合 GsonBuilder.注意它分两个方向:serialize 管"输出",deserialize 管"读取",都设 false 才是真正双向忽略:
import com.google.gson.annotations.Expose;
public class Player {
public String name;
@Expose(serialize = false) // 输出 JSON 时跳过它;但反序列化时仍会读取
public String password;
}
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
(另外注意:excludeFieldsWithoutExposeAnnotation() 这个开关一开,没标注 @Expose 的字段默认全部不参与序列化.用这个特性前要把所有想保留的字段都标上 @Expose.)
上面例子里,password 只设了 serialize = false,效果是 toJson 不输出密码,但 fromJson 仍会读.适合"登录后从服务器响应读用户信息,但绝不把密码写进响应"的场景.要"读取也不读"(比如服务器端拒绝接收客户端传来的密码),就写 @Expose(serialize = false, deserialize = false).
五、小项目:通讯录导出导入
把第 11 期的通讯录,从"自制 姓名,电话 格式"升级成 JSON:带嵌套、带数组,还能直接拿给任何程序用.
文件 Contact.java:
public class Contact {
public String name;
public String phone;
public Contact() { } // Gson 需要无参构造器
public Contact(String name, String phone) {
this.name = name;
this.phone = phone;
}
@Override
public String toString() {
return name + " " + phone;
}
}
文件 Main.java:
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.FileReader;
import java.io.FileWriter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception {
Gson gson = new Gson();
// 导出:List → JSON → 文件
List<Contact> contacts = new ArrayList<>();
contacts.add(new Contact("小明", "13800000000"));
contacts.add(new Contact("小红", "13900000000"));
String json = gson.toJson(contacts);
try (FileWriter writer = new FileWriter("contacts.json")) {
writer.write(json);
}
System.out.println("导出完成: " + json);
// 导入:文件 → JSON 字符串 → List
String loadedJson;
try (FileReader reader = new FileReader("contacts.json")) {
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = reader.read()) != -1) {
sb.append((char) ch);
}
loadedJson = sb.toString();
}
Type listType = new TypeToken<List<Contact>>() { }.getType();
List<Contact> loaded = gson.fromJson(loadedJson, listType);
System.out.println("导入 " + loaded.size() + " 个联系人:");
for (Contact c : loaded) {
System.out.println(c);
}
}
}
打开 contacts.json 看看,和之前 姓名,电话 的纯文本对比:现在它是标准的 JSON 数组,任何语言都能解析.把 Contact 加一个字段(比如 email),JSON 里自动出现.这就是"序列化按类结构自动展开"的威力.
朝花夕拾:第 11 期的 split,今天退休了
第 11 期读通讯录用 line.split(","),当时就埋了个钩子:分隔符一旦出现在数据里(名字叫"小明,小刚"?)解析就崩;字段一多,split 的数组下标就眼花.今天 JSON 把这两个痛点都治了:结构由 {}/[] 表达,解析由 Gson 完成.我们只描述"数据长什么样",剩下的活交给库.这也是后面 Spring Boot里 Jackson 自动把请求体转成对象的雏形,还要专门讲 Jackson 的深入用法.
总结
本期学会了和"接口世界的通用语言"打交道:
- JSON 语法:
{} 对象、[] 数组、四种值类型,和 Map/List 一一对应
- Gson 互转:
toJson 出、fromJson 进;字段名一一对应,类要有无参构造器
- 集合转换:类型擦除导致
List.class 不行,用 TypeToken<List<Player>>() { }.getType()
- 改名/忽略:
@SerializedName 双向改名,@Expose 跳过敏感字段
- 通讯录 JSON 导出导入小项目
下一期我们学习反射与注解:今天 Gson 为什么能"看穿"你的类、把字段自动写进 JSON?秘密都在下一期.


