Java基础——序列化

1.为什么序列化?

引自《Java 编程思想》

当创建对象时,在程序运行期间可以获取,但是程序终止时,所有的对象都会被清除,我们是无法再获取的。当然,你可以通过将信息写入文件或者数据库来达到目的。但是为了更方便,Java为我们提供了序列化机制,并且屏蔽了大部分细节。 ——Bruce Eckel

Java提供序列化作用:

  • Java的远程方法调用-RMI。
  • Java Beans的使用。

2.什么是序列化?

使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。:boom:

3. 如何序列化?

2.1 序列化机制一: Serializable 接口:boom:

Serializable接口是最简单的实现序列化的方式,如果类实现了Serializable接口,那么就可以表明该类的对象可被序列化!否则就会报错。常见的默认实现该接口的有所有基本类型的封装类型,String,容器类甚至Class 对象!

//可以看到源码中Serializable接口内部没有任何东西,只是一个标识!
public interface Serializable {
    //nothing!
}

序列化与反序列化方法:调用 ObjectOutputStream 和 ObjectInputStream :facepunch:

package ddx.序列化;

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person(18,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
        out.writeObject("person storage\n");
        out.writeObject(person);
        out.close();
        System.out.print("序列化"+"person storage\n"+ person);

        ObjectInputStream in  = new ObjectInputStream(new FileInputStream("person.out"));
        String title = (String)in.readObject(); //注意反序列化顺序与序列化顺序保持一致
        Person person1 = (Person) in.readObject();
        in.close();
        System.out.print("反序列化"+title+ person1);
    }
}
class Person implements Serializable{
    public int age;
    public String name;
    Person(int age , String s){
        this.age = age;
        this.name = s;
    }

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

运行结果

序列化person storage
Person{age=18, name='ddx'}
反序列化person storage
Person{age=18, name='ddx'}

输出文件

Java基础——序列化

注意点:再强调一下,对象序列化的主体是成员变量,而不包括静态变量:exclamation::exclamation::exclamation: 一个类序列化,另一个类反序列化,分别执行!
ackage ddx.序列化;

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person(18,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out") );
        out.writeObject("person storage\n");
        out.writeObject(person);
        out.close();
        System.out.println("序列化"+"person storage\n"+ person);
    }
}
class Person implements Serializable{
    public int age;
    public String name;
    public static int count = 0;
    Person(int age , String s){
        this.age = age;
        this.name = s;
        count++;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ",count='"+count+'\'' +
                '}';
    }
}
package ddx.序列化;

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
        String title = (String) in.readObject();
        Person person1 = (Person) in.readObject();
        in.close();
        System.out.println("反序列化" + title + person1);
    }
}
序列化person storage
Person{age=18, name='ddx',count='1'}
反序列化person storage
Person{age=18, name='ddx',count='0'}

//通过以上输出可以发现序列化不对静态变量做任何操作,只会保留初始值!!!!

2.2 Serializable 的序列化规则

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。这种情况被称为对象网所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

2.3 Externalizable接口

Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。

两者的区别除了上面需要手动完成还包括下面这点::raising_hand:

  • 使用Serializable接口的对象反序列化恢复对象时,是完全以存储它的二进制位构造的
  • 而使用Externalizable 接口的对象反序列化恢复对象时,必须至少要有一个默认的公共的构造函数!!!!然后调用readExternal方法!
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
}

演示代码:

package ddx.序列化;

import java.io.*;

public class Main1 {
    public static void main(String[] args ) throws IOException, ClassNotFoundException {
        info info1 = new info(10,"ddx");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("info.out"));
        out.writeObject(info1);
        System.out.println("序列化之前\n"+info1);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("info.out"));
        info info2 = (info) in.readObject();
        System.out.println("序列化之后\n"+info2);
    }
}
class info implements Externalizable{
    public int id;
    public String name;
    public info(int id, String name){
        this.id = id;
        this.name = name;
        System.out.println("info正在执行带参构造函数");
    }

    public info(){
        System.out.println("info正在执行无参构造函数");
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(this.id);
        out.writeObject(name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //1
        //in.readInt();
        //in.readObject();
        id = in.readInt();
        name = (String)in.readObject();
    }

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

2.4 transient 关键字

使用transient关键字选择不需要序列化的字段。比如一些敏感的私有的信息,可以选择使用改关键字,虚拟机会忽略该字段!!!:boom:

package ddx.序列化;

import java.io.*;

public class Main2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Message  message = new Message(201792237,533534);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Self_Message.out"));
        out.writeObject(message);
        out.close();
        System.out.println("序列化之前" + message);

        ObjectInputStream  in = new ObjectInputStream(new FileInputStream("Self_Message.out"));
        Message message1 = (Message) in.readObject();
        System.out.println("序列化之后" + message1);
    }
}
class Message implements Serializable {
    private int id;
    private transient int password; //transient 关键字修饰!!!
    Message(int id, int password){
        this.id = id;
        this.password = password;
    }

    @Override
    public String toString() {
        return "message{" +
                "id=" + id +
                ", password=" + password +
                '}';
    }
}
序列化之前message{id=201792237, password=533534}
序列化之后message{id=201792237, password=0}

从输出我们看到,使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。:boom::boom::boom:

2.5 序列化持久性

同一对象序列化多次,会将这个对象序列化多次吗?答案是否定的。

package ddx.序列化;

import java.io.*;

public class Main3 {

    public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person(20,"路飞");
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            //依次将4个对象写入输入流
            oos.writeObject(person);
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(t2);//重复序列化同一个对象!!!!!!

            oos.close();

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("teacher.txt"));
            Person p = (Person) in.readObject();
            Teacher tt1 = (Teacher)in.readObject();
            Teacher tt2 = (Teacher)in.readObject();
            Teacher tt3 = (Teacher)in.readObject();
            System.out.println(p);
            System.out.println(tt1);
            System.out.println(tt2);
            System.out.println(tt3);
        }
    }

}
class Teacher implements Serializable{
    public  String name;
    public Person person;
    Teacher(String name, Person person){
        this.name = name;
        this.person = person;
    }

    @Override
    public String toString() {
        return "Teacher{" +super.toString()+
                "name='" + name + '\'' +
                ", person=" + person +
                '}';
    }
}
正在初始化
ddx.序列化.Person@6f496d9f
Teacher{ddx.序列化.Teacher@723279cf  name='雷利', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48  name='红发香克斯', person=ddx.序列化.Person@6f496d9f}
Teacher{ddx.序列化.Teacher@10f87f48  name='红发香克斯', person=ddx.序列化.Person@6f496d9f}
//可以发现person并没有被多次序列化,而t2也只被序列化了一次!!!地址都相同!

从输出结果可以看出,Java序列化同一对象,并不会将此对象序列化多次得到多个对象。:eyes:

Java序列化算法

  1. 所有保存到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

图示上述序列化过程(顺序不同!原理一致)

4.版本控制

我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?

java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

public class Person implements Serializable {
    //序列化版本号
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    //省略构造方法及get,set
}

如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢?分三种情况:boom::boom::boom:

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

5.总结 :facepunch: :facepunch::facepunch:

  1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

文档写的非常优秀 :+1:

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!