写在前面 本文主要是对Java序列化学习的一些总结,一来是方便自己以后查阅,二来是希望通过本文能给他人带来一些帮助。
对象序列化 Java提供了一种对象序列化的机制,在该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以再从文件中读取出来,并且对它进行反序列化,也就是说,可以根据对象的类型信息、对象的数据、还有对象中的数据类型可以在内存中新建该对象。
在实际的应用中,我们为什么需要对象序列化机制呢?因为在一般情况下,只有当JVM进程处于运行时,JVM建立的对象才可能存在,也就是说,这些对象的生命周期不会比JVM进程的生命周期更长。但是在现实的应用中,有时需要在JVM进程停止运行之后能够保存(持久化)指定的对象,并且在将来某个时刻重新读取被保存的对象,或者我们希望将一个进程创建的对象传送到另一个JVM进程中。Java的对象序列化机制就能够帮助我们实现这些功能(这就意味着序列化机制是可以自动弥补不同操作系统之间的差异)。
一般来说,对象的序列化主要有两种用途:
把对象的字节序列持久化到硬盘,通常保存在一个文件中;
在网络上传输对象的字节序列;
我们在使用Java对象序列化时,会把对象的状态保存为一组字节序列,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的状态 ,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量,因为静态变量是类的状态。
这个整个过程都是Java虚拟机(JVM)独立完成的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
但是一个类的对象如果要想序列化成功,必须满足两个条件:
该类必须实现 java.io.Serializable
接口。
该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂 的。
如果你想知道一个Java标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现java.io.Serializable
接口即可。
序列化实现的示例
对象序列化时,首先要创建某个OutputStream
对象,然后将其封装在一个ObjectOutputStream
对象内,这时,只需要调用writeObject()
即可将对象序列化,并将其发送给OutputStream
(对象化序列是基于字节的,因要使用InputStream
和OutputStream
继承层次结构);
对象反序列化时,需要将一个InputStream
对象封装在ObjectInputStream
内,然后调用readObject()
方法;
对象序列化时,不仅保存了对象的“全景图”,而且能追踪对象内所包含的所有引用,并保存那些对象,接着又能对对象内包含的每个这样的引用进行追踪,并保存那些对象,这种情况有时被称为“对象网”。
这里我们先建立一个对象Person
,如下(示例参考 ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import java.io.Serializable;import java.util.Random;public class Person implements Serializable { private String name; private int age; private Gender gender; private int id; public Person (String name, int age, Gender gender) { this .name = name; this .age = age; this .gender = gender; this .id = (new Random()).nextInt(10 ); } 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 "[" + name + ", " + age + ", " + gender + ", " + id + "]" ; } }
下面,我们在一个进程里对其进行序列化,然后再反序列化出该对象(示例 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class Serialize { public static void main (String[] args) { Person p1 = new Person("wm0" , 10 , Gender.MALE); Person p2 = new Person("wm1" , 18 , Gender.MALE); System.out.println("p1 = " + p1); ObjectOutputStream out = null ; try { out = new ObjectOutputStream(new FileOutputStream("/home/matt/person.out" )); out.writeObject("Person1 storage\n" ); out.writeObject(p1); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/person.out" )); String s = null ; Person p11 = null ; try { s = (String) in.readObject(); p11 = (Person) in.readObject(); System.out.println("after Serialize: " + p11); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println("\n" + "p2 = " + p2); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout); out2.writeObject("Person2 storage\n" ); out2.writeObject(p2); out2.flush(); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); try { s = (String) in2.readObject(); Person p22 = (Person) in2.readObject(); System.out.println("after Serialize: " + p22); } catch (ClassNotFoundException e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } } }
这里有一点要注意的是:反序列化Person对象时,需要要能找到Person.class
,否者就会抛出ClassNotFoundException
的异常。
序列化的控制 通过上面的例子,我们可以看出序列化的使用,其实还是很简单的,但是,如果我们有特殊的需要那又该怎么办呢?下面我们介绍几种序列化的控制机制。
Externalizable接口 如果我们希望对象的一部分被序列化,而另一部分不被序列化;或者一个对象被还原之后,某子对象需要重新创建,从而不必将该子对象序列化。在这种情况下,我们可以通过实现Externalization
接口——该接口实现Serializable
接口,同时增加两个方法:writeExternal()
和readExternal()
,这两个方法会在序列化和反序列化还原的过程中被自动调用以便执行一些特殊操作。
这与恢复Serializable
对象不同,对于Serializable
对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器。但是对于一个Externalization
对象,所有普通的默认构造器都会被调用(包括字段定义时的初始化),然后调用readExternal()
。
必须要注意这一点:所有默认的构造器都会被调用,才能使Externalization
对象产生正确的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import java.io.Externalizable;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectInputStream;import java.io.ObjectOutput;import java.io.ObjectOutputStream;public class Blip3 implements Externalizable { private int i; private String s; public Blip3 () { System.out.println("Blip3 Constructor" ); } public Blip3 (String x, int a) { System.out.println("Blip3(String x, int a)" ); s = x; i = a; } public String toString () { return s + i; } public void writeExternal (ObjectOutput out) throws IOException { System.out.println("Blip3.writeExternal" ); out.writeObject(s); out.writeInt(i); } public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip3.readExternal" ); s = (String) in.readObject(); i = in.readInt(); } public static void main (String[] args) throws IOException, ClassNotFoundException { System.out.println("Constructing objects:" ); Blip3 b3 = new Blip3("A String " , 47 ); System.out.println(b3); ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/home/matt/Blip3.out" )); System.out.println("Saving object:" ); o.writeObject(b3); o.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/Blip3.out" )); System.out.println("Recovering b3:" ); b3 = (Blip3) in.readObject(); System.out.println(b3); } }
在上面的例子中,字段s
和i
只会在第二个构造器中初始化,而不是在默认的构造器中初始化。这意味着假如不在readExternal()
中初始化s
和i
,s
就会为null
,而i
就会为零(因为在创建对象的第一步中将对象的存储空间清理为0)。如果我们把writeExternal()
方法中两行注释掉,对象还原后,s
是null
,而i
是零。
我们如果从一个Externalization
对象继承,通常需要调用基类版本的writeExternal()
和readExternal()
来为基类组件提供恰当的存储和恢复功能。
因此,为了正常运行,我们不仅需要在writeExternal()
方法(没有任何默认行为来为Externalization
对象写入任何成员对象)中将来自对象的重要信息写入,还必须在readExternal()
方法中恢复数据。
Transient关键字 在进行序列化控制时,可能某个特定子对象不想让Java的序列化机制自动保存和恢复。如果子对象表示的是我们不希望将其序列化的敏感信息(如密码),那么我们就会面临这种情况。即使对象中的这些信息是private
属性,一经序列化处理,人们就可以通过读取文件或者拦截网络传输的方式来访问到它。
有两种方法可以实现上述要求:
将类实现Externalizable
接口,这样的话,没有任何东西是可以自动序列化,并且可以在writeExternal()
内部只对所需部门进行显式的序列化;
如果在操作的是一个Serializable
对象,那么所有序列化操作都会自动进行,为了能够进行控制,可以用transient
关键字逐个字段地关闭序列化,这个关键字的意思就是不用麻烦你保存或者回复数据——我自己会处理的 。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.util.Date;import java.util.concurrent.TimeUnit;public class Login implements Serializable { private Date date = new Date(); private String username; private transient String password; public Login (String name, String pwd) { username = name; password = pwd; } public String toString () { return "logon info: \n username: " + username + "\n date: " + date + "\n password: " + password; } public static void main (String[] args) throws Exception { Login a = new Login("Hulk" , "myLittlePony" ); System.out.println("logon a = " + a); ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/home/matt/Logon.out" )); o.writeObject(a); o.close(); TimeUnit.SECONDS.sleep(1 ); ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/Logon.out" )); System.out.println("Recovering object at " + new Date()); a = (Login) in.readObject(); System.out.println("logon a = " + a); } }
可以看到,其中的date
和username
域是一般的(不是transient
的),所以它们会被自动序列化。而password
是transient
的,所以不会被自动保存到磁盘;另外,自动序列化机制也不会尝试去恢复它。当对象被恢复时,password
域就会变成null
。我们还可以发现,date
字段也是从存储到了磁盘并从磁盘上被恢复出来,而且没有再重新生成。
由于实现Externalizable
接口的对象在默认情况下不保存它们的任何字段,所以transient
关键字只能和Serializable
对象一起使用。
重写writeObject()和readObject()方法 如果不是特别坚持使用Externalizable
接口,那么还有一种方法。我们可以实现Serializable
接口,并添加writeObject()
和readObject()
方法。这样一旦对象被序列化或者被反序列化还原,就会自动地分别调用这两个方法,而不是使用默认的序列化机制。(示例 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;public class SerialCtl implements Serializable { private String a; private transient String b; public SerialCtl (String aa, String bb) { a = "Not Transient: " + aa; b = "Transient: " + bb; } public String toString () { return a + " " + 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(); } 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 o = new ObjectOutputStream(buf); o.writeObject(sc); ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray())); SerialCtl sc2 = (SerialCtl) in.readObject(); System.out.println("After:\n" + sc2); } }
上述的例子中,非transient字段由defaultReadObject
保存,而transient字段必须在程序中明确保存和恢复。
静态变量的序列化 前面我们也已经提到过,静态变量是不会被序列化的,这里我们通过一个例子来看一下(示例 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;public class StaticTest implements Serializable { private static int id=10 ; public static void main (String[] args) { System.out.println("Constructing objects:" ); ObjectOutputStream o = null ; try { o = new ObjectOutputStream(new FileOutputStream("/home/matt/static.out" )); o.writeObject(new StaticTest()); o.close(); StaticTest.id=0 ; ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/static.out" )); StaticTest staticTest = null ; try { staticTest = (StaticTest) in.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println(staticTest.id); } catch (IOException e) { e.printStackTrace(); } } }
程序输出的结果为修改之后的结果,正如我们前面所述一样,对象序列化时并不会序列化静态变量,这一点可以这样理解:对象序列化是序列化对象的状态,而静态变量是类变量,也就是类的状态。因此,对象序列化并不保存静态变量 。
存储规则 这里我们通过一个例子来看一下Java序列化机制的存储规则,主要是多次写入同一个对象的情况(示例 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class StoreTest { public static void main (String[] args) { ObjectOutputStream o = null ; try { o = new ObjectOutputStream(new FileOutputStream("/home/matt/store.out" )); Person person=new Person("matt1" ,20 ,Gender.MALE); o.writeObject(person); person.setAge(22 ); System.out.println(new File("/home/matt/store.out" ).length()); o.writeObject(person); System.out.println(new File("/home/matt/store.out" ).length()); o.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("/home/matt/store.out" )); try { Person person1= (Person) in.readObject(); System.out.println(person1.getAge()); Person person2= (Person) in.readObject(); System.out.println(person2.getAge()); System.out.println(person1==person2); } catch (ClassNotFoundException e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } } }
在上述示例中,对于同一个对象,在修改完年龄值后又重新将该实例对象序列化到文件。通过运行的结果我们可以发现:
第二次将对象序列化到文件之后,文件的大小只增加了5个字节的大小;
第二次序列化的对象年龄值已经修改为22,但是从反序列化的结果来看,该实例对象的年龄值并未改变。
大家是不是感觉到非常的奇怪,通过下面的两点解释之后,大家可能就会明白这其中的原因了:
因为写入的是同一个对象,Java序列化机制为了节省磁盘空间,当写入文件的为同一个对象时,并不会将对象的内容再次进行存储,而只是再次存储一份引用,上面增加的5个字节的存储空间就是新增的引用和一些控制信息的空间,从反序列化的结果也可以看出,两个引用指向的是同一个对象;
虽然第二次存储时将年龄修改为22,但是因为Java序列化机制在第二次序列化同一个对象时,并保存具体的数据,只是保存了第一次的引用,所以反序列化时,得到的对象都是第一次序列化的对象。
序列化ID 这里可以可以参考Java中序列化的serialVersionUID作用 一文。
这里我们就简单说一下序列化ID的作用:
serialVersionUID
用来表明类的不同版本间的兼容性。它有两种生成方式: 一个是默认的1L;另一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段 。
在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID
;而在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID
。
当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID
,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。如果你添加了serialVersionUID
,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。
参考: