Java序列化与反序列化
java中将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。
序列化主要用途:(1)把对象的字节序列永久地保存到文件中; (2)在网络上传送对象的字节序列;
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。
发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
什么情况下需要序列化:
- 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
- 当你想用套接字在网络上传送对象的时候;
- 当你想通过RMI传输对象的时候;
序列化的几种方式
比较常见的做法有两种:一是把对象包装成JSON字符串传输,二是采用java对象的序列化和反序列化。随着Google工具protoBuf的开源,protobuf也是个不错的选择。
举例:
import java.io.Serializable; public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } }
序列化/反序列化:
import com.serializable.Person;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestSerializable {
@Test
public void testSeria() throws Exception {
File file = new File("person.txt");
Person lufei = new Person("路飞", 18);
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
os.writeObject(lufei);
os.close();
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file));
Object newPerson = oi.readObject();
oi.close();
System.out.println(newPerson);
}
}
运行结果:
person.txt中的内容使用utf-8编码之后:
��srcom.serializable.Personk�K�?�IageLnametLjava/lang/String;xpt路飞 |
序列化注意事项:
- 一个类的序列化要通过实现Serializable接口来实现。如果没有实现这个接口,则无法实现序列化和反序列化,实现序列化的类的子类也可以实现序列化。
- 子类实现了序列化接口,但是父类没有实现序列化接口的话,父类必须要实现一个无参数构造器,否则会抛异常。
- 运行过程中吐过遇到没实现序列化接口的类会抛出NotSerializableException异常。
- writeReplace() 方法可以使对象被写入到流之前,用一个对象来替换自己。
- java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 如果对象包含其他对象的引用,则writeObject()方法递归序列化这些对象。
- java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象。
- 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化。
- Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,增加一些存储空间来表示新增引用和一些控制信息的空间。
一致性/兼容性:
通过在运行时判断serialVersionUID来检查版本是否具有一致性。在进行反序列化时,JVM会把字节流中serialVersionUID与本地实体类的serialVersionUID进行比较, 如果相同则是认为一致的,否则就会抛出InvalidClassException异常。
serialVersionUID
private static final long serialVersionUID;
该静态变量在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者的serialVersionUID与对应的发送者版本号不同,则抛出InvalidClassException异常。
有两种生成方式:
一个是默认的1L,比如:private static final long serialVersionUID = 1L;
一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL;
如果可序列化类未显式声明 serialVersionUID,则运行时将根据该类计算一个默认的serialVersionUID 值。不过,强烈建议所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。
显式地定义serialVersionUID有两种用途:
(1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
(2)当你序列化了一个类实例后,希望更改一个字段或添加一个字段,不设置serialVersionUID,所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。如果你添加了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置。
序列化时,类的所有数据成员应可序列化除了声明为transient或static的成员。
使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化。
transient举例:
测试类:
运行结果:
对敏感字段加密
情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作。
举例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
import java.io.Serializable;
public class Person2 implements Serializable {
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 = "new password";//模拟加密
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 Person2());
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
Person2 t = (Person2) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
原密码:pass 加密后的密码new password 要解密的字符串:new password 解密后的字符串:pass
说明:writeObject 方法中,对密码进行了加密,在 readObject 中则对 password 进行解密,只有拥有密钥的客户端,才可以正确的解析出密码,确保了数据的安全。
可能会遇到的问题:
1)多个引用写入
@Test public void testSeria() throws Exception { File file = new File("person.txt"); Person lufei = new Person("路飞", 18); ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file)); os.writeObject(lufei); lufei.setAge(20); os.writeObject(lufei); os.close(); ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file)); Person lufei1 = (Person) oi.readObject(); Person lufei2 = (Person) oi.readObject(); oi.close(); System.out.println(lufei1.getAge()); System.out.println(lufei2.getAge()); }
输出结果:
18
18
分析:在默认情况下,对于一个实例的多个引用,只会写入一次。可以通过ObjectOutputStream的rest方法或着writeUnshared方法实现多次写入。
os.writeObject(lufei); lufei.setAge(20); os.reset(); os.writeObject(lufei); os.close();
输出结果:
18
20
2)类中字段修改
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
如果反序列化中的Person类中添加了新的属性,private int weight; 表示体重
@Test
public void testSeria() throws Exception {
File file = new File("person.txt");
Person lufei = new Person("路飞", 18);
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
os.writeObject(lufei);
os.close();
ObjectInputStream oi = new ObjectInputStream(new FileInputStream(file));
Person lufei1 = (Person) oi.readObject();
oi.close();
System.out.println("weight="+lufei1.getWeight());
}
运行结果:
weight=0