1、序列化是干什么的?
简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。
2、什么情况下需要序列化
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
3、当对一个对象实现序列化时,究竟发生了什么?
在没有序列化前,每个保存在堆(Heap)中的对象都有相应的状态(state),即实例变量(instance ariable)比如:
- Foo myFoo = new Foo();
- myFoo .setWidth(37);
- myFoo.setHeight(70);
当 通过下面的代码序列化之后,MyFoo对象中的width和Height实例变量的值(37,70)都被保存到foo.ser文件中,这样以后又可以把它 从文件中读出来,重新在堆中创建原来的对象。当然保存时候不仅仅是保存对象的实例变量的值,JVM还要保存一些小量信息,比如类的类型等以便恢复原来的对 象。
- FileOutputStream fs = new FileOutputStream("foo.ser");
- ObjectOutputStream os = new ObjectOutputStream(fs);
- os.writeObject(myFoo);
4、实现序列化(保存到一个文件)的步骤
a)Make a FileOutputStream
- FileOutputStream fs = new FileOutputStream("foo.ser");
b)Make a ObjectOutputStream
- ObjectOutputStream os = new ObjectOutputStream(fs);
c)write the object
- os.writeObject(myObject1);
- os.writeObject(myObject2);
- os.writeObject(myObject3);
d) close the ObjectOutputStream
- os.close();
5、举例说明
import java.io.*; public class Box implements Serializable { private int width; private int height; public void setWidth(int width){ this.width = width; } public void setHeight(int height){ this.height = height; } public static void main(String[] args){ Box myBox = new Box(); myBox.setWidth(50); myBox.setHeight(30); try{ FileOutputStream fs = new FileOutputStream("foo.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(myBox); os.close(); }catch(Exception ex){ ex.printStackTrace(); } } }
6、相关注意事项
a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
2. 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Java的对象序列化是指将那些实现了Serializable接口的对象转换成一个字符序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。 只要对象实现了Serializable接口(记住,这个接口只是一个标记接口,不包含任何的方法
如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而饭序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。
对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现了Serializable接口)。下面这段代码演示了此过程:
package test.serializable; /** *@chenfeic * *只是一个简单的类,用于测试序列化 */ import java.io.Serializable; public class Data implements Serializable { private static final long serialVersionUID = 7247714666080613254L; public int n; public Data(int n) { this.n = n; } public String toString(){ return Integer.toString(n); } }
package test.serializable; import java.io.Serializable; import java.util.Random; /** * * @author chenfei * * 用于测试序列化,每个对象Worm对象都与worm中的下一段链接,同时又有属于不同类(Data)的对象引用数组链接 */ public class Worm implements Serializable { private static final long serialVersionUID = 5468335797443850679L; private Data[] d = { new Data(random.nextInt(10)), new Data(random.nextInt(10)), new Data(random.nextInt(10)) }; private static Random random = new Random(47); private Worm next; private char c; public Worm(int i , char x) { System.out.println("Worm constructor:" +i); c = x; if(--i > 0) { next = new Worm(i , (char)(x+1)); } } public Worm() { System.out.println("Default constructor!"); } public String toString() { StringBuilder sb = new StringBuilder(":"); sb.append(c); sb.append("("); for(Data data : d) { sb.append(data); } sb.append(")"); if(next!=null) { sb.append(next); } return sb.toString(); } }
package test.serializable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; 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 SerializableTest { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { Worm w = new Worm(6 ,'a'); System.out.println("序列化操纵之前"); System.out.println("w="+w); //序列化操作1--FileOutputStream ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("worm.out")); oos1.writeObject("Worm storage By FileOutputStream "); oos1.writeObject(w);//必须所有引用的对象都实现序列化(本例终究是Data这个类),否则抛出有java.io.NotSerializableException:这个异常 oos1.close(); //反序列化操作1---FileInputStream ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream("worm.out")); String s1 = (String)ois1.readObject(); Worm w1 = (Worm)ois1.readObject(); ois1.close(); System.out.println("反序列化操作1之后"); System.out.println(s1); System.out.println("w1:"+w1); //序列化操作2--ByteArrayOutputStream ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); ObjectOutputStream oos2 = new ObjectOutputStream(byteOutStream); oos2.writeObject("Worm storage By ByteOutputStream "); oos2.writeObject(w); oos2.flush(); //反序列操作2--ByteArrayInputStream ByteArrayInputStream byteInStream = new ByteArrayInputStream(byteOutStream.toByteArray()); ObjectInputStream ois2 = new ObjectInputStream(byteInStream); String s2 = (String)ois2.readObject(); Worm w2 = (Worm)ois2.readObject(); ois2.close(); System.out.println("反序列化操作2之后"); System.out.println(s2); System.out.println("w2:"+w2); } }
运行的结果如下:
Worm constructor:6 Worm constructor:5 Worm constructor:4 Worm constructor:3 Worm constructor:2 Worm constructor:1 序列化操纵之前 w=:a(853):b(119):c(802):d(788):e(199):f(881) 反序列化操作1之后 Worm storage By FileOutputStream w1::a(853):b(119):c(802):d(788):e(199):f(881) 反序列化操作2之后 Worm storage By ByteOutputStream w2::a(853):b(119):c(802):d(788):e(199):f(881)
思考:
1)反序列化后的对象,需要调用构造函数重新构造吗?
答案:不需要。对于Serializable对象,对象完全以它存储的二进制位作为基础来构造,而不调用构造器。
请看下面这段代码
package test.serializable; import java.io.Serializable; import java.util.Date; /** * * @author chenfei * * 用于测试序列化时的deep copy * */ public class House implements Serializable { private static final long serialVersionUID = -6091530420906090649L; private Date date = new Date(); //记录当前的时间 public String toString() { return "House:" + super.toString() + ".Create Time is:" + date; } }
运行结果如下所示:
序列化前 调用了构造器 反序列化后
从上面的结果中可以看到,在序列化前,当我们使用
Animal animal = new Animal("test",house);
时,调用了Animal的构造器(打印了输出语句),但是反序列后并没有再打印任何语句,说明并没有调用构造器。
2)序列前的对象与序列化后的对象是什么关系?是("=="还是equal?是浅复制还是深复制?)
答案:深复制,反序列化还原后的对象地址与原来的的地址不同。 我们还是看上面思考1)中给出的代码,前两个类不变化,修改第三个类(MyWorld.java)的部分代码,修改后的代码如下:
package test.serializable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Myworld { /** * @param args * @throws IOException * @throws ClassNotFoundException */ public static void main(String[] args) throws IOException, ClassNotFoundException { House house = new House(); System.out.println("序列化前"); Animal animal = new Animal("test",house); System.out.println(animal); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(animal); oos.writeObject(animal);//在写一次,看对象是否是一样, oos.flush(); oos.close(); ByteArrayOutputStream out2 = new ByteArrayOutputStream();//换一个输出流 ObjectOutputStream oos2 = new ObjectOutputStream(out2); oos2.writeObject(animal); oos2.flush(); oos2.close(); System.out.println("反序列化后"); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ObjectInputStream ois = new ObjectInputStream(in); Animal animal1 = (Animal)ois.readObject(); Animal animal2 = (Animal)ois.readObject(); ois.close(); ByteArrayInputStream in2 = new ByteArrayInputStream(out2.toByteArray()); ObjectInputStream ois2 = new ObjectInputStream(in2); Animal animal3 = (Animal)ois2.readObject(); ois2.close(); System.out.println("out流:" +animal1); System.out.println("out流:" +animal2); System.out.println("out2流:" +animal3); System.out.println("测试序列化前后的对象 == :"+ (animal==animal1)); System.out.println("测试序列化后同一流的对象:"+ (animal1 == animal2)); System.out.println("测试序列化后不同流的对象==:" + (animal1==animal3)); } }
运行结果如下:
序列化前 调用了构造器 test[test.serializable.Animal@bb7465']House:test.serializable.House@d6c16c.Create Time is:Sat Apr 06 00:11:30 CST 2013 反序列化后 out流:test[test.serializable.Animal@4f80d6']House:test.serializable.House@193722c.Create Time is:Sat Apr 06 00:11:30 CST 2013 out流:test[test.serializable.Animal@4f80d6']House:test.serializable.House@193722c.Create Time is:Sat Apr 06 00:11:30 CST 2013(与上面的相同) out2流:test[test.serializable.Animal@12cc95d']House:test.serializable.House@157fb52.Create Time is:Sat Apr 06 00:11:30 CST 2013(与上面只是值相同,但是地址不一样。) 测试序列化前后的对象 == :false 测试序列化后同一流的对象:true 测试序列化后不同流的对象==:false
从结果可以看到
序列化前后对象的地址不同了,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作,我们可以实现对任何可Serializable对象的”深度复制(deep copy)"——这意味着我们复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们的地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同。也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。
补充:
serialVersionUID 的作用?
在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值。 通过这种方式来解决不同的版本之间的串行话问题。
Java串行化机制定义的文件格式似乎很脆弱,只要稍微改动一下类的定义,原来保存的对象就可能无法读取。例如,下面是一个简单的类定义:
public class Save implements Serializable { String name; public void save() throws IOException { FileOutputStream f = new FileOutputStream("foo"); ObjectOutputStream oos = new ObjectOutputStream(f); oos.writeObject(this); oos.close(); } }
如果在这个类定义中增加一个域,例如final int val = 7;,再来读取原来保存的对象,就会出现下面的异常:
java.io.InvalidClassException:
Save; local class incompatible:
stream classdesc serialVersionUID = -2805284943658356093,
local class serialVersionUID = 3419534311899376629
上例异常信息中的数字串表示类定义里各种属性的编码值:
●类的名字(Save)。
●域的名字(name)。
●方法的名字(Save)。
●已实现的接口(Serializable)。
改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引起类似的异常警报。这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。解决这个问题的办法是在类里面新增一个域serialVersionUID,强制类仍旧使用原来的UID。新增的域必须是:
●static:该域定义的属性作用于整个类,而非特定的对象。
●final:保证代码运行期间该域不会被修改。
●long:它是一个64位的数值。
也就是说,新增的serialVersionUID必须定义成下面这种形式:static final long serialVersionUID=-2805284943658356093L;。其中数字后面加上的L表示这是一个long值。
当然,改动之后的类不一定能够和原来的对象兼容。例如,如果把一个域的定义从String改成了int,执行逆-串行化操作时系统就不知道如何处理该值,显示出错误信息:java.io.InvalidClassException: Save; incompatible types for field name。