聊一聊Java中的系列化
博客专区 > trayvon 的博客 > 博客详情
聊一聊Java中的系列化
trayvon 发表于7个月前
聊一聊Java中的系列化
  • 发表于 7个月前
  • 阅读 31
  • 收藏 1
  • 点赞 0
  • 评论 0
摘要: Java中一个比较常用且重要的功能就是系列化,一种是系列化为文本例如json格式,xml格式,这一类在redis缓存,Web应用中使用比较多,第二种是系列化为二进制格式的数据,这一种需要在网络传输对象的时候,例如RMI中使用比较多。本文主要介绍的就是Java本身提供的系列化为二进制格式的方式,如果有兴趣也可以搜索protostuff,Thrift,hessian等提供的系列化的方式。

简介

什么是系列化

首先我要弄明白的Java中的系列化指的是什么?这里分为广义的系列化和狭义的系列化来介绍,狭义的说就是:把Java对象转换为字节系列的过程,反系列化就是:把字节系列转换为Java对象的过程。

更广义的将:系列化不仅仅是可以把Java对象转换为字节系列,也可以是字符串,例如xml或者json格式的字符串。最常用的就是转换为json格式的字符串了,很多缓存中使用的就是这种方式,例如把Java对象转换为json字符串保持到Redis数据库中,需要时再从Redis数据库中取出来转换为Java对象。

为什么要系列化

  1. 想要把Java对象保持到文件中
  2. 想要把Java对象保持到数据库中,如:缓存到Redis数据库中
  3. 想要在网络上传输Java对象时,如:RMI(远程方法调用的时候,参数返回值,或者这些对象的封装对象)

本质上还是格式转换的问题,Java对象只有JVM能够识别,但是字节系列就是到能处理的了,至于怎么处理那就是协议的事情了。广义的系列化不仅仅是转换为字节系列还可以是字符串是因为我们一般不关心文件流或者网络传输层怎么处理我们的字符串。我们只需要保证,在系列化和反系列化的字符串相同就可以了。

Java中系列化的特点

serialVersionUID的作用:(相当于表示类的版本号)

serialVersionUID的最重要的作用就是做类的兼容升级,这是因为Java在反系列化的时候会从待系列化的字节系列中解析出serialVersionUID,如果和当前的类的serialVersionUID不一样就会抛出异常。 如果类中没有显示的设置serialVersionUID值,编译器为根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段等信息动态生成一个serialVersionUID。如果修改了类的字段或者方法名等信息,serialVersionUID也被隐式的重新生成。现在如果使用修改过后的类和修改之类之前的得到的系列化字节系列来进行反系列化,因为serialVersionUID不同,因此,会抛出异常。

而显示的设置serialVersionUID值就可以保证版本的兼容性,如果你在类中显式的声明serialVersionUID这个字段,编译器就不会自动生成serialVersionUID,这样就算类被修改了,用修改之后的类和修改之前的系列化字节系列来进行反序列化的时候serialVersionUID也是相同的,所以不会抛出异常。对于类新增的字段则会设置成null,删除的值则不会显示。

系列化的特点

  1. 如果一个类可被序列化,其子类也可以(不用显式实现Serializable接口),如果该类有父类,则根据父类是否实现Serializable接口,实现了则父类对象字段可以序列化,没实现,则父类对象字段不能被序列化。
  2. 声明为transient类型的成员数据不能被序列化。transient代表对象的临时数据
  3. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化
  4. 静态变量是可以被系列化的

Java系列化的几种方式

实现Serializable接口或者继承实现Serializable接口的类

Java的Serializable只是一个mixin接口(I can),就是如果一个类实现了这个接口就代表能够被系列化。其实对于继承了实现了Serializable接口的类的类也能够被系列化。如果对一个没有实现Serializable接口或者继承了Serializable接口的类的类进行系列化操作就会抛出NotSerializableException异常。Java默认的系列化方式是不会系列化transient字段的,下面通过一个例子来说明一下这种系列化方式。

实现了Serializable接口的类:

import java.io.Serializable;

public class Human implements Serializable{

    private static final long serialVersionUID = 1L;
    
    private String name;
    
    private char sex;//m male f femal
    
    private short age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public short getAge() {
        return age;
    }

    public void setAge(short age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Human [name=" + name + ", sex=" + sex + ", age=" + age + "]";
    }

}

继承之实现了Serializable接口的类

public class Student extends Human {

    private static final long serialVersionUID = 1L;
    
    private int id;
    
    private String dept;
    
    /**
     * transient字段不会被系列化
     */
    private static transient int instanceNum;
    
    public Student(){
        instanceNum++;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getDept() {
        return dept;
    }

    public void setDept(String dept) {
        this.dept = dept;
    }
    

    public static int getInstanceNum() {
        return instanceNum;
    }

    public static void setInstanceNum(int instanceNum) {
        Student.instanceNum = instanceNum;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + ", dept=" + dept + ", toString()=" + super.toString() + "]";
    }
}

没有实现Serializable接口,也没有继承实现Serializable接口的类

import java.math.BigDecimal;

public class Teacher {
    
    private int id;
    
    private BigDecimal salary;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public BigDecimal getSalary() {
        return salary;
    }

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Teacher [id=" + id + ", salary=" + salary + "]";
    }
    
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;

import cn.freemethod.serialize.Student;
import cn.freemethod.serialize.Teacher;


public class SerializeStart {
    
    public static void main(String[] args) {
        
        Student student = getStudent();
        Teacher teachear = getTeachear();
        
        serialize(student,Student.class);
        //系列化没有实现Serializable接口的类抛出异常
        serialize(teachear,Teacher.class);
        
    }
    
    private static void serialize(Object target,Class<?> clazz){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream bos = new ObjectOutputStream(baos);
            bos.writeObject(target);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        try {
            ObjectInputStream ois = new ObjectInputStream(bais);
            Object s = ois.readObject();
//          clazz.cast(s);
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    private static Student getStudent(){
        Student student = new Student();
        student.setId(1);
        student.setAge((short) 20);
        student.setName("tom");
        student.setSex('m');
        student.setDept("cs");
        return student;
    }
    
    private static Teacher getTeachear(){
        Teacher teacher = new Teacher();
        teacher.setId(1);
        teacher.setSalary(new BigDecimal("8000"));
        return teacher;
    }

}

readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)

使用readObject和和writeObject方式还是必须实现实现了Serializable接口,或者继承之实现了Serializable接口的类。不同的是只需要添加定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)的方法Java就会执行和writeObject来进行系列化,执行readObject来进行反系列化。下面还是通过一个例子来说明一下这中方式。我们知道transient字段是不会被系列化的,但是我们通过这种方式可以把transient字段也进行系列化。

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Leader implements Serializable {

    
    private static final long serialVersionUID = 1L;
    
    private int id;
    
    private String position;
    
    private static transient int instanceNum;
    
    public Leader(){
        instanceNum++;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

    public static int getInstanceNum() {
        return instanceNum;
    }
    
    private void writeObject(ObjectOutputStream oos) throws IOException{  
        oos.defaultWriteObject();  
        oos.writeInt(instanceNum); 
    }  
  
    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{  
        ois.defaultReadObject();  
        instanceNum = ois.readInt();  
    }

    @Override
    public String toString() {
        return "Leader [id=" + id + ", position=" + position + ", instanceNum=" + instanceNum + "]";
    }

}

我们的主类采用上面的主类的变形:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigDecimal;

import cn.freemethod.serialize.Leader;
import cn.freemethod.serialize.Student;
import cn.freemethod.serialize.Teacher;


public class SerializeStart {
    
    public static void main(String[] args) {
        
        testSerializeLeader();
    }
    
    public static void testSerializeLeader(){
        Leader leader = getLeader();
        System.out.println("系列化之前:");
        System.out.println(leader);
        serialize(leader,Leader.class);
        
    }
    
    public static void testSerializeStudent(){
        Student student = getStudent();
        serialize(student,Student.class);
    }
    
    public static void testSerializeTeacher(){
        Teacher teachear = getTeachear();
        serialize(teachear,Teacher.class);
    }
    
    private static void serialize(Object target,Class<?> clazz){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream bos = new ObjectOutputStream(baos);
            bos.writeObject(target);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        try {
            ObjectInputStream ois = new ObjectInputStream(bais);
            Object s = ois.readObject();
//          clazz.cast(s);
            System.out.println("系列化之后:");
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    private static Student getStudent(){
        Student student = new Student();
        student.setId(1);
        student.setAge((short) 20);
        student.setName("tom");
        student.setSex('m');
        student.setDept("cs");
        return student;
    }
    
    private static Teacher getTeachear(){
        Teacher teacher = new Teacher();
        teacher.setId(1);
        teacher.setSalary(new BigDecimal("8000"));
        return teacher;
    }
    
    private static Leader getLeader(){
        Leader leader = new Leader();
        leader.setId(1);
        leader.setPosition("principal");
        return leader;
    }
}

输出的结果是: 系列化之前: Leader [id=1, position=principal, instanceNum=1] 系列化之后: Leader [id=1, position=principal, instanceNum=1]

从上面的结果中的instanceNum没有变我们也知道,Java在进行反系列化的时候没有调用构造方法。(这里不太严谨,这个结论可以把instanceNum换成不是我们自定义系列化的来说明) ois.defaultReadObject();和oos.defaultWriteObject();是调用Java默认的反系列化和系列化方法。

类实现了Externalnalizable接口

实现了Externalnalizable接口的方式其实和上面一种使用readObject和和writeObject方式差不多。不同的是实现了Externalnalizable接口,就必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法。并且和Serializable接口不同的是,Externalnalizable接口反系列化的时候是调用了构造方法的。下面通过一个实例来说明。

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Worker implements Externalizable {
    
    private int id;
    
    private String department;
    
    private static int instanceNum;
    
    public int getId() {
        return id;
    }


    public void setId(int id) {
        this.id = id;
    }


    public static int getInstanceNum() {
        return instanceNum;
    }

    public String getDepartment() {
        return department;
    }


    public Worker(){
        instanceNum++;
        System.out.println("construct Worker");
    }
    

    public void setDepartment(String department) {
        this.department = department;
    }
    
    

    @Override
    public String toString() {
        return "Worker [id=" + id + ", department=" + department + ", instanceNum=" + instanceNum + "]";
    }


    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeUTF(department);
        out.writeInt(instanceNum);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readInt();
        this.department = in.readUTF();
        instanceNum = in.readInt();
    }

}

主类:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import cn.freemethod.serialize.Worker;


public class SerializeStart {
    
    public static void main(String[] args) {
        testSerializeWorker();
    }
    
    public static void testSerializeWorker(){
        Worker worker = getWorker();
        System.out.println("系列化之前:");
        System.out.println(worker);
        serialize(worker,Worker.class);
    }
    
    private static void serialize(Object target,Class<?> clazz){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream bos = new ObjectOutputStream(baos);
            bos.writeObject(target);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        try {
            ObjectInputStream ois = new ObjectInputStream(bais);
            Object s = ois.readObject();
//          clazz.cast(s);
            System.out.println("反系列化之后:");
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    private static Worker getWorker(){
        Worker worker = new Worker();
        worker.setId(1);
        worker.setDepartment("refectory");
        return worker;
    }

}

输出结果: construct Worker 系列化之前: Worker [id=1, department=refectory, instanceNum=1] construct Worker 反系列化之后: Worker [id=1, department=refectory, instanceNum=1]

我们可以看到在反系列化的时候也是调用了构造方法。

readResolve

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。我们把上面一个的Worker中添加了一个readResolve方法,主类不变再次测试一下。

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.math.BigDecimal;

public class Worker implements Externalizable {
    
    private int id;
    
    private String department;
    
    private static int instanceNum;
    
    public int getId() {
        return id;
    }


    public void setId(int id) {
        this.id = id;
    }

    public static int getInstanceNum() {
        return instanceNum;
    }

    public String getDepartment() {
        return department;
    }

    public Worker(){
        instanceNum++;
        System.out.println("construct Worker");
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    @Override
    public String toString() {
        return "Worker [id=" + id + ", department=" + department + ", instanceNum=" + instanceNum + "]";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeUTF(department);
        out.writeInt(instanceNum);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readInt();
        this.department = in.readUTF();
        instanceNum = in.readInt();
    }
    
     private Object readResolve(){
         Teacher teacher = new Teacher();
         teacher.setId(2);
         teacher.setSalary(new BigDecimal("10000"));
         return teacher;
     }

}

得到的结果为: construct Worker 系列化之前: Worker [id=1, department=refectory, instanceNum=1] construct Worker 系列化之后: Teacher [id=2, salary=10000]

可以看到反系列化使用的是readResolve方法得到的对象。

注意

我们知道在实现Serializable接口的时候为升级类做了兼容,例如为类添加了新的字段并且没有修改serialVersionUID,那么在使用升级之前的类的系列化数据并且使用升级之后的类作为模板来进行反系列化,那么新的字段将被设置为默认值。如果使用默认值违反了构造约束,就可以通过private readObjectNoData()方法来进行改进,例如抛出异常。

实现了Serializable接口,如果这个类被发布,那么就必须考虑类升级的兼容性

因为实现Serializable接口的方式的反系列化的方式没有调用构造函数,所以必须考虑因为系列化可能存在的违反构造约束的可能

总结

  1. 实现Serializable接口只是表明这个类及其子类是可以按Java提供的系列化标准进行系列化的,具体的系列化实现可以自己选择,可以选择JDK提供的方式,也可以选择其他方式,如:Protobuf,Thrift等方式
  2. 内部类不应该实现Serializable,静态成员类可以实现Serializable
  3. 使用readObject和writeObject来实现自定义系列化
  4. 通过扩展Externalizable来实现自定义系列化
  5. 提供readResolve来决定最终返回对象
  6. 通过提供readObjectNoData方法来处理没有数据的字段
  7. 如果你需要设计一个框架,或者做一个库,强烈建议阅读《Effective Java》的系列化相关章节。
  8. 最后如果想提升代码的可读性,建议阅读《代码整洁之道》,如果想提升代码健壮性建议阅读《Effective Java》
共有 人打赏支持
粉丝 12
博文 91
码字总数 140014
作品 1
×
trayvon
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: