Java IO系列2 InputStream之ObjectInputStream
一、对象序列化
Java.io.ObjectOutputStream
void writeObject( Object object)
写出指定对象到ObjectOutputStream,这个方法将存储指定对象的类、类的签名以及这个类及其超类中所有的的非静态和非瞬时的域的值。
Java.io.ObjectInputStream
void readObject()
从ObjectInputStream中读入一个对象,读回对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时域的值,它执行的反序列化允许恢复多个对象引用。
1.Serializable接口
1.当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的,否则反序列化时抛出InvalidClassException。如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的Field值不会序列化到二进制流中
2.对象引用序列化
以下内容引用李刚的《疯狂Java讲义》
如果把上面的三个对象都序列化了,共享的Person怎么序列化呢!
其实Java序列化机制采用了一种特殊的序列化算法,
1、所有保存到磁盘中的对象都有有一个序列化编号。
2、当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化,只有该对象从未(在本次虚拟机中)被序列化过,系统才会讲过该对象转换成字节序列并输出。
3、如果某个对象已经序列化过,程序只是直接输出一个序列化编号,而不是再次重新序列化该对象。
由于该机制,如果多次序列化同一个对象时,只有第一次序列化时才会把该对象转换成字节序列输出,但是序列化一个可变对象时,只有第一次使用writeObject( )方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeOject()方法时,程序只是输出前面的序列化编号,即使后面的Field值已经改变,改变的Field值也不会输出
2. writeObject()方法与readObject()方法
当某个字段被声明为transient后,默认序列化机制就会忽略该字段,除了将transitive关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在Person类中添加两个方法:writeObject()与readObject(),如下所示:
在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,如5.1节所述,此时会忽略掉age字段。然后再调用writeInt()方法显示地将age字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。再次执行SimpleSerial应用程序,则又会有如下输出:
arg constructor
[John, 31, MALE]
必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。
序列化文件的格式
Java.io.ObjectStreamConstants
3.Externalizable接口
调用被序列化类的无参数构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,所以,实现
Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
4.readResolve()方法与writeReplace()方法
当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。此时对第2节使用的Person类进行修改,使其实现Singleton模式,如下所示:
同时要修改SimpleSerial应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:
执行上述应用程序后会得到如下结果:
arg constructor
[John, 31, MALE]
false
值得注意的是,从文件person.out中获取的Person对象与Person类中的单例对象并不相等。为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象,如下所示:
再次执行本节的SimpleSerial应用后将如下输出:
arg constructor
[John, 31, MALE]
true
无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
当进行序列化的时候:
首先JVM会先调用writeReplace方法,在这个阶段,我们可以进行张冠李戴,将需要进行序列化的对象换成我们指定的对象.
跟着JVM将调用writeObject方法,来将对象中的属性一个个进行序列化,我们可以在这个方法中控制住哪些属性需要序列化.
当反序列化的时候:
JVM会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,反序列化回来.
然后在readResolve方法中,我们也可以指定JVM返回我们特定的对象(不是刚刚序列化回来的对象).
注意到在writeReplace和readResolve,我们可以严格控制singleton的对象,在同一个JVM中完完全全只有唯一的对象,控制不让singleton对象产生副本.
Java.io.ObjectOutputStream
void writeObject( Object object)
写出指定对象到ObjectOutputStream,这个方法将存储指定对象的类、类的签名以及这个类及其超类中所有的的非静态和非瞬时的域的值。
Java.io.ObjectInputStream
void readObject()
从ObjectInputStream中读入一个对象,读回对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时域的值,它执行的反序列化允许恢复多个对象引用。
1.Serializable接口
1.当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的,否则反序列化时抛出InvalidClassException。如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的Field值不会序列化到二进制流中
public class Computer {
public String name;
public int numKernel;
private String ip;
public Computer(String name,int numKernel) {
System.out.println("Computer initialization ");
}
@Override
public String toString() {
return "Computer [name=" + name + ", numKernel=" + numKernel + ", ip=" + ip + "]";
}
}
public class LenovoComputer extends Computer implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
// public LenovoComputer(){
// System.out.println("LenovoComputer 无参数 initialization ");
// }
public LenovoComputer(String name,int kernel){
super(name, kernel);
this.name = name ;
this.numKernel = kernel;
System.out.println("LenovoComputer initialization ");
}
@Override
public String toString() {
super.toString();
return "LenovoComputer [name=" + name + ", numKernel=" + numKernel + "]";
}
}
public class ObjectStreamDemo implements IDemo{
@Override
public void test() {
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("Object.txt"));
LenovoComputer computer = new LenovoComputer("Lenovo", 4);
outputStream.writeObject(computer);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("Object.txt"));
try {
computer = (LenovoComputer) inputStream.readObject();
System.out.println(computer.toString());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Computer initialization
LenovoComputer initialization
java.io.InvalidClassException: datastream.LenovoComputer; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:147)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:755)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
at datastream.ObjectStreamDemo.test(ObjectStreamDemo.java:23)
2.对象引用序列化
以下内容引用李刚的《疯狂Java讲义》
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
public class Teacher implements Serializable {
private String name;
private Person student;
public Teacher(String name, Person student) {
super();
this.name = name;
this.student = student;
}
}
Person per = new Person("悟空", 500);
Teacher t1 = new Teacher("唐僧", per);
Teacher t2 = new Teacher("菩提老祖", per);
如果把上面的三个对象都序列化了,共享的Person怎么序列化呢!
其实Java序列化机制采用了一种特殊的序列化算法,
1、所有保存到磁盘中的对象都有有一个序列化编号。
2、当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化,只有该对象从未(在本次虚拟机中)被序列化过,系统才会讲过该对象转换成字节序列并输出。
3、如果某个对象已经序列化过,程序只是直接输出一个序列化编号,而不是再次重新序列化该对象。
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
由于该机制,如果多次序列化同一个对象时,只有第一次序列化时才会把该对象转换成字节序列输出,但是序列化一个可变对象时,只有第一次使用writeObject( )方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeOject()方法时,程序只是输出前面的序列化编号,即使后面的Field值已经改变,改变的Field值也不会输出
2. writeObject()方法与readObject()方法
当某个字段被声明为transient后,默认序列化机制就会忽略该字段,除了将transitive关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在Person类中添加两个方法:writeObject()与readObject(),如下所示:
public class Person implements Serializable {
...
transient private Integer age = null;
...
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
}
在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,如5.1节所述,此时会忽略掉age字段。然后再调用writeInt()方法显示地将age字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。再次执行SimpleSerial应用程序,则又会有如下输出:
arg constructor
[John, 31, MALE]
必须注意地是,writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。详情可以看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。
序列化文件的格式
Java.io.ObjectStreamConstants
3.Externalizable接口
<span style="font-size:12px;">public interface Externalizable extends java.io.Serializable {
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or
* calling the writeObject method of ObjectOutput for objects, strings,
* and arrays.
*
* @serialData Overriding methods should use this tag to describe
* the data layout of this Externalizable object.
* List the sequence of element types and, if possible,
* relate the element to a public/protected field and/or
* method of this Externalizable class.
*
* @param out the stream to write the object to
* @exception IOException Includes any I/O exceptions that may occur
*/
void writeExternal(ObjectOutput out) throws IOException;
/**
* The object implements the readExternal method to restore its
* contents by calling the methods of DataInput for primitive
* types and readObject for objects, strings and arrays. The
* readExternal method must read the values in the same sequence
* and with the same types as were written by writeExternal.
*
* @param in the stream to read data from in order to restore the object
* @exception IOException if I/O errors occur
* @exception ClassNotFoundException If the class for an object being
* restored cannot be found.
*/
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}</span>
实现Externalizable接口后,之前基于Serializable接口 的序列化机制就将失效,序列化的细节由程序员自己实现,当读取对象时,会
调用被序列化类的无参数构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,所以,实现
Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
4.readResolve()方法与writeReplace()方法
当我们使用Singleton模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能略有不同。此时对第2节使用的Person类进行修改,使其实现Singleton模式,如下所示:
public class Person implements Serializable {
private static class InstanceHolder {
private static final Person instatnce = new Person("John", 31, Gender.MALE);
}
public static Person getInstance() {
return InstanceHolder.instatnce;
}
private String name = null;
private Integer age = null;
private Gender gender = null;
private Person() {
System.out.println("none-arg constructor");
}
private Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
...
}
同时要修改SimpleSerial应用,使得能够保存/获取上述单例对象,并进行对象相等性比较,如下代码所示:
public class SimpleSerial {
public static void main(String[] args) throws Exception {
File file = new File("person.out");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
oout.writeObject(Person.getInstance()); // 保存单例对象
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oin.readObject();
oin.close();
System.out.println(newPerson);
System.out.println(Person.getInstance() == newPerson); // 将获取的对象与Person类中的单例对象进行相等性比较
}
}
执行上述应用程序后会得到如下结果:
arg constructor
[John, 31, MALE]
false
值得注意的是,从文件person.out中获取的Person对象与Person类中的单例对象并不相等。为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象,如下所示:
public class Person implements Serializable {
private static class InstanceHolder {
private static final Person instatnce = new Person("John", 31, Gender.MALE);
}
public static Person getInstance() {
return InstanceHolder.instatnce;
}
private String name = null;
private Integer age = null;
private Gender gender = null;
private Person() {
System.out.println("none-arg constructor");
}
private Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
private Object readResolve() throws ObjectStreamException {
return InstanceHolder.instatnce;
}
...
}
再次执行本节的SimpleSerial应用后将如下输出:
arg constructor
[John, 31, MALE]
true
无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
当进行序列化的时候:
首先JVM会先调用writeReplace方法,在这个阶段,我们可以进行张冠李戴,将需要进行序列化的对象换成我们指定的对象.
跟着JVM将调用writeObject方法,来将对象中的属性一个个进行序列化,我们可以在这个方法中控制住哪些属性需要序列化.
当反序列化的时候:
JVM会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,反序列化回来.
然后在readResolve方法中,我们也可以指定JVM返回我们特定的对象(不是刚刚序列化回来的对象).
注意到在writeReplace和readResolve,我们可以严格控制singleton的对象,在同一个JVM中完完全全只有唯一的对象,控制不让singleton对象产生副本.