Java 对象的序列化 (Serializable)和反序列化
对象序列化的目标是将对象保存到磁盘中,或允许在网路中直接传输对象,对象序列化机制允许把 Java对象转换成平台无关的二进制流,从而允许把二进制流永久的保存在磁盘上,通过网路把这种二进制流保存到另一个网络节点。比如在Web应用中需要保存到HtppSession或ServletContext属性的Java对象.
建议:程序创建的每个JavaBean类都应该是实现Serializable。
让一个Java对象实现序列化非常简单,只需让这个Java对象实现Serializable接口,不需要实现任何方法.
1.使用对象流实现序列化.
当一个类实现了Serializable接口时,表明这个类的对象就是可序列化的,程序可以通过两个步骤来序列化该对象.
- 创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其它节点流的基础之上.
- 调用ObjectOutputStream对象的writeObject方法输出该序列化对象.
下面通过例子来看看如何实现对象的序列化,以及将该序列化对象写入到文本文件里.
定义一个Person类,该Person类是一个普通的Java类,只是实现了Serializable接口,该接口标识该类的对象是可序列化的.
package com.test; import java.io.Serializable; public class Person implements Serializable{ private String name; private int age; public Person(String name, int age) { System.out.println("有参数的构造器"); 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; } }
然后使用ObjectOutputStream将该Person对象写入磁盘文件.
package com.test; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class WriteObject { public static void main(String[] args) { ObjectOutputStream oos=null; try { //创建一个ObjectOutputStream输出流 oos = new ObjectOutputStream(new FileOutputStream("object.txt")); Person person=new Person("孙悟空",500); //将Person对象写入输入流 oos.writeObject(person); } catch (Exception e) { e.printStackTrace(); } finally{ if(oos!=null){ try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
在上面的程序中,注意在最后将程序的IO流关闭. 运行程序之后,刷新一下项目,会在项目的根目录下面出现一个Object.txt文件的,打开文件之后你可以看到是一堆乱码,那是一些二进制文件,所以人眼是看不出来滴,接下面就通过程序代码来读取这堆乱码中的东西吧.
首先我们来看下,如何从二进制流中恢复Java对象,也就是我们所说的反序列化:
- 创建一个ObjectInputStream,这个输出流也是一个处理流,所以也必须建立在其它节点流的基础之上.
- 调用ObjectInputStream对象的readObject方法读取流中对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型.
下面通过程序代码来读取Object.txt中Person对象的name 和age属性的值。
package com.test; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class ReadObject { public static void main(String[] args) { ObjectInputStream ois=null; try { //创建一个ObjectInputStream 输出流 ois = new ObjectInputStream(new FileInputStream("object.txt")); //读取出Person对象 Person p=(Person) ois.readObject(); System.out.println("名字为:"+p.getName()+"\n 年龄为:"+p.getAge()); } catch (Exception e) { } finally{ if(ois!=null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
运行程序,得到结果如下:
名字为:孙悟空 年龄为:500
2.对象引用的序列化
如果某个类的属性类型不是基本类型或是String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的属性类是不可序列化的.
例如:Teacher类持有一个对Person类的引用,则只有当Person类是可序列化的,Teacher类才是序列化的,否则无论Teacher无论实现Serializable接口,都是不可序列化的。
package com.test; import java.io.Serializable; public class Teacher implements Serializable { private String name; //持有对Person类的引用 private Person student; public Teacher(String name, Person student) { super(); this.name = name; this.student = student; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Person getStudent() { return student; } public void setStudent(Person student) { this.student = student; } }
现在我们来看下如下的程序代码:
package com.test; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class WriterTeacher { public static void main(String[] args) { ObjectOutputStream oos=null; try { oos=new ObjectOutputStream(new FileOutputStream("teacher.txt")); Person person=new Person("孙悟空",500); Teacher t1=new Teacher("唐僧",person); Teacher t2=new Teacher("菩提老祖",person); oos.writeObject(person); oos.writeObject(t1); oos.writeObject(t2); oos.writeObject(person); oos.writeObject(t2); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
在上面的程序中,序列化了两个Teacher对象,而两个Teacher对像都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject输出同一个Teacher对象. 看上去我们序列化了四个对像,其实只是序列化了三个对象.而且序列的两个Teacher对象的Student引用其实是同一个Person对象. 信不信由你,我们就用程序代码来验证一下:
package com.test; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class ReadTeacher { public static void main(String[] args) { ObjectInputStream ois=null; try { ois=new ObjectInputStream(new FileInputStream("teacher.txt")); //依次读取ObjectInputStream输入流中的四个对象 Teacher t1=(Teacher)ois.readObject(); Teacher t2=(Teacher)ois.readObject(); Person person=(Person)ois.readObject(); Teacher t3=(Teacher)ois.readObject(); //输出true System.out.println("t1的student引用和p是否相同: "+(t1.getStudent()==person)); System.out.println(t1.getName()); //输出true System.out.println("t2的student引用和p是否相同: "+(t2.getStudent()==person)); //输出true System.out.println("t2和t3是否是同一个对象: "+(t2==t3)); //输出false System.out.println("t1和t2是否是同一个对象: "+(t1==t2)); System.out.println("t1和t3是否是同一个对象: "+(t1==t3)); } catch (Exception e) { e.printStackTrace(); } finally{ if(ois!=null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
来看下程序运行的结果吧:
t1的student引用和p是否相同: true 唐僧 t2的student引用和p是否相同: true t2和t3是否是同一个对象: true t1和t2是否是同一个对象: false t1和t3是否是同一个对象: false
关于上面的结果,我们就需要了解下Java的序列化机制了.
Java的序列化机制采用了一个特殊的算法,其算法内容是:
- 所有保存到磁盘中的对象都有一个序列化编号.
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有当该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出.
- 如果某个对像已经序列化过的,程序将直接输出一个序列化编号,而不是重新序列化该对象.
由上面的序列化算法,我们可以得到一个结论,当第二次、第三次序列化该对象时,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。
注意:当使用Java序列化机制序列化可变对象时一定要注意,只有当第一调用writeObject方法时才会将对象转换成字节序列,并写出到ObjectOutputStream;在后面的程序中,如果该对象的属性发生了改变,即再次调用writeObject方法输出该对象时,该编号的属性不会被输出.
3.自定义序列化
所谓自定义序列化:简单的说,也就是在一些特殊的情形下,我们不希望类里面的某些属性序列化而已。有两种方法可以实现自定义序列化。
3.1 使用transient关键字实现自定义序列化.
注意:transient关键字只能修饰属性,不可修饰Java程序中的其它成分.
下面的程序对age使用了transient关键字修饰.,Person 类和上面的几乎完全一样,不同的地方,你懂的。。。。
package com.test; import java.io.Serializable; public class Person implements Serializable{ private String name; private transient int age; public Person(String name, int age) { System.out.println("有参数的构造器"); 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; } }
下面我们就先序列化一个Person对象,然后在反序列化该Person对象,得到反序列化的Person对象后输出该对象的age属性值。
package com.test; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class TransientTest { public static void main(String[] args) { ObjectOutputStream oos=null; ObjectInputStream ois=null; try { //创建一个ObjectOutputStream输出流 oos=new ObjectOutputStream(new FileOutputStream("transient.txt")); Person per=new Person("孙悟空",500); //系统会将per对象转换成字节序列并输出 oos.writeObject(per); //创建一个ObjectInoutStream 输入流 ois=new ObjectInputStream(new FileInputStream("transient.txt")); Person p=(Person)ois.readObject(); System.out.println("年龄: "+p.getAge()); } catch (Exception e) { e.printStackTrace(); } } }
输出的结果是:
有参数的构造器 年龄: 0
由于上面程序的Person类的age属性使用transient关键字修饰,所以age输出的结果是0.
由上面的结果我们也可以隐隐约约的看出,使用transient关键字修饰属性虽然方便,但被transient修饰的属性将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该属性值。所以接下来,我们就来看一下Java的另一种自定义序列化机制.
3.2 第二种 自定义序列化方法.
这中自定义序列化机制可以让程序控制如何序列化各属性,甚至完全不序列化某些属性(与使用transient关键字的效果相同);
序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
关于这三个方法的介绍,大家可以参考下英文的JDK API,里面说的很清楚,这里就不介绍了。
package com.test; import java.io.IOException; import java.io.Serializable; public class Person implements Serializable{ private String name; private transient int age; public Person(String name, int age) { System.out.println("有参数的构造器"); 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; } private void writeObject(java.io.ObjectOutputStream out) throws IOException{ out.writeObject(new StringBuffer(name).reverse()); out.writeInt(age); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ this.name=((StringBuffer)in.readObject()).reverse().toString(); this.age=in.readInt(); } }
上面程序中两个粗体字标示的方法(新增的两个方法)用以实现自定义的序列化,对于这样的Person对象而言,程序序列化、反序列化时并没有任何区别----区别在于序列化后的对象流,即使有黑客截获到这个Person对象流,他看到name将是我们加密后的name 值,这样就提高了序列化的安全性.
之后我们在按前面的代码读出age的值,就不会是0了.
序列化就先说这么多了,有不懂的,可以讨论一下。。