java序列化的节制
正如各人看到的那样,默认的序列化机制并不难哄骗。然而,假使有非凡要求又该怎么办呢?我们大概有非凡的安详问题,不但愿工具的某一部门序列化;可能某一个子工具完全不必序列化,因为工具规复今后,那一部门需要从头建设。
此时,通过实现Externalizable接口,用它取代Serializable接口,便可节制序列化的详细进程。这个Externalizable接口扩展了Serializable,并增添了两个要领:writeExternal()和readExternal()。在序列化和从头装配的进程中,会自动挪用这两个要领,以便我们执行一些非凡操纵。
下面这个例子展示了Externalizable接口要领的简朴应用。留意Blip1和Blip2险些完全一致,除了极微小的不同(本身研究一下代码,看看是否能发明):
//: Blips.java // Simple use of Externalizable & a pitfall import java.io.*; import java.util.*; class Blip1 implements Externalizable { public Blip1() { System.out.println("Blip1 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip1.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip1.readExternal"); } } class Blip2 implements Externalizable { Blip2() { System.out.println("Blip2 Constructor"); } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip2.writeExternal"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip2.readExternal"); } } public class Blips { public static void main(String[] args) { System.out.println("Constructing objects:"); Blip1 b1 = new Blip1(); Blip2 b2 = new Blip2(); try { ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blips.out")); System.out.println("Saving objects:"); o.writeObject(b1); o.writeObject(b2); o.close(); // Now get them back: ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blips.out")); System.out.println("Recovering b1:"); b1 = (Blip1)in.readObject(); // OOPS! Throws an exception: //! System.out.println("Recovering b2:"); //! b2 = (Blip2)in.readObject(); } catch(Exception e) { e.printStackTrace(); } } } ///:~
该措施输出如下:
Constructing objects: Blip1 Constructor Blip2 Constructor Saving objects: Blip1.writeExternal Blip2.writeExternal Recovering b1: Blip1 Constructor Blip1.readExternal
未规复Blip2工具的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“民众的”(public),Blip2的构建器则否则,这样便会在规复时造成违例。试试将Blip2的构建器属性酿成“public”,然后删除//!注释标志,看看是否能获得正确的功效。
规复b1后,会挪用Blip1默认构建器。这与规复一个Serializable(可序列化)工具差异。在后者的环境下,工具完全以它生存下来的二进制位为基本规复,不存在构建器挪用。而对一个Externalizable工具,所有普通的默认构建行为城市产生(包罗在字段界说时的初始化),并且会挪用readExternal()。必需留意这一事实——出格留意所有默认的构建行为城市举办——不然很难在本身的Externalizable工具中发生正确的行为。
下面这个例子展现了生存和规复一个Externalizable工具必需做的全部工作:
//: Blip3.java // Reconstructing an externalizable object import java.io.*; import java.util.*; class Blip3 implements Externalizable { int i; String s; // No initialization public Blip3() { System.out.println("Blip3 Constructor"); // s, i not initialized } public Blip3(String x, int a) { System.out.println("Blip3(String x, int a)"); s = x; i = a; // s & i initialized only in non-default // constructor. } public String toString() { return s + i; } public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Blip3.writeExternal"); // You must do this: out.writeObject(s); out.writeInt(i); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Blip3.readExternal"); // You must do this: s = (String)in.readObject(); i =in.readInt(); } public static void main(String[] args) { System.out.println("Constructing objects:"); Blip3 b3 = new Blip3("A String ", 47); System.out.println(b3.toString()); try { ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blip3.out")); System.out.println("Saving object:"); o.writeObject(b3); o.close(); // Now get it back: ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blip3.out")); System.out.println("Recovering b3:"); b3 = (Blip3)in.readObject(); System.out.println(b3.toString()); } catch(Exception e) { e.printStackTrace(); } } } ///:~
#p#分页标题#e#
个中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着如果不在readExternal中初始化s和i,它们就会成为null(因为在工具建设的第一步中已将工具的存储空间排除为1)。若注释掉跟从于“You must do this”后头的两行代码,并运行措施,就会发明当工具规复今后,s是null,而i是零。
若从一个Externalizable工具担任,凡是需要挪用writeExternal()和readExternal()的基本类版本,以便正确地生存和规复基本类组件。
所觉得了让一切正常运作起来,千万不行仅在writeExternal()要领执行期间写入工具的重要数据(没有默认的行为可用来为一个Externalizable工具写入所有成员工具)的,而是必需在readExternal()要领中也规复那些数据。初次操纵时大概会有些不习惯,因为Externalizable工具的默认构建行为使其看起来好像正在举办某种存储与规复操纵。但实情并非如此。
1. transient(姑且)要害字
节制序列化进程时,大概有一个特定的子工具不肯让Java的序列化机制自动生存与规复。一般地,若谁人子工具包括了不想序列化的敏感信息(如暗码),就谋面对这种环境。纵然那种信息在工具中具有“private”(私有)属性,但一旦经序列化处理惩罚,人们就可以通过读取一个文件,可能拦截网络传输获得它。
为防备工具的敏感部门被序列化,一个步伐是将本身的类实现为Externalizable,就象前面展示的那样。这样一来,没有任何对象可以自动序列化,只能在writeExternal()明晰序列化那些需要的部门。
然而,若操纵的是一个Serializable工具,所有序列化操纵城市自动举办。为办理这个问题,可以用transient(姑且)逐个字段地封锁序列化,它的意思是“不要贫苦你(指自念头制)生存或规复它了——我会本身处理惩罚的”。
譬喻,假设一个Login工具包括了与一个特定的登录会话有关的信息。校验登录的正当性时,一般都想将数据生存下来,但不包罗暗码。为做到这一点,最简朴的步伐是实现Serializable,并将password字段设为transient。下面是详细的代码:
//: Logon.java // Demonstrates the "transient" keyword import java.io.*; import java.util.*; class Logon implements Serializable { private Date date = new Date(); private String username; private transient String password; Logon(String name, String pwd) { username = name; password = pwd; } public String toString() { String pwd = (password == null) ? "(n/a)" : password; return "logon info: \n " + "username: " + username + "\n date: " + date.toString() + "\n password: " + pwd; } public static void main(String[] args) { Logon a = new Logon("Hulk", "myLittlePony"); System.out.println( "logon a = " + a); try { ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Logon.out")); o.writeObject(a); o.close(); // Delay: int seconds = 5; long t = System.currentTimeMillis() + seconds * 1000; while(System.currentTimeMillis() < t) ; // Now get them back: ObjectInputStream in = new ObjectInputStream( new FileInputStream("Logon.out")); System.out.println( "Recovering object at " + new Date()); a = (Logon)in.readObject(); System.out.println( "logon a = " + a); } catch(Exception e) { e.printStackTrace(); } } } ///:~
可以看到,个中的date和username字段保持原始状态(未设成transient),所以会自动序列化。然而,password被设为transient,所以不会自动生存到磁盘;别的,自动序列化机制也不会作规复它的实验。输出如下:
logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: myLittlePony Recovering object at Sun Mar 23 18:25:59 PST 1997 logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 password: (n/a)
#p#分页标题#e#
一旦工具规复本钱来的样子,password字段就会酿成null。留意必需用toString()查抄password是否为null,因为若用过载的“+”运算符来装配一个String工具,并且谁人运算符碰着一个null句柄,就会造成一个名为NullPointerException的违例(新版Java大概会提供制止这个问题的代码)。
我们也发明date字段被生存到磁盘,并从磁盘规复,没有从头生成。
由于Externalizable工具默认时不生存它的任何字段,所以transient要害字只能陪伴Serializable利用。
2. Externalizable的替代要领
若不是出格在意要实现Externalizable接口,尚有另一种要领可供选用。我们可以实现Serializable接口,并添加(留意是“添加”,而非“包围”可能“实现”)名为writeObject()和readObject()的要领。一旦工具被序列化可能从头装配,就会别离挪用那两个要领。也就是说,只要提供了这两个要领,就会优先利用它们,而不思量默认的序列化机制。
这些要领必需含有下列精确的签名:
private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
从设计的角度出发,环境变得有些扑朔迷离。首先,各人大概认为这些要领不属于基本类可能Serializable接口的一部门,它们应该在本身的接口中获得界说。但请留意它们被界说成“private”,这意味着它们只能由这个类的其他成员挪用。然而,我们实际并不从这个类的其他成员中挪用它们,而是由ObjectOutputStream和ObjectInputStream的writeObject()及readObject()要领来挪用我们工具的writeObject()和readObject()要领(留意我在这里用了很大的抑制力来制止利用沟通的要领名——因为怕夹杂)。各人大概奇怪ObjectOutputStream和ObjectInputStream如何有权会见我们的类的private要领——只能认为这是序列化机制玩的一个花招。
在任何环境下,接口中的界说的任何对象城市自动具有public属性,所以假使writeObject()和readObject()必需为private,那么它们不能成为接口(interface)的一部门。但由于我们精确地加上了签名,所以最终的结果实际与实现一个接口是沟通的。
看起来好像我们挪用ObjectOutputStream.writeObject()的时候,我们通报给它的Serializable工具好像会被查抄是否实现了本身的writeObject()。若谜底是必定的是,便会跳过通例的序列化进程,并挪用writeObject()。readObject()也会碰着同样的环境。
还存在另一个问题。在我们的writeObject()内部,可以挪用defaultWriteObject(),从而抉择采纳默认的writeObject()动作。雷同地,在readObject()内部,可以挪用defaultReadObject()。下面这个简朴的例子演示了如何对一个Serializable工具的存储与规复举办节制:
//: SerialCtl.java // Controlling serialization by adding your own // writeObject() and readObject() methods. import java.io.*; public class SerialCtl implements Serializable { String a; transient String b; public SerialCtl(String aa, String bb) { a = "Not Transient: " + aa; b = "Transient: " + bb; } public String toString() { return a + "\n" + 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) { SerialCtl sc = new SerialCtl("Test1", "Test2"); System.out.println("Before:\n" + sc); ByteArrayOutputStream buf = new ByteArrayOutputStream(); try { ObjectOutputStream o = new ObjectOutputStream(buf); o.writeObject(sc); // Now get it back: ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buf.toByteArray())); SerialCtl sc2 = (SerialCtl)in.readObject(); System.out.println("After:\n" + sc2); } catch(Exception e) { e.printStackTrace(); } } } ///:~
在这个例子中,一个String保持原始状态,其他设为transient(姑且),以便证明非姑且字段会被defaultWriteObject()要领自动生存,而transient字段必需在措施中明晰生存和规复。字段是在构建器内部初始化的,而不是在界说的时候,这证明白它们不会在从头装配的时候被某些自动化机制初始化。
若筹备通过默认机制写入工具的非transient部门,那么必需挪用defaultWriteObject(),令其作为writeObject()中的第一个操纵;并挪用defaultReadObject(),令其作为readObject()的第一个操纵。这些都是不常见的挪用要领。举个例子来说,当我们为一个ObjectOutputStream挪用defaultWriteObject()的时候,并且没有为其通报参数,就需要采纳这种操纵,使其知道工具的句柄以及如何写入所有非transient的部门。这种做法很是未便。
transient工具的存储与规复回收了我们更熟悉的代码。此刻思量一下会产生一些什么工作。在main()中会建设一个SerialCtl工具,随后会序列化到一个ObjectOutputStream里(留意这种环境下利用的是一个缓冲区,而非文件——与ObjectOutputStream完全一致)。正式的序列化操纵是在下面这行代码里产生的:
o.writeObject(sc);
个中,writeObject()要领必需核查sc,判定它是否有本身的writeObject()要领(不是查抄它的接口——它基础就没有,也不是查抄类的范例,而是操作反射要领实际搜索要领)。若谜底是必定的,就利用谁人要领。雷同的环境也会在readObject()上产生。或者这是办理问题独一实际的要领,但确实显得有些离奇。
3. 版本问题
有时候大概想改变一个可序列化的类的版本(好比原始类的工具大概生存在数据库中)。尽量这种做法获得了支持,但一般只应在很是非凡的环境下才用它。另外,它要求操纵者对背后的道理有一个较量深的认识,而我们在这里还不想到达这种深度。JDK 1.1的HTML文档对这一主题举办了很是全面的阐述(可从Sun公司下载,但大概也成了Java开拓包联机文档的一部门)。