序列化应用场景:网络传输、文件存储、进程间通讯
Difference between Serialization & Parcelable & Json
- Serializable
- 只需要继承 Serializable 接口,实现简单
- 序列化逻辑通过反射实现,且会产生大量临时对象,性能差
- 只能用在 Java 上,无法跨语言
- 向前兼容性不好,实践无法落地(serializationID)
- 序列化的对象数据会包含类信息,序列化体积不佳
- 可以自定义序列化和反序列化流程(writeObject、readObject)
- Parcelable
- 需要实现序列化和反序列化方法
- 效率高
- 主要应用于进程间通讯,不适用于文件序列化和网络传输
- Json
- 只包含有单纯的数据信息
- 文本格式,可读性好,可以跨平台
- 性能好
Bundle:A mapping from String keys to various Parcelable values
Bundle 具体支持的类型:
- String
- primitives
- Arraylist<String / primitives>
- Serializable
- Parcelable
Gson 用法
Gson 提供了 fromJson 和 toJson 两个直接用于解析和生成 Json 字符串的方法,前者实现反序列化,后者实现了序列化
//POJO 类的生成与解析
public class User {
public String name;
public int age;
public String emailAddress;
}
Gson gson = new Gson();
User user = new User("怪盗kidou",24);
String jsonObject = gson.toJson(user); // {"name":"怪盗kidou","age":24}
Gson gson = new Gson();
String jsonString = "{\"name\":\"怪盗kidou\",\"age\":24}";
User user = gson.fromJson(jsonString, User.class);
@SerializedName 注解
@SerializedName(value = "emailAddress", alternate = {"email", "email_address"})
public String emailAddress;
手动序列化和反序列化,对应 JsonReader、JsonWriter
String json = "{\"name\":\"怪盗kidou\",\"age\":\"24\"}";
User user = new User();
JsonReader reader = new JsonReader(new StringReader(json));
reader.beginObject(); // throws IOException
while (reader.hasNext()) {
String s = reader.nextName();
switch (s) {
case "name":
user.name = reader.nextString();
break;
case "age":
user.age = reader.nextInt(); //自动转换
break;
case "email":
user.email = reader.nextString();
break;
}
}
reader.endObject(); // throws IOException
JsonWriter writer = new JsonWriter(new OutputStreamWriter(System.out));
writer.beginObject() // throws IOException
.name("name").value("怪盗kidou")
.name("age").value(24)
.name("email").nullValue() //演示null
.endObject(); // throws IOException
writer.flush(); // throws IOException
//除了beginObject、endObject还有beginArray和endArray,两者可以相互嵌套,注意配对即可
//beginArray后不可以调用name方法,同样beginObject后在调用value之前必须要调用name方法
使用 GsonBuilder 导出 null 值
Gson在默认情况下是不动导出值null的键的,如:
public class User {
public String name;
public int age;
public String email;
}
Gson gson = new Gson();
User user = new User("怪盗kidou",24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24}
可以看出,email 字段是没有在 json 中出现的,当我们在调试是、需要导出完整的 json 串时或 API 接口中要求没有值必须用Null时,就会比较有用
Gson gson = new GsonBuilder()
.serializeNulls()
.create();
User user = new User("怪盗kidou", 24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24,"email":null}
Serializable 字段过滤
- transient 关键字
- 自定义 readObject、writeObject
Gson 字段过滤
基于 Expose 注解
@Expose
@Expose(deserialize = true,serialize = true) //序列化和反序列化都都生效,等价于上一条
@Expose(deserialize = true,serialize = false) //反序列化时生效
@Expose(deserialize = false,serialize = true) //序列化时生效
@Expose(deserialize = false,serialize = false)// 和不写注解一样
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create();
基于版本
Gson 在对基于版本的字段导出提供了两个注解 @Since 和 @Until,和GsonBuilder.setVersion(Double) 配合使用。@Since 和 @Until都接收一个 Double 值
class SinceUntilSample {
@Since(4)
public String since = "since";
@Until(5)
public String until = "until";
}
public void sineUtilTest(double version){
Gson gson = new GsonBuilder().setVersion(version).create();
System.out.println(gson.toJson(sinceUntilSample));
}
基于访问修饰符
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.FINAL, Modifier.STATIC, Modifier.PRIVATE)
.create();
基于自定义策略
Gson gson = new GsonBuilder()
.addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
// 这里作判断,决定要不要排除该字段,return true为排除
if ("finalField".equals(f.getName())) return true; //按字段名排除
Expose expose = f.getAnnotation(Expose.class);
if (expose != null && expose.deserialize() == false) return true; //按注解排除
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
// 直接排除某个类 ,return true为排除
return (clazz == int.class || clazz == Integer.class);
}
})
.create();
POJO 与 JSON 的字段映射规则
GsonBuilder.setFieldNamingStrategy 方法用于实现将 POJO 的字段与 JSON 的字段相对应。@SerializedName 注解拥有最高优先级,在加有 @SerializedName 注解的字段上 FieldNamingStrategy 不生效
Gson gson = new GsonBuilder()
.setFieldNamingStrategy(new FieldNamingStrategy() {
@Override
public String translateName(Field f) {
//实现自己的规则
return null;
}
})
.create();
TypeToken
解决 Gson 解析时遇到的泛型擦除问题
class Pojo2<T>(val s1: String, val s2: String, val t: T)
class PojoInner(val i: String, val j: String)
class JsonTest {
private val gson = GsonBuilder().create()
fun test() {
val type = object : TypeToken<Pojo2<PojoInner>>() {}.type
val pojo2 = gson.fromJson<Pojo2<PojoInner>>(
"{\"s1\":\"1\",\"s2\":\"2\",\"t\":{\"i\":\"3\",\"j\":\"4\"}}",
type
)
}
}
为什么要用 TypeToken 来定义反序列化的类型?
正如上面说的,如果直接把 Pojo2<PojoInner> 的类型传过去,但是因为运行时泛型被擦除了,所以得到的其实是 Pojo2<Object>,那么后面的 Gson 就不知道要转成 Pojo2<PojoInner> 类型了,这时 Gson 会默认转成 LinkedTreeMap 类型
为什么带有大括号?
这个大括号就是精髓所在。大家都知道,在 Java 语法中,在这个语境,{ } 是用来定义匿名类,这个匿名类是继承了 TypeToken 类,它是 TypeToken 的子类
为什么要通过子类来获取泛型的类型?
这是 TypeToken 能够获取到泛型类型的关键,这是一个巧妙的方法。 这个想法是这样子的,既然像 List<String> 这样中的泛型会被擦除掉,那么我用一个子类 SubList extends List<String> 这样的话,在 JVM 内部中会不会把父类泛型的类型给保存下来呢?我这个子类需要继承的父类的泛型都是已经确定了的呀,果然,JVM 是有保存这部分信息的,它是保存在子类的 Class 信息中
class Pojo2<T>(val s1: String, val s2: String, val t: T)
//反编译后的 Pojo2
# annotations
.annotation system Ldalvik/annotation/Signature;
value = {
"<T:",
"Ljava/lang/Object;",
">",
"Ljava/lang/Object;"
}
.end annotation
class PojoInner(val i: String, val j: String)
//PojoInner 不存在 Ldalvik/annotation/Signature信息
class CustomTypeToken : TypeToken<Pojo2<PojoInner>>()
//反编译后的 CustomTypeToken
.annotation system Ldalvik/annotation/Signature;
value = {
"Lcom/google/gson/reflect/TypeToken<",
"Lcom/example/test/serialization/Pojo2<",
"Lcom/example/test/serialization/PojoInner;",
">;>;"
}
.end annotation
ProtoBuf
- 优点
- 性能好 / 效率高
- 支持“向后兼容”和“向前兼容”
- 支持多种编程语言
- 缺点
- 二进制格式导致可读性差
- 缺乏自描述
Proto2 注意事项
- 必须不可以改变已存在标签的数字
- 必须不可以增加或删除必须(required)字段
- 可以删除可选(optional)或重复(repeated)字段
- 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的
proto3 与 proto2 的区别
- 字段规则移除了 “required”,并把 “optional” 改名为 “singular”
- 移除了 default 选项
- 在 proto2 中,可以使用 default 选项为某一字段指定默认值。在 proto3 中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法
- 在字段被设置为默认值的时候,该字段不会被序列化。这样可以节省空间,提高效率
- 但这样就无法区分某字段是根本没赋值,还是赋值了默认值。这在 proto3 中问题不大,但在 proto2 中会有问题
- 比如,在更新协议的时候使用 default 选项为某个字段指定了一个与原来不同的默认值,旧代码获取到的该字段的值会与新代码不一样
参考文档