对于Java序列化之前有用过但一直没有细致的了解过,今天进行了系统的学习和了解,以供之后的使用和复习。
举个例子,我们需要保存一个对象到文件中之后在还原,或者将一个对象作为网络传输的对象。我们就要先将一个对象序列化为一个可以用输入输出流来操作的字节序列,之后在需要的时候又能复原对象。
给大家一个很简单的例子来展示序列化和反序列化:
创建一个Person类,实现Serializable接口:
import java.io.Serializable;
public class Person implements Serializable{
private int age;
private String firstName;
private String lastName;
public Person() {
super();
}
public Person(int age, String firstName, String lastName) {
super();
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person [age=" + age + ", firstName=" + firstName
+ ", lastName=" + lastName + "]";
}
}
主函数:
public class Main {
public static void main(String[] args) {
//将之前的输入我们注释掉
//序列化
FileOutputStream fileOutputStream = null ;
ObjectOutputStream objectOutputStream = null ;
Person jack = new Person(22,"Jack","Chai") ;
try {
fileOutputStream = new FileOutputStream("storage\\serializable.txt");
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(jack);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally{
try {
objectOutputStream.close();
fileOutputStream.close();
} catch (IOException e) {
System.out.println("IOException when IOStream close");
}
}
//反序列化
FileInputStream fileInputStream = null ;
ObjectInputStream objectInputStream = null ;
try {
fileInputStream = new FileInputStream("storage\\serializable.txt");
objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();
System.out.println(person.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally{
try {
objectInputStream.close();
fileInputStream.close();
} catch (IOException e) {
System.out.println("IOException when IOStream close");
}
}
}
}
最后看到输出结果:
很简单是不是。那么如果我们没有实现Serializable接口会怎样呢?
那么我们看看这个异常是如何抛出的:
java.io包中的ObjectOutputStream源码:
public final void writeObject(Object obj) throws IOException {
......
try {
//1 看到该函数调用了writeObject0方法
writeObject0(obj, false);
} catch (IOException ex) {
......
}
}
private void writeObject0(Object obj, boolean unshared)
throws IOException{
......
try {
......
//3 这里检测我们的对象是不是字符串的实例,是不是数组,
//是不是枚举还有就是是不是Serializable实例,这下我
//们便知道为什么我们没有引入Serializable接口会抛出
//该异常了吧,而且有趣的是我们的枚举不需要实例化便可以
//序列化
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
//2 这里我们看到抛出的NotSerializableException
//那我们看前面的各种条件
throw new NotSerializableException(cl.getName());
}
}
} finally {
......
}
}
有时候我们可能更改了我们的类,当然这是我们软件编码中最糟糕的情况之一了,而且越到后期这种改动所带来的破坏越大,不过有时候还是难以避免,在这时候我们如何读出原来文件中的类?这时候如果你知道我们的序列化允许重构会帮你很大的忙。
简单的例子:我这次更改一下我们的Person类,并将我们上次存入serializable.txt文件中的类读出来。
Person类v2版本:
import java.io.Serializable;
public class Person implements Serializable{
private int age;
private String firstName;
private String lastName;
//将上一个版本的Person添加一个性别的属性
private Gender gender;
enum Gender{
MALE , FEMALE
}
public Person() {
super();
}
public Person(int age, String firstName, String lastName) {
super();
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person [age=" + age + ", firstName=" + firstName
+ ", lastName=" + lastName + ", gender=" + gender + "]";
}
}
主函数:
public class Main {
public static void main(String[] args) {
//反序列化
FileInputStream fileInputStream = null ;
ObjectInputStream objectInputStream = null ;
try {
fileInputStream = new FileInputStream("storage\\serializable.txt");
objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();
System.out.println(person.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally{
try {
objectInputStream.close();
fileInputStream.close();
} catch (IOException e) {
System.out.println("IOException when IOStream close");
}
}
}
}
那我们看一看结果:
咦?说好的可以重构呢?通过查看其他资料得知,序列化其中是使用一个hash值,这个值是通过类中所有的东西计算出来的。序列化将该hash值与序列化流中的hash值相比较。相同才可以。那么按这样说就是不能重构喽,因为我们只要更改了里面的东西,hash就不一样了。然而我们让版本二的Person和我们版本一的hash一样就好了,这里就要用到serialVersionUID这个字段了,这个字段其实就是让我们自己设定hash的。那么我们获得之前的hash值然后赋给我们第二版本的Person中的serialVersionUID字段就可以了。而获得上一个版本的Person的hash值可以使用serialver命令获得。我们将撤回到上一个版本的Person。
于是我们获得此字段的值,将其加入我们版本2中的Person中。
public class Person implements Serializable{
//添加一个serialVersionUID,并设置其值为命令获得的值
private static final long serialVersionUID = 987882963008866333L;
private int age;
private String firstName;
private String lastName;
//将上一个版本的Person添加一个性别的属性
private Gender gender;
enum Gender{
MALE , FEMALE
}
public Person() {
super();
}
public Person(int age, String firstName, String lastName) {
super();
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person [age=" + age + ", firstName=" + firstName
+ ", lastName=" + lastName + ", gender=" + gender + "]";
}
}
主函数依然还是只反序列化,不变。测试结果:
看到反序列化得到了响应的值,而之前没有的属性赋值为null,增加了属性如此,减少属性会怎样呢?我删去了年龄字段之后再次运行结果为:
如此序列化重构成功。
我们去看一下我们上面测试所得到的文件中的内容,即序列化产生的数据。
我们不能全看懂那么我们可以看到com.csdn.jack.test.serializable.Person,age,firstName,lastName…看上去很不爽,这要是在网络传输中基本上是明文了。而我们可以自己来重新自定义我们的序列化数据。比如我想去处理一下Person类中的的firstName,让其前面加上一个字符串,当然学会这个的话你便可以使用加密算法对其经行加密,其实一个道理嘛。
public class Person implements Serializable{
private static final long serialVersionUID = 987882963008866333L;
private int age;
transient private String firstName; //在这里加上一个transient
private String lastName;
private Gender gender;
enum Gender{
MALE,FEMALE
}
public Person() {
super();
}
public Person(int age, String firstName, String lastName) {
super();
this.age = age;
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person [age=" + age + ", firstName=" + firstName
+ ", lastName=" + lastName + ", gender=" + gender + "]";
}
//实现特殊方法writeObject
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject("header#"+firstName);
}
//实现特殊方法readObject
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException {
in.defaultReadObject();
firstName = ((String) in.readObject()).split("#")[1];
}
}
首先,我们先用transient修饰符,使修饰的变量不会被默认序列化,然后我们在writeObject中自定义它的序列化,例如我是在firstName添加了一个前缀header#,而我们再实现readObject方法,在读取恢复firstName。先看一下运行结果:
结果反序列化正确。
然后我们去看一下文件中的内容:
注意看最后,有一个header#Jack,当然我们也可以将其中自定义部分换成自己的加密解密算法,然后序列化出的字节就是加密的了,这样在网络传输的时候就相对安全了。