Java序列化

一篇搞定Java序列化

作者:efan006

序列化

可以理解为一种数据对象和二进制串的转化协议,用于数据持久化和数据传输

Serializable

用法

  1. 数据对象实现java.io.Serializable,否则会抛出NotSerializableException
  2. 通过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);
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

使用细节

  1. transient关键字修饰的变量,可以阻止序列化该变量。反序列化时,transient 变量会被赋值为初始值
  2. 在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略 
    • ArrayList内部就是将元素transient了,然后自定义writeObject和readObject方法,因为ArrayList是动态数组,这样做可以避免大量空对象被序列化
    • 加解密密数据可以在writeObject 和 readObject 中实现
  3. 序列化不关注静态变量
  4. 父类不实现Serializable的话那么父类的变量不会被序列化
  5. serialVersionUID一致时两个相同的类才能被序列化成功,没有特殊情况都用固定的1L即可 
    • 特性案例:Server端重新生成serialVersionUID,可以导致Client端反序列化失败,强制客户端升级
  6. 序列化会破坏单例模式

    • 要么用静态内部类的方式写单例

      public class Singleton {
          private static class Holder {
              private static Singleton singleton = new Singleton();
          }
      
          private Singleton(){}
      
          public static Singleton getSingleton(){
              return Holder.singleton;
          }
      }
            
            
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 要么单例对象重写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;
          }
      }
            
            
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

Parcelable

特点

Android特有,比Serializable高效,节省内存,可用于Intent传递和IPC,但不推荐用于持久化,因为存在版本差异

用法

  1. 实现Parcelable
  2. 重写writeToParcel和Parcelable.Creator里的createFromParcel方法,注意顺序一定要一致

XML(eXtensible Markup Language)

特点

  1. XML被设计为传输和存储数据,W3C标准
  2. 具有自我描述性,但仅仅是纯文本,不会处理任何事情

用法

  1. 基于事件驱动的SAX、Pull方式,内存占用少,使用比较复杂,还有把XML全部加载成树结构的DOM方式,使用灵活但内存占用多
  2. 我比较懒,直接用了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);
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

使用细节

某些文本,比如 JavaScript 代码,包含大量 “<” 或 “&” 字符。为了避免错误,可以将脚本代码定义为 CDATA。CDATA 部分中的所有内容都会被解析器忽略。

    //CDATA  由 <![CDATA[ 开始,由 ]]> 结束
    //以下五种符号需要做转义
    &lt;        <   小于
    &gt;        >   大于
    &amp;       &   和号
    &apos;      '   省略号
    &quot;      "   引号
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

JSON(JavaScript Object Notation)

特点

  1. 用“属性-值”的方式来描述对象,保持了人眼可读的优点
  2. 相比XML数据更加简洁(文件大小将近一半),解析更快
  3. JS的先天支持,广泛应用于web browser场景中,有良好的扩展性和兼容性

fastjson 使用细节

  1. 要有默认构造方法,或者有@JSONCreator的构造方法,不然会报JSONException
  2. 要有成员要public或者要有get/set方法(我们工程约定统一用get/set方法),不然解析不到
  3. 用@JSONField(name=”xxx”)指定key名字,不指定的话采用变量名,可能会被代码混淆导致解析错误
  4. 用@JSONField(format=”yyyyMMdd”)配置日期格式
  5. 有些场景属性互相依赖,需要在构造方法中做初始化,可以使用@JSONCreator来指定构造函数 
    • @JSONCreator只能有一个
    • 参数必须添加注解@JSONField(name=”xxx”)
    • 子类的@JSONCreator不会继承父类的@JSONCreator
  6. 有些场景属性相互依赖,需要在bean反序列化后对数据做二次处理,可以使类实现JavaBeanDeserializerListener,在onDeserializerSuccess做二次处理
  7. 序列化默认跳过null,如果需要序列化,可以再序列化方法里传SerializerFeature WriteMapNullValue:是否输出值为null的字段,默认为false 
    还有其他feature: 
    • QuoteFieldNames 输出key时是否使用双引号,默认为true
    • WriteNullNumberAsZero 数值字段如果为null,输出为0,而非null
    • WriteNullListAsEmpty List字段如果为null,输出为[],而非null
    • WriteNullStringAsEmpty 字符类型字段如果为null,输出为”“,而非null
    • WriteNullBooleanAsFalse Boolean字段如果为null,输出为false,而非null
  8. 顺序问题 
    • 数组和List不会乱序
    • 在1.1.26版本 JSONObject和Bean序列化后会乱序,
    • 在1.1.41会按key的顺序重新排序,
    • 在1.1.42之后可以通过@JSONField(ordinal = 1)指定顺序
    • 还可以在类上配置@JSONType(orders = {})来设置顺序
  9. 反序列化遇到重复的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: 
      1. 对构造方法、get/set方法的规范比较多(见上文),不留神容易遇到坑
      2. 旧版本bug比较多,最好使用最新版
    • gson: 
      1. 类型不对不报错,直接返回默认值
      2. 默认不序列化null,如果需要序列化:Gson gson = new GsonBuilder().serializeNulls().create();
      3. 如果需要反序列化null为”“,需要注册TypeAdapter,如果需要反序列化null为空list,需要添加@JsonAdapter注解(可参考我针对这个写的一篇博客日志)
    • jackson:序列化默认不会跳过null,如果需要跳过 
      • 可以在数据对象上使用@JsonInclude(Include.NON_NULL)
      • 也可以在序列化方法上mapper.setSerializationInclusion(Include.NON_NULL);

Protobuf(Google Protocol Buffers)

特点

  1. Google开发的一种轻便高效的数据存储格式, 适合内部对性能要求高的RPC调用
  2. 特点是二进制格式,数据量小(相当于json的40%左右),解包速度快,非常适合数据存储或交换
  3. proto2仅支持Java,C++,Python三种语言, proto3还支持JavaScript, Objective-C, Ruby, C#, Go.

用法

写proto文件定义数据结构

//可以写注释,会自动转到java类里

import "xxx.proto"                                      //import表示依赖其他proto文件
option java_package = "com.fs.fsprobuf";                //java_package 代表这个文件所在的包名
option java_outer_classname = "User";                   //java_outer_classname 即为类名

message Person {                                        //message代表一个类

    enum PhoneType{                                     //enum是一个枚举类型
        MOBILE = 0;                                     
        HOME = 1;
        WORK = 2;
    }

    message PhoneNumber{
        required string number = 1;                     //required 代表该字段必填,没传值会报错
        optional PhoneType type = 2[default = HOME]     //optional 代表该字段可选,并可以为其设置默认值,默认值格式[defalut=]。
    }

    required int32 id = 2;                              //整型int32 布尔值bool
    required string name = 1;                           //required int32 userId = 1 (修饰词 类型 域名 = 标记值)这是一个域 
    optional string email = 3[default="123"];           //标记值是用来在二进制编码里唯一确认当前域的
    repeated PhoneNumber phone = 4;                     //repeated 代表一个list
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

生成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");
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

注意事项

  1. 升级proto定义,不可修改现有域的标记值,不可添加/删除required域。可以删除optional,repeated域。添加optional,repeated域必须采用不同的标记值(之前删除过的也不能用)
  2. 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
  1. 然后下载wire-compiler-VERSION-jar-with-dependencies.jar工具,这个需要去maven仓库下载下载地址 注意要选择与gradle引入的wire版本一致
  2. 使用cmd执行命令生成java文件
java -jar -Dfile.encoding=UTF-8 wire-compiler-2.2.0-jar-with-dependencies.jar --proto_path=. --java_out=. *.proto  
  
  
  • 1
  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");
        }
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

使用wire项目的工具,方法数显著裁剪了(还是之前3个变量的类,方法20多个,188行代码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值