当前位置:   article > 正文

java基础知识点2:序列化与反序列化详解_java序列化和反序列化

java序列化和反序列化

一、初识序列化与反序列化

1.1、序列化与反序列化介绍

1)Serialization(序列化):将 java 对象以一连串的字节序列保存在本地磁盘中的过程,也可以说是保存 java 对象状态的过程。序列化可以将数据永久保存在磁盘上(通常保存在文件中),避免程序运行结束后对象从内存中消失,字节序列也方便在网络中传输。

2)deserialization(反序列化):将保存在磁盘文件中的 java 字节序列重新转换成 java 对象称为反序列化。

1.2、序列化和反序列化的应用

1)对象序列化机制(object serialization)是 java 语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在 JVM 中的活动对象和字节数组(流)之间进行转换。

2)两个进程在远程通信时,可能发送多种数据,包括文本、图片、音频、视频等,这些数据都是以二进制序列的形式在网络上传输。而 java 是面向对象的开发方式,一切都是对象,想要在网络中传输 java 对象,需要通过序列化和反序列化去实现,发送发需要将 java 对象转换为字节序列,然后在网络上传送,接收方收到字符序列后,会通过反序列化将字节序列恢复成 java 对象。

3)在 java 中,对象的序列化和反序列化被广泛的应用到 RMI(远程方法调用)及网络传输中;

1.3、java序列化的优点

1)实现了数据的持久化,通过序列化可以把数据持久地保存在硬盘上(磁盘文件)。即使 JVM 停机,字节流还会在硬盘上等待,在下一次 JVM 启动时,反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间。
2)利用序列化实现远程通信,即在网络上传送对象的字节序列,方便网络传输。序列化成字节流形式的对象(二进制形式)也可以节约网络带宽。

二、serializable 接口实现序列化和反序列化

2.1、serializable 接口的使用

1)Serializable 接口是一个标记接口,不用实现任何方法,仅用于标识可序列化的语义。

2)序列化步骤
步骤一:创建一个 ObjectOutputStream 输出流;
步骤二:调用 ObjectOutputStream 对象的 writeObject() 输出可序列化对象。

class Person implements Serializable {

    private String name;
    private int age;

    // 加一条输出语句(演示反序列化时有用)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("我是构造方法");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

class WriteObject {
    // 把 Person 对象序列化到文件 D:\\person.txt
    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
            // 将 Person 对象序列化到文件 person.txt
            Person person = new Person("涛涛", 21);
            oos.writeObject(person);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>>D盘中找到 person.txt 文件,会发现该文件保存的内容如下:
 sr Person??GI I ageL namet Ljava/lang/String;xp   t 娑涙稕
  • 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
  • 34
  • 35
  • 36
  • 37
  • 38

3)反序列化步骤
步骤一:创建一个 ObjectInputStream 输入流;
步骤二:调用 ObjectInputStream 对象的 readObject() 得到序列化的对象。

class Person implements Serializable {
    private String name;
    private int age;

    // 加一条输出语句(提示:注意看输出结果)
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("我是构造方法");
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

class ReadObject {
    public static void main(String[] args) {
    	// 将上面序列化到 person.txt 的 person 对象反序列化回来
        try {
            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
            // 将文件 person.txt 中的内容反序列化为 Person 对象
            Person person = (Person) ois.readObject();
            System.out.println(person);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
Person{name='涛涛', age=21}
  • 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
  • 34
  • 35
  • 36
  • 37

可能有读者发现了,构造方法中的输出语句并没有输出。嘿嘿,这是因为反序列化实现 Serilaziable 接口的类并不会调用构造方法。反序列的对象是由 JVM(java虚拟机) 以存储的二进制位为基础来构造,不通过构造方法生成。

2.2、serializable 接口的注意事项

2.2.1、有父类的实现 Serilizable 接口的类
如果实现 Serializable 接口的类有父类,则父类也必须可以序列化,若父类没有实现序列化接口,则父类必须有无参构造函数,否则会抛异常 java.io.InvalidClassException。因为在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果没有在父类无参构造函数中对父类变量进行初始化的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

class Person {
    public String name;
    public int age;

    public Person() {
        name = "涛涛";
        //age = 21;
        System.out.println("Person无参构造方法");
    }
}

class Student extends Person implements Serializable {
    private int grade;

    public Student(int grade) {
        this.grade = grade;
        System.out.println("Student构造方法");
    }

    @Override
    public String toString() {
        return "Student{" +
                "grade=" + grade +
                '}' + " " + "Person{" +
                "name=" + super.name +
                "," +
                "age=" + super.age +
                '}';
    }
}


class Test {
    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\student.txt"));
            // 序列化
            Student student1 = new Student(1);
            oos.writeObject(student1);

            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\student.txt"));
            // 反序列化
            Student student2 = (Student) ois.readObject();
            System.out.println(student2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
Person无参构造方法
Student构造方法
Person无参构造方法
Student{grade=1} Person{name=涛涛,age=0}
  • 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
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

2.2.2、静态变量的序列化
序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

class Test implements Serializable {
    // 定义一个静态变量
    public static int staticVar = 5;

    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\test.txt"));
            // 序列化
            Test t1 = new Test();
            out.writeObject(t1);

            // 修改 staticVar 的值
            t1.staticVar = 10;

            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream oin = new ObjectInputStream(new FileInputStream("D:\\test.txt"));
            // 反序列化
            Test t2 = (Test) oin.readObject();

            // 输出 staticVar 的值
            System.out.println("staticVar = " + t2.staticVar);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
staticVar = 10
  • 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

main 方法中,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,而最后的输出是 10。也就是说,在序列化时并没有保存静态变量 staticVar,因为 staticVar 的值是在序列化之后修改的,如果静态变量也被序列化的话,那么毕竟打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态,也就是 5 才对。(如果将 staticVar 修改为 “public int staticVar = 5;” 再运行程序会发现输出结果为 5)

2.2.3、自定义序列化
有些时候,我们有这样的需求,某些属性不需要序列化。使用 transient 关键字选择不需要序列化的字段。

class Person implements Serializable {
    // 不需要序列化名字与年龄
    private transient String name;
    private transient int age;
    // 需要序列化身高
    private int height;

    public Person(String name, int age, int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height +
                '}';
    }
}

class Test {
    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"));
            Person person = new Person("涛涛", 21, 185);
            // 序列化
            oos.writeObject(person);
            System.out.println("序列化的person:" + person);
            // 反序列化
            Person p1 = (Person)ios.readObject();
            System.out.println("反序列化的p1:" + p1);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
序列化的person:Person{name='涛涛', age=21, height=185}
反序列化的p1:Person{name='null', age=0, height=185}
  • 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
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

从输出我们看到,使用 transient 修饰的属性,java 序列化时会忽略此字段,所以反序列化出的对象,被 transient 修饰的属性是默认值。

三、Externalizable 接口实现序列化与反序列化

1)Externalizable 接口继承 Serializable 接口。

2)实现 Externalizable 接口必须实现 readExternal() 和 writeExternal() 方法,这两个方法是抽象方法,对应的是 Serializable 接口的 readObject() 方法和 writeObject() 方法,可以理解为把 Serializable 的两个方法抽象出来。

3)因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,所以 Externalizable 没有 Serializable 的限制,static 和 transient 关键字修饰的属性也能进行序列化

4)必须提供 public的无参构造方法,因为在反序列化实现 Externalizabale 接口的类的时需要通过反射创建对象。如果没有无参数的构造方法,在运行时会抛出异常:java.io.InvalidClassException。

5)相较于Serializabale 接口,Externalizable 接口带来了一定的性能提升,但同时复杂度也提高了,所以一般通过实现 Serializable 接口进行序列化。

// 具体代码示例
class ExPerson implements Externalizable {

    private String name;
    private int age;

    // 注意,必须加上 public无参构造器
    public ExPerson() {
    }

    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "ExPerson{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    // 重写 writeExternal() 方法
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 只定义 name 的序列化实现细节
        out.writeObject(name);
        //out.writeInt(age);
    }

    // 重写 readExternal() 方法
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 只定义 name 的反序列化实现细节
        name = (String)in.readObject();
        //age = in.readInt();
    }

    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\ExPerson.txt"));
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\ExPerson.txt"));
            // 调用 ObjectOutputStream 中的 writeObject() 方法序列化对象,但会自动执行重写的 writeExternal() 方法
            oos.writeObject(new ExPerson("涛涛", 21));
            // 调用 ObjectInputStream 中的 readObject() 方法反序列化对象,但会自动执行重写的 readExternal() 方法
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
ExPerson{name='涛涛', age=0}
  • 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
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

悄悄我发现了啥,输出结果中 age 等于 0 ?没错,它确实等于 0 ,这是因为当使用 Externalizable 接口来进行序列化与反序列化时,需要开发人员重写 writeExternal() 与 readExternal() 方法,并在方法体中声明属性的具体实现。由于上面的代码中,将 age 变量的实现细节注释了,所以输出的 age 为默认值。

四、再来三个知识点

4.1、不能序列化的成员变量

如果一个实现了 Serializable 或 Externalizable 接口的类的某个成员变量既不是基本类型,也不是 String 类型,那这个引用类型的成员变量(如下述的 Student 类型)也必须是可序列化的。否则这个这个类也不能序列化,运行时将抛出 NotSerializationException 异常。

class Student {
    //省略相关属性与方法
}

class Teacher implements Serializable {

    private String name;
    private Student student;

    public Teacher(String name, Student student) {
        this.name = name;
        this.student = student;
    }

    public static void main(String[] args) {
        try {
        	// 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
            Student student = new Student("颜回", 21);
            Teacher teacher = new Teacher("孔子", student);
            // 序列化 teacher 对象
            oos.writeObject(teacher);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 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

在这里插入图片描述

4.2、同一对象多次序列化

同一对象多次序列化,并不会得到多个二进制流,既不会反序列化为多个对象。而是只有第一次序列化为二进制流,以后都只是保存序列化版本号,且按自上而下的顺序依次保存。(反序列化时的顺序与序列化时的顺序一致

class Test {
    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
            // 第一次序列化 person 对象
            Person person = new Person("涛涛", 21);
            oos.writeObject(person);
            // 第二次序列化 person
            oos.writeObject(person);

            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
            // 依次反序列化出 p1、p2
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();
            
            // 判断 p1、p2 是否是同一对象
            int i1 = System.identityHashCode(p1);
            int i2 = System.identityHashCode(p2);
            System.out.println("i1 = " + i1);
            System.out.println("i2 = " + i2);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
i1 = 1349393271
i2 = 1349393271
  • 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

知识点:
System.identityHashCode(Object x):不管所给参数对象的类是否重载了 hashCode() 方法,都会返回 Object 类默认 hashCode() 方法会返回的值。所以可以用来判断是不是同一个对象(地址是否相同)

4.3、序列化版本号 serialVersionUID

1)JVM 首先会通过类名来区分 Java 类,类名不同,则不是同一个类。当类名相同时,JVM 就会通过序列化版本号来区分 Java 类,如果序列化版本号相同就为同一个类,序列化版本号不同就为不同的类。

2)JVM 根据类信息自己计算一个版本号。在序列化一个对象时,如果没有指定序列化版本号,后期对该类的源码进行修改并重新编译后,可能会导致修改前后的序列化版本号不一致,因为 JVM 会提供一个新的序列化版本号给该类对象,此时再用以往的反序列化代码去反序列化该类的对象,会导致反序列化使用的class的版本号与序列化时使用的不一致,就会抛出异常 java.io.InvalidClassException。
在这里插入图片描述
3) 开发人员手动提供序列化版本号。java 序列化提供了一个 “private static final long serialVersionUID” 的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。所以序列化一个类时最好指定一个序列化版本号,方便项目升级。

class Person implements Serializable {
	// 指定序列化版本号为 1
    private static final Long serialVersionUID = 1L;
}
  • 1
  • 2
  • 3
  • 4
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/492852
推荐阅读
相关标签
  

闽ICP备14008679号