深入剖析Java编程中的序列化
Java提供一种机制叫做序列化,通过有序的格式或者字节序列持久化java对象,其中包含对象的数据,还有对象的类型,和保存在对象中的数据类型。
所以,如果我们已经序列化了一个对象,那么它可以被读取并通过对象的类型和其他信息进行反序列化,并最终获取对象的原型。
ObjectInputStream和ObjectOutputStream对象是高级别的流对象,包含序列化和反序列化的方法。
ObjectOutputStream拥有很多序列化对象的方法,最常用的是:
privatevoidwriteObject(ObjectOutputStreamos)throwsIOException { }
类似的ObjectInputStream提供如下方法:
privatevoidreadObject(ObjectInputStreamis)throwsIOException,ClassNotFoundException { }
那么哪里会需要序列化呢?序列化通常在需要通过网络传输数据,或者保存对象到文件的场合使用。这里说的数据是对象而不是文本。
现在的问题是,我们的网络架构和硬盘都只能识别二进制和字节,而不能识别Java对象。
序列化就是把Java对象中的value/states翻译为字节,以便通过网络传输或者保存。另外,反序列化就是通过读取字节码,并把它翻译回java对象。
serialVersionUID概念
serialVersionUID是用于保证同一个对象(在序列化中会被用到)可以在Deserialization过程中被载入。serialVersionUID是用于对象的版本控制。你可以参考serialVersionUIDinjavaserialization获取更多信息。
对于序列化:
步骤如下:
让我们看一个列子:
在src->org.arpit.javapostsforlearning创建Employee.java
1.Employee.java
packageorg.arpit.javapostsforlearning; importjava.io.Serializable; publicclassEmployeeimplementsSerializable{ intemployeeId; StringemployeeName; Stringdepartment; publicintgetEmployeeId(){ returnemployeeId; } publicvoidsetEmployeeId(intemployeeId){ this.employeeId=employeeId; } publicStringgetEmployeeName(){ returnemployeeName; } publicvoidsetEmployeeName(StringemployeeName){ this.employeeName=employeeName; } publicStringgetDepartment(){ returndepartment; } publicvoidsetDepartment(Stringdepartment){ this.department=department; } }
就如你所见的,如果你需要序列化任何类,那么你必须实现Serializable接口,这个接口是标记接口(markerinterface)。
Java中的标记接口(markerinterface)就是一个没有任何字段或者方法的接口,简单的来说,java中把空接口叫做标记接口(markerinterface)
2.SerializeMain.java
packageorg.arpit.javapostsforlearning; importjava.io.FileOutputStream; importjava.io.IOException; importjava.io.ObjectOutputStream; publicclassSerializeMain{ /** *@authorArpitMandliya */ publicstaticvoidmain(String[]args){ Employeeemp=newEmployee(); emp.setEmployeeId(101); emp.setEmployeeName("Arpit"); emp.setDepartment("CS"); try { FileOutputStreamfileOut=newFileOutputStream("employee.ser"); ObjectOutputStreamoutStream=newObjectOutputStream(fileOut); outStream.writeObject(emp); outStream.close(); fileOut.close(); }catch(IOExceptioni) { i.printStackTrace(); } } }
对于反序列化:
步骤是
在包src->org.arpit.javapostsforlearning中,创建DeserializeMain.java
3.DeserializeMain.java
packageorg.arpit.javapostsforlearning; importjava.io.IOException; importjava.io.ObjectInputStream; publicclassDeserializeMain{ /** *@authorArpitMandliya */ publicstaticvoidmain(String[]args){ Employeeemp=null; try { FileInputStreamfileIn=newFileInputStream("employee.ser"); ObjectInputStreamin=newObjectInputStream(fileIn); emp=(Employee)in.readObject(); in.close(); fileIn.close(); }catch(IOExceptioni) { i.printStackTrace(); return; }catch(ClassNotFoundExceptionc) { System.out.println("Employeeclassnotfound"); c.printStackTrace(); return; } System.out.println("DeserializedEmployee..."); System.out.println("Empid:"+emp.getEmployeeId()); System.out.println("Name:"+emp.getEmployeeName()); System.out.println("Department:"+emp.getDepartment()); } }
4.运行:
首先运行SerializeMain.java,然后运行DeserializeMain.java,你会得到如下的结果:
DeserializedEmployee... Empid:101 Name:Arpit Department:CS
就这样,我们序列化了一个employee对象,并对它进行反序列化。这看起来和简单,但是如果其中包含对象引用,继承,那么情况就会变得复杂。接下来让我们一个接一个的看一下例子,看看如何在各种场合中实现序列化。
案例1-如果对象引用了其他对象,那该如何
我们已经看过最简单的序列化例子,现在看看,如何处理对象中引用了其他对象的场合。我们该如何序列化?引用对象也会被序列化吗?对的,你不需要显式的序列化引用对象。当你序列化任何对象,如果它包含引用对象,那么Java序列化会自动序列化该对象的整个对象图。例如,Employee现在引用了一个address对象,并且Address也引用了其他对象(例如,Home),那么当你序列化Employee对象的时候,所有其他引用对象,例如address和home将会被自动地被序列化。让我们来创建Address类,并它Address的对象作为引用,添加到employee类中。
Employee.java:
packageorg.arpit.javapostsforlearning; importjava.io.Serializable; publicclassEmployeeimplementsSerializable{ intemployeeId; StringemployeeName; Stringdepartment; Addressaddress; publicintgetEmployeeId(){ returnemployeeId; } publicvoidsetEmployeeId(intemployeeId){ this.employeeId=employeeId; } publicStringgetEmployeeName(){ returnemployeeName; } publicvoidsetEmployeeName(StringemployeeName){ this.employeeName=employeeName; } publicStringgetDepartment(){ returndepartment; } publicvoidsetDepartment(Stringdepartment){ this.department=department; } publicAddressgetAddress(){ returnaddress; } publicvoidsetAddress(Addressaddress){ this.address=address; } }
在org.arpit.javapostsforlearning包中,创建Address.java
Address.java:
packageorg.arpit.javapostsforlearning; publicclassAddress{ inthomeNo; Stringstreet; Stringcity; publicAddress(inthomeNo,Stringstreet,Stringcity){ super(); this.homeNo=homeNo; this.street=street; this.city=city; } publicintgetHomeNo(){ returnhomeNo; } publicvoidsetHomeNo(inthomeNo){ this.homeNo=homeNo; } publicStringgetStreet(){ returnstreet; } publicvoidsetStreet(Stringstreet){ this.street=street; } publicStringgetCity(){ returncity; } publicvoidsetCity(Stringcity){ this.city=city; } }
在包org.arpit.javapostsforlearning中,创建SerializeDeserializeMain.java
SerializeDeserializeMain.java:
packageorg.arpit.javapostsforlearning; importjava.io.FileInputStream; importjava.io.FileOutputStream; importjava.io.IOException; importjava.io.ObjectInputStream; importjava.io.ObjectOutputStream; publicclassSerializeDeserializeMain{ /** *@authorArpitMandliya */ publicstaticvoidmain(String[]args){ Employeeemp=newEmployee(); emp.setEmployeeId(101); emp.setEmployeeName("Arpit"); emp.setDepartment("CS"); Addressaddress=newAddress(88,"MGroad","Pune"); emp.setAddress(address); //Serialize try { FileOutputStreamfileOut=newFileOutputStream("employee.ser"); ObjectOutputStreamoutStream=newObjectOutputStream(fileOut); outStream.writeObject(emp); outStream.close(); fileOut.close(); }catch(IOExceptioni) { i.printStackTrace(); } //Deserialize emp=null; try { FileInputStreamfileIn=newFileInputStream("employee.ser"); ObjectInputStreamin=newObjectInputStream(fileIn); emp=(Employee)in.readObject(); in.close(); fileIn.close(); }catch(IOExceptioni) { i.printStackTrace(); return; }catch(ClassNotFoundExceptionc) { System.out.println("Employeeclassnotfound"); c.printStackTrace(); return; } System.out.println("DeserializedEmployee..."); System.out.println("Empid:"+emp.getEmployeeId()); System.out.println("Name:"+emp.getEmployeeName()); System.out.println("Department:"+emp.getDepartment()); address=emp.getAddress(); System.out.println("City:"+address.getCity()); } }
运行它:
当你运行SerializeDeserializeMain.java。你会得到这样的结果:
java.io.NotSerializableException:org.arpit.javapostsforlearning.Address atjava.io.ObjectOutputStream.writeObject0(UnknownSource) atjava.io.ObjectOutputStream.defaultWriteFields(UnknownSource) atjava.io.ObjectOutputStream.writeSerialData(UnknownSource) atjava.io.ObjectOutputStream.writeOrdinaryObject(UnknownSource) atjava.io.ObjectOutputStream.writeObject0(UnknownSource) atjava.io.ObjectOutputStream.writeObject(UnknownSource)
我们将解释哪里出错了。我忘记了说,Address类也必须是serializable。那么Address类必须继承serialzable接口。
Address.java:
importjava.io.Serializable; publicclassAddressimplementsSerializable{ inthomeNo; Stringstreet; Stringcity; publicAddress(inthomeNo,Stringstreet,Stringcity){ super(); this.homeNo=homeNo; this.street=street; this.city=city; } publicintgetHomeNo(){ returnhomeNo; } publicvoidsetHomeNo(inthomeNo){ this.homeNo=homeNo; } publicStringgetStreet(){ returnstreet; } publicvoidsetStreet(Stringstreet){ this.street=street; } publicStringgetCity(){ returncity; } publicvoidsetCity(Stringcity){ this.city=city; } }
再次运行:
当你再次运行SerializeDeserializeMain.java。你可以得到如下的结果
DeserializedEmployee... Empid:101 Name:Arpit Department:CS City:Pune
案例2:如果我们不能访问引用对象的源代码(例如,你不能访问上面的Address类的源码)
如果我们不能访问到address类,那么我们该如何在Address类中实现serializable接口?是否有另外的途径来实现呢?对的,你可以创建另外一个类,并继承Address,然后让它继承serializable接口,但是对于下面的情况,这个方案会失败:
如果引用类被定义为final
如果引用类引用了另外一个非可序列化的对象
那么,我们该如何序列化Employee对象?解决的办法是,标记transient。如果你不需要序列化任何字段,只需把它标记为transient。
transientAddressaddress
在Employee类中,标记了address为transient之后,运行程序。你会得到nullPointerException,因为在反序列化过程中,Address引用将会是null。
案例3-如果我仍然需要保存引用对象的状态呢?(例如address对象)
如果你在反序列化过程中,标记了address为transient,它将会返回null结果。但是如果你仍然需要保存它的状态,你就需要序列化address对象。Java序列化提供一个机制,如果你有特定签名的private方法,那么它们就会在序列化和反序列化过程中被调用,所以我们将重写Employee类的writeObject和readObject方法,然后它们就会在Employee对象序列化/反序列化过程中被调用。
Employee.java:
packageorg.arpit.javapostsforlearning; importjava.io.IOException; importjava.io.ObjectInputStream; importjava.io.ObjectOutputStream; importjava.io.Serializable; publicclassEmployeeimplementsSerializable{ intemployeeId; StringemployeeName; Stringdepartment; transientAddressaddress; publicintgetEmployeeId(){ returnemployeeId; } publicvoidsetEmployeeId(intemployeeId){ this.employeeId=employeeId; } publicStringgetEmployeeName(){ returnemployeeName; } publicvoidsetEmployeeName(StringemployeeName){ this.employeeName=employeeName; } publicStringgetDepartment(){ returndepartment; } publicvoidsetDepartment(Stringdepartment){ this.department=department; } publicAddressgetAddress(){ returnaddress; } publicvoidsetAddress(Addressaddress){ this.address=address; } privatevoidwriteObject(ObjectOutputStreamos)throwsIOException,ClassNotFoundException { try{ os.defaultWriteObject(); os.writeInt(address.getHomeNo()); os.writeObject(address.getStreet()); os.writeObject(address.getCity()); } catch(Exceptione) {e.printStackTrace();} } privatevoidreadObject(ObjectInputStreamis)throwsIOException,ClassNotFoundException { try{ is.defaultReadObject(); inthomeNo=is.readInt(); Stringstreet=(String)is.readObject(); Stringcity=(String)is.readObject(); address=newAddress(homeNo,street,city); }catch(Exceptione){e.printStackTrace();} } }
另外有一点需要牢记的,ObjectInputStream读取数据的顺序和ObjectOutputStream写入数据的顺序是一致的.
在包org.arpit.javapostsforlearning中创建Address.java
Address.java:
packageorg.arpit.javapostsforlearning; importjava.io.Serializable; publicclassAddress{ inthomeNo; Stringstreet; Stringcity; publicAddress(inthomeNo,Stringstreet,Stringcity){ super(); this.homeNo=homeNo; this.street=street; this.city=city; } publicintgetHomeNo(){ returnhomeNo; } publicvoidsetHomeNo(inthomeNo){ this.homeNo=homeNo; } publicStringgetStreet(){ returnstreet; } publicvoidsetStreet(Stringstreet){ this.street=street; } publicStringgetCity(){ returncity; } publicvoidsetCity(Stringcity){ this.city=city; } }
在包org.arpit.javapostsforlearning中创建SerializeDeserializeMain.java
SerializeDeserializeMain.java:
packageorg.arpit.javapostsforlearning; importjava.io.FileInputStream; importjava.io.FileOutputStream; importjava.io.IOException; importjava.io.ObjectInputStream; importjava.io.ObjectOutputStream; publicclassSerializeDeserializeMain{ /** *@authorArpitMandliya */ publicstaticvoidmain(String[]args){ Employeeemp=newEmployee(); emp.setEmployeeId(101); emp.setEmployeeName("Arpit"); emp.setDepartment("CS"); Addressaddress=newAddress(88,"MGroad","Pune"); emp.setAddress(address); //Serialize try { FileOutputStreamfileOut=newFileOutputStream("employee.ser"); ObjectOutputStreamoutStream=newObjectOutputStream(fileOut); outStream.writeObject(emp); outStream.close(); fileOut.close(); }catch(IOExceptioni) { i.printStackTrace(); } //Deserialize emp=null; try { FileInputStreamfileIn=newFileInputStream("employee.ser"); ObjectInputStreamin=newObjectInputStream(fileIn); emp=(Employee)in.readObject(); in.close(); fileIn.close(); }catch(IOExceptioni) { i.printStackTrace(); return; }catch(ClassNotFoundExceptionc) { System.out.println("Employeeclassnotfound"); c.printStackTrace(); return; } System.out.println("DeserializedEmployee..."); System.out.println("Empid:"+emp.getEmployeeId()); System.out.println("Name:"+emp.getEmployeeName()); System.out.println("Department:"+emp.getDepartment()); address=emp.getAddress(); System.out.println("City:"+address.getCity()); } }
运行:
当你运行SerializeDeserializeMain.java.你会得到如下的结果:
DeserializedEmployee... Empid:101 Name:Arpit Department:CS City:Pune
现在我们就得到了address对象的状态,就如它被序列化前的一样。
序列化中的继承:
现在我们看看继承是如何影响序列化的。不管父类是不是可序列化,这将引出很多个例子。如果父类是非可序列化的,我们将如何处理,并且它是如何工作的。让我们看看例子。
我们将创建一个Person.java,作为Employee的父类。
案例4:如果父类是可序列化的
如果父类可序列化,那么所有的继承类将是可序列化的。
案例5:如果父类为非可序列化呢?
如果父类为非可序列化的,那么我们的处理办法会很不一样。
如果父类为非可序列化的,那么它必然不会有参数构造函数。
Person.java
packageorg.arpit.javapostsforlearning; publicclassPerson{ Stringname="default"; Stringnationality; publicPerson() { System.out.println("Person:Constructor"); } publicPerson(Stringname,Stringnationality){ super(); this.name=name; this.nationality=nationality; } publicStringgetName(){ returnname; } publicvoidsetName(Stringname){ this.name=name; } publicStringgetNationality(){ returnnationality; } publicvoidsetNationality(Stringnationality){ this.nationality=nationality; } }
在包org.arpit.javapostsforlearning中创建Employee.java
Employee.java:
packageorg.arpit.javapostsforlearning; importjava.io.Serializable; publicclassEmployeeextendsPersonimplementsSerializable{ intemployeeId; Stringdepartment; publicEmployee(intemployeeId,Stringname,Stringdepartment,Stringnationality) { super(name,nationality); this.employeeId=employeeId; this.department=department; System.out.println("Employee:Constructor"); } publicintgetEmployeeId(){ returnemployeeId; } publicvoidsetEmployeeId(intemployeeId){ this.employeeId=employeeId; } publicStringgetDepartment(){ returndepartment; } publicvoidsetDepartment(Stringdepartment){ this.department=department; } }
在org.arpit.javapostsforlearning包中创建SerializeDeserializeMain.java
SerializeDeserializeMain.java:
packageorg.arpit.javapostsforlearning; importjava.io.FileInputStream; importjava.io.FileOutputStream; importjava.io.IOException; importjava.io.ObjectInputStream; importjava.io.ObjectOutputStream; publicclassSerializeDeserializeMain{ /** *@authorArpitMandliya */ publicstaticvoidmain(String[]args){ //Serialize Employeeemp=newEmployee(101,"Arpit","CS","Indian"); System.out.println("Beforeserializing"); System.out.println("Empid:"+emp.getEmployeeId()); System.out.println("Name:"+emp.getName()); System.out.println("Department:"+emp.getDepartment()); System.out.println("Nationality:"+emp.getNationality()); System.out.println("************"); System.out.println("Serializing"); try { FileOutputStreamfileOut=newFileOutputStream("employee.ser"); ObjectOutputStreamoutStream=newObjectOutputStream(fileOut); outStream.writeObject(emp); outStream.close(); fileOut.close(); }catch(IOExceptioni) { i.printStackTrace(); } //Deserialize System.out.println("************"); System.out.println("Deserializing"); emp=null; try { FileInputStreamfileIn=newFileInputStream("employee.ser"); ObjectInputStreamin=newObjectInputStream(fileIn); emp=(Employee)in.readObject(); in.close(); fileIn.close(); }catch(IOExceptioni) { i.printStackTrace(); return; }catch(ClassNotFoundExceptionc) { System.out.println("Employeeclassnotfound"); c.printStackTrace(); return; } System.out.println("Afterserializing"); System.out.println("Empid:"+emp.getEmployeeId()); System.out.println("Name:"+emp.getName()); System.out.println("Department:"+emp.getDepartment()); System.out.println("Nationality:"+emp.getNationality()); } }
运行:
当你运行SerializeDeserializeMain.java后,你会得到如下的输出,如果父类是非可序列化的,那么在反序列化过程中,所有继承于父类的实例变量值,将会通过调用非序列化构造函数来初始化。这里name继承于person,所以在反序列化过程中,name将会被初始化为默认值。
案例6-如果父类是可序列化,但你不需要继承类为可序列化
如果你不希望继承类为可序列化,那么你需要实现writeObject()和readObject()方法,并且需要抛出NotSerializableException异常。
案例7-可否序列化静态变量?
不能。因为静态变量是类级别的,不是对象级别的,当你序列化一个对象的时候,是不能序列化静态变量。
总结:
- 序列化是java对象的values/states转化为字节并在网络中传输或者保存它的过程。另外反序列化是把字节码翻译为对应的对象的过程。
- 序列化的好处是,JVM的独立性,也就是说,一个对象可以在一个平台中被序列化,然后在另外一个不同的平台反序列化。
- 如果你需要序列化任何对象,你必须实现标记接口Serializable。
- Java中的标记接口(Markerinterface)就是没有字段或者方法的接口,或者更简单的说,空接口
- serialVersionUID是用于保证同一个对象(在序列化中会被用到)可以在Deserialization过程中被载入。serialVersionUID是用于对象的版本控制。
- 当你需要序列化任何包含引用对象的对象,那么Java会自动序列化该对象的整个对象图。
- 如果你不希望序列化某个字段,你可以标记它为trasient
- 如果父类为可序列化,那么它的继承类也将是可序列化的。
- 如果父类为非可序列化,那么在反序列化过程中,所有继承于父类的实例变量值将会通过调用非可序列化的构造器来初始化。
- 如果你需希望子类为可序列化的,那么你需要实现writeObject()和readObject()方法,并在这两个方法中抛出NotSerializableException异常
- 你不能序列化静态变量。