
今天写实验时遇到了Serializable,但对这个接口确实不甚了解,于是就去查看了jdk文档,顺便记录我对这个Serializable接口的理解。
首先我们在文档中看到java.io.Serializable这个接口有非常多的子接口:
序列化AdapterActivator, Attribute, Attribute, Attributes, BindingIterator, ClientRequestInfo, ClientRequestInterceptor, Codec, CodecFactory, Control, Current, Current, Current, CustomValue, DataInputStream, DataOutputStream, Descriptor, DHPrivateKey, DHPublicKey, DocAttribute, DomainManager, DSAPrivateKey, DSAPublicKey, DynAny, DynAnyFactory, DynArray, DynEnum, DynFixed, DynSequence, DynStruct, DynUnion, DynValue, DynValueBox, DynValueCommon, ECPrivateKey, ECPublicKey, ExtendedRequest, ExtendedResponse, Externalizable, IdAssignmentPolicy, IDLEntity, IDLType, IdUniquenessPolicy, ImplicitActivationPolicy, Interceptor, IORInfo, IORInterceptor, IORInterceptor_3_0, IRObject, Key, LifespanPolicy, Name, NamingContext, NamingContextExt, NotificationFilter, ObjectReferenceFactory, ObjectReferenceTemplate, ORBInitializer, ORBInitInfo, PBEKey, POA, POAManager, Policy, PolicyFactory, PrintJobAttribute, PrintRequestAttribute, PrintServiceAttribute, PrivateKey, PublicKey, QueryExp, RelationType, RemoteRef, RequestInfo, RequestProcessingPolicy, RSAMultiPrimePrivateCrtKey, RSAPrivateCrtKey, RSAPrivateKey, RSAPublicKey, RunTime, SecretKey, ServantActivator, ServantLocator, ServantManager, ServantRetentionPolicy, ServerRef, ServerRequestInfo, ServerRequestInterceptor, Streamablevalue, SupportedValuesAttribute, ThreadPolicy, UnsolicitedNotification, Valuebase, ValueExp
但是这个接口中一个成员函数或者成员变量都没有,那么这个接口到底有什么作用呢?
官方文档对其的定义是:类通过实现java.io.Serializable接口以启用其序列化功能。未实现此接口的类无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于表示可序列化的语义。
从上面的描述中,我们可以知道Serializable接口的作用就是实现序列化。
什么是序列化?序列化和反序列化有什么作用呢?序列化就是把Java对象转化为字节序列的过程。与之相对应的反序列化就是把字节序列恢复为Java对象的过程。
对象的寿命通常随着生成该对象的程序的终止而终止,有时候需要把在内存中的各种对象的状态(也就是实例变量,不是方法)保存下来,并且可以在需要时再将对象恢复。而Java就为我们提供了这样的机制,即序列化。
所以说,序列化最重要的作用就是在传递和保存对象时保证对象的完整性和可传递性。对象转换成为有序字节流,以便于在网络上传输或者保存到本地。
反序列化的最重要作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
总结:核心作用就是对象状态的保存和重建,Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里(系列化),并且可以从其它地方把该Byte 流里的数据读出来(反序列化)。
所以说,序列化的用途其实很明确:
- 当你想把内存中的对象状态保存到文件或者数据库中时,请使用序列化。
- 当你想把对象通过网络传播出去时,请使用序列化。
类通过实现java.io.Serializable接口以启用其序列化功能。
官方文档很明确的告诉了我们只要一个类实现了Serializable接口,那么这个类就被序列化了。
class User implements Serializable{
private static final long serialVersionUID=1L;//稍后我们会说明这一行代码是做什么的。
String name;
int age;
public User(String name,int age){
this.name=name;
this.age=age;
}
public String toString(){
return "name"+name+"tage:"+age;
}
}
通过ObjectOutputStream 的writeObject()方法可以把这个类的对象写到一个地方(文件),再通过ObjectInputStream 的readObject()方法把这个对象读出来。
File file = new File("file"+File.separator+"out.txt");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(fos);
User user = new User("tom", 22);
System.out.println(user);
oos.writeObject(user); //写入对象
oos.flush();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
oos.close();
} catch (IOException e) {
System.out.println("oos关闭失败:"+e.getMessage());
}
}
} catch (FileNotFoundException e) {
System.out.println("找不到文件:"+e.getMessage());
} finally{
try {
fos.close();
} catch (IOException e) {
System.out.println("fos关闭失败:"+e.getMessage());
}
}
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(fis);
try {
User user = (User)ois.readObject(); //读出对象
System.out.println(user);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
ois.close();
} catch (IOException e) {
System.out.println("ois关闭失败:"+e.getMessage());
}
}
} catch (FileNotFoundException e) {
System.out.println("找不到文件:"+e.getMessage());
} finally{
try {
fis.close();
} catch (IOException e) {
System.out.println("fis关闭失败:"+e.getMessage());
}
}
输出结果:
name:tom age:22 name:tom age:22
结果完全一样。如果我把User类中的implements Serializable 去掉,User类就不能序列化了。此时再运行上述程序,就会报java.io.NotSerializableException异常。
serialVersionUID注意到上面程序中有一个 serialVersionUID ,实现了Serializable接口之后,Eclipse就会提示你增加一个 serialVersionUID,虽然不加的话上述程序依然能够正常运行。
序列化 ID 在 Eclipse 下提供了两种生成策略:
- 一个是固定的1L
- 一个随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具,根据类名、接口名、成员方法及属性等来生成)
上面程序中,输出对象和读入对象使用的是同一个User类。
如果是通过网络传输的话,如果User类的serialVersionUID不一致,那么反序列化就不能正常进行。例如在客户端A中User类的serialVersionUID=1L,而在客户端B中User类的serialVersionUID=2L 那么就不能重构这个User对象。
客户端A中User类
class User implements Serializable{
private static final long serialVersionUID = 1L;
String name;
int age;
public Usern(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"tage:"+age;
}
}
客户端B中User类
class User implements Serializable{
private static final long serialVersionUID = 2L;
String name;
int age;
public Usern(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"tage:"+age;
}
}
试图重构就会报java.io.InvalidClassException异常,因为这两个类的版本不一致,local class incompatible,重构就会出现错误。
如果没有特殊需求的话,使用用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
同时,强烈建议使用private修饰符显示声明serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类——serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
静态变量序列化序列化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且序列化保存的只是变量的值,对于变量的任何修饰符都不能保存。
如果把User类中的name定义为static类型的话,试图重构,就不能得到原来的值,只能得到null。说明对静态成员变量值是不保存的。这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
transient关键字经常在实现了 Serializable接口的类中能看见transient关键字。这个关键字并不常见。 transient关键字的作用是:阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
当某些变量不想被序列化,同是又不适合使用static关键字声明,那么此时就需要用transient关键字来声明该变量。
例如用 transient关键字修饰name变量
class User implements Serializable{
private static final long serialVersionUID = 1L;
transient String name;
int age;
public User(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "name:"+name+"tage:"+age;
}
}
在反序列化视图重构对象的时候,作用与static变量一样: 输出结果为:
name:null age:22
在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
注:对于某些类型的属性,其状态是瞬时的,这样的属性是无法保存其状态的。例如一个线程属性或需要访问IO、本地资源、网络资源等的属性,对于这些字段,我们必须用transient关键字标明,否则编译器将报措。
序列化中的继承问题- 当一个父类实现序列化,子类自动实现序列化,不需要显示实现Serializable接口。
- 一个子类实现了Serializable接口,它的父类都没有实现Serializable接口,要想将父类对象也序列化,就需要让父类也实现Serializable接口。
第二种情况中:如果父类不实现 Serializable接口的话,就需要有默认的无参的构造函数。这是因为一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。在反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。在这种情况下,在序列化时根据需要在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。
例如:
class People{
int num;
public People(){} //默认的无参构造函数,没有进行初始化
public People(int num){ //有参构造函数
this.num = num;
}
public String toString(){
return "num:"+num;
}
}
class User extends People implements Serializable{
private static final long serialVersionUID = 1L;
String name;
int age;
public User(int num,String name,int age){
super(num); //调用父类中的构造函数
this.name = name;
this.age = age;
}
public String toString(){
return super.toString()+"tname:"+name+"tage:"+age;
}
}
在一端写出对象的时候
User user = new User(10,"tom", 22); //调用带参数的构造函数num=10,name = "tim",age =22 System.out.println(user); oos.writeObject(user); //写出对象
在另一端读出对象的时候
User user = (User)ois.readObject(); //反序列化,调用父类中的无参构造函数。 System.out.println(user);
输出为
num:0 name:tom age:22
发现由于父类中无参构造函数并没有对num初始化,所以num使用默认值为0。
要允许不可序列化类的子类型序列化,可以假定该子类型负责保存和恢复超类型的公用 (public)、受保护的 (protected) 和(如果可访问)包 (package) 字段的状态。仅在子类型扩展的类有一个可访问的无参数构造方法来初始化该类的状态时,才可以假定子类型有此职责。如果不是这种情况,则声明一个类为可序列化类是错误的。该错误将在运行时检测到。
在反序列化过程中,将使用该类的公用或受保护的无参数构造方法初始化不可序列化类的字段。可序列化的子类必须能够访问无参数构造方法。可序列化子类的字段将从该流中恢复。
当遍历一个图形时,可能会遇到不支持 Serializable 接口的对象。在此情况下,将抛出 NotSerializableException,并将标识不可序列化对象的类。
在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException;
writeObject 方法负责写入特定类的对象的状态,以便相应的 readObject 方法可以恢复它。通过调用 out.defaultWriteObject 可以调用保存 Object 的字段的默认机制。该方法本身不需要涉及属于其超类或子类的状态。通过使用 writeObject 方法或使用 DataOutput 支持的用于基本数据类型的方法将各个字段写入 ObjectOutputStream,状态可以被保存。
readObject 方法负责从流中读取并恢复类字段。它可以调用 in.defaultReadObject 来调用默认机制,以恢复对象的非静态和非瞬态字段。defaultReadObject 方法使用流中的信息来分配流中通过当前对象中相应指定字段保存的对象的字段。这用于处理类演化后需要添加新字段的情形。该方法本身不需要涉及属于其超类或子类的状态。通过使用 writeObject 方法或使用 DataOutput 支持的用于基本数据类型的方法将各个字段写入 ObjectOutputStream,状态可以被保存。
在序列化流不列出给定类作为将被反序列化对象的超类的情况下,readObjectNoData 方法负责初始化特定类的对象状态。这在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时发生。在序列化流已经被篡改时也将发生;因此,不管源流是“敌意的”还是不完整的,readObjectNoData 方法都可以用来正确地初始化反序列化的对象。
将对象写入流时需要指定要使用的替代对象的可序列化类,应使用准确的签名来实现此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplace 方法将由序列化调用,前提是如果此方法存在,而且它可以通过被序列化对象的类中定义的一个方法访问。因此,该方法可以拥有私有 (private)、受保护的 (protected) 和包私有 (package-private) 访问。子类对此方法的访问遵循 java 访问规则。
在从流中读取类的一个实例时需要指定替代的类应使用的准确签名来实现此特殊方法。
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
此 readResolve 方法遵循与 writeReplace 相同的调用规则和访问规则。
总结序列化给我们提供了一种技术,用于保存对象的变量和传输。虽然也可以使用别的一些方法实现同样的功能,但是Java给我们提供的方法使用起来是非常方便的。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)