Java-序列化

序列化的含义和意义

序列化指将Java对象转换成字节序列, 这些字节序列可以保存在磁盘上, 或者进行网络传输。反序列化即指将序列化后的字节序列重新恢复成对象。序列化机制使得对象可以脱离程序的运行而独立存在。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,这个接口不包含任何方法或成员变量,只是一个标记。

序列化/反序列化

序列化通过ObjectOutputStream来实现,它可以把一个Java对象写入字节流。

当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名.

反序列化使用ObjectInputStream对象,它可以从字节流中读取一个Java对象。若反序列化时要使用readObject()读出多个对象,注意要与写入的顺序一致。

示例:

public class SerializableDemo {
    public static void main(String[] args) throws Exception {
        writeObj();
        readObj();
    }

    private static void writeObj() throws Exception {
        File file = new File("E:\\person.ser");
        FileOutputStream out = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);

        Person person = new Person("王大锤", 25);
        objectOutputStream.writeObject(person);
        System.out.println("写入的对象:" + person);
        objectOutputStream.close();
    }

    private static void readObj() throws Exception{
        File file = new File("E:\\person.ser");
        FileInputStream in = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(in);

        Person person = (Person)objectInputStream.readObject();
        System.out.println("读取的对象:" + person);
        objectInputStream.close();
    }

}

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

    public Person(String name, int age) {
        System.out.println("Person's constructor");
        this.name = name;
        this.age = age;
    }

    //...省略getter、setter、toString
}

程序输出:

Person's constructor
写入的对象:Person{name='王大锤', age=25}
读取的对象:Person{name='王大锤', age=25}

可以看到反序列化时,Person的构造器并没有执行。反序列化时,是由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时将不被执行

看另一个示例:

public static void main(String[] args) throws Exception {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOut);

        Person person = new Person("张三", 20);
        objectOutputStream.writeObject(person);

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
        Person readPerson = (Person)objectInputStream.readObject();

        System.out.println("反序列化得到的Person对象:" + readPerson);
        System.out.println("反序列化得到的对象与原对象是否是同一对象:" + (person==readPerson));

}

输出:

反序列化得到的Person对象:Person{name='张三', age=20}
反序列化得到的对象与原对象是否是同一对象:false

可见反序列化得到的对象与原来的person不是同一个对象,即反序列化得到的是一个新的对象

serialVersionUID

每一个可序列化类都会带有一个long类型的serialVersionUID静态常量值如果没有人为显式定义过serialVersionUID,那编译器会根据类的各种信息为它自动声明一个

serialVersionUID是序列化前后类的唯一标识符,在反序列化时,jvm会把字节流中的序列号id和被序列化类的序列号id进行比较,只有两个id相同才能成功反序列化,否则抛出InvalidClassException异常

若是没有人为指定序列号id,则若是改变类时(如添加新的成员变量),默认的serialVersionUID值将会改变

示例:

执行上述例子的writeObj()方法后,在Person类定义中添加一个birth成员变量:private Date birth;

再执行readObj()进行反序列化,此时抛出异常:即字节流中的serialVersionUID与Person类的不同

Exception in thread "main" java.io.InvalidClassException: io.stream.serializable.Person; local class incompatible: stream classdesc serialVersionUID = 5578959987866231607, local class serialVersionUID = -8466270221378179612

一般对于可序列化类,为了serialVersionUID的确定性,都要人为指定一个serialVersionUID值,可以使用idea的自动生成功能:IDEA自动生成serialVersionUID

反序列化时的异常

在反序列化时readObject()可能抛出的异常有

  • ClassNotFoundException:没有找到对应的Class;
  • InvalidClassException:Class不匹配。

抛出ClassNotFoundException的情况:一台电脑上的Java程序把一个Java对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义该类,所以无法反序列化,抛出该异常。

抛出InvalidClassException的情况:如serialVersionUID中的示例情况。

序列化的特殊情况

  • 当要序列化的对象中包含其他引用类型变量的引用时,该变量对应的类也要实现Serializable接口
  • 对同一个对象多次序列化,只有第一次序列化时JVM才会将对象转换为字节序列,之后的序列化只是直接输出一个序列化编号。且反序列化时多次使用readObject()读出的是同一个对象。

示例:

示例中的Person类同上。新增了一个ID类:

package stream.serializable;

import java.io.Serializable;

public class ID implements Serializable {
    private static final long serialVersionUID = -5829455553074732547L;
    private int number;
    private Person person;

    //...省略getter、setter、toString
}

创建一个Person对象person和一个ID对象id,id中的Person指向person。将person和id序列化。再反序列,可看到readId.getPerson()==readPerson返回true。

public static void main(String[] args) throws Exception {
    File file = new File("E:\\id.ser");
        FileOutputStream out = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);

        ID id = new ID();
        Person person = new Person("王大锤", 30);
        id.setNumber(1);
        id.setPerson(person);
        objectOutputStream.writeObject(id);
        objectOutputStream.writeObject(person);

        FileInputStream in = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(in);
        ID readId = (ID) objectInputStream.readObject();
        Person readPerson = (Person) objectInputStream.readObject();

        System.out.println(readId);
        System.out.println(readPerson);
        System.out.print("readId.getPerson()==readPerson:");
        System.out.print(readId.getPerson()==readPerson);
}
  • 凡是被static修饰的字段是不会被序列化的
    • 因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域。
  • 凡是被transient修饰符修饰的字段也是不会被序列化的
    • transient修饰符的作用就是修饰不想被序列号的成员变量,一般是对于一些要保密的字段,如密码。对于被transient修饰的字段,在序列号时以null值填充(对于值类型,则使用0、false等默认值)。

还有一个点是序列化的受控和加强,具体见第3个参考链接中最后一个小节。

参考

廖雪峰Java教程-序列化

菜鸟教程

序列化/反序列化,我忍你很久了,淦! (程序羊)

《疯狂Java讲义 第4版》

---------------------------------------------------------

posted @ 2020-10-27 23:39  bxxiao  阅读(121)  评论(0)    收藏  举报