对象序列化
序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或者用于网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则是指从IO流中恢复该Java对象。如要让某个对象支持序列化机制,则必须要让它的类是可序列化的(Serializable)。为了让某个类是可序列化的,该类需要继承如下两个接口之一:
1. Serializable接口
2. Externliazble接口
我们先从Serializable接口开始说起。当类继承了Serializable接口时,该类是可序列化的,即创建出来的对象,可以写入磁盘或者用于网络传输(基本上用于网络传输的类都是序列化的,否则程序会出现异常。若有接触过Java web开发的同学,在web应用中的HttpSession 或ServletContext就是可序列化的类)。
我们如何操作输入/输出可序列化的对象呢,答案是使用ObjectInputStream与ObjectOutputStream的字节流进行操作,这两个流不仅仅是字节流还是处理流,因此需要一个任意节点流作为基础。
下面实现将可序列化对象写入文件,并从该文件读取该对象,并打印该对象的私有属性值
//可序列化 类 class Person implements Serializable{ String name; int age; public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
//实现可序列化对象的传输
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Person p = (Person)ois.readObject(); System.out.println("对象序列化,我叫"+p.getName()+",我今年"+p.getAge()+"岁了!"); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
上述代码中,我们先创建了p对象,并将这个p对象写入到当前程序目录下的out.txt文件里,在从该文件中取出对象,并打印该对象的属性值。运行效果如下:
构造方法:我叫HJL,我今年23岁了!
对象序列化,我叫HJL,我今年23岁了!
out.txt文件内容如下:
�� sr File.Person��,H:2{: I ageL namet Ljava/lang/String;xp t HJL
这里要提醒一点,不是所有的输入流,对磁盘或者网络能写入对象,便可以实现对象的序列化,不如说通过重定向标准输入流,对System.out原先是输出到控制台(显示器)变成输出到指定文件,这样是可以将对象写入到文件中,不过文件中存储的只是当前程序使用该对象时的引用地址,当程序关闭时,该地址时不存在的,因此实现对象序列化的两个步骤,1时让类继承Serializable或者Extemalizable接口并创建对象,第二,通过ObjectInputStream或者ObjectOutputStream操作可序列化的对象。
对象引用的序列化:
在之前的代码中,我们可以看到的继承Serializable接口的属性都是基本数据类型。若是该类中有引用数据类型时,为了保证该类的实例能正常的序列化与反序列化,该类的引用数据类型对应的类也需要继承Serializable或者Extemaliazble接口。
如下代码:
class Teacher implements Serializable{ Person student; String name; public Teacher(String name,Person student){ this.student = student; this.name = name; } public Person getStudent() { return student; } public void setStudent(Person student) { this.student = student; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Teacher t1 = (Teacher)ois.readObject(); Teacher t2 = (Teacher)ois.readObject(); Person p = (Person)ois.readObject(); Teacher t3 = (Teacher)ois.readObject(); System.out.println(t1); System.out.println(t2); System.out.println(t3); System.out.println(p); System.out.println(t1.getStudent() == p); System.out.println(t2.getStudent() == p); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); Teacher t1 = new Teacher("PP",p); Teacher t2 = new Teacher("CC",p); oos.writeObject(t1); oos.writeObject(t2); oos.writeObject(p); oos.writeObject(t2); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
其运行结果为:
构造方法:我叫HJL,我今年23岁了! File.Teacher@7ba4f24f File.Teacher@3b9a45b3 File.Teacher@3b9a45b3 File.Person@7699a589 true true
是所以会产生这样的运行结果,是因为在java序列机制中采用了一种特殊的序列化算法,其算法内容如下:
1. 所有保存到磁盘中的对象都有一个序列化编号。
2. 当程序试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,只有该对象从未(本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
3. 若该对象已经序列化过了,程序将知识直接输出一个序列化编号,而不是再次重新序列化该对象。
因此在上述代码中,当从文件中取出对象时,先是序列化了p对象,再是序列化t1对象,然后序列化t2对象时,发现p对象已经序列化过了,返回序p的列化编号,第三序列化p对象时,也发现被序列化过了,因此返回p的序列化编号,最后,序列化t2对象时,发现t2对象也被序列化过了,因此也返回t2的序列化编号。PS,写入/读取对象都是按顺序来的。
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Teacher t1 = (Teacher)ois.readObject(); Teacher t2 = (Teacher)ois.readObject(); Person p = (Person)ois.readObject(); Teacher t3 = (Teacher)ois.readObject(); System.out.println(t1); System.out.println(t1.getStudent().getName()); System.out.println(t2); System.out.println(t2.getStudent().getName()); System.out.println(t3); System.out.println(p); System.out.println(t1.getStudent() == p); System.out.println(t2.getStudent() == p); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); Teacher t1 = new Teacher("PP",p); System.out.println("read: t1.student.name"+t1.getStudent().getName()); oos.writeObject(t1); p.setName("HJJ"); Teacher t2 = new Teacher("CC",p); System.out.println("read: t2.student.name"+t2.getStudent().getName()); oos.writeObject(t2); oos.writeObject(p); oos.writeObject(t2); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
运行效果如下:
构造方法:我叫HJL,我今年23岁了! read: t1.student.nameHJL read: t2.student.nameHJJ File.Teacher@7ba4f24f HJL File.Teacher@3b9a45b3 HJL File.Teacher@3b9a45b3 File.Person@7699a589 true true
程序中第一段粗体字代码先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号。
程序中两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象将完全相同,程序输出第二次读取的Person对象的name实例变量的值依然是“HJL”,表明改变后的Person对象并没有被写入----这与Java序列化机制相符。
自定义序列化:
在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息等等,这时不希望系统将该实例变量值进行序列化:或者某个实例变量的类型时不可序列的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSetializableException异常。
当对某个对象进行序列化时,系统会自动把该对象的所有实例变量一次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化,这种情况被称为递归序列化。
通过在实例变量前面使用transient关键字修饰,可以指定Java序列化时无须理会该实例变量。如下Person类与前面的Person类几乎完全一样,只是它的age使用了transint关键子修饰。
修改的Person类:
class Person implements Serializable{ String name; transient int age; public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Person p = (Person)ois.readObject(); System.out.println(p.getAge()); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
运行结果如下:
构造方法:我叫HJL,我今年23岁了!
0
由于我们使用了transient关键字修饰了类的成员变量,因此,该成员变量理论上是不被序列化以及反序列化,实际上是该变量以空值的形式进行序列化与反序列化,而int类型的空值是为0。因此上述代码中,从文件取出了对象,并在控制台打印对象的age变量,该变量的为0。
尽管transient关键字修饰实例变量虽然简单,方便,但被transient修饰的实例变量将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时,无法取得该实例变量值。Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(与使用transient关键字的效果相同),而这种自定义序列化机制,是在要实现序列化以及反序列化的类中,通过重写ObjectInputStream的readObject()方法与ObjectOutputStream的writeObject()方法。
一般在默认的情况下,writeObject()方法会调用out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象状态的目的,而readObject()方法会调用in.defalutWriteObject来反序列化该对象。
除这两种之外,还有另外一种方法,readObjectNoData(),当对方收到的java版本与我发送的java版本不一致的时候,或者因为传输过程中,该反序列化流变得不完整,系统会使用该方法,实现反序列化对象,不过该对象是初始化的状态。
下面我们通过修改上述的person类来实现:
class Person implements Serializable{ String name; int age; public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); } private void writeObject(java.io.ObjectOutputStream out) throws IOException{ // TODO Auto-generated method stub //将name实例变量值反转后写入二进制流中 out.writeObject(new StringBuffer(name).reverse()); // out.writeInt(age); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ // TODO Auto-generated method stub this.name = ((StringBuffer)in.readObject()).reverse().toString(); // this.age = in.readInt(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Person t = (Person)ois.readObject(); System.out.println(t.getAge()); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){
IO io = new IO(); io.writeObject(); io.readObject(); } }
运行结果如下:
构造方法:我叫HJL,我今年23岁了!
0
writeReplace()方法:这是自定义序列化的实现方法之一,比上述的writeObject更彻底,甚至可以在序列化对象时将该对象其换成其他对象。
同样是person类,但这是重写的是writeReplace();
class Person implements Serializable{ String name; // transient int age; int age; public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); } private Object writeReplace(){ ArrayList<Object> list = new ArrayList<Object>(); list.add(name); list.add(age); return list; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
在上述代码中,我们重写了writeReplace()的方法,在方法中创建了一个list,对list添加数据以及返回list。然后我们继续对这个类的实例写入文件中,并读取文件中该实例数据,并打印该实例数据的类型:
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); //打印从文件获取对象的类型 System.out.println(ois.readObject().getClass().getTypeName()); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); Teacher t = new Teacher("CC", p); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
运行效果如下:
构造方法:我叫HJL,我今年23岁了!
java.util.ArrayList
我们可以看到,虽然我们是写入了person类的p对象到out.txt文件中,但中out.txt文件中取出来的对象类型确实list数据类型的。这是因为在Person类中有writeReplace()方法,当要将该类的实例进行序列化前,先会调用writeReplace()方法,并取得该方法的返回值,将该方法的返回值进行序列化后通过对象流存入到了out.txt文件中,所以我们从out.txt文件取对象时,该对象是list类的实例而不是person类的实例。
与上述方法相对应的就是readResolve方法,该方法是作用是当类的实例进行反序列化后,会调用readResolve方法,并获取该方法的返回值,且该返回值会代替原来反序列化的对象:
class Person implements Serializable{ String name; // transient int age; int age; public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); }
private Object readResolve(){ Teacher t = new Teacher("CC",this); return t; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
我们继续对person类进行修改,添加了readResolve()的方法。IO类保持不变,运行后效果如下:
构造方法:我叫HJL,我今年23岁了!
File.Teacher
值得注意的是,writeReplace()方法与readResolve()方法,是可以使用任意的访问控制符的,如果该类有父类,并且父类实现了writeReplace()方法或者readResolve()方法,并且子类没有重写该方法,将会使得子类序列化或者反序列化的时候执行父类的writeReplace()方法或者readResolve()方法,这明显是程序要的结果,而且开发人员也很难发现错误。但总是让子类重写writeReplace()方法或这readResolve方法无疑是一种负担,因此建议是用final来进行修饰,或者使用peivate进行修饰。
Externalizable接口
在介绍如何实现序列化时,提到了两种实现方法,其一是继承Serializable接口,其二是继承Externalizable接口。若类继承了Externalizable接口,该类要实现如下的两个方法:
1. readExternal(ObjectInput in):
2. writeExternal(ObjectOutput out):
上述两个方法与类实现自定义序列化时的readObject()与writeObject()方法很类似,都是用于实现自定义序列化,只是一个使用的是Serializable接口,一个是使用Externalizable接口。
下面我们修改person类,接口Externalizable接口,并重写上述两个方法实现,使用Externalizable接口实现自定义序列化:
class Person implements Externalizable{ String name; int age; public Person(){} public Person(String name,int age){ this.name = name; this.age = age; System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!"); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // TODO Auto-generated method stub this.name = ((StringBuffer)in.readObject()).reverse().toString(); // this.age = in.readInt(); } @Override public void writeExternal(ObjectOutput out) throws IOException { // TODO Auto-generated method stub out.writeObject(new StringBuffer(name).reverse()); // out.writeInt(age); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
这里要注意的是,使用Externalizable接口实现对象序列化,但反序列化时,先是根据类的无参构造方法来创建实例,然后才执行readExternal()方法,因此实现Externalizable的序列化类必须提供public的无参构造方法
IO类如下:
public class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Person p = (Person)ois.readObject(); System.out.println("对象序列化,我叫"+p.getName()+",我今年"+p.getAge()+"岁了!"); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); Teacher t = new Teacher("CC", p); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }
运行结果如下:
构造方法:我叫HJL,我今年23岁了!
对象序列化,我叫HJL,我今年0岁了!
由于writeExternal()方法只对name成员变量进行序列化以及readExternal()方法也只反序列化name成员变量,因此age成员变量从out.txt文件取出来后值为0。
关于对象序列化,还有以下几点需要注意的:
1.对象的类名,实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法,类变量(被static修饰的成员变量),transient实例的成员变量(也称瞬态实例变量)都不会被序列化
2. 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例
3.反序列化对象时必须有序列化对象的class文件
4.当通过文件,网络来读取序列化后的对象时,必须按实际写入的顺序读取,不可乱读。
版本问题:
我们开发java时,都会被开发的java类来制定版本,以便于我们管理。但反序列化Java对象时必须提供该对象的class文件,假如说,我使用person类创建实例,并写入了文件中,过了一会,我修改了person类的成员变量(可以是添加成员变量,可以是删除成员变量以及修改成员变量名),然后从该文件中取出了person类的对象。那么便存在着一个问题,就是从person类中取出来的对象是旧的person类,而现在的person.class文件是新得person类,旧的person类的成员变量与新的person类的成员变量不尽相同,引发了对person类两个不同版本的兼容性问题。
为了解决上述问题。Java序列机制中提供了private static final的serialVersionUID值,用于标识该Java类的序列化版本,若修改person类后,只要serialVersionUID的值与旧版本的值一致,序列机制也会把它们当成同一个序列化版本。
那么修改类的那么内容后,serialVersionUID无需修改值或者需要修改值呢?根据对象序列化主要注意的点中,对象序列化是对对象的类名,实例变量(static与transient修饰的成员变量不算)都会被序列化,因此若一个类修改了成员变量,添加了成员变量,删除了成员变量的情况,是需要进行修改serialVersionUID的值,除此之外是不用对serialVersionUID进行修改的。
总结:序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或者用于网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。