1、什么是序列化与反序列化?
) s& K$ M4 \: c 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。) v$ H8 B; b" z
2、Java如何实现序列化和反序列化?
% T7 d* j% b( u/ ? 在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。* K% Q' x8 v3 \
但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。& _% W) ~- v) q/ x1 E
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。$ E% H& Q, @. I R* A
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
) S% w$ f- z1 h' D 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。这里先来一段代码:$ i* U) D }: \6 |( {2 L
创建一个User类,用于序列化及反序列化' s0 j& Q' }9 c5 F
public class User implements Serializable{
private String name;
private int age;
private Date birthday;
private transient String gender;
private static final long serialVersionUID = -6849794470754667710L;
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 Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", birthday=" + birthday +
'}';
}
} 对User进行序列化及反序列化的Demo
M% l# v$ O: g/ s/ d% c8 I2 g public class SerializableDemo {
public static void main(String[] args) {
//Initializes The Object
User user = new User();
user.setName("hollis");
user.setGender("male");
user.setAge(23);
user.setBirthday(new Date());
System.out.println(user);
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User newUser = (User) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//output
//User{name='hollis', age=23, gender=male, birthday=Tue Feb 02 17:37:38 CST 2016}
//User{name='hollis', age=23, gender=null, birthday=Tue Feb 02 17:37:38 CST 2016} 1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。; B) S. T' v3 v2 k6 ^% I
2、通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
* {7 A9 @$ n3 s9 b1 I, M+ | 3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
* e& D/ `8 _/ L2 }$ L 4、序列化并不保存静态变量。
% H( u# ~4 q) T# E: J& [6 p 5、要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
" o, i. f4 [- o7 [2 Q 6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。5 U5 f* M' K2 w: D8 T5 ?
7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
' g0 ^, r' V, x7 x* B8 j/ o 3、如何自定义序列化和反序列化呢? ; C0 M% D" a, R: t) `6 p6 ^& g) \
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData; // non-private to simplify nested class access
private int size;
} 带着这个问题,我们来看java.util.ArrayList的源码从上面的代码中可以知道ArrayList实现了java.io.Serializable接口,那么我们就可以对它进行序列化及反序列化。因为elementData是transient的,所以我们认为这个成员变量不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法:
# J: ^1 q c6 j6 R3 p" ~2 x: M public static void main(String[] args) throws IOException, ClassNotFoundException {
List<String> stringList = new ArrayList<String>();
stringList.add("hello");
stringList.add("world");
stringList.add("hollis");
stringList.add("chuang");
System.out.println("init StringList" + stringList);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
objectOutputStream.writeObject(stringList);
IOUtils.close(objectOutputStream);
File file = new File("stringlist");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
List<String> newStringList = (List<String>)objectInputStream.readObject();
IOUtils.close(objectInputStream);
if(file.exists()){
file.delete();
}
System.out.println("new StringList" + newStringList);
}
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang] 了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。那么为什么code 4的结果却通过序列化和反序列化把List中的元素保留下来了呢?
8 p# U* @: z' n3 } 4、writeObject和readObject方法 + f* d. j5 k% C8 W1 N5 Z
在ArrayList中定义了来个方法: writeObject和readObject。$ ~! U& A3 V1 n
这里先给出结论:5 s5 b' y5 _6 ^0 G6 G) s
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。 如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。 用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。# w& Y5 U. r% W8 f" _$ T
那么为什么ArrayList要用这种方式来实现序列化呢? R9 E6 {3 P# j
ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。
+ d/ a( _% A4 X7 Y 前面说过,为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient来声明elementData。 但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来。
: F0 c6 ~% f' f1 {0 P writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。& F) o5 i3 M, x- j
readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中
' A, i+ p; \; V5 l: p5 g 至此,我们先试着来回答刚刚提出的问题:2 r/ M* H6 d6 B9 ~0 P5 t
如何自定义的序列化和反序列化策略
* _/ \- I! N# o) g, @; ~2 u' E/ k 答:可以通过在被序列化的类中增加writeObject 和 readObject方法。那么问题又来了:
/ g5 k' A2 M+ f( T' e 虽然ArrayList中写了writeObject 和 readObject 方法,但是这两个方法并没有显示的被调用啊。
8 O! h" M$ H& I# |% u- _% e 那么如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的呢?
* E5 n8 w: t8 T% r 在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用。
" G8 a. m1 ]( B" X) c% W 5、serializable接口 0 o2 Q+ s7 L9 E. f* x
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。" |3 Q' j# q% f. i" z+ I
该接口并没有字段和方法,为什么只有实现了该接口的对象才能被序列化呢?
% d7 x1 R6 ]( P3 D: g 在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException。& C9 {. n3 g( h0 p) ~. b' p
序列化ID:
* {! _( i8 r4 \ b private static final long serialVersionUID = 1L; 序列化 ID在 IDE 下提供了两种生成策略,一个设为固定的 1L,另一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成)。一般如果没有特殊需求,用默认的 1L 就可以,这样可以确保反序列化成功。因为不同的序列化id之间不能进行序列化和反序列化。