17.序列化与反序列化

104 阅读8分钟

java的序列化和反序列化

序列化和反序列化的定义

Java序列化就是指把Java对象转换为字节序列的过程

Java反序列化就是指把字节序列恢复为Java对象的过程。

序列化最重要的作用:在传递和保存对象时,保证对象的完整性和可传递性。

对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。

反序列化的最重要的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。

总结:核心作用就是对象状态的保存和重建,整个过程核心点就是字节流中所保存的对象状态及描述信息。

实现序列化和反序列化的2种实现

1.实现Serializable接口,readObject/writeObject

若Student类仅仅实现了Serializable接口。

并定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)。

则采用以下方式进行序列化与反序列化。

ObjectOutputStream采用默认的序列化方式,自定义方法对Student对象的非transient的实例变量进行序列化。

ObjcetInputStream采用默认的反序列化方式,自定义方法对Student对象的非transient的实例变量进行反序列化。

注意使用默认的序列化 反序列化,只能对对象的非transient的实例变量进行序列化

2.实现Externalnalizable接口,readExternal/writeExternal

③若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。

ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。 
ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。

实现序列化的另外一种方式为实现接口Externalizable,Externlizable的部分源码例如以下:

没错,Externlizable接口继承了java的序列化接口,并添加了两个方法:

​ void writeExternal(ObjectOutput out) throws IOException;

​ void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

​ 首先。我们在序列化对象的时候,因为这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性能够序列化。

哪些不能够序列化,所以。对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自己主动调

用readExternal()方法,依据序列顺序挨个读取进行反序列,并自己主动封装成对象返回,然后在測试类接收,就完毕了反序列。 所以说Exterinable的是Serializable的一个扩展。

package testReadObject;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

class Person implements Externalizable {
    private static final long serialVersionUID = 1L;
    String userName;
    String password;
    String age;


    public Person(String userName, String password, String age) {
        super();
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    public Person() {
        super();
    }

    public String getAge() {
        return age;
    }

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

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //添加一个新的对象
        Date date = new Date();
        out.writeObject(this.userName);
        out.writeObject(this.password);
        out.writeObject(this.age);
        out.writeObject(this.date);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        //注意这里的接受顺序是有限制的哦,否则的话会出错的
        // 比如上面先write的是A对象的话,那么以下先接受的也一定是A对象...
        userName = (String) in.readObject();
        System.out.println("反序列化后的用户名:" + userName);
        password = (String) in.readObject();
        System.out.println("反序列化后的密码:" + password);
        age = (String) in.readObject();
        System.out.println("反序列化后的年龄:" + age);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = (Date) in.readObject();
        System.out.println("反序列化后的日期为:" + sdf.format(date));
    }

    @Override
    public String toString() {
        //年龄是没有被序列化,所以在反序列化的时候是读取不到数据的
        return "username:" + userName + ",密 码:" + password + ",年龄:" + age;
    }
}

public class Test {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        Person person = new Person("小浩1", "123456", "21");
        System.out.println("序列化之前的数据:\n" + person.toString());
        ObjectOutput objectOutput = new ObjectOutputStream
            							(new FileOutputStream("a.txt"));
        person.writeExternal(objectOutput);

        System.out.println("------------------------------------------------");
        ObjectInputStream ois=new ObjectInputStream
            							(new FileInputStream("a.txt"));
        person.readExternal(ois);
    }
}

Serializable接口、transient 关键字

一个类只有实现了serializable接口,它的对象才能被序列化

Serializable接口

 实现该接口可以实现序列化,Serializable接口没有方法或字段,仅用于标识可序列化的语义。

但是,如果一个类没有实现这个接口,想要被序列化的话,抛出NotSerializableException异常

serialVersionUID

  关于serialVersionUID,在日常项目中,我们可能会忽略,但是其作用不可小视。在实体类中,如果我们不加上serialVersionUID,可能会发生意想不到的错误。serialVersionUID其实是验证版本一致性的,在进行序列化和反序列化后,通过serialVersionUID来验证是否是同一个对象。

​ 可以在IDEA中设置实现Serializable接口后自动生成。在实体类里面 实现 java.io.Serializable 接口,然后选中实体类名称 使用快捷键 Alt + Insert,选择“Adjust code style setting”,会生成serialVersionUID。自动生成的serialVersionUID与UUID类似,几乎不会重复。如

private static final long serialVersionUID = -8105780453909276536L;

假设没有明白指定serialVersionUID。序列化的时候会依据字段和特定的算法生成一个serialVersionUID,当属性有变化时这个id发生了变化,所以反序列化的时候

就会失败。抛出“本地classd的唯一id和流中class的唯一id不匹配”。

jdk文档关于serialVersionUID的描写叙述写道:

​ 假设可序列化类未显式声明 serialVersionUID,则序列化执行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。只是,强烈建议 全部可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的具体信息具有较高的敏感性,依据编译器实现的不同可能千差万别。这样在反序列化过程中可能会导致意外的 InvalidClassException。

因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性。序列化类必须声明一个明白的 serialVersionUID 值。

​ 还强烈建议使用 private 修饰符显示声明 serialVersionUID(假设可能),原因是这样的声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没实用处。

​ 数组类不能声明一个明白的 serialVersionUID,因此它们总是具有默认的计算值,可是数组类没有匹配 serialVersionUID 值的要求。

结论

1.如果声明了serialVersionUID,那么即使序列化之后删除了某些字段再反序列化也是可以成功的。
2.如果没有声明serialVersionUID,那么序列化之后删除了某些字段再反序列化会报错,原因是没有声明serialVersionUID,serialVersionUID是根据类中的字段信息计算出来的。

测试方法:先执行序列化,然后去掉字段后在反序列化。

package test;

import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    String userName;
    String password;
    String age;


    public Person(String userName, String password, String age) {
        super();
        this.userName = userName;
        this.password = password;
        this.age = age;
    }


    public Person() {
        super();
    }


    public String getAge() {
        return age;
    }

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

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }


    @Override
    public String toString() {
        //注意这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的
        return "username:" + userName + ",密 码:" + password + ",年龄:" + age;
    }
}

public class Test {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
       /*  Person person = new Person("小浩1", "123456", "21");
        System.out.println("序列化之前的数据:\n" + person.toString());
        ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("a.txt"));
        objectOutput.writeObject(person);*/
      ObjectInputStream ois=new ObjectInputStream(new FileInputStream("a.txt"));
        Person newPerson = (Person) ois.readObject();
        System.out.println("-------------------------------------------------");
        System.out.println("序列化之后的数据:\n" + newPerson.toString());
    }
}

transient关键字

  transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。此外,针对密码等敏感信息,也不应该进行序列化,因此需要在其前面加上 transient 关键字。

序列化相关的基础知识

java.io.ObjectOutputStream是实现序列化的关键类,它可以将一个对象转换成二进制流,然后可以通过ObjectInputStream将二进制流还原成对象。

主要的作用是用于写入对象信息与读取对象信息。 对象信息一旦写到文件上那么对象的信息就可以做到持久化了

1、需要序列化的类必须实现java.io.Serializable接口,否则会抛出NotSerializableException异常

2、如果检测到反序列化后的类的serialVersionUID和对象二进制流的serialVersionUID不同,则会抛出 异常。

3、Java的序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型必须也要实现java.io.Serializable接口。

4、对于不采用默认序列化或无须序列化的成员变量,可以添加transient关键字,并不是说添加了transient关键字就一定不能序列化。

5、每个类可以实现readObject、writeObject方法实现自己的序列化策略,即使是transient修饰的成员变量也可以手动调用ObjectOutputStream的writeInt等方法将这个成员变量序列化。

参考链接:blog.csdn.net/qq_35029061…