java序列化是将java对象保存在文件或者通过网络传输的机制,通过实现接口Serializable或者Externalizable标识该类的对象可以序列化和反序列化。
如果希望保存java对象的状态并在以后的某个时刻在内存中重建该对象,我们可以通过java序列化的机制实现。
标识类对象是可序列化的只要实现Serializable接口即可,该接口不包括任何字段和方法,它只是一个空的接口。如以下代码片断,Student类实现了该接口并自动获得了序列化的能力。
//实现了Serializable接口的类自动拥有序列化的能力
public class Student implements Serializable {
}
对象的状态是由成员变量决定的,所以序列化保存的是成员变量的值。static修饰的是类变量,序列化的时候static变量不会保存,另外,我们还可以通过java提供的transient关键字来显示标识变量不需要序列化。因此java序列化的是除了static和transient修饰的其它成员变量。
如何使用java的序列化机制呢?
java提供了ObjectOutputStream和ObjectInputStream两个对象输入输出流来实现序列化。假设Student类需要序列化,类的定义如下:
import java.io.Serializable;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午7:30
*/
public class Student implements Serializable {
private Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
下面代码是将Student对象序列化的例子
该例中将对象序列化到了文件student.out中,通过ObjectOutputStream将对象jack写到文件输出流中。接着通过ObjectInputStream将jack对象反序列化并强制转换为Student类。(注意此时需要捕获ClassNotFoundException类,假如此时JVM找不到Student Class对象,将会抛出该异常。)
import java.io.*;
/**
* 文件描述: 对象序列化Demo
*
* @Author luanmousheng
* @Date 17/7/1 下午7:34
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
/**
* 对象输出流,具体输出流是文件输出
*/
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
/**
* 通过ObjectOutputStream将对象写入到文件输出流
*/
oos.writeObject(jack);
oos.close();
/**
* 对象输入流,具体输入是文件输入流
*/
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
/**
* 通过ObjectInputStream将对象从文件输入流读入
*/
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
Student{age=23, name='jack'}
若Student类未实现Serializable接口,将会抛出运行时异常 java.io.NotSerializableException。
若将Student的age字段设置为transient的,反序化后age字段将是空值:
import java.io.Serializable;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午7:40
*/
public class Student implements Serializable{
//transient修饰的字段默认不会被序列化
private transient Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo运行后的输出为:
Student{age=null, name='jack'}
可以看到输出后的age为null,transient修饰的字段将不会被序列化。
前面我们说过java序列化包括的是除了static和transient修饰的其它成员变量。假设将Student的name字段设置为static,看下Demo输出结果:
import java.io.Serializable;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午7:55
*/
public class Student implements Serializable{
private Integer age;
private static String name = "student";
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo输出结果为:
Student{age=23, name='jack'}
看到反序列化后name的值是”jack”而不是”student”,这是为何?
这正是因为不会序列化static修饰的变量而导致的结果。序列化后的文件其实name字段值为空,反序列化后去方法区查找静态变量name的值,这个name值正是之前Demo类通过Student的构造函数将name值设置为”jack”的值,所以最终反序列化后的name值为”jack”。
也就是说这个”name”值不是通过反序列化得到的,而是通过去方法区拿到的全局的值。
如果读者对上例有疑惑,可以继续看下面的例子:
import java.io.*;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午20:30
*/
public class Student implements Serializable{
private Integer age;
private static String name = "student";
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
//增加了设置静态变量的方法
public static void setName(String name) {
Student.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
import java.io.*;
/**
* 文件描述:
*
* @Author luanmousheng
* @Date 17/7/1 下午20:52
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
oos.writeObject(jack);
oos.close();
//反序列化前将静态变量设置为"tom"
Student.setName("tom");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo输出结果为:
Student{age=23, name='tom'}
输出结果的name值为”tom”,而不是写入时的”jack”。因为在反序列化之前已经将静态变量name的值设置为”tom”,反序列化后会去取这个静态变量的值。:smile:
如果没有为类添加这两个方法,序列化和反序列化是通过ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法进行的。
如果为类添加writeObject()和readObject()方法,将不会再调用ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法,我们可以更加精确的操作对象的序列化。
例如如果将Student类的age字段设置为transient了,此时我们又想去序列化该字段,那么我们通过这两个方法来实现:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午21:12
*/
public class Student implements Serializable{
private transient Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
//默认的序列化
oos.defaultWriteObject();
oos.writeInt(age);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//默认的反序列化
ois.defaultReadObject();
this.age = ois.readInt();
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
此时Demo的输出结果将会输出age的值:
Student{age=23, name='jack'}
注意,这两个方法都是private的。方法中首先调用了序列化的默认方法,之后才是自定义的逻辑。还要注意的是序列化的顺序和反序列化的顺序必须一致。否则会读出未知的结果。例如将Student的age和name都设置为transient,在writeObject和readObject方法中调整写入和读出的顺序:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午21:29
*/
public class Student implements Serializable{
private transient Integer age;
private transient String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
//默认的序列化
oos.defaultWriteObject();
oos.writeInt(age);
oos.writeUTF(name);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//默认的序列化
ois.defaultReadObject();
//此处读入的顺序和写入的顺序不一致
this.name = ois.readUTF();
this.age = ois.readInt();
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo输出结果让人莫名其妙:
Student{age=1507332, name=''}
此处如果将oos.writeUTF方法改为oos.writeObject、ois.readUTF方法改为ois.readObject,会抛出异常 OptionalDataException
实现了该接口的类基于之前Serializable的序列化机制就会失效,Externalizable接口已经实现了Serializable。看下面的例子:
import java.io.*;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午21:53
*/
public class Student implements Externalizable{
private Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
将Student改为上面的代码,运行Demo类的输出结果为:
java.io.InvalidClassException: com.lms.serializable.Student; no valid constructor
异常抛出,提示没有可用的构造器,其实就是没有默认的构造函数。实现了Externalizable接口的类对象,反序列化的时候会先通过无参的构造函数new一个对象,然后将各个字段的值赋值给该对象,Student没有默认的构造函数,所以上例抛出异常。
为Student添加默认构造函数后的Demo输出结果为:
Student{age=null, name='null'}
可以看出,所有字段为空,这是因为实现的Externalizable接口的对象序列化需要开发者自己去保证,也就是需要实现writeExternal和readExternal方法,实现了writeExternal和readExternal方法后的Student类如下:
import java.io.*;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午22:14
*/
public class Student implements Externalizable{
private Integer age;
private String name;
public Student() {
}
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(age);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
age = in.readInt();
name = (String) in.readObject();
}
}
Demo输出结果为:
Student{age=23, name='jack'}
可以看出正确输出了结果。
如果一个类是单例,反序列化后是同一个对象吗?我们将Student改为单例模式的(Student用单例模式确实有点牵强),看下例:
import java.io.*;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午22:40
*/
public class Student implements Serializable{
private Integer age;
private String name;
private static class Instance {
private static Student jack = new Student(23, "jack");
}
private Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public static Student getInstance() {
return Instance.jack;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo类修改为:
import java.io.*;
/**
* 文件描述:Demo类
*
* @Author luanmousheng
* @Date 17/7/1 下午23:00
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
//取单例
Student jack = Student.getInstance();
oos.writeObject(jack);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
//看反序列化后是否是同一个对象
System.out.println(jack == stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo输出结果为:
Student{age=23, name='jack'}
false
可以看出,反序列化后不是同一个对象。那要怎么解决这个问题呢?可以在单例类Student中加个私有方法readResolve()方法。
import java.io.*;
/**
* 文件描述: 需要序列化的类需要实现Serializable接口
*
* @Author luanmousheng
* @Date 17/7/1 下午23:20
*/
public class Student implements Serializable{
private Integer age;
private String name;
private static class Instance {
private static Student jack = new Student(23, "jack");
}
private Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public static Student getInstance() {
return Instance.jack;
}
//增加readResolve方法,直接返回单例
private Object readResolve() {
return Instance.jack;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo输出结果为:
Student{age=23, name='jack'}
true
可以看到,反序列化后的对象和序列化之前的对象是同一个对象。
无论是实现Serializable或者Externalizable接口的对象,反序列化的时候都会调用readResolve方法,因此这里的readResolve方法直接返回了单例,而不是返回反序列化后的对象,保证了单例的逻辑。
实现Serializable接口的类需要生成对应的序列化id(serialVersionUID)。如:
private static final long serialVersionUID = -5881678404557689785L;
如果没有显示为类添加该字段(IDE可以设置成自动生成),java将会根据字节码文件动态生成序列化ID。每次重新编译生成的序列化ID都不一样。所以前面的Student例子都不是完美的,没有生成序列化ID。
如果显示为类添加该字段,那么不管编译多少次,只要没有修改该字段的值,该字段就不会变。
为何需要序列化ID?
如果一个类X在多个客户端使用,其中一个客户端A收到来自客户端B的类X对象x。客户端A通过网络收到对象x的二进制序列化文件后会进行反序列化操作。首先客户端A会取出对象x的序列化ID,并将该ID和客户端A本地类X Class对象的序列化ID进行比较,如果不相等那就会提示序列化失败,如果相等那就可以正常进行序列化。本人所在的公司现在使用的RPC框架、阿里开源的dubbo所使用的网络传输对象就是实现了Serializable接口。
如果序列化两个相同的对象,java做了特殊处理,看下例:
import java.io.*;
/**
* 文件描述: Demo类
*
* @Author luanmousheng
* @Date 17/7/1 下午23:40
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
//第一次序列化对象
oos.writeObject(jack);
oos.flush();
//第一次序列化对象后的文件大小
System.out.println(new File("student.out").length());
//第二次序列化同一个对象
oos.writeObject(jack);
//第二次序列化对象后的文件大小
System.out.println(new File("student.out").length());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
//第一次反序列化对象
Student stu1 = (Student) ois.readObject();
//第二次反序列化对象
Student stu2 = (Student) ois.readObject();
ois.close();
//比较是否同一个对象
System.out.println(stu1 == stu2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo输出结果为:
189
194
true
可以看到两次反序列化后的对象是同一个对象。
第一次序列化后的文件大小为189,第二次序列化后的文件大小为194,只增加了5字节,这显示是java做了优化导致的。两个相同对象当然不需要冗余存储,这增加的5字节是一些引用信息,这些引用指向同一个对象,反序列化后重建引用关系。
另外需要注意的是,如果在第二次序列化同一个对象之前,改变了这个对象某些字段的值,因为虚拟机发现之前已经将该对象序列化过了,这时只会存储写的引用,不会实际去写,所以第二次序列化之前对这些字段的改变实际上是没有效果的。
例如:
import java.io.*;
/**
* 文件描述:Demo类
*
* @Author luanmousheng
* @Date 17/7/1 下午23:54
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
//第一次序列化对象
oos.writeObject(jack);
oos.flush();
//第一次序列化对象后的文件大小
System.out.println(new File("student.out").length());
//第二次序列化之前将name值改为"lucy"
jack.setName("lucy");
//第二次序列化同一个对象
oos.writeObject(jack);
//第二次序列化对象后的文件大小
System.out.println(new File("student.out").length());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
//第一次反序列化对象
Student stu1 = (Student) ois.readObject();
//第二次反序列化对象
Student stu2 = (Student) ois.readObject();
ois.close();
//比较是否同一个对象
System.out.println(stu1);
System.out.println(stu2);
System.out.println(stu1 == stu2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果为:
189
194
Student{age=23, name='jack'}
Student{age=23, name='jack'}
true
可以发现输出的两个对象值完全相同,第二次反序列化之前将值改为”lucy”是无效的。