在软件开发中,我们常常需要将某些对象的值保存到文件中,当我们在下一次加载该软件时,就能够将通过读文件来将程序中需要用到的对象的状态还原,这是非常有用的一项技术
1、在java的Io系统中存在着这样的面向对象的流——ObjectInputStream和ObjectOutputStream类,这两个类含有多种方法:如用于存储对象的
public final void writeObject()throws IOException/
public final Object readObject()throws IOException,
ClassNotFoundException/
同时这两个类又是基于数据流对象的(也是基于“字节流”的),其中还有以下的专门用来读取数值的方法wirteInt()/readDouble()……….
这和DataInputStream、DataOutputStream类中的一些方法是一样的;之所以这样,体现了一切皆对象的思想
还有flush()方法
2、面向对象的流类的构造方法
public ObjectInputStream(InputStream in)
public ObjectOutputStream(OutputStream out)
通常都是用FileInputStream和FileOutputStream对象来作为参数,通过调用writeObject(想要保存的对象名)/readObject()方法就能够对对象进行读写了,如:
class Student implements Serializable{………..}
//定义Student类,同时实现Serializable接口
Student std = new Student() ;
ObjectOutputStream objectWriter =
new ObjectOutputStream(new FileOutputStream(“data.txt”)) ;
objectWriter.writeObject(std) ; //将std读入文件
objectWriter.close();
ObjectInputStream objectReader =
new ObjectInputStream(new FileInputStream(“data.txt”)) ;
Student std2 = (Student)objectReader.readObject() ;
objectReader.close();
//从文件中将保存的对象读出来
3、这就叫做对象的序列化,但是一个对象要想能够进行序列化,就必须实现
Serializable接口;这个接口中没有任何的方法,也就是说,这个接口实际上就是一个标记接口;只要让一个类实现这个接口就能够实现序列化,而且在该类中并不需要重写任何方法;
4、像上面的那样,通过实现Serializable接口来实现对象的序列化的方式,是一种默认的方式,在实际当中,我们可能处于安全的考虑,不想将对象的全部信息都写道文件当中,或者我们希望将对象中的某些敏感的数据通过加密后再写到文件当中,之后在读取文件对象时,再经过解密后,再将数据读到对象的成员变量中;
也就是说,我们实际上想要自定义对象的序列化方式;这就需要用到继承了Serializable接口的Externalizble接口;在这个接口中有两个方法
public void writeExternal(ObjectOutput out)throws IOException
public void readExternal(ObjectInput in)throws IOException,
ClassNotFoundException
如果想要自定义一个对象的序列化方式,就要在类定义时,实现这个接口,并且重写这两个方法;在这两个方法的内部可以实现自定义的序列化,如:
class Student implements Externalizable
{
int a ;
String id ;
double d ;
public void writeExternal(ObjectOutput out)
throws IOException{ //重写方法
try{
out.writeInt(a); //只是将成员变量a和id写入文件当中
out.writeObject(id) ;
}catch(Exception
e){
System.out.println(“Err“);
}
}
public void readExternal(ObjectInput in)throws IOException,
ClassNotFoundException{
try{
a= readInt() ;
id = (String)readObject() ;
} catch(Exception e){
System.out.println(“NO”);
}
}
}
public class Count{
public void main(String [] args){
Student std = new Student() ;
ObjectOutputStrean writeFile =
new ObjectOutputStream(new FileOutputStream(“data.txt”));
writeFile.writeObject(std) ;
writeFile.close() ;
ObjectInputStream readFile =
new ObjectInputStream(new FileInputStream(“data.txt”));
Student std2 = (Student)readFile.readObject() ;
readFile.close() ; }
5、实际上工作的机理是,当ObjectOutputStream或者ObjectInputStream对象调用writeObject()和readObject()方法时,程序会自动调用对应对象中重写的writeExternal()和readExternal()方法,从而实现了自定义序列化;
6、在两种序列化的方式中,只存在区别的,
①当通过默认的方式实现对象的序列化的过程中:在ObjectInputStream流对象调用readObject()方法来恢复一个对像,并将对象的引用返回的过程中,并没有隐含的调用对象的任何构造方法;也就是说程序是完全按照二进制流中的数据来创建一个对象的
②但是自定义的序列化方式不同:在ObjectInputStream流对象调用readObject方法来恢复一个对象,并将对象的引用返回的过程中,实际上隐含的调用了对象的不带参数的构造方法,之后才从流中读取数据来还原对象的属性;
③所以用自定义的方式时,必须确保对应的类中有属性为public的不带参数的构造方法,否则程序是不能够正常的将对象还原点的
7、java中的对象的序列化机制是非常的有趣的,之所以这样说,是因为这种序列化的机制,不仅能够将指定的对象序列化,同时还能够追踪指定对象中含有的所有的其他对象的引用,将这些引用指向的对象也序列化(如果这些对象也实现了指定的Serializable接口或Externalizble接口);而这个过程是程序自动实现的
这个例子见《Thinking in java 》
8、上面的这个例子是通过第一种默认的方式来说明“序列化对象网”的;
自定义的方式,照样用也可以有这样的效果
9、对象的版本化
我们知道很多的软件都有升级的版本,很多时候,程序中的类会不断的演化,当类定义发生变化时,保存在文件中的类的原来版本的数据大部分将无法读入到新的类中了,那么这样就会形成不兼容,而我们需要的结果是,还能够用旧版本对象存在文件中的数据来为新对象中的旧的数据赋值,对于新增加的成员数据在通过其他方法进行恢复;如果要实现这样的效果的话,就需要了解对象的版本化问题:在java运行过程中实际上会默认的为每个类生成一个唯一对应的版本号serialVersionUID;当我们将一个对象读入文件中时,实际上也告诉了文件当前存入的对象所属类的版本号;当我们改变了这个类的定义(比如添加了一些新的方法或成员变量)时,类的版本号就会改变,这样但我们通过读取旧文件中的数据来恢复新的对象时,显然会因为新旧对象的版本号不匹配,而导致程序不能正常执行。
实际上解决这个问题是非常的简单的,那就是为类指定一个固定的版本号,当我们改变这个类的定义时,不去改变这个版本号就行了,
在上面的这个例子中已经使用了这个方法,就是在类的定义中加上一个量:
public final long serialVersionUID = -3424249794808075076L;
这个serialVersionUID实际上是指定了这个类的版本号;这样一来不管对应类如何改变,版本号都是serialVersionUID;这样就能够顺利的解决这个问题;
但是这个解决方法在图像化的java程序中有时并不好用,解决方法还需另行寻找