1.什么是序列化
Java的对象序列化其实就是将一个实现了serializable接口的对象转换成一个二进制byte数组,这样日后使用这个对象时候就能将这个对象及其数据通用反序列化转换回来,重新构建。
在接口开发或者其它容易跨平台操作时,使用对象序列化就意味着能自动补全操作系统的差异,例如在windows系统上创建一个对象,序列化之后通过网络传到linux系统上反序列化回来,那么这个对象在Linux上仍可以继续使用!
简而言之:
● 序列化: 将数据结构或对象转换成二进制串的过程。
● 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
2.怎么序列化
在实现序列化是有个前提条件的:只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常。
举个例子:
public class Student implements Serializable {
private String name = null;
private Integer age = null;
private Gender gender = null;
public Student () {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return "[" + name + ", " + age + ", " + gender + "]";
}
}
新建一个miantest 进行序列化和反序列化
public class maintest{
public static void main(String[] args) throws Exception {
File file = new File("studnet.out");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Studnet studnet = new Studnet ("John", 101, Gender.MALE);
oout.writeObject(studnet );
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newStudent = oin.readObject(); // 没有强制转换到Person类型
oin.close();
System.out.println(newStudent);
}
}
此时可以看到在反序列时并没有调用任何构造器的,直接读取字节文件。
还有一点需要注意的事当Student对象被保存到Student.out文件中之后,我们可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的CLASSPATH中包含有Student.class,否则会抛出ClassNotFoundException。
总结下就是:
假定一个Student类,它的对象需要序列化,可以有如下三种方法:
方法一:若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化
ObjectOutputStream采用默认的序列化方式,对Student对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对对Student对象的非transient的实例变量进行反序列化。
方法二:若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。(适用于加解密)
ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。
方法三:若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。
3.为什么序列化
序列化使用于一下场景:
1):当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
2):当你想用套接字在网络上传送对象的时候;
3):当你想通过RMI传输对象的时候;
4.反序列化会遇到什么问题,如何解决
1)对敏感字段加密:
情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
private static final long serialVersionUID = 1L;
private String password = "pass";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private void writeObject(ObjectOutputStream out) {
try {
PutField putFields = out.putFields();
System.out.println("原密码:" + password);
password = "encryption";//模拟加密
putFields.put("password", password);
System.out.println("加密后的密码" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//模拟解密,需要获得本地的密钥
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t = (Test) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
2)序列化 ID 问题
情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。
问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。
解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
3)部分字段不进行序列化
情景:当对象被序列化时(写入字节序列到目标文件)时,服务端需要这个状态,而客户端并不需要,此时完全没有必要将此状态序列化到客户端。
例如:
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class ClassLib implements Serializable {
private transient InputStream is;
private int majorVer;
private int minorVer;
ClassLib(InputStream is) throws IOException {
System.out.println("ClassLib(InputStream) called");
this.is = is;
DataInputStream dis;
if (is instanceof DataInputStream)
dis = (DataInputStream) is;
else
dis = new DataInputStream(is);
if (dis.readInt() != 0xcafebabe)
throw new IOException("not a .class file");
minorVer = dis.readShort();
majorVer = dis.readShort();
}
int getMajorVer() {
return majorVer;
}
int getMinorVer() {
return minorVer;
}
void showIS() {
System.out.println(is);
}
}
public class TransDemo {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("usage: java TransDemo classfile");
return;
}
ClassLib cl = new ClassLib(new FileInputStream(args[0]));
System.out.printf("Minor version number: %d%n", cl.getMinorVer());
System.out.printf("Major version number: %d%n", cl.getMajorVer());
cl.showIS();
try (FileOutputStream fos = new FileOutputStream("x.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(cl);
}
cl = null;
try (FileInputStream fis = new FileInputStream("x.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
System.out.println();
cl = (ClassLib) ois.readObject();
System.out.printf("Minor version number: %d%n", cl.getMinorVer());
System.out.printf("Major version number: %d%n", cl.getMajorVer());
cl.showIS();
} catch (ClassNotFoundException cnfe) {
System.err.println(cnfe.getMessage());
}
}
}
5.相关注意事项
a)序列化时,只对对象的状态进行保存,而不管对象的方法;
b)当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
c)当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
d)并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
1.安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
2.资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现。
参考案列:
Java序列化与反序列化