黑马程序员----Java对象序列化

时间:2023-02-18 15:06:05

 ——- android培训java培训、期待与您交流! ———-

1. 什么是java对象序列化

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。

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

类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。

要允许不可序列化类的子类型序列化,可以假定该子类型负责保存和恢复超类型的公用 (public)、受保护的 (protected) 和(如果可访问)包 (package) 字段的状态。仅在子类型扩展的类有一个可访问的无参数构造方法来初始化该类的状态时,才可以假定子类型有此职责。如果不是这种情况,则声明一个类为可序列化类是错误的。该错误将在运行时检测到。
在反序列化过程中,将使用该类的公用或受保护的无参数构造方法初始化不可序列化类的字段。可序列化的子类必须能够访问无参数构造方法。可序列化子类的字段将从该流中恢复。

2.示例
实例化汽车对象

首先定义一个汽车品牌枚举类,每个枚举类型都会默认继承类java.lang.Enum,而该类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。

public enum CarBrand {

    BENZ,AUDI;

}

定义一个汽车类

public class Car implements Serializable {

    String color;
    CarBrand brand;

    public Car(String color, CarBrand brand) {
        super();
        this.color = color;
        this.brand = brand;
        System.out.println("构造函数");
        }


    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public CarBrand getBrand() {
        return brand;
    }
    public void setBrand(CarBrand brand) {
        this.brand = brand;
    }

    @Override
    public String toString() {
        return "Car [color=" + color + ", brand=" + brand + "]";
    }
}

通过类SaveCar实现汽车的存储与读取;

public class SaveCar {

    public static void main(String[] args) throws  IOException, ClassNotFoundException {
        // TODO Auto-generated method stub

        File file = new File("Car.out");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        Car car = new Car("Red", CarBrand.AUDI);
        oos.writeObject(car);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Object o = ois.readObject();
        System.out.println(o);
    }
}

执行后输出结果为

构造函数 Car [color=Red, brand=AUDI]

此时必须注意的是,当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。

当Person对象被保存到person.out文件中之后,我们可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的CLASSPATH中包含有Person.class(哪怕在读取Person对象时并没有显示地使用Person类,如上例所示),否则会抛出ClassNotFoundException。

此时在定义一个类Vendor(汽车的生产商),并将其定义为汽车的一个属性

public class Vendor{

    private String name;

    public Vendor(String name) {
        super();
        this.name = name;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name ;
    }
}
public class Car implements Serializable {

    String color;
    CarBrand brand;
    Vendor vendor;

    public Car(String color, CarBrand brand, Vendor vendor) {
        super();
        this.color = color;
        this.brand = brand;
        this.vendor = vendor;
    }

    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public CarBrand getBrand() {
        return brand;
    }
    public void setBrand(CarBrand brand) {
        this.brand = brand;
    }
    public Vendor getVendor() {
        return vendor;
    }
    public void setVendor(Vendor vendor) {
        this.vendor = vendor;
    }

    @Override
    public String toString() {
        return "Car [color=" + color + ", brand=" + brand + ", vendor="
                + vendor + "]";
    }   
}

再次运行SaveCar

public class SaveCar {

    public static void main(String[] args) throws  IOException, ClassNotFoundException {
        // TODO Auto-generated method stub

        File file = new File("Car.out");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));

        Car car = new Car("Red", CarBrand.AUDI,new Vendor("广州一汽"));
        oos.writeObject(car);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Object o = ois.readObject();
        System.out.println(o);
    }

}

系统报错

Exception in thread "main" java.io.NotSerializableException:Vendor
    at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source)
    at java.io.ObjectOutputStream.writeSerialData(Unknown Source)
    at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)
    at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    at java.io.ObjectOutputStream.writeObject(Unknown Source)
    at SaveCar.main(SaveCar.java:21)

因为Vendor为不可实例话对象,在实例话Car时,Car引用的Vendor,因此会报错,当将Vendor实现Serializable接口时,就能正常实例化Car。
如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。

3.默认序列化机制
如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

4.transient关键字
当某个字段被声明为transient后,默认序列化机制就会忽略该字段。此处将Person类中的age字段声明为transient,如下所示,

如果在Vendor没有实现Serializable ,而又想实例化Car,可以在定义Vendor字段是用transient关键字修饰,这样在实例化Car时就可以不实例化Vendor字段,此时输出结果为:

Car [color=Red, brand=AUDI, vendor=null]

5.writeObject()方法与readObject()方法

对于上述已被声明为transitive的字段Vendor,除了将transitive关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在Car类中添加两个方法(是不是有点“作”啊):writeObject()与readObject(),【此时仍然要求Vendor是可序列化的】如下所示:

private void writeObject(ObjectOutputStream out) throws IOException {  
        out.defaultWriteObject();  
        out.writeObject(vendor);  
    }  

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        vendor = (Vendor)in.readObject();  
    }  

在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉Vendor字段。然后再调用writeObject()方法显示地将Vendor字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。

6.Externalizable接口
JDK中提供了另一个序列化接口–Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。
Externalizable接口提供了两个方法:

@Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub

    }

@Override
public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        // TODO Auto-generated method stub

    }  

按如下方式实现上述方法

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub
        out.writeUTF(color);
        out.writeObject(brand);
        out.writeObject(vendor);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        // TODO Auto-generated method stub
        color = in.readUTF();
        brand = (CarBrand) in.readObject();
        vendor = (Vendor) in.readObject();
    } 

运行SaveCar,会抛出异常

Exception in thread "main" java.io.InvalidClassException: Car; no valid constructor//该类没有可访问的无参数构造方法 

使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的public 修饰的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。
此时仍然要求Vendor类是可序列化的。

当进行反序列化时,必须保证反序列化环境中有被载入的对象的类存在。否则会报错。
单例模式的对象进行序列化与反序列化时,由于构造方法是私有的,所以不能用第二种方式进行反序列化。
当对一个单例类的实例进行序列化与反序列化时,得到的对象是一个全新的对象,因此必须对可序列化的单例类进行优化。

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

因此可以在Car中添加一个方法私有方法readResolve(),直接返回Car中创建的Car对象。