序列化是指对象经过写出描述本身状态的数值来记录本身的过程,即将对象表示成一系列有序字节,Java提供了将对象写入流和从流中恢复对象的方法。对象能包含其它的对象,而其它的对象又能够包含另外的对象。Java序列化可以自动的处理嵌套的对象。对于一个对象的简单域,writeObject()直接将其值写入流中。当遇到一个对象域时,writeObject()被再次调用,若是这个对象内嵌另外一个对象,那么,writeObject()又被调用,直到对象能被直接写入流为止。程序员所须要作的是将对象传入ObjectOutputStream的writeObject()方法,剩下的将有系统自动完成。
要实现序列化的类必须实现的java.io.Serializable或java.io.Externalizable接口,不然将产生一个NotSerializableException。该接口内部并无任何方法,它只是一个"tagging interface",仅仅"tags"它本身的对象是一个特殊的类型。类经过实现 java.io.Serializable接口以启用其序列化功能。未实现此接口的类将没法使其任何状态序列化或反序列化。可序列化类的全部子类型自己都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成一组byte,这样往后要用这个对象时候,你就能把这些byte数据恢复出来,并据此从新构建那个对象了。java
Java中,一切都是对象,在分布式环境中常常须要将Object从这一端网络或设备传递到另外一端。这就须要有一种能够在两端传输数据的协议。Java序列化机制就是为了解决这个问题而产生。程序员
Java序列化支持的两种主要特性:bash
Java序列化的目的(我目前能理解的):
网络
下面咱们经过一个简单的例子来看下Java默认支持的序列化。咱们先定义一个类,而后将其序列化到文件中,最后读取文件从新构建出这个对象。在序列化一个对象的时候,有几点须要注意下:
分布式
class SuperClass implements Serializable{
private String name;
private int age;
private String email;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getEmail() {
return email;
}
public SuperClass(String name,int age,String email) {
this.name=name;
this.age=age;
this.email=email;
}
}
复制代码
下面咱们来看下main方法里面的序列化过程,代码以下:ide
public static void main(String[] args) throws IOException,ClassNotFoundException {
System.out.println("序列化对象开始!");
SuperClass superClass=new SuperClass("gong",27, "1301334028@qq.com");
File rootfile=new File("C:/data");
if(!rootfile.exists()) {
rootfile.mkdirs();
}
File file=new File("C:/data/data.txt");
if(!file.exists()) {
file.createNewFile();
}
FileOutputStream fileOutputStream=new FileOutputStream(file);
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(superClass);
objectOutputStream.flush();
objectOutputStream.close();
System.out.println("序列化对象完成!");
System.out.println("反序列化对象开始!");
FileInputStream fileInputStream=new FileInputStream(new File("C:\\data\\data.txt"));
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
SuperClass getObject=(SuperClass) objectInputStream.readObject();
System.out.println("反序列化对象数据:");
System.out.println("name:"+getObject.getName()+"\nage:"+getObject.getAge()+"\nemail:"+getObject.getEmail());
}
复制代码
代码运行结果以下:函数
序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com
复制代码
经过上面的例子,咱们看到Java默认提供了序列化与反序列化机制,对于单个实体类来讲,整个过程都是自动完成的,无需程序员进行额外的干预。若是咱们想让某些关键的域不参与序列化过程呢?Java提供了方法,接着往下看。工具
若是咱们如今想让上面SuperClass类走age和email不参与序列化过程,那么只须要在其定义前面加上transient关键字便可:ui
private transient int age;
private transient String email;
复制代码
这样咱们在进行序列化的时候,字节流中不不包含age和email的数据的,反序列的时候会赋予这两个变量默认值。仍是运行刚才的工程,这时候咱们结果以下:this
序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:0
email:null
复制代码
若是默认的序列化过程不能知足需求,咱们也能够自定义整个序列化过程。这时候咱们只须要在须要序列化的类中定义writeObject方法和readObject方法便可。咱们仍是以SuperClass为例,如今咱们添加自定义的序列化过程,transient关键字让Java内置的序列化过程忽略修饰的变量,咱们经过自定义序列化过程,仍是序列化age和email,咱们来看看改动后的结果:
private String name;
private transient int age;
private transient String email;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getEmail() {
return email;
}
public SuperClass(String name,int age,String email) {
this.name=name;
this.age=age;
this.email=email;
}
private void writeObject(ObjectOutputStream objectOutputStream)
throws IOException {
objectOutputStream.defaultWriteObject();
objectOutputStream.writeInt(age);
objectOutputStream.writeObject(email);
}
private void readObject(ObjectInputStream objectInputStream)
throws ClassNotFoundException,IOException {
objectInputStream.defaultReadObject();
age=objectInputStream.readInt();
email=(String)objectInputStream.readObject();
}
复制代码
运行结果以下:
反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com
复制代码
咱们看到,执行结果和默认的结果是一致的,咱们经过自定义序列化机制,修改了默认的序列化过程(让transient关键字失去了做用)。
注意:
细心的同窗可能发现了咱们在自定义序列化的过程当中调用了defaultWriteObject()和defaultReadObject()方法。这两个方法是默认的序列化过程调用的方法。若是咱们自定义序列化过程仅仅调用了这两个方法而没有任何额外的操做,这其实和默认的序列化过程没任何区别,你们能够试一下。
默认状况下是这样的
子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据丢失),可是在子类中属性仍能正确序列化。
若是咱们想在序列化的时候保存父类的域,那么在序列化子类实例的时候必须显式的保存父类的状态。咱们将前面的例子稍做修改:
class SuperClass{
protected String name;
protected int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public SuperClass(String name,int age) {
this.name=name;
this.age=age;
}
}
class DeriveClass extends SuperClass implements Serializable{
private String email;
private String address;
public DeriveClass(String name,int age,String email,String address) {
super(name,age);
this.email=email;
this.address=address;
}
public String getEmail() {
return email;
}
public String getAddress() {
return address;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(name);
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
name=(String)in.readObject();
age=in.readInt();
}
@Override
public String toString() {
return "name:"+getName()+"\nage:"+getAge()+"\nemail:"+getEmail()+"\naddress"+getAddress();
}
}
复制代码
main方法咱们修改成序列化子类对象便可:
DeriveClass superClass=new DeriveClass("gong",27,"1301334028@qq.com","NJ");
DeriveClass getObject=(DeriveClass) objectInputStream.readObject();
System.out.println("反序列化对象数据:");
System.out.println(getObject);
复制代码
运行代码发现报错了,报错以下:
Exception in thread "main" java.io.InvalidClassException: com.learn.example.DeriveClass; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.learn.example.RunMain.main(RunMain.java:88)
复制代码
咱们来仔细分析下,为何会这样。DeriveClass支持序列化,其父类不支持序列化,因此这种状况下,子类在序列化的时候须要额外的序列化父类的域(若是有这个须要的话)。那么在反序列的时候,因为构建DeriveClass实例的时候须要先调用父类的构造函数,而后才是本身的构造函数。反序列化时,为了构造父对象,只能调用父类的无参构造函数做为默认的父对象,所以当咱们取父对象的变量值时,它的值是调用父类无参构造函数后的值。若是你考虑到这种序列化的状况,在父类无参构造函数中对变量进行初始化。或者在readObject方法中进行赋值。 咱们只须要在SuperClass中添加一个空的构造函数便可:
public SuperClass() {}
复制代码
这种状况下,子类也支持序列化操做的。通常状况下,无需作特殊的操做便可。
上面的例子,咱们都没有看到这个serialVersionUID这个字段,为何咱们也能正常的序列化也反序列化呢?这是由于Eclipse默认为咱们生成了一个序列化ID。
Eclipse下提供了两种生成策略,一个是固定的1L,一个是随机生成一个不重复的long类型数据(其实是使用JDK工具生成),在这里有一个建议,若是没有特殊需求,就是用默认的1L就能够,这样能够确保代码一致时反序列化成功。
注意:虚拟机是否容许反序列化,不只取决于类路径和功能代码是否一致,一个很是重要的一点是两个类的序列化ID是否一致(就是 privatestatic final long serialVersionUID = 1L)虽然两个类的功能代码彻底一致,可是序列化ID不一样,他们没法相互序列化和反序列化(这种状况特别是在网络传输后,远程创建对象的时候须要注意)
经过前面的例子,咱们将数据序列化到data.txt文件中,下面咱们经过二进制查看工具来看下Java序列化后的字节流是如何存储到文件中的,它的格式是怎么样的?咱们将上面的SuperClass类改造下:
class SuperClass implements Serializable{
private static final int serialVersionUID=1;
protected String name;
protected int age;
public SuperClass() {}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public SuperClass(String name,int age) {
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "name:"+getName()+"\nage:"+getAge();
}
}
复制代码
写入的数据以下:
SuperClass superClass=new SuperClass("gong",27);
复制代码
下面咱们打开data.txt来看下存储的内容:具体的存储内容如图所示: