欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

<4> Object Serialization和ObjectOutputStream/ObjectInputStream

程序员文章站 2022-04-03 19:53:10
...
[续...]
九、Object Serialization
前面章节介绍的都是如何读写java基本类型(byte,int,String等),但是java是面向对象的语言,必然有方便处理对象的IO。

对象序列化首先是在RMI(Remote Method Invocation)中使用的,后来在JavaBean中使用。java.io.ObjectOutputStream类提供了writeObject()方法把java对象输出到stream。java.io.ObjectInputStream类提供了readObject()方法从stream里读取对象。

9.1 Reading and Writing Objects
对象序列化(Object serialization)以byte序列保存对象的状态,根据保存的信息可以重组对象。java的序列化最初是为RMI设计的。RMI允许一台虚拟机上的对象调用另一台虚拟机上对象的方法。这需要有一种方式能把参数和返回值转变成字节流或转变自字节流,这就是对象序列化能提供的功能。

9.2 Object Streams
通过object output Stream来序列化对象,通过object input stream反序列化对象。
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants 
public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants


其中ObjectOutput接口是java.io.DataOutput的子接口,ObjectInput是java.io.DataInput的子接口。虽然这些都不是filter ouput stream, 但他们可以通过构造方法包装底层实际的数据流:
public ObjectOutputStream(OutputStream out) throws IOException 
public ObjectInputStream(InputStream in) throws IOException

比如:
try 
{ 
    Point p = new Point(34, 22); 
    FileOutputStream fout = new FileOutputStream("point.ser"); 
    ObjectOutputStream oout = new ObjectOutputStream(fout); 
    oout.writeObject(p); 
    oout.close(); 
} 
catch (Exception e) 
{
    System.err.println(e);
}

这样对象p的状态就被写到文件里保存起来了(也就是被序列化了)。然后我们可以反序列化,从保存状态的文件中把对象p还原:
try 
{ 
    FileInputStream fin = new FileInputStream("point.ser"); 
    ObjectInputStream oin = new ObjectInputStream(fin); 
    Object o = oin.readObject(); 
    Point p = (Point) o; 
    oin.close(); 
} 
catch (Exception e)  
{ 
    System.err.println(e); 
}


9.3 How Object Serialization Works
对象拥有属性。这些属性被保存在对象类的non-static,non-transient字段里。
如果要自己来实现把对象的属性写到流里,还要考虑父类也包含一些属性,读取的时候还要按照一定的顺序保证读写一致才能还原,这很复杂。幸运的是,Sun已经做了所有的工作。java1.1之后能读取对象的nonstatic,nontransient字段以良好的格式保存他们到流里。我们要做的就是把ObjectInputStream包装在你想把对象序列化到哪里的流(比如文件流,比如网络流)的外面,然后调用write方法就可以了。读取的时候需要显式的把对象转换成真正的类型,因为读取后返回的只是Object类型。

9.4 Performance
序列化通常在程序中是很简单的保存对象状态的方式,但是,序列化是非常低效的。如果你能为自己的程序定制一种文件格式,然后以这种格式保存程序的状态,这会比序列化快多了。
第二,序列化会延迟或阻止垃圾回收(garbage collection)。每次把对象输出到object output stream,stream会保存对这个对象的引用,直到stream被reset或close。这就意味着对象一直不能被回收,最糟糕的就是当这个stream在程序运行期间一直被打开。
解决办法就是保存对象后就关闭流,或调用流的reset方法。
public void reset() throws IOException


[b]9.5 The Serializable Interface[/b]
无条件的序列化会导致安全问题。比如,序列化能*的访问对象的private field. 通过把object ouput stream包装在byte ouput stream, hacker能把对象转成byte array. 这样就能*的操纵和修改对象了,然后再把篡改过的对象输出到input stream放到程序中。

安全性不是随意序列化的唯一问题,有些对象比如java.net.Socket,i/o stream只在运行的程序中有意义。这些类对象序列化没有任何意义。

基于一些考虑,java不允许任意的序列化,我们只能序列化实现了java.io.Serialaization接口的类。
public interface Serializable

这个接口没声明任何方法和属性,只是单纯的表示一个类可以被序列化。[b]实现了可以被序列化的父类的子类也潜在的允许序列化了[/b],所以我们看到很多没有显式实现这个接口的类也能被序列化。

9.5.1 Classes that implement Serializabele but are't
这个特别需要注意。一个原则上可以被序列化的类并不一定能序列化成功。好几个方面能阻止一个实现了Serializable接口的类被序列化,这时可能会抛出NotSerializableException:
public class NotSerializableException extends ObjectStreamException


Problem1: References to nonserializable objects
第一种常见的阻止一个可序列化的类被序列化的问题就是这个类包含了没有实现Serializable接口的对象引用(以及引用的引用的引用,直到引用链的末尾)。一个对象如果被序列化,那么它的所有属性,所有引用的对象也都必须能序列化。

Problem2: Missing a no-argument constructor in superclass
第二个常见的阻止可序列化类被反序列化的问题是这个类的父类不可序列化没有一个无参构造方法。java.lang.Object没有实现Serializable,所以所有的类至少有一个不可序列化的父类。当一个对象被反序列化时,这个类的继承树上最近的那个没有实现Serializable接口的父类会被调用来构造这个非序列化的父类的状态,此时如果这个父类没有无参构造方法,这个对象就不能被反序列化。

problem3: Deliberate(故意) throwing of NotSerializableException
有少部分类故意避免被序列化。有时候出于必要,比如安全或其他原因,即使一个类的父类实现了Serializable接口,子类可以选择故意在writeObject()方法里抛出NotSerializableException来避免被序列化。
private void writeObject(ObjectOutputStream out) throws IOException { 
    throw new NotSerializableException(); 
} 
private void readObject(ObjectInputStream in) throws IOException 
{ 
    throw new NotSerializableException(); 
}


9.5.2 Locating the offending object(定位导致失败的属性)
当序列化一个类的时候发生了NotSerializableException异常,这时候我们需要定位是哪个对象属性导致的,这是个很难的问题。但是NotSerializableException异常的detailMessage属性包含了这个unserialiazable类的名称,可以通过异常的getMessage()方法获取。这就减轻了很大的工作量。

9.5.3 Making nonserializable fields transient
当我们定位出了导致不能序列化的对象后,最简单的解决方案是把包含这个对象的属性设置成transient。

9.6 versioning
当一个对象被输出到stream,只有对象的状态和对象类的名字被保存下来,类定义的字节码并没有被保存。我们不能保证一个序列化的对象在相同的环境下被反序列化。序列化之后反序列化之前类的定义可能被修改。比如方法,构造方法,static和transient字段的变化。但并不是所有的变化都会阻止反序列化。比如一个类的static字段不会被序列化,所以无论是增加还是删除类的static字段都没有影响,同样序列化也会忽略掉类的所有方法。但是删除一个实例属性就会影响反序列化。这样就存在了兼容的和不兼容的修改。

9.6.1 Compatible and incompatible changes
兼容的修改就是不影响对象的序列化的修改,不兼容的修改就是那些能阻止反序列化的修改。

下面是兼容的修改的列表:
1.大多数对构造方法和实例方法的修改,不管是否为static。这里说的是大多数,例外的是跟序列化处理相关的方法,特别是跟writeObject()和readObject()方法相关的。
2.所有对static字段的修改,包括修改类型,名称,增加还是删除。序列化会忽略static字段。
3.所有对transient字段的修改。
4.增加实例字段。反序列化时,新增字段会被设置为默认值。
5.增加或删除类实现的接口(Serializable接口除外)。
6.增加和删除内部类。
7.修改字段的访问属性(public,private等)。
8.把字段从static修改为nonstatic,从transient到nontransient.这跟增加字段是一样的。

下面是非兼容的修改:
1. 修改类的名称。
2. 修改实例字段的类型。
3. 修改实例字段的名字。
4. 修改字段从nonstatic到static或nontransient到transient。这相当于删除实例字段。
5. 修改类的父类。这样可能影响对象继承的状态。
6. 以不兼容的方式修改writeObject()或readObject()方法。
7.修改类从Serializable到Externalizable或Externalizable到Serializable。

9.6.2 version ids
为了帮助识别兼容或不兼容的类修改,每个流可以有一个stream unique identifier, SUID for short,被保存到static字段里serialVersionUID.
每次发布类的不兼容修改的新版本时都应该修改这个字段。

参考:
《java I/O》 Elliotte Rusty Harold