【JavaIO流】序列化与反序列化

序列化与反序列化

序列化:是一种对象持久化的手段:指将对象转换为字节流的过程。

反序列化:是根据字节流恢复对象的过程。

序列化的作用:

  • 把对象的字节序列化永久的存放在磁盘上,通常放在一个文件夹下。
  • 在网络上传输对象的字节序列。

对象序列化用于将一个对象转换为字节流的过程,可以将对象序列化之后保存在磁盘中或者通过网络发送至另外一个程序,对象序列化可以把对象转换为与平台无关的字节流,在不同平台都可以反序列化为之前的对象。


序列化和反序列化的实现步骤

1、首先,如果一个类要进行序列化的相关操作,那么就必须要实现Serializable 接口,进行标记。表示此类可以被序列化。

2、其次,我们要使用ObjectOutputStream / ObjectInputStream 进行序列化和反序列化。

  • ObjectOutputStream把对象转换成流写入到文件或者网络上。

  • ObjectInputStream读取文件中的对象,对它及进行恢复。

  • 小技巧:【Intellij idea】快捷键自动生成序列化ID配置

class B implements Serializable {
     
    private String kind;

    public B(String kind) {
     
        this.kind = kind;
    }
    
    //省略get/set方法
}

//Serializable  标记接口
class Dog implements Serializable {
     
    private static final long serialVersionUID = -7054078879904171713L;
    //默认就是1L
    private String name;
    private transient int age;   //当你不想让某个字段序列化,或者不想让某个字段按照默认方式
    //序列化  就是用这个字段

    public Dog(String name, int age) {
     
        this.name = name;
        this.age = age;
    }
    
    //省略get/set方法
    
    //重新定义序列化方式
    private void writeObject(ObjectOutputStream out) {
     
        try {
     
            //会对一些没有被transient修饰的字段使用默认方式序列化
            out.defaultWriteObject();
            //md5
            age = age * 9 + 10;
            out.writeInt(age);
        } catch (IOException e) {
     
            e.printStackTrace();
        }
    }

    //重新定义反序列化方式
    private void readObject(ObjectInputStream in) {
     
        try {
     
            in.defaultReadObject();
            int i = in.readInt();  //加密过的age
            age = (i - 10) / 9;
        } catch (IOException e) {
     
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
     
            e.printStackTrace();
        }
    }
}

public class ObjectStreamTest {
       //对象持久化的过程   数据落地
    public static void main(String[] args) {
     
        Dog dog = new Dog("迪克", 8);
        B b = new B("sada");
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        try {
     
            out = new ObjectOutputStream(
                    new FileOutputStream("D:\\Java文件IO\\Test\\dog.txt"));
//            out.writeObject(dog);
//            out.writeObject(b);
            out.writeUTF(dog.getName());
            out.writeInt(dog.getAge());
            System.out.println("---------------反序列化的结果如下-------------------");
            in = new ObjectInputStream(
                    new FileInputStream("D:\\Java文件IO\\Test\\dog.txt"));
            Dog d1 = (Dog) in.readObject();  //只有name
            B b1 = (B)in.readObject();
            System.out.println(b1);
            System.out.println(d1);
        } catch (IOException e) {
     
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
     
            e.printStackTrace();
        } finally {
     
            try {
     
                out.close();
            } catch (IOException e) {
     
                e.printStackTrace();
            }
        }

    }
}
  • 通过上面代码,我们也可以看出,序列化的方式是可以自己指定的,如果我们想将需要序列化的对象以特定的形式处理,就可以自己指定序列化的规则,不过需要给定相应的反序列化规则。
  • 同样的,我们会发现ObjectOutputStream可以写很多东西,基本数据类型都可以写入,我们常用的写入对象的方法就是writeObject(Object o)
  • 并且我们在读取时,必须和写入的顺序是一致的,否则无法读取成功。

SerialVersionUID

SerialVersionUID是系统自动生成的标识。主要作用是判断序列化和反序列化时的对象是否一致,如果SerialVersionUID相同,那么就是一致的,如果不相同,就会抛出异常。

并且系统自动生成的SerialVersionUID是根据类的字段返回的64位长整型的哈希值,所以当我们修改字段时,SerialVersionUID也会随之改变。

所以我们要注意!!!如果我们已经序列化了,那么再次之后不能对类进行任何改动,否则SerialVersionUID就会改变 ,随之而来的就是反序列化失败!


transient关键字

如果类成员变量被 transient 关键字修饰,那么这个变量就不能被序列化或者不能按照默认方式进行序列化。

  • 如果不想让某个字段按照默认方式进行序列化,就需要重写writeObject()readObject()方法指定序列化及反序列化的规则。

相信 transient 关键字大家一定见过,尤其是源码,集合ArrayList 的 elements 数组就是被 transient 修饰的。目的是什么呢?People 的 pwd 这个成员变量本质上是不应该让别人可见的吧,密码不能谁都可以看吧。所以如果我们要保存此对象,pwd这个变量,我们就不能让他序列化。因此,我们就可以对指定的变量加上transient关键字,使之不能被序列化或者不能按照默认方式进行序列化。


序列化的注意事项

1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。如果不实现该接口序列化时会抛出异常。

ObjectOutputStream的writeObject的调用栈:writeObject —> writeObject0—>writeOrdinaryObject—>writeSerialData—>invokeWriteObject。
在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException。

2、通过ObjectOutputStreamObjectInputStream对对象进行序列化及反序列化(非节点流需要通过其他流来链接数据源),使用writeObject()readObject()方法。

3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。

序列化操作的时候系统会把当前类的serialVersionUID(特殊处理)写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。
serialVersionUID 用来表明类的不同版本间的兼容性。有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。

  • 显式定义UID的作用:
    • 确保不同版本有不同的UID。
    • 序列化一个类实例,更改一个字段或者添加一个字段,不设置UID,任何更改导致无法反序列化实例,并抛出异常。

4、序列化并不保存静态变量。

5、如果要想将父类(不只是父类)对象也序列化,就需要让父类也实现Serializable 接口。否则会抛出异常。

6、根据需要添加transient关键字和重写ReadObjectWriteObject方法。

  • 如果没有重写的话,则默认调用是 ObjectOutputStreamdefaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。

7、transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

  • transient 只能修饰属性,加上关键字之后,该属性将不使用默认的序列化和反序列化方式 ,而是用我们重写的方式去序列化和反序列化。
  • transient 关键字的使用场景:
    • 当你不想让某个字段序列化时,直接加上关键字,不用做后续处理。
    • 为了安全起见,有时候我们不需要在网络间传输一些数据,即不希望明文形式存储到文件中(如身份证号码,密码,银行卡号等),这时需要重写writeObjectreadObject()方法。
    • 一般情况下,当对一个变量进行不序列化的标记之后紧跟着就会对这个变量进行特殊的序列户和序列化(重写writeObjectreadObject()方法)。

如:ArrayList集合中的底层结构实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient,以至于按照特定的规则进行序列化和反序列化。


创建新对象的方式

1、使用new关键字

这是最常见也是最简单的创建对象的方式了。通过这种方式,我们可以调用任意的构造函数(无参的和带参数的)。

Employee emp1 = new Employee();

2、使用Class类的newInstance方法

我们也可以使用Class类的newInstance方法创建对象。这个newInstance方法调用无参的构造函数创建对象。

//我们可以通过下面方式调用newInstance方法创建对象:
Employee emp2 = (Employee)Class.forName("org.programming.mitra.exercises.Employee").newInstance();
//或者
Employee emp2 = Employee.class.newInstance();

3、使用clone方法

无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。

//要使用clone方法,我们需要先实现Cloneable接口并实现其定义的clone方法
Employee emp4 = (Employee) emp3.clone();

4、使用反序列化

当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象。在反序列化时,JVM创建对象并不会调用任何构造函数。

//为了反序列化一个对象,我们需要让我们的类实现Serializable接口
ObjectInputStream in = new ObjectInputStream(newFileInputStream("data.obj"));
Employee emp5 = (Employee)in.readObject();

四种创建方式说明

方法 是否调用构造函数
使用new关键字 调用了构造函数
使用Class类的newInstance方法 调用了构造函数
使用clone方法 没有调用构造函数
使用反序列化 没有调用构造函数

你可能感兴趣的:(JavaIO流)