目录
2. ObjectOutputStream 与 ObjectInputStream
4. 序列号与 InvalidClassException 异常
1. 对象的序列化与反序列化简介
(1)什么是对象序列化与反序列化?
对象序列化是指将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序;从字节流创建对象的相反的过程称为对象反序列化。序列化的字节流是与平台无关的,在一个平台上序列化的对象可以在不同的平台上反序列化。
(2)对象序列化与反序列化的作用
序列化的作用:在传递和保存对象时保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中;反序列化的作用:根据字节流中保存的对象状态及描述信息,通过反序列化重建对象。
2. ObjectOutputStream 与 ObjectInputStream
Java 提供了一组字节流 ObjectOutputStream 与 ObjectInputStream 供对象进行序列化与反序列化操作。而一个对象要想拥有序列化、反序列化功能,最简单的方法就是实现 java.io.Serializable 接口。这个接口是一个标记接口,即其内部无任何字段与方法定义,可以理解为给对象添加了一个标记,没有这个标记的对象就不能序列化。
(1)声明一个可序列化的对象
//对象实现序列化接口
public class Person implements Serializable {
private String name;
private Integer age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
//getter方法与setter方法,在此略去
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
(2)使用 ObjectOutputStream 序列化对象
void | writeObject(Object obj) 将指定的对象写入 ObjectOutputStream。 |
@Test
public void serialize() throws IOException {
//序列化路径
String filePath = "src\\com\\java\\day11\\file.txt";
//文件输出流
FileOutputStream outputStream = new FileOutputStream(filePath);
//序列化流
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
//对象序列化
oos.writeObject(new Person("张三",25));
oos.writeObject(new Person("李四",25));
oos.writeObject(new Person("王五",25));
oos.writeObject(new Person("赵六",25));
//关闭资源
oos.close();
}
(3)使用 ObjectInputStream 反序列化对象文件
Object | readObject() 从 ObjectInputStream 读取对象。 |
@Test
public void deserialize() throws IOException, ClassNotFoundException {
//反序列化路径
String filePath = "src\\com\\java\\day11\\file.txt";
//文件输入流
FileInputStream inputStream = new FileInputStream(filePath);
//反序列化流
ObjectInputStream ois = new ObjectInputStream(inputStream);
//对象反序列化
Person person = null;
//available()方法只能对节点流使用,对处理流无效
while (inputStream.available()>0){
person = (Person) ois.readObject();
System.out.println(person);
}
//关闭资源
ois.close();
}
3. 瞬态关键字 transient
瞬态是与静态相对的,静态关键字 static 所修饰的成员属性属于类而不属于对象,只要类被加载就可以直接调用,因此被 static 关键字所修饰的成员属性不能够被序列化。出于安全的考虑,如果对于某个成员属性希望在内存中使用它但却并不想把它的信息记录在文件中,应该怎么做呢?
对于此种情况,Java 为我们提供了另外一个关键字 transient 来应对,只要是被 transient 关键字修饰的成员属性,就被认为是只存在于程序执行阶段,即它的生命周期只能在内存中。该属性仍属于对象,但是属性中的信息却不能够在内存之外。
特别要注意的是:transient 关键字只能修饰成员属性而不能修饰成员方法和类。虽然它可以修饰没有实现 Serializable 接口的成员属性,但此属性所在的类本身就不能序列化,因此这样做没有任何意义。
(1)定义一个可序列化的拥有瞬态与静态属性的类 User
public class User implements Serializable {
private String userName;//普通私有属性
private transient String userPass;//瞬态私有属性
public static String country;//静态属性
public User() {
}
public User(String userName, String userPass) {
this.userName = userName;
this.userPass = userPass;
}
//getter方法与setter方法,在此略去
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", userPass='" + userPass + '\'' +
'}';
}
}
(2)实例化一个 User 对象先序列化再反序列化
//对象序列化
User user = new User("张三","12345678");
User.country = "中国";
oos.writeObject(user);
//反序列化结果
User{userName='张三', userPass='null'}
从上面的结果中可以看出,静态属性 country 压根就没有被当做此对象的属性被序列化,而瞬态属性 userPass 虽然被序列化了,但它的属性信息却没有被序列化,因此只能反序列化该属性的默认值 null。
4. 序列号与 InvalidClassException 异常
继续拿上面的 User 类举例:如果在序列化 user 对象之后删除 User 类的无参构造方法,也就是说原先的 User 被改变了。此时反序列化记录在文件中的 user ,发现系统出现异常:
刚才不是好好的嘛,为什么会出现异常呢?其实每个实现 Serializable 接口的类在被 javac.exe 从 .java 文件编译成 .class 文件时会首先查看该类是否有一个序列号,若没有则会创建一个。每次修改了 .java 文件之后都会重新编译一次,也就生成了新的序列号,此时对象文件反序列化所获取到的序列号与重新编译对生成类的序列号不同,就不能序列化为该类对象,而是抛出 InvalidClassException 异常。
因此在声明可序列化的类时,可以手动的添加一个序列号,使它在每次被编译时都会默认使用该序列号创建 .class 文件。即使之后修改了此类,也不会改变它的 .class 文件的序列号,这也就避免了因为修改类而使得反序列化抛出异常的情况发生。序列号有其固定的声明格式,必须严格按照以下格式才能成功声明类的序列号:
static final long serialVersionUID = 42L;
下面为 User 类声明一个序列号:
public class User implements Serializable {
//序列号
//变量名必须为serialVersionUID,不可修改
//变量值可任意,但要避免和其他类重复
static final long serialVersionUID = 123456789L;
private String userName;//普通私有属性
private transient String userPass;//瞬态私有属性
public static String country;//静态属性
//剩余内容。。。。。。
}