说起来对象序列化的文章已经非常多了,这里记录下自己学习理解的思路,如果文中有任何问题请告知。
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,可能需要我们将对象脱离JVM,保存在字节流中并通过网络传输或者保存到磁盘或文件中,并在之后将它重建为一个存活的对象。这其中涉及到两点,即,序列化和反序列化。
定义
序列化:将对象的表示转换为字节流的过程。
反序列化:从字节流中重新构建一个对象的过程。
1.前置分析:
在前面字节流中有说过ObjectIuputStream
和ObjectOutputStream
,这两个类有方法readObject()
或writeObject()
可以序列化和反序列化一个完整的对象。这里以ObjectOutputStream
序列化一个对象为例,查看源码如下:
/**
**序列化对象 writeObject源码示例
**/
public final void writeObject(Object obj) throws IOException {
//尽管是final方法,但是通过enableOverride字段实现重写该方法
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
//序列化对象的主线方法
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
可以看到其中writeObject0()
方法才是真的序列化方法,继续往下看:
/**
**writeObject0源码示例
**/
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
**代码有删减**
for (;;) {
// REMIND: skip this check for strings/arrays?
Class repCl;
desc = ObjectStreamClass.lookup(cl, true);
//通过序列化对象的对应的ObjectStreamClass实例,判断是否有WriteReplace方法
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
**代码有删减**
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {
//序列化对象的方法
writeOrdinaryObject(obj, desc, unshared);
} else {
//抛出异常
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
**代码有删减**
}
在这里可以看到其实支持的对象类型有这么几种,String
、Array
数组、Enum
和Serializable接口
,先看Object
类型的方法writeOrdinaryObject()
,如下:
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \"" +
obj.getClass().getName() + "\", " + obj.toString() + ")");
}
try {
desc.checkSerialize();
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
这里存在判断if (desc.isExternalizable() && !desc.isProxy())
,点看看过之后,大体上通过ObjectOutputStream
序列化对象的源码就看完了,如果感兴趣的话,可以看看类似的ObjectInputStream
反序列化源码,在这里就不继续放上来了。下面可以开始细说序列化操作了。
2.序列化
2.1默认序列化
在Java里,我们通过Serializable
接口实现序列化操作,在这个接口中没有任何东西,看过源码之后就知道这个接口只是一个标识,在writeObject0
方法中,如果对象不是String
、Array
数组、Enum
或者没有实现Serializable
接口,就会抛出异常NotSerializableException
。现在可以看一个简单的示例,如下:
/**
**序列化示例
**/
public class SerializableDemo implements Serializable{
private String name;
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setName("小二");
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=小二]
其中,如果不实现Serializable
接口,就像在上面源码写的那样,报异常:java.io.NotSerializableException: Test.SerializableDemo
,所以这个接口确实不能缺少。这时候,我们就简单的序列化并持久化了一个对象,并把字段的值持久化下来。这种默认的序列化可以实现,却会存在一些限制,如果有些字段是不能被序列化并保存的呢?比如密码等。那我们就需要定制按需求的序列化类。
2.2 影响序列化机制
2.2.1 transient关键字
Java平台提供了这样一个关键字transient
,通过它可以将字段表示成瞬时状态,就不会将这个字段值序列化了,如下:
/**
**transient关键字示例
**/
public class SerializableDemo implements Serializable{
private String name;
private transient Integer age;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setName("小二");
demoOut.setAge(105);
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=小二, age=null]
可以很清楚的看到,age
字段是不会被序列化并保存的,这样就解决了上面说的保存数据的问题。
2.2.2 writeObject()&readObject()
仅仅是这样就够了么?我们序列化一个对象的时候,保存的仅仅是对象中的数据,那么如果我们确实想保存这个字段,但是要求加密呢?通过transient
并不能满足我们的需求,但是在上面看的源码中,writeExternalData
方法或writeSerialData
方法中就包含有对对象的操作,有通过对象直接调用writeExternal(this)
方法也有通过反射调用对象的writeObject
方法完成需要的操作,所以,我们可以为类对象添加writeObject()
或readObject()
方法实现对序列化对象的操作,如下所示:
/**
**writeObject()&readObject()方法示例
**/
public class SerializableDemo implements Serializable{
private String name;
private transient int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age-5);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt()+5;
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setName("小二");
demoOut.setAge(105);
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=小二, age=105]
虽然在age
字段使用了transient
关键字,但是在writeObject
方法中,通过ObjectOutputStream
对象写入了数据,所以还是存在数据的,在开始的源码中已经有写了,writeObject
方法是在后面的if判断中,但是关键字的作用是在之前起作用的。
2.2.3 readResolve()
说到这里,还有一个很关键的问题,我们这样序列化,然后反序列化出来的对象并不是通过常规方法构造的,那么当一个类是单例的,那么反序列化的时候,会不会出现多实例的问题?因为这个实例是直接从字节流中直接构造出来的,我们常规的限制并不会起作用。这时在类ObjectInputStream
中的方法readOrdinaryObject
可以看到,当示例都已经构造好了,最后还有这样一个判断:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
判断是否有readResolve
方法,有的话就反射调用它,然后返回的对象就是反射调用后的对象,这里就可以看出,我们可以通过这个方法来操作了。
通过上述源码,我们知道可以通过readResolve
来操作反序列化的单例实现问题,如下所示:
/**
**序列化单例问题
**/
public class SerializableDemo implements Serializable{
private static SerializableDemo demo = new SerializableDemo("小二",100);
private String name;
private transient int age;
private SerializableDemo(String name,int age){
this.name = name;
this.age = age;
}
public static SerializableDemo getInstance(){
return demo;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age-5);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt()+5;
}
private Object readResolve(){
return demo;
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = SerializableDemo.getInstance();
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println("demoIn==demoOut?"+(demoIn==demoOut));
}
}
输出:
demoIn==demoOut?true
这样我们的单例就成功了,其实这里并不推荐使用readResolve()
方法实现单例模式,在之前的源码中我们看到,ObjectOutputStream
类中还支持一种类型Enmu
,如果是单例的,可以直接使用枚举类型会更好一些,因为是Java来维护这个单例而不用我们来维护。
2.2.4 版本号
有一点需要注意的是,序列化对象的版本机制,默认情况下,序列化对象的版本是通过对象中各个字段方法计算出来的,如果修改了类,那么默认的版本号就会出现改变,所以,可以申明一个fianl
的版本号来保证版本不会变化,如下所示:
/**
**Serializable版本号
**/
private static final long serialVersionUID = 21098328942;
说完了Serializable
接口,接下来可以看一下它的子接口Externalizable
接口。
2.3 Externalizable接口
可以看下它的源码,如下:
/**
**Externalizable接口源码
**/
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
在这个接口中只有两个方法,writeExternal()
方法实现序列化,而readExternal
方法实现反序列化。如果类实现了Externalizable
接口,那么这两个方法在其中扮演什么角色呢?是不是像之前的readObject
和writeObject
?下面测试一番,如下代码:
/**
**Externalizable测试用例
**/
public class SerializableDemo implements Externalizable{
private String name;
private transient int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age-5);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt()+5;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
//接口实现方法中不做任何操作
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
// TODO Auto-generated method stub
//接口实现方法中不做任何操作
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setAge(100);
demoOut.setName("小二");
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=null, age=0]
可以看到在重写的两个方法中,如果没有写任何东西,那么序列化的就是字段类型的默认值,而不是现有值,而且其中的readObject
和writeObject
都不起作用,其实在之前的源码方法writeExternalData()
中就有写到,如下:
/**
**ObjectOutputStream中关于Externalizable序列化
**/
private void writeExternalData(Externalizable obj) throws IOException {
Object oldObj = curObj;
ObjectStreamClass oldDesc = curDesc;
PutFieldImpl oldPut = curPut;
curObj = obj;
curDesc = null;
curPut = null;
if (extendedDebugInfo) {
debugInfoStack.push("writeExternal data");
}
try {
if (protocol == PROTOCOL_VERSION_1) {
obj.writeExternal(this);
} else {
bout.setBlockDataMode(true);
obj.writeExternal(this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curObj = oldObj;
curDesc = oldDesc;
curPut = oldPut;
}
在这里并没有反射调用其他方法,只是这个类型只过来调用了writeExternal(this)
方法,根本不会有其他调用,所以如果在这个方法中没有操作就会值为默认值或null
,那么修改一下,在这两个方法中来操做呢?如下所示:
/**
**Externalizable测试用例
**/
public class SerializableDemo implements Externalizable{
private String name;
private transient int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
// TODO Auto-generated method stub
name = (String)in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setAge(100);
demoOut.setName("小二");
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=小二, age=100]
这样就可以通过Externalizable接口正常的序列化这个对象了。
3 序列化代理
正常来说,通过上面介绍的方法可以正常序列化和反序列化一个对象,但是反序列化的时候毕竟不是常规方法构造出来的,总会留下各种不安全的因素,所以在《Effective Java》中有介绍一种序列化代理思想,通过代理来反序列化的时候,就可以正常通过构造方法构建对象,如下:
/**
**序列化代理 示例
**/
public class SerializableDemo implements Serializable{
private String name;
private int age;
public SerializableDemo() {
super();
// TODO Auto-generated constructor stub
}
public SerializableDemo(String name,int age){
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String getName(){
return this.name;
}
private void setName(String name){
this.name = name;
}
private Object writeReplace(){
return new SerializableDemoProxy(this);
}
@Override
public String toString() {
return "SerializableDemo [name=" + name + ", age=" + age + "]";
}
private static class SerializableDemoProxy implements Serializable{
private String name;
private int age;
public SerializableDemoProxy() {
super();
// TODO Auto-generated constructor stub
}
public SerializableDemoProxy(SerializableDemo demo){
this.age = demo.age;
this.name = demo.name;
}
private Object readResolve(){
return new SerializableDemo(name,age);
}
}
public static void main(String[] arg0) throws IOException, ClassNotFoundException{
File f = new File("/test/test.txt");
FileOutputStream fout = new FileOutputStream(f);
ObjectOutputStream out = new ObjectOutputStream(fout);
SerializableDemo demoOut = new SerializableDemo();
demoOut.setAge(100);
demoOut.setName("小二");
out.writeObject(demoOut);
out.flush();
FileInputStream fin = new FileInputStream(f);
ObjectInputStream in = new ObjectInputStream(fin);
SerializableDemo demoIn = (SerializableDemo)in.readObject();
System.out.println(demoIn);
}
}
输出:
SerializableDemo [name=小二, age=100]
可以看到使用起来其实并没有区别,但是在序列化的时候,其实是使用代理的类SerializableDemoProxy
,而不是直接用的当前类,而且反序列化的时候,也是反序列化当前这个代理类,然后通过内部的readObject()
方法来通过构造器构造的,不会有直接构造的问题,而且使用的时候完全看不出来有任何区别。
说到这里,其实序列化的内容已经被说完了,当中如果有疑惑或者错误的地方,请帮忙指出,谢谢
文章参考:
《Java编程思想》
《Java程序设计语言》
《Effective Java》
博客:理解Java对象序列化
网友评论