您的位置:首页 > 编程语言 > Java开发

浅谈Java中的序列化

2016-02-20 12:19 633 查看

Java序列化

概念

什么是序列化和反序列化

Serialization(序列化)是一种
将对象以一连串的字节
描述的过程;反序列化deserialization是一种
将这些字节重建成一个对象的过程


什么情况下需要序列化

a)当你想把的
内存中的对象保存到一个文件中或者数据库
中时候;

b)当你想用
套接字Socket在网络上传送对象
的时候;

c)当你想通过
跨进程通信传输对象
的时候;

相当于在两端传输数据的协议,约定好怎么序列化然后怎么正确的反序列化,Java序列化机制就是为了解决这个问题而产生

如何实现序列化

将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以序列化。

如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而反序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。

public class MyTest implements Serializable {
private static final long serialVersionUID = 1L;
private String name = "LiaBin";
private transient String home = "China";
private static int age = 24;
private static Test test1 = new Test(10);
private Test test2 = new Test(10);
public static void main(String[] args) {// 以下代码实现序列化
MyTest myTest = new MyTest();
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt")); // 序列化到硬盘文件
// 输出流保存的文件名为my.out ObjectOutputStream能把Object输出成Byte流
oos.writeObject(myTest);
oos.flush(); // 缓冲流
oos.close(); // 关闭流

ByteArrayOutputStream bo = new ByteArrayOutputStream(); //序列化到内存
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(new MyTest());
oo.flush();
oo.close();
byte[] bytes = bo.toByteArray();
for (Byte b : bytes) {
// 此时跟myout.txt中用16进制格式查看的内容是一样的,只是这里的表示是2进制
System.out.print(b + " ");
}
System.out.println();

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
myTest.fan(myTest);// 调用下面的 反序列化 代码
}
public void fan(MyTest myTest) { // 反序列的过程
ObjectInputStream oin = null;// 局部变量必须要初始化
try {
oin = new ObjectInputStream(new FileInputStream("myout.txt"));
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
name = "BinJing";
age = 1;
test1.setLevel(20);//静态变量生效
test2.setLevel(20);//无效
MyTest mts = null;
try {
mts = (MyTest) oin.readObject();// 由Object对象向下转型为MyTest对象
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// false,说明是深拷贝,堆中不同对象
System.out.println("myTest==mts: " + (myTest == mts));
// name=LiaBin
System.out.println("name=" + mts.name);
// home=null transient修饰变量,不序列化,所以是默认值null
System.out.println("home=" + mts.home);
// age=1 序列化会忽略静态字段,因为他们不属于对象的任何状态,打印静态变量当然会去静态变量全局区查找,所以此时是修改之后的值1
System.out.println("age=" + mts.age);
// test1.level:20 test1是个静态属性不被系列化
System.out.println("test1.level:" + mts.test1.getLevel());
// test2.level:10 一起序列化,如果Test没实现Serializable接口,异常退出
System.out.println("test2.level:" + mts.test2.getLevel());
}
}
public class Test implements Serializable{
private int level;
public Test(int level) {
super();
this.level = level;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}


总结一下:

当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化,注意是深拷贝而不是浅拷贝;

static,transient后的变量不能被序列化;

serialVersionUID作用

如果没有设置这个值,你在序列化一个对象之后,改动了该类的字段或者方法名之类的,那如果你再反序列化想取出之前的那个对象时就可能会抛出异常,因为你改动了类中间的信息,serialVersionUID是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,当修改后的类去反序列化的时候发现该类的serialVersionUID值和之前保存在问价中的serialVersionUID值不一致,所以就会抛出异常。

而显示的设置serialVersionUID值就可以保证版本的兼容性,如果你在类中写上了这个值,
就算类变动了,它反序列化的时候也能和文件中的原值匹配上。而新增的值则会设置成null,删除的值则不会显示


所以如果没设置serialVersionUID的话,类如果发生变动,那么最新序列化的serialVersionUID该值根据最新属性值计算得来,所以跟文件中的序列化值就对应不上了,反序列化失败。

注意方法的改变不影响,因为序列化只序列化属性,跟方法无关

如果你不在类中声明SerialVersionUID的话,
Java会在运行时替你生成一个
,不过这个生成的过程会受到类元数据包括字段数,字段类型,字段的访问限制符,类实现的接口等因素的影响.

Java的序列化机制会替你生成一个的。它的生成机制受很多因素的影响,包括类中的字段,还有访问限制符,类实现的接口,甚至是不同的编译器实现,任何类的修改或者使用了不同的编译器生成SerialVersionUID都各不相同,很可能最终导致重新加载序列化的数据中止

Eclipse会根据类元数据计算得出serialVersionUID值

private static final long serialVersionUID = 7539660831609822000L; //默认根据所有的属性值计算出来的,如果没定义serialVersionUID,那么默认使用的就是通过所有属性值计算出来的该值


一般情况下,值直接申明为1L就行了

private static final long serialVersionUID = 1L;


举个例子

public class Test implements Serializable {
private String home = "China";
//此时默认的serialVersionUID是-7477377630168477287L
public static void main(String args[]) {
Test myTest = new Test();
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt"));
oos.writeObject(myTest);
oos.flush();
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
fan();
}
public static void fan() {
ObjectInputStream oin = null;
try {
oin = new ObjectInputStream(new FileInputStream("myout.txt"));
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
Test mts = null;
try {
mts = (Test) oin.readObject();// 由Object对象向下转型为Test对象
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("home=" + mts.home);
}
}


此时没错,没有定义serialVersionUID值,那么按照默认序列化行为

后续开发中,新添了一个新的age属性,然后直接从文件中反序列化,代码如下

public class Test implements Serializable {
private String home = "China";
//此时默认的serialVersionUID是-7477377630168477287L
private int age = 20;
//添加age属性之后,默认的serialVersionUID值是-3342077822325805408
public static void main(String args[]) {
fan();
}
public static void fan() {
ObjectInputStream oin = null;
try {
oin = new ObjectInputStream(new FileInputStream("myout.txt"));
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
Test mts = null;
try {
mts = (Test) oin.readObject();// 由Object对象向下转型为Test对象
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("home=" + mts.home);
}
}


那么此时报异常,因为两者的serialVersionUID不一致

java.io.InvalidClassException: Test; local class incompatible: stream classdesc serialVersionUID = -3342077822325805408, local class serialVersionUID = 7477377630168477287


解决方案:添加如下行即可

private static final long serialVersionUID = 1L;


自定义序列化

当进行序列化的时候:

1. 首先JVM会先调用
writeReplace
方法,在这个阶段,我们可以进行张冠李戴,将需要进行序列化的对象换成我们指定的对象.

2. 跟着JVM将调用
writeObject
方法,来将对象中的属性一个个进行序列化,我们可以在这个方法中控制住哪些属性需要序列化.

当反序列化的时候:

1. JVM会调用
readObject
方法,将我们刚刚在writeObject方法序列化好的属性,反序列化回来.

2. 然后在
readResolve
方法中,我们也可以指定JVM返回我们特定的对象(不是刚刚序列化回来的对象).

注意到在writeReplace和readResolve,我们可以严格控制singleton的对象,在同一个JVM中完完全全只有唯一的对象,控制不让singleton对象产生副本.

注:writeReplace调用在writeObject前;readResolve调用在readObject之后

单例的序列化

为了使一个单例类变成可序列化的,仅仅在声明中添加“
implements Serializable
”是不够的。因为一个串行化的对象在每次反序列化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入
readResolve
方法

public class AnoTest implements Serializable {
private static final long serialVersionUID = 1L;
private static final AnoTest INSTANCE = new AnoTest();
private AnoTest() {
}
public static AnoTest getInstance() {
return INSTANCE;
}
//单例序列化,定义该方法
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
public static void main(String args[]) {
AnoTest anoTest = getInstance();// 此时把堆中的AnoTest对象序列化,但是INSTANCE是个静态变量,所以不会重复序列化
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt"));
oos.writeObject(anoTest);
oos.flush();
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
fan();
}
public static void fan() {
ObjectInputStream oin = null;
try {
oin = new ObjectInputStream(new FileInputStream("myout.txt"));
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
AnoTest mts = null;
try {
mts = (AnoTest) oin.readObject();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("mts==INSTANCE ? " + (mts == INSTANCE)); //定义readResolve方法,返回true
//System.out.println("mts==INSTANCE ? " + (mts == INSTANCE)); //注释readResolve方法,返回false
}
}


这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。

子类/父类序列化行为

参考Java自定义序列化行为解析

一个可序列化的类继承自一个非序列化的有状态超类

class AbstractSerializeDemo {
private int x, y;
public void init(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void printXY() {
System.out.println("x:" + x + ";y:" + y);
}
}

public class SerializeDemo extends AbstractSerializeDemo implements Serializable {
private int z;
public SerializeDemo() {
super.init(10, 50);
z = 100;
}
public void printZ() {
super.printXY();
System.out.println("z:" + z);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
SerializeDemo sd = new SerializeDemo();
sd.printZ();
out.writeObject(sd);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
SerializeDemo sd2 = (SerializeDemo) in.readObject();
sd2.printZ();
}
}


输出结果:

x:10;y:50

z:100

x:0;y:0

z:100

子类的值域保留下来了,但是超类的值域丢失了,这对jvm来说是正常的,因为超类不可序列化;

为了解决这个问题,只能自定义序列化行为,具体做法是在SerializeDemo里加入以下代码:

private void writeObject(ObjectOutputStream os) throws IOException {
os.defaultWriteObject();//java对象序列化默认操作
os.writeInt(getX());
os.writeInt(getY());
}
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
is.defaultReadObject();//java对象反序列化默认操作
int x=is.readInt();
int y=is.readInt();
super.init(x,y);
}


writeObject和readObject方法为JVM会在序列化和反序列化java对象时会分别调用的两个方法,修饰符都是private

我们在序列化的默认动作之后将超类里的两个值域x和y也写入object流;与之对应在反序列化的默认操作之后读入x和y两个值,然后调用超类的初始化方法

再次执行程序之后的输出为:

x:10;y:50

z:100

x:10;y:50

z:100

Android中的序列化

Android中的新的序列化机制

在Android系统中,针对内存受限的移动设备,因此对性能要求更高,Android系统采用了新的IPC(进程间通信)机制,要求使用性能更出色的对象传输方式。因此
Parcel
类被设计出来,其定位就是轻量级的高效的对象序列化和反序列化机制。Parcel的序列化和反序列化的读写全是在内存中进行,所以效率比JAVA序列化中使用外部存储器会高很多

Android Serializable与Parcelable原理与区别 这里可以看到Parcelable的原理,读写全在内存,速度快

Android中启动一个activity都是通过AMS来进行的,我们知道AMS在系统进程中,所以此时跨进程通信的话,如果需要传输对象,那么就需要序列化了

如何选择

Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

如activity间传输数据

Serializable可将数据持久化方便保存,所以在需要保存在本地硬盘文件或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不能很好的保存数据的持续性在外界有变化的情况下,不推荐使用Parcelable进行数据持久化
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  序列化