一篇搞定Java序列化
作者:efan006
序列化Serializable
用法使用细节 Parcelable
特点用法 XMLeXtensible Markup Language
特点用法使用细节 JSONJavaScript Object Notation
特点fastjson 使用细节各种JSON库的比较on Android ProtobufGoogle Protocol Buffers
特点用法
写proto文件定义数据结构生成java文件导入jar包 注意事项另一种选择
序列化
可以理解为一种数据对象和二进制串的转化协议,用于数据持久化和数据传输
Serializable
用法
数据对象实现java.io.Serializable,否则会抛出NotSerializableException通过ObjectOutputStream和ObjectInputStream对对象进行序列化和反序列化
@Override
public <T>
void write(OutputStream output, T bean)
throws Exception {
ObjectOutputStream oos =
null;
try {
oos =
new ObjectOutputStream(output);
oos.writeObject(bean);
}
catch (IOException e) {
e.printStackTrace();
}
finally {
IOUtils.closeQuietly(oos);
}
}
@Override
public <T> T
read(InputStream input, Class<T> entityClass)
throws Exception {
ObjectInputStream ois =
null;
try {
ois =
new ObjectInputStream(input);
return (T) ois.readObject();
}
catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
finally {
IOUtils.closeQuietly(ois);
}
}
1234567891011121314151617181920212223242526
使用细节
transient关键字修饰的变量,可以阻止序列化该变量。反序列化时,transient 变量会被赋值为初始值在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略
ArrayList内部就是将元素transient了,然后自定义writeObject和readObject方法,因为ArrayList是动态数组,这样做可以避免大量空对象被序列化加解密密数据可以在writeObject 和 readObject 中实现 序列化不关注静态变量父类不实现Serializable的话那么父类的变量不会被序列化serialVersionUID一致时两个相同的类才能被序列化成功,没有特殊情况都用固定的1L即可
特性案例:Server端重新生成serialVersionUID,可以导致Client端反序列化失败,强制客户端升级
序列化会破坏单例模式
要么用静态内部类的方式写单例
public class Singleton {
private static class Holder {
private static Singleton singleton =
new Singleton();
}
private Singleton(){}
public static Singleton
getSingleton(){
return Holder.singleton;
}
}
1234567891011
要么单例对象重写readReslove方法
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton
getSingleton() {
if (singleton ==
null) {
synchronized (Singleton.class) {
if (singleton ==
null) {
singleton =
new Singleton();
}
}
}
return singleton;
}
private Object
readResolve() {
return singleton;
}
}
123456789101112131415161718
Parcelable
特点
Android特有,比Serializable高效,节省内存,可用于Intent传递和IPC,但不推荐用于持久化,因为存在版本差异
用法
实现Parcelable重写writeToParcel和Parcelable.Creator里的createFromParcel方法,注意顺序一定要一致
XML(eXtensible Markup Language)
特点
XML被设计为传输和存储数据,W3C标准具有自我描述性,但仅仅是纯文本,不会处理任何事情
用法
基于事件驱动的SAX、Pull方式,内存占用少,使用比较复杂,还有把XML全部加载成树结构的DOM方式,使用灵活但内存占用多我比较懒,直接用了Square Retrofit里的XML解析器-SimpleXML,使用就像JSON一样简单
public class XmlConverter implements IConverter {
private final Serializer serializer =
new Persister();
@Override
public <T> String
toString(T bean)
throws Exception {
ByteArrayOutputStream output =
new ByteArrayOutputStream();
write(output, bean);
return output.toString();
}
@Override
public <T> T
toBean(String string, Class<T> entityClass)
throws Exception {
if (entityClass == String.class
|| entityClass == CharSequence.class){
return (T)string;
}
else if (entityClass ==
char[].class){
return (T)string.toCharArray();
}
else {
return serializer.read(entityClass, string);
}
}
@Override
public <T>
void write(OutputStream output, T bean)
throws Exception {
serializer.write(bean, output);
}
@Override
public <T> T
read(InputStream input, Class<T> entityClass)
throws Exception {
return serializer.read(entityClass, input);
}
}
123456789101112131415161718192021222324252627282930313233
使用细节
某些文本,比如 JavaScript 代码,包含大量 “<” 或 “&” 字符。为了避免错误,可以将脚本代码定义为 CDATA。CDATA 部分中的所有内容都会被解析器忽略。
//CDATA 由
<![CDATA[ 开始,由 ]]> 结束
//以下五种符号需要做转义
<
< 小于
> > 大于
& & 和号
' ' 省略号
" " 引号
1234567
JSON(JavaScript Object Notation)
特点
用“属性-值”的方式来描述对象,保持了人眼可读的优点相比XML数据更加简洁(文件大小将近一半),解析更快JS的先天支持,广泛应用于web browser场景中,有良好的扩展性和兼容性
fastjson 使用细节
要有默认构造方法,或者有@JSONCreator的构造方法,不然会报JSONException要有成员要public或者要有get/set方法(我们工程约定统一用get/set方法),不然解析不到用@JSONField(name=”xxx”)指定key名字,不指定的话采用变量名,可能会被代码混淆导致解析错误用@JSONField(format=”yyyyMMdd”)配置日期格式有些场景属性互相依赖,需要在构造方法中做初始化,可以使用@JSONCreator来指定构造函数
@JSONCreator只能有一个参数必须添加注解@JSONField(name=”xxx”)子类的@JSONCreator不会继承父类的@JSONCreator 有些场景属性相互依赖,需要在bean反序列化后对数据做二次处理,可以使类实现JavaBeanDeserializerListener,在onDeserializerSuccess做二次处理序列化默认跳过null,如果需要序列化,可以再序列化方法里传SerializerFeature WriteMapNullValue:是否输出值为null的字段,默认为false 还有其他feature:
QuoteFieldNames 输出key时是否使用双引号,默认为trueWriteNullNumberAsZero 数值字段如果为null,输出为0,而非nullWriteNullListAsEmpty List字段如果为null,输出为[],而非nullWriteNullStringAsEmpty 字符类型字段如果为null,输出为”“,而非nullWriteNullBooleanAsFalse Boolean字段如果为null,输出为false,而非null 顺序问题
数组和List不会乱序在1.1.26版本 JSONObject和Bean序列化后会乱序,在1.1.41会按key的顺序重新排序,在1.1.42之后可以通过@JSONField(ordinal = 1)指定顺序还可以在类上配置@JSONType(orders = {})来设置顺序 反序列化遇到重复的key,以后面的为准
各种JSON库的比较(on Android)
依赖比较: fastjson(202K) < gson(226K) < jackson(33+190+828=1051K)流行度: gson > fastjson > jackson //p.s. fastjson仅在中国流行速度比较:小数据量上fastjson和gson差不多,优于jackson;大数据量上,fastjson和jackson差不多,gson很差
p.s. fastjson在jvm平台上的性能比在Android上优秀,因为运用了ASM库 用法比较:
fastjson:
对构造方法、get/set方法的规范比较多(见上文),不留神容易遇到坑旧版本bug比较多,最好使用最新版 gson:
类型不对不报错,直接返回默认值默认不序列化null,如果需要序列化:Gson gson = new GsonBuilder().serializeNulls().create();如果需要反序列化null为”“,需要注册TypeAdapter,如果需要反序列化null为空list,需要添加@JsonAdapter注解(可参考我针对这个写的一篇博客日志) jackson:序列化默认不会跳过null,如果需要跳过
可以在数据对象上使用@JsonInclude(Include.NON_NULL)也可以在序列化方法上mapper.setSerializationInclusion(Include.NON_NULL);
Protobuf(Google Protocol Buffers)
特点
Google开发的一种轻便高效的数据存储格式, 适合内部对性能要求高的RPC调用特点是二进制格式,数据量小(相当于json的40%左右),解包速度快,非常适合数据存储或交换proto2仅支持Java,C++,Python三种语言, proto3还支持JavaScript, Objective-C, Ruby, C#, Go.
用法
写proto文件定义数据结构
import "xxx.proto"
option java_package =
"com.fs.fsprobuf";
option java_outer_classname =
"User";
message Person {
enum PhoneType{
MOBILE =
0;
HOME =
1;
WORK =
2;
}
message PhoneNumber{
required
string number =
1;
optional PhoneType
type =
2[
default = HOME]
}
required
int32 id =
2;
required
string name =
1;
optional
string email =
3[
default=
"123"];
repeated PhoneNumber phone =
4;
}
12345678910111213141516171819202122232425
生成java文件
使用官方工具把proto文件编译成java文件,放到工程里与package对应的路径下
protoc-java
.exe --java_out=./ *
.proto
1
导入jar包
下载官方的protobuf-java的jar包引入到工程里,使用工具类进行数据读写
@overide
public <T>
void write(OutputStream output, T bean)
throws Exception {
if (bean
instanceof MessageLite){
((MessageLite)bean).writeTo(output);
}
else {
throw new IllegalArgumentException(bean.getClass().getName() +
"is not a protobuf message");
}
}
@overide
public <T> T
read(InputStream input, Class<T> entityClass)
throws Exception {
if (MessageLite.class.isAssignableFrom(entityClass)) {
Parser<MessageLite> parser;
try {
Field field = entityClass.getDeclaredField(
"PARSER");
parser = (Parser<MessageLite>) field.get(
null);
return (T)parser.parseFrom(input);
}
catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(
"Found a protobuf message but " + entityClass.getName() +
" had no PARSER field.");
}
}
else {
throw new IllegalArgumentException(entityClass.getName() +
"is not a protobuf message");
}
}
12345678910111213141516171819202122232425
注意事项
升级proto定义,不可修改现有域的标记值,不可添加/删除required域。可以删除optional,repeated域。添加optional,repeated域必须采用不同的标记值(之前删除过的也不能用)proto文件转成的java类 带有大量的方法(实测仅3个变量的类,生成了近100个方法,800行代码)。这在Android上极易导致方法数超65535上限
另一种选择
我们也可以采用Square Wire 项目的proto工具 1. 首先也是写proto文件,一样的 2. 在项目gradle里引入wire组件
compile
'com.squareup.wire:wire-runtime:2.2.0'
1
然后下载wire-compiler-VERSION-jar-with-dependencies.jar工具,这个需要去maven仓库下载下载地址 注意要选择与gradle引入的wire版本一致使用cmd执行命令生成java文件
java
-jar -Dfile.encoding
=UTF
-8 wire
-compiler-2.2.0-jar-with-dependencies.jar
--proto_path
=. --java_out
=. *.proto
1
使用工具类读写
@Override
public <T>
void write(OutputStream output, T bean)
throws Exception {
if (bean
instanceof Message){
((Message)bean).encode(output);
}
else {
throw new IllegalArgumentException(bean.getClass().getName() +
" is not a protobuf message");
}
}
@Override
public <T> T
read(InputStream input, Class<T> entityClass)
throws Exception {
if (Message.class.isAssignableFrom(entityClass)) {
ProtoAdapter<Message> adapter;
try {
Field field = entityClass.getDeclaredField(
"ADAPTER");
adapter = (ProtoAdapter<Message>)field.get(
null);
return (T)adapter.decode(input);
}
catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(
"Found a protobuf message but " + entityClass.getName() +
" had no ADAPTER field.");
}
}
else {
throw new IllegalArgumentException(entityClass.getName() +
" is not a protobuf message");
}
}
12345678910111213141516171819202122232425
使用wire项目的工具,方法数显著裁剪了(还是之前3个变量的类,方法20多个,188行代码)