I/O篇(4)——对象序列化

来源:互联网 发布:js在线解压缩工具下载 编辑:程序博客网 时间:2024/06/07 22:51

一.什么是对象序列化
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象(也就是持久化: 持久化指的是一个对象的生命周期并不取决于程序是否正在执行.),并在将来能重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
Java的对象序列化是将那些实现了Serializable接口的对象转换成一个字序列,并能够在以后将这个字节序列完全恢复为原来的对象。必须注意的是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,默认的对象序列化不会关注类中的静态变量。(但是如果想序列化静态变量也可以实现,下面会提到)
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。


二.序列化的前提
如果需要让某个对象支持序列化机制, 则必须让他的类是可序列化的(serializable). 该类必须实现如下接口之一:

  • Serializable:标记接口,无须实现任何方法,只是表明该类是可序列化的。
  • Externalizable:Serializable的子接口, 使用该接口之后,之前基于Serializable接口的序列化机制就将失效。当使用该接口时,序列化的细节需要由程序员去完成。

所有在网络上传输的对象都应该是可序列化的,否则将会出现异常;所有需要保存到磁盘里的对象的类都必须可序列化
通常建议:程序创建的每个JavaBean类都实现Serializable


三.序列化的实现
一般用ObjectOutputStream/ObjectIputStream来实现对象的序列化和反序列化

//要序列化和反序列化的对象类public class Person implements Serializable{    private String name;    private int age;    public Person(String name , int age){        super();        this.name = name;        this.age = age;    }    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 static void main(String[] args) {        Person per = new Person("Mr.Z", 18);        String destPath = "D:/wotest.out";        File dest = new File(destPath);        ObjectOutputStream oos = null;        try {            oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));            oos.writeObject(per);            oos.close();        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }}
//对象反序列化public static void main(String[] args) {        String srcPath = "D:/wotest.out";        File src = new File(srcPath);        ObjectInputStream ois = null;        try {            ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(src)));            //这里返回的是Object类型的对象,要强制转型成其真实类型(这里强转为Person对象)            Person per = (Person) ois.readObject();            ois.close();            System.out.println("name:"+per.getName()+","+"age:"+per.getAge());        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (ClassNotFoundException e) {            e.printStackTrace();        }}

四.序列化应注意的问题
1.对象的类名, 实例变量(基本类型/数组/引用对象)都会被序列化; 而方法,类变量(即static静态变量),transient实例变量都不会被序列化.

2.反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象所属的class文件,否则会引发ClassNotFoundException异常;

3.如果我们向文件中使用序列化机制写入了多个Java对象,使用反序列化机制恢复对象必须按照实际写入的顺序读取。

4.在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器,整个对象都是通过从InputStream中取得数据回复而来的.

5.当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参的构造器,要么也是可序列化的,否则反序列化将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造器,则该父类定义的Field值不会被序列化到二进制流中
详解:
要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

6.如果某个类的成员变量不是基本类型或String类型,而是另一个引用类型, 那么这个引用类必须是可序列化的, 否则拥有该类型成员变量的类也是不可序列化的.

7.所有保存到磁盘(或传输到网络中)的对象都有一个序列化编号,虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。即使两个类的功能代码完全一致,但是序列化 ID 不同,他们也 无法相互序列化和反序列化。

8.当程序试图序列化一个对象时, 程序将先检查该对象是否已经被序列化过, 只有该对象(在本次虚拟机的上下文Context中)从未被序列化过, 系统才会将该对象转换成字节序列并输出,如果某个对象已经序列化过(即使该对象的实例变量后来发生了改变), 程序将不再重新序列化该对象

9.对象序列化不仅保存了对象的全景图,而且还能追踪对象内所包含的所有引用并保存哪些对象.

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


五.序列化版本
Java序列化机制允许为序列化的类提供一个private static final long serialVersionUID = xxxL;值, 该值用于标识该Java类的序列化版本; 一个类升级之后, 只要他的serialVersionUID值不变, 序列化机制也会把它们当成同一个序列化版本(由于提供了serialVersionUID之后JVM不需要再次计算该值,因此还有个小小的性能好处).
如果不显式定义serialVersionUID的值,可能会造成以下问题:

  • 该值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类的版本不兼容而失败.
  • 不利于程序在不同JVM之间移植, 因为不同的编译器对该变量的计算策略可能不同, 而从造成类虽然没有改变, 但因为JVM的不同, 也会出现序列化版本不兼容而导致无法正确反序列化的现象.

六.序列化过程的控制(自定义序列化)
在一些的场景下, 如果一个类里面包含的某些变量不希望对其序列化(或某个实例变量是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException异常); 或者将来这个类的实现可能改动(最大程度的保持版本兼容), 或者我们需要自定义序列化规则, 在这种情景下我们可选择实用自定义序列化.

1.Externalizable接口
可以通过实现Externalizable接口来对序列化过程进行控制,如果实现该接口,那么序列化操作的细节都需要自己实现.这个Externalizable接口继承了Serializable接口,同时增添了两个方法:wirteExternal(ObjectOutput out)和readExternal(ObjectInput in).这两个方法会在序列化和反序列化的过程中被自动调用,以便执行一些特殊操作
要注意的是,对于Serializable对象来说,对象完全以它存储的二进制数据为基础来构造而不调用构造器.但对于一个Externalizable对象,会调用其无参构造器创建一个新的对象,然后调用readExternal(ObjectInput in)方法,根据程序将被保存对象的字段的值分别填充到新对象中.由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。

public class Blip implements Externalizable {    private int i;    private String s;    public Blip(){        System.out.println("Blip Constructor");    }    public Blip(int i,String s){        System.out.println("Blip(int i,String s)");        this.i = i;        this.s = s;    }    public String toString(){        return s+i;    }    public void writeExternal(ObjectOutput out) throws IOException {        System.out.println("Blip.writeExternal");        //You must do this        out.writeObject(s);        out.writeInt(i);    }    public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {        System.out.println("Bilp.readExternal");        //You must do this        s = (String) in.readObject();        i = in.readInt();    }    public static void main(String[] args) {        System.out.println("Constructing objects:");        Blip blip = new Blip(47,"A String");        System.out.println(blip);        try {            System.out.println("Saving object:");            ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("D:/blip.out")));            oos.writeObject(blip);            oos.close();            System.out.println("Recovering blip:");            ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream("D:/blip.out")));            try {                blip = (Blip) ois.readObject();            } catch (ClassNotFoundException e) {                e.printStackTrace();            }            System.out.println(blip);        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }    }}

输出结果:
Constructing objects:
Blip(int i,String s)
A String47
Saving object:
Blip.writeExternal
Recovering blip:
Blip Constructor
Bilp.readExternal
A String47
结果分析:
其中,属性s和i只在有参构造器中初始化,而不是在默认的构造其中初始化.这意味着如果不再readExternal()中初始化s和i,那么s就会为null,而i就会为0(因为创建对象的第一步中将对象的存储空间清理为0).如果注释掉”You must do this”后面的两行代码,然后运行程序,就会发现当对象被还原后,s是null,i是0
我们如果从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和回复功能.

2.Serializable接口
如果只是实现默认的序列化,那么只要实现该标记接口就行了,如果需要对序列化过程进行控制除了Externalizable接口外,Serializable接口也可以完成,有两种方法:
(1)transient(瞬时)关键字(transient关键字只能和Serializable对象一起使用,且只能修饰字段不可修饰其他成员,因为Externalizable对象在默认情况下不保存它们自己的任何字段)
当我们对序列化进行控制时,可能某个特定子对象不想让java的序列化机制自动保存与恢复.如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),通常就会面临这样的情况.即使对象中的这些信息是private属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它.
所以当我们正在操作一个Serializable对象时,为了能够予以控制,可以用transient关键字逐个属性地关闭序列化.
例如:某个Login对象保存某个特定的登录信息.登录的合法性通过校验之后,我们想把数据保存下来,但不包括密码.为了做到这一点,最简单的办法是实现Serializable,并将password属性标记为transient.
代码如下:

public class Login implements Serializable {    private Date date = new Date();    private String username;    private transient String password;    public Login(String username,String password){        this.username = username;        this.password = password;    }    public String toString(){        return "Login info:\n   username:"+username+"\n date:"+date+"\n password:"+password;    }    public static void main(String[] args){        Login lg = new Login("Mr.Z","HelloWorld");        System.out.println("login lg ="+lg);        try {            ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("D:/login.out")));            oos.writeObject(lg);            oos.close();            TimeUnit.SECONDS.sleep(1);            ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream("D:/login.out")));            System.out.println("Recovering object at "+new Date());            lg = (Login) ois.readObject();            System.out.println("login lg ="+lg);        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        } catch (InterruptedException e) {            e.printStackTrace();        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }}

输出结果:
login lg =Login info:
username:Mr.Z
date:Fri Jun 03 13:10:13 CST 2016
password:HelloWorld
Recovering object at Fri Jun 03 13:10:14 CST 2016
login lg =Login info:
username:Mr.Z
date:Fri Jun 03 13:10:13 CST 2016
password:null
结果分析:
可以看到,其中的date和username属性是普通的(不是transient的),所以它会被自动序列化.而password是transient的,所以不会被自动保存到磁盘,而且,自动序列化机制也不会尝试去恢复它.当对象被恢复时,password属性就会变成null.

(2)为Serializable对象添加writeObject(OutputStream stream)/readObject(InputStream stream)方法
(可以添加的方法还有:readObjectNoData(),writeReplace(),readResolve())
我们还可以实现Serializable接口,并添加(注意是添加,不是覆盖或实现,因为这两个方法不是Serializable接口中的方法)writeObject(OutputStream stream)/readObject(InputStream stream)方法来进行序列化过程的控制.这样一旦对象被序列化或者被反序列化时,就会自动地调用这两个方法.也就是说,只要我们添加了这两个方法,就会使用它们而不是默认的序列化机制.
添加的这两个方法有准确的方法特征签名(添加的时候不能更改):

  • private void writeObject(ObjectOutputStream stream) throws IOException;
  • private void readObject(ObjectInputStream stream) throws IOException;

实际上这两个方法是由ObjectOutputStream /ObjectInputStream 对象的writeObject()/readObject()方法自动调用的.
在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象看看是否实现了它自己的writeObject()方法,如果实现了,就跳过正常的序列化过程并调用它的writeObject()方法,如果没有实现就进行默认的序列化.readObject()的情形与此相同
另外在writeObjet()内部,我们可以调用ObjectOutputStream .defaultWriteObject()来执行默认的writeObject().类似的,在readObject内部,我们可以但调用ObjectInputStream .defaultReadObject().而且建议readObject()/writeObject()的方法内首先调用defaultReadObject()/defaultWriteObject();

public class SerialCtl implements Serializable {    private String a;    private transient String b;    public SerialCtl(String a,String b){        this.a = "Not Transient"+a;        this.b = "Transient"+b;    }    public String toString(){        return a+"\n"+b;    }    private void writeObject(ObjectOutputStream os) throws IOException{        os.defaultWriteObject();        os.writeObject(b);    }    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException{        is.defaultReadObject();        b = (String) is.readObject();    }    public static void main(String[] args) throws IOException, ClassNotFoundException {        SerialCtl sc = new SerialCtl("Test1", "Test2");        System.out.println("Before:\n"+sc);        ByteArrayOutputStream buf = new ByteArrayOutputStream();        ObjectOutputStream oos = new ObjectOutputStream(buf);        oos.writeObject(sc);        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));        SerialCtl sc2 = (SerialCtl) ois.readObject();        System.out.println("After:\n"+sc2);    }}

输出结果:
Before:
Not TransientTest1
TransientTest2
After:
Not TransientTest1
TransientTest2
结果分析:
这个例子中有一个String属性是普通属性,而另一个String属性是transient属性,用来证明非transient属性由defaultWriteObject()方法保存,而transient属性必须在程序中明确保存和恢复.属性是在构造器内部而不是定义处进行初始化的,以此可以证明它们在反序列化还原期间没有被一些自动化机制初始化.
如果我们打算使用默认机制写入对象的废transient部分,那么必须调用defaultWriteObject()方法作为writeObject()中的第一个操作,并让defaultReadObject()方法作为readObject()中的第一个操作.
在main()中,创建SerialCtl对象,然后将其序列化到ObjectOutputStream中.序列化发生在这行代码中:oos.writeObject(),writeObject()方法必须检查sc,判断它是否拥有自己的writeObject()方法(不是检查接口——这里根本没有接口,也不是检查类型,而是利用反射来真正的搜索方法).如果有,那么就会使用它.对readObject()也采用了类似的方法.


七.readResolve()方法
当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能会略有不同。
无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象,而被创建的对象则会被垃圾回收掉。

public class Person implements Serializable {    private static class InstanceHolder {        private static final Person instatnce = new Person("John", 31, Gender.MALE);    }    public static Person getInstance() {        return InstanceHolder.instatnce;    }    private String name = null;    private Integer age = null;    private Gender gender = null;    private Person() {        System.out.println("none-arg constructor");    }    private Person(String name, Integer age, Gender gender) {        System.out.println("arg constructor");        this.name = name;        this.age = age;        this.gender = gender;    }}
public class SimpleSerial {    public static void main(String[] args) throws Exception {        File file = new File("person.out");        ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));        oout.writeObject(Person.getInstance()); // 保存单例对象        oout.close();        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));        Object newPerson = oin.readObject();        oin.close();        System.out.println(newPerson);        System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较    }}

输出结果:
arg constructor
[John, 31, MALE]
false
结果分析:
从文件person.out中获取的Person对象与Person类中的单例对象并不相等。为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象.

public class Person implements Serializable {    private static class InstanceHolder {        private static final Person instatnce = new Person("John", 31, Gender.MALE);    }    public static Person getInstance() {        return InstanceHolder.instatnce;    }    private String name = null;    private Integer age = null;    private Gender gender = null;    private Person() {        System.out.println("none-arg constructor");    }    private Person(String name, Integer age, Gender gender) {        System.out.println("arg constructor");        this.name = name;        this.age = age;        this.gender = gender;    }    private Object readResolve() throws ObjectStreamException {        return InstanceHolder.instatnce;    }}

输出结果:
arg constructor
[John, 31, MALE]
true


八.序列化存储规则
1.下面代码将输出什么:

 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));    Test test = new Test();    //试图将对象两次写入文件    out.writeObject(test);    out.flush();    System.out.println(new File("result.obj").length());    out.writeObject(test);    out.close();    System.out.println(new File("result.obj").length());    ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));    //从文件依次读出两个文件    Test t1 = (Test) oin.readObject();    Test t2 = (Test) oin.readObject();    oin.close();    //判断两个引用是否指向同一个对象    System.out.println(t1 == t2);

输出结果:
31
36
true
结果分析:
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得代码中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

2.下面代码将输出什么:

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));Test test = new Test();test.i = 1;out.writeObject(test);out.flush();test.i = 2;out.writeObject(test);out.close();ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));Test t1 = (Test) oin.readObject();Test t2 = (Test) oin.readObject();System.out.println(t1.i);System.out.println(t2.i);

输出结果:
1
1
结果分析:
第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题。

0 0