Java序列化与反序列化
序列化与反序列化
Java对象是有生命周期的,当生命周期结束它就会被回收,但是可以通过将其转换为字节序列永久保存下来或者通过网络传输给另一方。
把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。
Serializable接口
一个类实现java.io.Serializable接口就可以被序列化或者反序列化。实际上,Serializable接口中没有任何变量和方法,它只是一个标识。如果没有实现这个接口,在序列化或者反序列化时会抛出NotSerializableException异常。
下面是一个实现了Serializable接口的类以及它的序列化与反序列化过程。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("test");
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test1 test1 = (Test1) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
运行结果:
Test{name='test'}
serialVersionUID
private static final long serialVersionUID = -3297006450515623366L;
serialVersionUID是一个序列化版本号,实现Serializable接口的类都会有一个版本号。如果没有自己定义,那么程序会默认生成一个版本号,这个版本号是Java运行时环境根据类的内部细节自动生成的。最好我们自己定义该版本号,否则当类发生改变时,程序为我们自动生成的序列化版本号也会发生改变,那么再将原来的字节序列反序列化时就会发生错误。
下面是将Test1类加入一个变量age,此时再进行反序列化的结果。可以看出,序列化版本号已发生改变,程序认为不是同一个类,不能进行反序列化。
java.io.InvalidClassException: test.Test1; local class incompatible: stream classdesc serialVersionUID = 9097989105451761251, local class serialVersionUID = -7756223913249050270
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at test.SerialTest.main(SerialTest.java:11)
为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示地定义serialVersionUID,为他赋予明确的值。
那么在IDEA中,怎么手动生成呢?
在settings->Editor->Inspections下,搜索serial,开启Serializable class without 'serialVersionUID’的拼写检查,然后将光标放在实现Serializable的接口上,按住ALt+Enter键,选择添加serialVersionUID即可。
Transient关键字
transient修饰类的变量,可以使变量不被序列化。反序列化时,被transient修饰的变量的值被设为初始值,如int类型被设为0,对象型被设为null。
ObjectOutputStream类和ObjectInputStream类
ObjectOutputStream的writeObject方法可以序列化对象,ObjectInputStream的readObject可以反序列化对象。ObjectOutputStream实现了接口ObjectOutput,所以可以进行对象写操作。ObjectInputStream实现了接口ObjectInput,所以可以对对象进行读操作。
静态变量序列化
给Test类中增加一个静态变量,赋值为12,然后在序列化之后修改其值为10,反序列化之后打印它的值。发现打印的值为10,之前的12并没有被保存。
静态变量是不参与序列化的,序列化只是用来保存对象的状态,而静态变量属于类的状态。
父类序列化
让Test继承一个没有实现Serializable接口的类,设置父类中变量的值,对Test类的实例进行序列化与反序列化操作。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
test.setSex(12);
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test test1 = (Test) ois.readObject();
System.out.println(test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test extends TestFather implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
"sex='" + sex + '\'' +
'}';
}
}
class TestFather {
protected Integer sex;
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "TestFather{" +
"sex='" + sex + '\'' +
'}';
}
}
运行结果:
Test{name='huihui'sex='null'}
发现虽然对sex进行了复制,但是反序列化结果仍然为null。
现在让TestFather类实现Serializable接口,运行结果如下。所以当我们想要序列化父类的变量时,也需要让父类实现Serializable接口。
Test{name='huihui'sex='12'}
同理,如果Test类中有任何变量是对象,那么该对象的类也需要实现Serializable接口。查看String源代码,确实实现了Serializable接口。大家可以测试一下字段的类不实现Serializable接口的情况,运行会直接报java.io.NotSerializableException异常。
敏感字段加密
如果对于某些字段我们并不想直接暴露出去,需要对其进行加密处理,那么就需要我们自定义序列化和反序列化方法。使用Serializable接口进行序列化时,如果不自定义方法,则默认调用ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。下面我们来尝试一下自己实现序列化与反序列化过程。
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putField = out.putFields();
System.out.println("原name:" + name);
// 模拟加密
name = "change";
putField.put("name", name);
System.out.println("加密后的name:" + name);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField getField = in.readFields();
Object object = getField.get("name", "");
System.out.println("要解密的name:" + object.toString());
name = "huihui";
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
原name:huihui
加密后的name:change
要解密的name:change
解密后的name:huihui
这种写法重写了writeObject方法和readObject方法,下面一种接口也可以实现相同的功能。
Externalizable接口
除了Serializable接口,Java还提供了一个Externalizable接口,它继承了Serializable接口,提供了writeExternal和readExternal两个方法,实现该接口的类必须重写这两个方法。同时还发现,类还必须提供一个无参构造方法,否则会报java.io.InvalidClassException异常。
先不深究为什么要加一个无参构造方法,我们先试一下这个接口的序列化效果。将类Test改为如下所示:
class Test implements Externalizable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public Test() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
再次运行测试方法,发现输出的name是null。在readObject处打断点,发现会调用无参构造方法。
name其实并没有被序列化与反序列化,writeExternal方法和readExternal方法中是需要我们自己来实现序列化与反序列化的细节的。在反序列化时,会首先调用类的无参考构造方法创建一个新对象,然后再填充每个字段。
我们对writeExternal方法和readExternal方法进行重写:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
}
此时运行测试方法,发现Test类被正常序列化与反序列化。
序列化存储规则
当多次序列化一个对象时,是会序列化多次还是会序列化一次呢?
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
// 序列化,存储对象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
// 两次写入文件
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,从文本中取出对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
// 读取两个对象
Test test1 = (Test) ois.readObject();
Test test2 = (Test) ois.readObject();
System.out.println(test1 == test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
运行结果:
73
78
true
可以发现,当第二次写入对象时,文件的长度仅仅增加了5个字节,并且在判等时,两个引用指向同一地址。
Java序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。