JAVA序列化与反序列化

来源:互联网 发布:java中size是什么意思 编辑:程序博客网 时间:2024/06/07 05:22

我们在创建对象时,只要你需要,对象会一直存在,但是当程序终止时,无论如何它都不会继续存在。但仍然存在某些情况,需要在程序结束后仍保存其信息,比如某个程序中有关用户偏好设置的对象,我们希望在程序结束时这些信息也继续存在,当重新启动程序时,可以直接直接获得该对象来进行初始化。如果在语言层面能将对象声明是“持久性”的,并自动为我们处理实现细节,那将是非常方便的。
Java的对象序列化是将那些实现了“serializable”接口的对象转换成一个字节序列,并能够在以后通过该字节序列完全恢复原来的对象。我们可以将该字节序列输出到流、文件中,并从中恢复对象。这一过程甚至可以在网络上进行。这也意味着序列化机制能弥补不同操作系统之间的差异,也就是说,可以在windows系统的计算机上创建一个对象,并序列化,通过网络发送给一台运行Unix系统的计算机,然后重新组装对象,而不用关心在不同的机器上表示会有所不同,也不用关心字节的顺序或者其他的任何细节。
对象序列化的概念主要为了支持两种特性:一是Java的远程方法调用Remote Method Invocation(RMI),它是存活在其他计算机上的对象就像存活在本机上一样,当向远程方法发送消息时,需要将参数和返回值序列化来进行传输。
二是Java Beans,用来保存配置的状态信息。

seriallizable的基本使用:

只要对象实现了serializable接口(该接口是个标记接口,没有任何方法),对象便可以被序列化,对象的序列化非常“聪明”,它不仅保存对象的“全景图”,而且会追踪对象中所包含的所有引用,并保存那些对象,接着又能对那些对象内包含的每个引用进行追踪,依次类推。从程序的角度看,这是一个递归的过程。这种情况有时也被成为“对象网”。
要序列化一个对象,首先需要创建某些“OutputStream”对象,然后将其封装在一个ObjectOutputStream对象之中,这时,通过writeObject()即可将对象序列化,并将其发送给OutputStream对象,要反序列化恢复对象,需要将InputStream对象封装在ObjectInputStream中,然后调用readObject()。

public class SerialDemo {    public static void main(String[] args) {        try {            //将animal对象序列化为字节序列,并存储到animal.out文件中            Animal animal = new Animal(4,"andy");            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("animal.out"));            out.writeObject(animal);            out.close();            //将animal对象从存储在animal.out文件中的字节序列中恢复            ObjectInputStream in = new ObjectInputStream(new FileInputStream("animal.out"));            Animal recoverdAnimal = (Animal) in.readObject();            System.out.println(recoverdAnimal);            in.close();        } catch (IOException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (ClassNotFoundException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }    static class Animal implements Serializable{        int age;        String name;        public Animal(int age, String name) {            this.age = age;            this.name = name;        }        @Override        public String toString() {            return "Animal [age=" + age + ", name=" + name + "]";        }    }}

输出结果为:

Animal [age=4, name=andy]

但进行序列化和反序列化的必须是实现了Serializable接口的对象,如果对一个未实现Serializable的对象进行序列化和反序列化的操作会抛出NotSerializableException异常。为了验证这一点将上面代码的Animal的Serializable接口去掉再次运行,运行的结果如果:

java.io.NotSerializableException: com.io.SerialDemo$Animal    at java.io.ObjectOutputStream.writeObject0(Unknown Source)    at java.io.ObjectOutputStream.writeObject(Unknown Source)    at com.io.SerialDemo.main(SerialDemo.java:16)

注意在将一个Serializable的对象进行还原的过程,也就是反序列化的过程中,没有调用任何构造器,整个对象都是从InputStream中取得数据恢复而来的。

序列化的控制:

1. transient关键字

默认的序列化机制不难操控,但如果有特殊的需要怎么办,如果不希望对象的某些部分被序列化,如果对象中某些字段表示我们不希望对其序列化的敏感信息(如密码),我们可以通过transient关键字来关闭某些字段的序列化。

2. Externalizable接口

在这些情况下,如果不通过transient来关闭某些字段的序列化,也可通过实现Externalizable接口替代实现Serializable接口来对序列化的过程进行控制。这个Externlizable接口继承了Serializable接口,同时增添了两个方法writeExternal(),readExternal(),这两个方法在序列化和反序列化的过程中被自动调用,以便执行一些特殊操作。

public class SerialDemo {    public static void main(String[] args) {        try {            //将animal对象序列化为字节序列,并存储到animal.out文件中            Animal animal = new Animal(4,"andy");            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("animal.out"));            out.writeObject(animal);            out.close();            //将animal对象从存储在animal.out文件中的字节序列中恢复            ObjectInputStream in = new ObjectInputStream(new FileInputStream("animal.out"));            Animal recoverdAnimal = (Animal) in.readObject();            System.out.println(recoverdAnimal);            in.close();        } catch (IOException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (ClassNotFoundException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }    static class Animal implements Externalizable{        int age;        String name;        public Animal(){            System.out.println("Animal default constructor");        }        public Animal(int age, String name) {            this.age = age;            this.name = name;        }        @Override        public void writeExternal(ObjectOutput out) throws IOException {            System.out.println("writeExternal method is called");        }        @Override        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {            System.out.println("readExternal method is called");        }        @Override        public String toString() {            return "Animal [age=" + age + ", name=" + name + "]";        }    }}

运行结果:

writeExternal method is calledAnimal default constructorreadExternal method is calledAnimal [age=0, name=null]

**关于Externalizable接口有两点说明:

  1. 对于Serializable接口,对象完全以它存储的二进制位为基础来进行构造,而不调用构造器,而对于一个Externalizable对象,所有的普通默认构造器都会被调用(包括字段定义时的初始化),然后调用readExternal()方法。 所以实现了Externalizable接口的类必须拥有public 的默认无参构造器。如果把上面代码中Animal类的默认构造器去掉,或这去掉默认构造器的public声明,会抛出java.io.InvalidClassException: com.io.SerialDemo$Animal; no valid constructor的错误。

  2. 如果类实现了Externalizable接口,类中的有关信息需要调用writeExternal()和readExternal()方法来将提供恰当的存储和恢复功能。换而言之,相较于serializable接口提供了默认的序列化和反序列化机制,Externalizable的序列化和反序列化机制需要通过重写writerExternal()和readExternal()方法来实现。**

    Externalizable的替代方法:
    如果不是特别坚持实现Externalizable接口,那么还有一种方法,我们可以实现Serializable接口,并添加(注意 我说的是添加,而并非覆盖或者实现,Serializable不包含任何方法)名为readObject()、writeObject()方法。如果一旦对象被序列化或者被反序列化还原,就会自动地分别调用这两个方法,也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制。在readObject()和writerObject()方法中可以调用defaultReadObject()、defaultWriterObject()方法来使用默认的类加载机制。

public class SerialDemo {    public static void main(String[] args) {        SerialC s = new SerialC("asd","xiaoming");        ByteArrayOutputStream buf = new ByteArrayOutputStream();        try {            ObjectOutputStream out = new ObjectOutputStream(buf);            out.writeObject(s);            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));            s = (SerialC) in.readObject();            System.out.println(s);        } catch (IOException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (ClassNotFoundException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }    static class SerialC implements Serializable{        private String a;        private transient String b;        public SerialC(String a, String b) {            this.a = a;            this.b = b;        }        private void writeObject(ObjectOutputStream stream) throws IOException{            stream.defaultWriteObject();            stream.writeObject(b);        }        private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{            stream.defaultReadObject();            b = (String) stream.readObject();        }        @Override        public String toString() {            return "SerialC [a=" + a + ", b=" + b + "]";        }    }}

运行结果:

SerialC [a=asd, b=xiaoming]

可见,虽然字段b声明为transient,是不会被序列化和反序列化的,但我们在readObject()、writeObject()方法中添加了将其序列化和反序列化的逻辑,也就是stream.writeObject(b),
b = (String) stream.readObject()。这样b的信息也保存了下来。

序列化与static:

Java序列化是不能序列化static变量的,因为其保存的是对象的状态,而static变量保存在全局数据区,在对象未实例化时就已经生成,属于类的状态。虽然class对象也能序列化,我们能否通过序列化对象对应的class对象来序列化static值呢?
为了方便理解,我们举例说明:

 class Edt implements Serializable{     public static int color = 1;     public static void setColor(int i){         color = i;     }     public static void getColor(){         System.out.println(color);     }     public static void main(String[] args){        try {            Edt.setColor(13);            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialStatic.out"));            out.writeObject(Edt.class);            out.close();            Edt.setColor(16);            ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialStatic.out"));            @SuppressWarnings("unchecked")            Class<Edt> clz = (Class<Edt>) in.readObject();            System.out.println(clz.getDeclaredField("color").getInt(null));        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }      } }

结果Edt类中color的值是16,而不是13,class类虽然是Serializable的,但它不能按照我们所期望的方式运行,所以加入序列化static值,必须手动实现,即通过之前讲过的,添加readObject()、writeObject()方法来将static变量序列化。按照我的理解,在序列化class对象时,只是序列化了class对象的结构,而未序列化class对象中比如字段的值。

序列化与版本:

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID 。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化
特性使用案例
读者应该听过 Façade 模式,它是为应用程序提供统一的访问接口,案例程序中的 Client 客户端使用了该模式,案例程序结构图如图 1 所示。
图 1. 案例程序结构
这里写图片描述
图 1. 案例程序结构
Client 端通过 Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通过网络将二进制对象数据传给 Client,Client 负责反序列化得到 Façade 对象。该模式可以使得 Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object 类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client 端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序

父类与序列化:

要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。如果对象中引用没有被transient关键字声明,并且引用指向的对象没有实现Serializable接口,那么便会抛出java.io.NotSerializableException。道理很简单,上面我们说过,在序列化对象时会追踪对象中的引用,并序列化引用所指向的对象,如果该对象没有实现Serializable接口,那么就意味着序列化了一个不能序列化的对象,所以会报错。

对敏感字段加密:
服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作。

序列化存储对象规则:

ava 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,反序列化时,恢复引用关系。这样就节省了很多的磁盘空间

public class SerialDemo {    public static void main(String[] args) {        try {            C a = new C(0);            File file = new File("serial.out");            FileOutputStream fos = new FileOutputStream(file);            ObjectOutputStream out = new ObjectOutputStream(fos);            out.writeObject(a);            System.out.println(file.length());            a.setId(10);            out.writeObject(a);            System.out.println(file.length());            out.close();            ObjectInputStream in = new ObjectInputStream(new FileInputStream("serial.out"));            C a1 = (C) in.readObject();            C a2 = (C) in.readObject();            System.out.println("a1 : "+a1+ " ,a1.id : "+ a1.getId());            System.out.println("a2 : "+a2+ " ,a2.id : "+ a2.getId());            in.close();        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }    static class C implements Serializable{        private int id;        public C(int i){            id = i;        }        public void setId(int i){            id = i;        }        public int getId(){            return id;        }    }}

结果:

4954a1 : com.io.SerialDemo$C@4c873330 ,a1.id : 0a2 : com.io.SerialDemo$C@4c873330 ,a2.id : 0

可以看到第一次读入对象a时,文件的大小变为49 ,第二次读入相同的内容时,文件大小只增加了5。说明序列化并没有重新写入整个对象,而是单单写入了引用。
而且在第一次写入了a对象后,我们更改了a的id,并再次写入a。从文件中反序列化得到的对象的id均是之前设置的0。进一步说明了第二次存入的只是上一次写入对象的引用。

序列化与单例模式:

单例模式就是一个类就产生一个对象,这在实际应用中还是应用比较多的,但如果将对象序列化便会产生一个问题,反序列化得到的对象与之前的对象并不是同一个对象。这样就破坏了单例模式。可以通过在实现了Serializable接口的类中添加readResolve()方法,如果存在该方法,ObjectInputStream的readObject()方法就会直接跳过默认的反序列化逻辑,直接返回readResolve()方法所返回的对象。我们可以在readResolve()方法中返回类的唯一对象,从而保证单例模式

public class Singleton implements Serializable{    private static Singleton instance = new Singleton();    private Singleton(){}    public static Singleton getInstance(){        return instance;    }    private Object readResolve(){        return instance;    }    public static void main(String[] args) {        Singleton s = Singleton.getInstance();        ObjectOutputStream out;        try {            out = new ObjectOutputStream(new FileOutputStream("serialStatic.out"));            out.writeObject(s);            out.close();            ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialStatic.out"));            Object ss = in.readObject();            System.out.println( s == ss);        } catch (Exception e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}

上述代码返回true,删除readResolve()方法,结果会返回false,大家可以试一试,