序列化与反序列化
概念
把对象的状态信息转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
- 在网络上传送对象的字节序列。
从对象流中读取或输入的对象必须是支持 java.io.Serializable 或 java.io.Externalizable 接口的对象。readObject和writeObject方法用于从对象流中读取写入对象。在 Java 中,字符串和数组都是对象,所以在序列化期间将其视为对象。读取时,需要将其强制转换为期望的类型。
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
serialVersionUID
对象序列化时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联。在实现Serializable接口后,我们需要生成这么一串版本号。该序列号在反序列化过程中用于验证序列化对象的接收者是否为该对象加载了与序列化对象所兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者发送的类的对象的版本号不一样,则反序列化时将会抛出InvalidClassException。例如:
public class User implements Serializable{
private final long serialVersionUID = 1L;
}
如果实现了该接口但未显示声明serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。但是强烈建议显示的去申明serialVersionUID,因为编译的实现和计算serialVersionUID的千差万别有可能在跨编译器编译时生成了不同的serialVersionUID,导致反序列化的时候抛出InvalidClassException。
API
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数中指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
package java.io;
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants{ }
package java.io;
public interface ObjectOutput extends DataOutput, AutoCloseable {
//部分
public void writeObject(Object obj) throws IOException;
}
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
package java.io;
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants{ }
package java.io;
public interface ObjectInput extends DataInput, AutoCloseable {
//部分
public Object readObject() throws ClassNotFoundException, IOException;
}
java.lang.ClassNotFoundException(父类 java.lang.ReflectiveOperationException与java.io.IOException一个级别)
对象序列化包括如下步骤:写入
- 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流。
通过对象输出流的writeObject()方法将对象写入。(对象持久化)。
final 修饰:java.io.ObjectOutputStream 类中的
public final void writeObject(Object obj) throws IOException{ }
对象反序列化的步骤如下:读取
- 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流。
通过对象输入流的readObject()方法读取对象。
final 修饰:java.io.ObjectInputStream 类中
public final Object readObject(){ } //返回Object 类型,所以返回值要强制转换成对象的原类型
同一缓冲区的2个相同反序列对象具有相同内存地址,但跟原对象地址不一样。不同缓冲区的2个相同反序列对象具有不同的内存地址。
将一个对象序列化到一个对象流中两次,则第二次存入流中的对象实际是第一次存入流中的对象的别名,之后再两次从对象流中读出,则取出的两个对象指向同一个对象(与序列化之前的对象不一致,相当于克隆)。
将一个对象序列化倒两个对象流中,再次取出时两个对象不一致
序列化方式一:实现Serializable接口(隐式序列化)
通过实现Serializable接口,可以实现自动序列化所有非static和 非transient关键字修饰的成员变量。也可以手动序列化,见序列化方式三。
writeObject和readObject方法会在序列化、反序列化的过程中被自动调用。方法调用后可以关闭或刷新对象流。
若一个类的字段有引用对象,那么在序列化该类的时候不仅该类要实现Serializable接口,这个引用类型也要实现Serializable接口。
如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。即:如果直接父类中无构造函数,可以反序列化,但直接父类中定义了构造函数,则必须还要显式添加一个无参的构造函数。否则,只能在父类也实现Serializable接口。该无参构造函数只要不是被private修饰就可以,这与Externalizable接口的实现类的构造方法的要求不一样。
Serializable 源码:
package java.io;
public interface Serializable {
}
完整例子:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class Student implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private int age;
public static int QQ = 1234;
private transient String address = "CHINA";
public Student(String name, int age ){
this.name = name;
this.age = age;
}
public String toString() {
return "name: " + name + "\n"
+"age: " + age + "\n"
+"QQ: " + QQ + "\n"
+ "address: " + address;
}
public void SetAge(int age) {
this.age = age;
}
}
public class Demo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("原来的对象:");
Student stu = new Student("Ming", 16);
System.out.println(stu);
//创建目标输出流(缓冲区)
ByteArrayOutputStream buff = new ByteArrayOutputStream();
//创建对象输出流
ObjectOutputStream out = new ObjectOutputStream(buff);
//将序列化对象存入缓冲区
out.writeObject(stu);
//修改相关值
Student.QQ = 6666;
// 发现打印结果QQ的值被改变(没被序列化,static修饰的变量属于类,每个对象里都是这个修改后的值)
stu.SetAge(18); //发现值没有被改变
//从缓冲区取回被序列化的对象
ObjectInputStream in =
new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
Student newStu = (Student) in.readObject();
System.out.println("序列化后取出的对象:");
System.out.println(newStu);
}
}
Output:
原来的对象:
name: Ming
age: 16
QQ: 1234
address: CHINA
序列化后取出的对象:
name: Ming
age: 16
QQ: 6666
address: null
分析:发现address(被transient修饰)和QQ(被static修饰)没有被序列化。static成员不属于对象,所以没办法序列化(如果将static变量序列化,该变量仍可在其他对象中改变;但每个该类的对象的static变量值都应该是一致的,即修改后的,而此时某对象序列化后的字节序列中的staic变量值并没有被同时修改,反序列化后数据会不一致),序列化是序列化某个对象。由于address被反序列化后没有对应的引用,所以为null。而且Serializable不会调用构造方法。
序列化可以保存对象的初始信息,在以后还可以回到这个初始状态。
class Demo2 implements Serializable{
String s="name";
public Demo2(int x) {
}
}
class Demo1 extends Demo2 implements Serializable{
private static final long serialVersionUID = 1L;
public transient int age = 23;
public String name ;
public Demo1(String name) {
super(45);
this.name = name;
}
public String toString() {
return "年龄" + age + " " + name;
}
可正常序列化与反序列化Demo1 子类。父类子类构造器都不会在反序列化时被调用。
package delete;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class Demo2 {
String s="name";
public Demo2(int x) {
System.out.println("constructor 1");
}
public Demo2( ) {
System.out.println("constructor null");
}
}
public class Demo1 extends Demo2 implements Serializable{
private static final long serialVersionUID = 1L;
public transient int age = 23;
public String name ;
public Demo1(String name) {
super(45);
System.out.println("constructor 2");
this.name = name;
}
public String toString() {
return "年龄" + age + " " + name;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Demo1 stu = new Demo1("Ming");
System.out.println(stu);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(stu);
ObjectInputStream in =
new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
Demo1 stu1 = (Demo1) in.readObject();
System.out.println(stu1);
}
}
Output:
constructor 1
constructor 2
年龄23 Ming
constructor null
年龄0 Ming
可正常序列化与反序列化Demo1 子类。反序列化时,子类Demo1 的构造器不会被调用,
但父类Demo2 的无参构造器会被调用。
class Demo2 {
String s="name";
public Demo2(int x) {
}
}
Demo1 extends Demo2 implements Serializable{
private static final long serialVersionUID = 1L;
public transient int age = 23;
public String name ;
public Demo1(String name) {
super(45);
this.name = name;
}
public String toString() {
return "年龄" + age + " " + name;
}
可正常序列化子类,不能正常反序列化Demo1 子类,
会抛出java.io.InvalidClassException: no valid constructor
序列化方式二:实现Externalizable接口(显式序列化)
Externalizable接口继承自Serializable,。在实现该接口时,必须实现writeExternal()和readExternal()方法,这些方法将代替定制的 writeObject 和 readObject 方法。而且只能通过手动进行序列化,并且两个方法是自动调用的。因此,这个序列化过程是可控的,可以自己选择进行序列化的部分。
Serialization 对象可以使用 Serializable 或 Externalizable 接口。对象持久性机制也可以使用它们。要存储的每个对象都需要检测是否支持 Externalizable 接口。如果对象支持 Externalizable,则调用 writeExternal 方法。如果对象不支持 Externalizable 但实现了 Serializable,则使用 writeObject保存该对象。
在重构 Externalizable 对象时,先使用无参数的公共构造方法创建一个实例,(所以在使用Externalizable时相应的实现类必须提供一个public的无参构造器,否则会抛出java.io.InvalidClassException)然后调用 readExternal 方法。通过从 ObjectInputStream 中读取 Serializable 对象可以恢复这些对象。
如果没有定义无参公共构造方法,则序列化会正常,反序列化时产生InvalidClassException。
实现Externalizable 接口的类必须显式提供一个public的无参构造器,除public修饰外,其余访问修饰符都会产生InvalidClassException异常。
对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,调用 readObject 来恢复对象、字符串和数组。readExternal 方法必须按照与 writeExternal 方法写入值时使用的相同顺序和类型来读取这些值。(多写少读也可以运行)。
Externalizable 源码:
package java.io;
import java.io.ObjectOutput;
import java.io.ObjectInput;
public interface Externalizable extends java.io.Serializable{
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
完整例子:
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class Demo implements Externalizable{
private int i ;
private String s;
public Demo() {}
public Demo(String x, int a) {
System.out.println("Demo对象已构造完成");
s = x;
i = a;
}
public String toString() {
return s+" "+i;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//规定了对象中哪些成员会进行序列化,同时添加一些进行序列化时的其他行为
System.out.println("Demo.writeExternal");
out.writeObject(s);
out.writeInt(i);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
System.out.println("Demo.readExternal");
s = (String)in.readObject();
i = in.readInt();
}
public static void main(String[] args)
throws FileNotFoundException, IOException, ClassNotFoundException {
System.out.println("Constructing objects");
Demo b = new Demo("A Stirng", 47);
System.out.println(b);
ObjectOutputStream o =
new ObjectOutputStream(new FileOutputStream("D:\\45\\test.txt"));
System.out.println("保存对象");
o.writeObject(b);
o.close();
//获得对象
System.out.println("获取对象");
ObjectInputStream in =
new ObjectInputStream(new FileInputStream("D:\\45\\test.txt"));
System.out.println("Recovering b");
b = (Demo)in.readObject();
in.close();
System.out.println(b);
}
}
Output:
Constructing objects
Demo对象已构造完成
A Stirng 47
保存对象
Demo.writeExternal
获取对象
Recovering b
Demo.readExternal
A Stirng 47
序列化方式三:实现Serializable接口+添加writeObject()和readObject()方法。(显+隐序列化)
手动序列化时可以实现将被transient关键字修饰的字段序列化。要想实现手动序列化需要在实现了Serializable接口的类中添加两个私有的方法writeObject()和readObject(),在这两个方法中控制字段的序列化。注意这里是添加,不是重写或者覆盖。但是添加的这两个方法必须有相应的格式。
- 方法必须要被private修饰 ——->才能被调用
- 第一行调用默认的defaultRead/WriteObject(); ——->隐式序列化非static和transient
调用方法。与序列化方式二类似 —>显式序列化
简单例子:
private void writeObject(ObjectOutputStream oos)throws IOException{oos.defaultWriteObject();
oos.writeObject(password); //transient password,实现transient变量的序列化
}
private void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{ois.defaultReadObject();
password = (String)ois.readObject();
}
完整例子:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Demo1 implements Serializable{private static final long serialVersionUID = 1L;
public transient int age = 23;
public String name ;
public Demo1(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeInt(age);
}
private void readObject(ObjectInputStream stream)
throws ClassNotFoundException, IOException {
stream.defaultReadObject();
age = stream.readInt();
}
public String toString() {
return "年龄" + age + " " + name;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Demo1 stu = new Demo1("Ming");
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(stu);
ObjectInputStream in =
new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()));
Demo1 stu1 = (Demo1) in.readObject();
System.out.println(stu1);
}
}
Output://可以把transient修饰的变量也序列化和反序列化了
年龄23 Ming
//注释掉stream.writeInt(age)和age= stream.readInt()后,输出:年龄0 Ming
//由于age被trancient修饰,所以需要显式序列化。
static的序列化
当我们要读取对象中static变量的值时,它不可能在反序列化的文件里找到新的值,而是去全局数据区中取值。
**序列化静态变量可以在所在类实现public static void deserializeStaticState / serializeStaticState (ObjectOutputStream os) throws IOException两个方法。**之后在main函数里调用。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
abstract class Shape implements Serializable {
private static final long serialVersionUID = 1L;
public static final int RED = 1, BLUE = 2, GREEN = 3;
private int xPos, yPos, dimension;
private static Random rand = new Random(47);
private static int counter = 0;
public abstract void setColor(int newColor);
public abstract int getColor();
public Shape(int xVal, int yVal, int dim) {
xPos = xVal;
yPos = yVal;
dimension = dim;
}
public String toString() {
return getClass() +
"color[" + getColor() + "] xPos[" + xPos +
"] yPos[" + yPos + "] dim[" + dimension + "]\n";
}
public static Shape randomFactory() {
int xVal = rand.nextInt(100);
int yVal = rand.nextInt(100);
int dim = rand.nextInt(100);
switch(counter++ % 3) {
default:
case 0: return new Circle(xVal, yVal, dim);
case 1: return new Square(xVal, yVal, dim);
case 2: return new Line(xVal, yVal, dim);
}
}
}
class Circle extends Shape {
private static final long serialVersionUID = 1L;
private static int color = RED;
public Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
class Square extends Shape {
private static final long serialVersionUID = 1L;
private static int color;
public Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
color = RED;
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
class Line extends Shape {
private static final long serialVersionUID = 1L;
private static int color = RED;
******************************************************************
* public static void serializeStaticState(ObjectOutputStream os) throws IOException {
* os.writeInt(color);
* }
* public static void deserializeStaticState(ObjectInputStream os) throws IOException {
* color = os.readInt();
* }
*****************************************************************
public Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) { color = newColor; }
public int getColor() { return color; }
}
public class Test {
public static void main(String[] args) throws Exception {
List<Class<? extends Shape>> shapeTypes = new ArrayList<Class<? extends Shape>>();
// Add references to the class objects:
shapeTypes.add(Circle.class);
shapeTypes.add(Square.class);
shapeTypes.add(Line.class);
List<Shape> shapes = new ArrayList<Shape>();
// Make some shapes:
for(int i = 0; i < 10; i++)
shapes.add(Shape.randomFactory());
// Set all the static colors to GREEN:
for(int i = 0; i < 10; i++)
((Shape)shapes.get(i)).setColor(Shape.GREEN);
// Save the state vector:
*******************************************************************
* ObjectOutputStream out =
* new ObjectOutputStream(new FileOutputStream("CADState.out"));
* out.writeObject(shapeTypes);
* Line.serializeStaticState(out);
* out.writeObject(shapes);
*******************************************************************
// Display the shapes:
System.out.println(shapes);
System.out.println("-------------------------------------");
ObjectInputStream in = new ObjectInputStream(new FileInputStream("CADState.out"));
// Read in the same order they were written:
**********************************************************************
* List<Class<? extends Shape>> shapeTypes1 =
* (List<Class<? extends Shape>>)in.readObject();
* Line.deserializeStaticState(in);
* List<Shape> shapes1 = (List<Shape>)in.readObject();
********************************************************************
System.out.println(shapes1);
//以下是用序列化的Class对象,得到static变量
Field f =shapeTypes1.get(0).getDeclaredField("color");
System.out.println(f); //private static int Circle.color
Circle k= (Circle)shapes1.get(0);
f.setAccessible(true);
System.out.println( f.get(k)); //3
}
}
换言之,一个Class对象是由类定义决定的,与程序运行时的数值变化无关,所以,即使在源文件A中修改了static变量,之后又将Class对象序列化,但在源文件B中反序列化时,Class对象中还是没有保存static的修改值,仍是当初类定义时的那些东西。因此,反序列化时,会去全局数据区找static变量的值,如果B中没改过static变量,则找到的值一定是初始定义处的值。
两接口区别
实现复杂度:
Serializable 实现简单,Java对其有内建支持。(该保存啥都已经是早就确定好了的)。
Externalizable 实现复杂,由开发人员自己完成。
执行效率:
Serializable 所有成员变量由Java统一保存,性能较低。
Externalizable 开发人员决定哪个成员变量要保存,可能造成速度提升。
占用空间:
Serializable 保存全部成员变量信息,占用空间较大。
Externalizable 存储部分成员变量,可能造成空间减少。
还没有评论,来说两句吧...