java对象序列化的总结

来源:互联网 发布:mac地址是什么意思 编辑:程序博客网 时间:2024/06/18 04:38

网上关于java对象序列化的博客文档很多,本文是我个人最近学习java对象序列化,查阅资料的一些总结,参考了许多博主的文章,后文有附,希望对大家有所帮助,如发现错误,或有任何不同见解,恳请指出!

一、什么是Java对象序列化

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

使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意的是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。

简而言之:
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

二、如何实现序列化

1. Serializable接口

Serializable接口是一个空接口,它的主要作用就是标识该类的对象是可序列化的。在ObjectOutputStream的writeObject0(Object obj, boolean unshared)方法中可以看到对被写对象类型的判断。若对象不可序列化,将抛出NotSerializableException。

因此,在java中,要想让一个类能够序列化,只需让该类实现Serializable接口即可。下面做一个简单的测试。

首先创建一个枚举类Gender,表示性别。
public enum Gender {    Male,Female}
下面创建要序列化的Person类。包含三个字段name,age,gender。
public class Person implements Serializable{    private static final long serialVersionUID = 1L;    public static int id;    private String name;    private int age;    private Gender gender;    public Person() {        super();        System.out.println("none-arg constructor");    }    public Person(String name, int age, Gender gender, int id) {        super();        this.name = name;        this.age = age;        this.gender = gender;        Person.id = id;        System.out.println("args constructor");    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public Gender getGender() {        return gender;    }    public void setGender(Gender gender) {        this.gender = gender;    }    @Override    public String toString() {        return "Person [name=" + name + ", age=" + age + ", Gender=" + gender + ", id=" + id + "]";    }}
最后测试对象的序列化与反序列化
public class Test{    @Test    public void writeReadObject() throws IOException, ClassNotFoundException{        File file = new File("ObjectSerial.out");        Person p = new Person("张三", 20, Gender.Male, 2017);        System.out.println("序列化的对象:" + p);        //序列化将对象写入文件        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));        out.writeObject(p);        out.close();        //反序列化从文件中读对象        ObjectInput in = new ObjectInputStream(new FileInputStream(file));        Person obj = (Person) in.readObject();        in.close();        System.out.println("反序列化的对象:" + obj);        System.out.println(obj == p);    }}
程序运行结果为:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017]反序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017]false

由结果可知,通过序列化成功将对象写入文件,并反序列化从文件中读出了该对象。同时思考问题:
①基本类型、String类默认可以序列化的。枚举类默认继承类java.lang.Enum,而该类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。因此,Person类的三个成员变量都是可以序列化,那么如果Person类含有其他引用类型的实例变量呢?————>分析见第二小节。
② obj == p 的值为 false 可知,反序列化出的对象与原对象并不是同一个对象(本地不同,实际应用涉及传输时肯定也不同),但是反序列化创建对象时却没有调用任何构造方法,那么是如何创建的?————>
③序列化不关注类变量,那么static的 id 为什么反序列化时获取到了呢?————>原因:该测试是在本地进行的(而且是同一线程),所以获取的是jvm加载好的类信息,如果是传到另一台机器或者关掉程序重新读入序列化的对象,id就是初始时的信息。

2. 引用类型实例变量的序列化

2.1 自定义类型实例变量

首先创建一个Car类,有两个成员变量brand,price。
public class Car {    private String brand;    private int price;    public Car() {        super();    }    public Car(String brand, int price) {        super();        this.brand = brand;        this.price = price;    }    public String getBrand() {        return brand;    }    public void setBrand(String brand) {        this.brand = brand;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    @Override    public String toString() {        return "Car [brand=" + brand + ", price=" + price + "]";    }}
然后在Person类中加入一个Car类型的成员变量,同时修改构造器和toString。
public class Person implements Serializable{    ...    private Car car;    ...}
测试序列化
public class Test{    @Test    public void writeReadObject() throws IOException, ClassNotFoundException{        File file = new File("ObjectSerial.out");        Person p = new Person("张三", 20, Gender.Male, 2017, new Car("BMW", 300000));        System.out.println("序列化的对象:" + p);        //序列化将对象写入文件        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));        out.writeObject(p);        out.close();        //反序列化从文件中读对象        ObjectInput in = new ObjectInputStream(new FileInputStream(file));        Person obj = (Person) in.readObject();        in.close();        System.out.println("反序列化的对象:" + obj);    }}
执行结果:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male, id=2017, car=Car [brand=BMW, price=300000]]java.io.NotSerializableException: javalearn.Serialization.Car

可见,在创建Person对象后,程序进行序列化时,抛出了NotSerializableException,序列化失败,说明引用类型的实例变量必须也实现了Serializable接口,否则无法实例化。

2.2 容器类实例变量

仍使用上2.1小节中的Car类,同时修改Person类,增加成员变量List<Car> cars,同时修改构造器和toString。
public class Person implements Serializable{    ...    private List<Car> cars;    ...}
测试序列化
public class Test{    @Test    public void writeReadObject() throws IOException, ClassNotFoundException{        File file = new File("ObjectSerial.out");        List<Car> cars = new ArrayList<>();        cars.add(new Car("BWM", 200000));        cars.add(new Car("Audi", 300000));        Person p = new Person("张三", 20, Gender.Male, 2017, cars);        System.out.println("序列化的对象:" + p);        //序列化将对象写入文件        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));        out.writeObject(p);        out.close();        //反序列化从文件中读对象        ObjectInput in = new ObjectInputStream(new FileInputStream(file));        Person obj = (Person) in.readObject();        in.close();        System.out.println("反序列化的对象:" + obj);    }}
执行结果为:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male, 2017, cars=[Car [brand=BWM, price=200000], Car [brand=Audi, price=300000]]]java.io.NotSerializableException: javalearn.Serialization.Car

与2.1一样,出现了异常。原因是List容器变量中的实例未实现序列化。

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

那么在实际应用中,怎么样来改变默认的序列化机制,在过程中忽略掉敏感数据,或者简化序列化过程呢?java提供几种解决方法,详细见下节。

3. 影响默认序列机制的几种方法

3.1 transient关键字

transient关键字是变量修饰符,如果用transient声明一个实例变量,当对象进行存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。当某个字段被声明为transient后,默认序列化机制就会忽略该字段。

将Person类的age属性声明为transientpublic class Person implements Serializable{    ...    private transient int age;    ...}
再次执行Test.writeReadObject(),输出为:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male]反序列化的对象:Person [name=张三, age=0, Gender=Male]

可知,声明为transient的age属性未被序列化。那么,如果想让一个声明为transient的属性可以被序列化,除了删除transient关键,还有其他方法吗?答案是有的,见3.2节。

3.2 writeObject( )方法与writeObject( )方法

在需要序列化的类中定义writeObject( )和writeObject( )方法。

public class Person implements Serializable{    ...    private transient int age;    ...    private void writeObject(ObjectOutputStream out) throws IOException{        out.defaultWriteObject();        out.writeInt(age);    }    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{        in.defaultReadObject();        age = in.readInt();    }    ...}

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

再次执行Test.writeReadObject(),输出为:    args constructor    序列化的对象:Person [name=张三, age=20, Gender=Male]    反序列化的对象:Person [name=张三, age=20, Gender=Male]可见,age属性又能够序列化了。

补充说明:
①方法writeObject处理对象的序列化。如果声明该方法,它将会被ObjectOutputStream调用而不是默认的序列化进程。尽管它们被外部类调用但事实上这是两个private的方法。并且它们既不存在于java.lang.Object,也没有在Serializable中声明。那么ObjectOutputStream如何使用它们的呢?ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法必须被声明为private。

②在两个方法的开始处,都调用了defaultWriteObject()和defaultReadObject()。它们做的是默认的序列化进程,来写/读所有的non-transient和 non-static字段(但他们不会去做serialVersionUID的检查),通常来说,所有我们想要自己处理的字段都应该声明为transient。这样的话,默认的序列化机制defaultWriteObject和defaultReadObject便可以专注于其余字段,而我们则可为这些特定的tranient字段进行定制序列化。使用那两个默认的方法并不是强制的,而是给予了处理复杂应用时更多的灵活性。

③write的顺序和read的顺序需要对应,譬如有多个字段都用wirteInt写入流中,那么readInt需要按照顺序将其赋值。

3.3 Externalizable接口

无论是使用transient关键字,还是使用writeObject()readObject()方法,其实质都是基于Serializable接口的序列化。JDK中还提供了另一个序列化接口——Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。

Externalizable接口继承于Serializable,在Externalizable中声明了两个方法readExternal()writeExternal(),子类必须实现二者。

public class Person implements Extenalizable{    private String name;    private transient int age;    private Gender gender;    public Person() {        super();        System.out.println("none-arg constructor");    }    public Person(String name, int age, Gender gender) {        super();        this.name = name;        this.age = age;        this.gender = gender;        System.out.println("args constructor");    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public Gender getGender() {        return gender;    }    public void setGender(Gender gender) {        this.gender = gender;    }    //保留Serializable的默认序列化机制    private void writeObject(ObjectOutputStream out) throws IOException{        out.defaultWriteObject();    }    //保留Serializable的默认序列化机制    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{        in.defaultReadObject();    }    //重写writeExternal    @Override    public void writeExternal(ObjectOutput out) throws IOException {    }    //重写readExternal    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {    }    @Override    public String toString() {        return "Person [name=" + name + ", age=" + age + ", Gender=" + gender + "]";    }}
执行Test.writeReadObject(),输出为:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male]none-arg constructor反序列化的对象:Person [name=null, age=0, Gender=null]

从该结果,一方面可以看出Person对象中任何一个字段都没有被序列化,说明Serializable的默认序列化机制失效了。另一方面,还可以发现这次序列化过程调用了Person类的无参构造器。
Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。因此,输出结果中所有字段的值均为空。
另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,在此次序列化过程中Person类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public,否则程序会抛出异常:java.io.InvalidClassException : no valid constructor。

对上述Person类作进一步的修改,使其能够对name,age和gender字段进行序列化,如下代码所示:

public class Person implements Extenalizable{    ...    public Person() {        super();        System.out.println("none-arg constructor");    }    ...    //重写writeExternal    @Override    public void writeExternal(ObjectOutput out) throws IOException {        out.writeObject(name);        out.writeInt(age);        out.writeObject(gender);    }    //重写readExternal    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        name = (String) in.readObject();        age = in.readInt();        gender = (Gender) in.readObject();    }    ...}
再次执行Test.writeReadObject(),输出为:args constructor序列化的对象:Person [name=张三, age=20, Gender=Male]none-arg constructor反序列化的对象:Person [name=张三, age=20, Gender=Male]

补充说明:
①Serializable是内建支持的也就是直接implement即可,但Externalizable的实现类必须提供readExternal()writeExternal()实现。对于Serializable来说,Java自己建立对象图和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要程序员自己控制如何写/读,麻烦但可以有效控制序列化的存储的内容。

三、子类、父类序列化问题

1. 父类实现了序列化接口

由于父类实现的接口,子类不用显式声明,自动实现该接口。因此,父类可以序列化,子类自然也可以序列化。

//父类public class Animal  implements Serializable{    public String name;    public int age;    public Animal() {        super();        System.out.println("none-arg Animal constructor");    }    public Animal(String name, int age) {        super();        this.name = name;        this.age = age;        System.out.println("arg Animal constructor");    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    @Override    public String toString() {        return "Animal [name=" + name + ", age=" + age + "]";    }}
//子类public class Dog extends Animal{    private static final long serialVersionUID = -5498857812409621833L;    private String host;    public Dog() {        super();        System.out.println("none-arg Dog constructor");    }    public Dog(String name, int age, String host) {        super(name, age);        this.host = host;        System.out.println("arg Dog constructor");    }    public String getHost() {        return host;    }    public void setHost(String host) {        this.host = host;    }    @Override    public String toString() {        return "Dog [host=" + host + ", name=" + name + ", age=" + age + "]";    }}
//测试序列化public Test{@Test    public void writeReadObject() throws IOException, ClassNotFoundException{        File file = new File("ObjectSerial.out");        Dog d = new Dog("black", 3, "张三");        System.out.println("序列化对象为:" + d);        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(file));        out.writeObject(d);        out.close();        ObjectInput in = new ObjectInputStream(new FileInputStream(file));        Dog obj = (Dog) in.readObject();        in.close();        System.out.println("反序列化对象为:" + obj);    }}
输出结果为:arg Animal constructorarg Dog constructor序列化对象为:Dog [host=wang, name=black, age=3]反序列化对象为:Dog [host=wang, name=black, age=3]

可见子类可以序列化。

1. 父类未实现序列化接口,子类实现

测试代码同上,改为让子类实现Serializable。

执行Test.writeReadObject(),输出结果为:arg Animal constructorarg Dog constructor序列化对象为:Dog [host=wang, name=black, age=3]none-arg Animal constructor反序列化对象为:Dog [host=wang, name=null, age=0]

由输出可知,反序列化时调用了父类无参的构造函数,且从父类继承的属性无法序列化。

总结:
①父类实现序列化时,子类自然可以序列化。
②只有子类实现序列化时,子类对象可以进行序列化,但是会丢失从父类继承而来的属性,同时要求父类必须有一个无参的构造器(反序列化时需要,若父类没有无参构造器,可写不可读,有兴趣可以自己测试)。

四、serialVersionUID的作用

1. serialVersionUID是什么?

s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D​:​ ​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D可以显式的在类中声明,若不显式声明,则由JVM根据类的内容自动生成一个serialVersionUID,类中的信息的变化会导致serialVersionUID的变化。
(测试发现类信息发生变化时,如增加删除修改成员变量、方法时,会导致serialVersionUID变化,无法识别原先序列化的对象。但是对方法内部的修改,并不会导致serialVersionUID变化,有兴趣可以自己测试)

因此,若一个类没有显式地指定serialVersionUID,而且类信息发生了变化,则读取类变化前存入磁盘中的对象时就会报错,抛出InvalidClassException。

2. 何时应该修改serialVersionUID?

若类信息已经修改较多或者修改成不兼容的模式,导致原来输出到磁盘的内容不应再转换至原对象,此时则应该修改serialVersionUID。

3. 如何创建serialVersionUID?

serialVersionUID是为了在序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。

serialVersionUID可以自己指定,也可以让IDE自动生成。在Eclipse中有两种生成方式:
一个是默认的1L,如:private static final long serialVersionUID = 1L;
一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,如:private static final long serialVersionUID = -5498857812409621833L。

两种方式并没有什么实质区别,对于第一种,需要了解哪些情况是可兼容的,哪些根本就不兼容。 参考文档:http://Java.sun.com/j2se/1.4/pdf/serial-spec.pdf

在可兼容的前提下,可以保留旧版本号,如果不兼容,或者想让它不兼容,就手工递增版本号。1L–>2L–>3L……

第二种方式,是根据类的结构产生的hash值。增减一个属性、方法等,都可能导致这个值产生变化。若不想继续向下兼容,只需删除原有serialVesionUid声明语句,再自动生成一下。

( 五、readResolve()方法)

参考文档:
http://blog.csdn.net/jediael_lu/article/details/26813153关于serialVesionUid的说明。
http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html理解java对象序列化。
http://blog.csdn.net/simon_steve_sun/article/details/8439254
http://blog.csdn.net/u012554102/article/details/51902697
http://www.360doc.cn/article/573136_72970335.html

0 0
原创粉丝点击