static字段不会被序列化
//这是父类
class Data implements Serializable{
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
//这是子类,继承父类自动继承Serializable接口
class sub extends Data{
private static int m = 6;
private int k = 10;
public sub(int n) {
super(n);
}
}
父类Data有一个n的属性,通过构造器赋值
子类sub有额外的静态属性m,非静态属性k=10
类 | 父类属性 | 子类属性 |
---|---|---|
父类Data | n | |
子类sub | 继承来的n | 静态m=6,非静态k=10 |
假设我们构造一个子类对象 new sub(5);
那么当前子类对象的属性为
类 | 父类属性 | 子类属性 |
---|---|---|
父类Data | n = 5 | |
子类sub | 继承来的n=5 | 静态m=6,非静态k=10 |
按照序列化的结论,静态属性不会被序列化,那么猜测序列化再反序列化之后属性应该为
类 | 父类属性 | 子类属性 |
---|---|---|
父类Data | n | |
子类sub | 继承来的n(未初始化,默认为0) | 静态m=6,非静态k=10 |
这里我省略了序列化和反序列化的过程,直接上代码结果
序列化结果
这里只有k 和 n 因为 n没有初始化 所以不显示,所以第一个结论验证成功,这是当然的,因为序列化只是为了序列化对象的状态
tip:如果在一个JVM存活周期内,在本地JVM进行序列化和反序列化,是有可能读到n=10时的,这个n=10不是序列化得到的,而是JVM在方法区找到new出来对象时的n
反序列化什么时候需要一个空参构造器
这部分篇幅较长,涉及到源码部分,有兴趣的可以查看我的另一篇文章
从源码解析JAVA序列化是否需要空参构造方法
序列化会递归域对象的序列化
这个验证看起来可能会有点吃力,我用的《thinking in java》上的例子
class Data implements Serializable{
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
public class Worm implements Serializable{
private static Random rand = new Random(47);
private Data[] d = {
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10)),
new Data(rand.nextInt(10))
};
private Worm next;
private char c;
public Worm(int i,char x){
System.out.println("Worm constructor: " + i);
c = x;
if(-- i > 0){
next = new Worm(i , (char)(x+1));
}
}
public Worm(){
System.out.println("default constructor");
}
public String toString(){
StringBuilder result = new StringBuilder(":");
result.append(c);
result.append("(");
for(Data dat : d)
result.append(dat);
result.append(")");
if(next != null)
result.append(next);
return result.toString();
}
//序列化和反序列化调用的方法入口
public static void main(String[] args) throws IOException, ClassNotFoundException {
Worm w =new Worm(6,'a');
System.out.println("w = " + w);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject("Worm storage\n");
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
String s = (String)in.readObject();
Worm w2 = (Worm) in.readObject();
System.out.println(s + "w2 = " + w2);
}
}
简单描述一下,Worm(蠕虫)类似一个链表的结构,有一个next指针指向下一个Worm,而每一个Worm有三个Date是随机生成的,toString方法会将Worm及Worm链接的next都打印出来,这里我们指定new Worm(6);
使用调试工具
序列化前的worm,有6个节点,每个节点的data都是随机生成的,而序列化会保存域的对象,已经递归的序列化的对象,那么我们猜测反序列化回来的w应该也是同样的结构,直接上结果
w2是反序列化回来的结果,可以看出不仅对象的域相同,对象的next对象都同样序列化了
不指定序列号,编译器会自动生成一个UID
还是用老朋友,data类
class Data implements Serializable{
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
可以看到没有指定序列号,没有指定序列号的时候,编译器会自动根据Class哈希出一个UID,这样只要对类稍微改动则版本就会发生改变
Data w = new Data(10);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject(w);
out.close();
这里用旧Data类持久化一个对象,然后我们对Data类稍微小改造,增加一个字段k
class Data implements Serializable{
private int k;//新增加的字段
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
按照推测,这个Data的UID已经发生变化,反序列化时因为UID与持久化对象的UID不同,会丢出InvalidClassException异常而反序列失败,我们上结果
Exception in thread "main" java.io.InvalidClassException: main.Data; local class incompatible: stream classdesc serialVersionUID = -1762858249998764225, local class serialVersionUID = -2944979104297389356
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at main.Worm.main(Worm.java:76)
验证成功了,说明确实编译器会为没有UID的对象根据类文件自动哈希一个UID,以便接受方验证类版本是否发生变化,但是不推荐不注明UID,因为自动编译的UID不可控,可能两个类文件结构相同,但是格式具有区别,或者是平台的区别,导致UID不同序列化失败,因此我们应该自己维护一个UID,并且每次对类修改时对UID进行更新。
指定类版本UID,但发送方和接收方类UID不相同时,可以序列化
序列化和反序列化时只会根据UID来验证类的一致性,因此有趣的是,即使序列化和反序列化的类文件结构不同,但是只要UID一样,也能够序列化成功,但是会丢失数据。
我们分情况验证:
- 序列化的类比反序列化的类多一条字段
还是Data,序列化的Data多出来一个k字段
class Data implements Serializable{
private static final long serialVersionUID = 1L;//这里指定了UID
private int k = 10;//比反序列化的Data多的字段
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
反序列化的Data没有k
class Data implements Serializable{
private static final long serialVersionUID = 1L;//这里指定了UID
//private int k = 10;
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
然后我们将Data序列化
有两个字段k=10,n=10;
那么反序列能成功吗,反序列之后k还存在吗
这里反序列化成功,并且反序列化之后k被丢弃了,这里我们可以得出结论:
序列化UID相同时,即使类版本不同,也能够序列化成功,序列化前多出来的字段会被丢弃
- 反序列化的类比序列化的类多一条字段
这里不贴详细代码了,只要把上面两个类相反就可以了
最后得出的结论是:
序列化UID相同时,即使类版本不同,也能够序列化成功,反序列化多出来的字段不会得到赋值
发送方序列化子类类型,接受反反序列化用父类也可以接受
举个例子
class Data implements Serializable{
private int n;
public Data(int n){
this.n = n;
}
public String toString(){
return Integer.toString(n);
}
}
class sub extends Data{
private static int m = 6;
private int k;
public sub(int n) {
super(n);
}
}
sub继承Data,序列化时我将子类sub的一个对象序列化,反序列化时我用父类Data接受,反序列化的结果会自动向上转型吗
首先序列化一个子类sub对象k=0,n=10
然后反序列化我会用Data去接收
ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
Data w2 = (Data) in.readObject();//接受序列化的子类Sub对象
序列化成功,同时通过debug发现,即时用Data接收,w2对象仍然是一个sub类型
为什么会这样呢,我们打开序列化的文件
¬í �sr main.subbÊ��uéªP� �I �kxr main.Dataç��Âût÷?� �I �nxp
有乱码但是不影响我们观察,我们可以看到序列化时,不仅序列化了对象域的值,同时会指明序列化的这个对象是什么,也会指明继承Serializable父类是什么,最后反序列化时总是反序列化为指明的对象
继续思考,如果反序列时用Data接收,那么比较UID时是比较Data的UID和sub的UID还是sub的UID和sub的UID
一定是sub的UID,不然反序列就失败了
Class文件可以序列化吗
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement
可以看到Class是继承了Serializable接口的,毋庸置疑是可以的
异想天开一下,可不可以通过Class的序列化传输一个可序列化的类,然后传输这个类的对象,接受方反序列得到本来没有的Class,然后通过这个Class反序列化这个类的对象
这里还是Data类
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
out.writeObject(Data.class);
Data d = new Data(10);
out.writeObject(d);
首先把Data.class序列化,然后将Data的对象d也序列化
然后我们将Data注掉
//class Data implements Serializable{
// private int n;
// public Data(int n){
// this.n = n;
// }
// public String toString(){
// return Integer.toString(n);
// }
//}
现在尝试接受Data.class和Data的对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
Class date = (Class) in.readObject();
in.readObject();
Exception in thread "main" java.lang.ClassNotFoundException: main.sub
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:628)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1620)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at main.Worm.main(Worm.java:76)
发现是不能序列化的,这里笔者回头看了一下Class文件的域,发现大多数都是static和transient,因此序列化Class对象没有什么意义
那我们就不能通过网络传输Class了吗
其实是可以的,但是需要借助自定义的ClassLoader,通过socket传输class的字节码文件,委托给自定义ClassLoader加载,就能够实现远程类加载
网友评论