先回顾一下java流之间的关系:
序列化: 指堆内存中的java对象数据,通过某种方式把对象存储到磁盘文件中,或者传递给其他网络节点(网络传输)。这个过程称为序列化,通常是指将数据结构或对象转化成二进制的过程。序列化的好处就是便于运输和存储
反序列化: 把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
java代码实现序列化和反序列化
public class Main {
public static void main(String[] args) {
User user = new User();
user.setAge(10);
user.setName("ccj");
System.out.println("对象序列化之前……");
System.out.println("age:" + user.getAge());
System.out.println("name:" + user.getName());
// 序列化
try {
// 将user对象写入文件
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:/user.txt"));
os.writeObject(user);
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try {
// 将文件中对象的数据转化成java对象
ObjectInputStream is = new ObjectInputStream(new FileInputStream("D:/user.txt"));
User u = (User)is.readObject();
is.close();
System.out.println("对象反序列化后……");
System.out.println("age:" + u.getAge());
System.out.println("name:" + u.getName());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 要序列化的对象需要实现Serializable接口,否则会报NotSerializableException
class User implements Serializable{
private Integer age;
private String name;
//省略get、set方法
}
从代码的运行结果可以看到,我们成功的将对象保存到文件中,然后再读取了出来,也就是进行了一次序列化和反序列化的过程
一 被transient修饰的变量不能被序列化
当一个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化,但是如果我们希望某个属性不被序列化(有些比较敏感的属性例如密码,不希望他在网络上进行传输),那么就可以给这个属性加上transient关键字,在对类进行序列化的时候,这个属性就不会被序列化,同样,当进行反序列化时,被transient修饰的变量对应的值也应该为空,也就是说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化
对于transient总结:
- transient只能修饰属性,不能修饰方法和类
- 本地变量,也就是局部变量,不能被transient修饰
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问
二 静态变量不能被序列化
被static修饰的变量是不可以被序列化的,但是如果对于上面的例子,我将age属性加上static修饰,代码的运行结果也是一样的,也就是说反序列化之后age的值还是为10,并不是null;这又是为何呢?原因就是反序列化后类中static型变量age的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的;如何证明这个结论?如果我在序列化之后反序列之前加上一行代码:User.age = 20;
也就是将age的值改为20,运行结果输出变成20,也就是说如果反序列后读取的age值不是当前JVM中对应static变量的值,而是反序列化得出的,那么age值应该还是序列化时的10,不应该是修改后的20
静态变量也可以被transient修饰,但是不管有没有被transient修饰,都是无法被序列化的
三 序列化ID的作用
序列化ID指的是在要进行序列化的类中显示的定义了一个名为“serialVersionUID”、类型为long的变量;例如:private static final long serialVersionUID = 1L;
这个序列化ID的作用就是为了保证可以成功的进行反序列化 ;java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
如果我们没有在类中显示的声明一个序列化ID,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。
假设有这样一种场景:我们对一个实体类进行了序列化,这个实体类是没有声明序列化ID的,那么会根据这个实体类编译时的class自动生成一个默认的序列化ID;接下来我们对这个实体类添加了一个字段,然后再进行反序列时会抛出以下的异常:
这个异常就是序列化版本不一致产生的异常,导致java反序列化失败,原因就是如果没有显示的声明序列化ID,那么是根据实体类编译时的class自动生成的,当我们对这个类添加了一个字段,那么再次编译产生的class肯定不同,生成的默认序列化ID也不同
为了避免这种问题的产生,当要对某个类进行序列化时,我们就显示的给这个类声明一个序列化ID,这样即使后面因为需求需要对这个实体类进行变更(增加属性、删除属性等),也不会出现序列化ID不一致的问题,导致反序列化失败
四 细节补充
要实现java序列化,我们都知道要让类去实现Serializable接口,但其实还可以实现Externalizable接口来实现序列化;这两种方式的区别在于若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,没有指定则不会被序列化,这与是否被transient修饰无关;也就是说如果是现Externalizable接口来实现序列化,并且在writeExternal方法中指定哪个变量可以进行序列化,那么即使这个变量被transient修饰了,也是可以被序列化的
看下面的代码例子:
public class Main implements Externalizable{
private transient String msg = "即使我被transient修饰了,我还是可以被序列化呀!";
// 这个方法指定了要对类中哪些变量进行序列化
@Override
public void writeExternal(ObjectOutput out) throws IOException{
out.writeObject(msg);
}
// 这个方法定义反序列化的规则
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
msg = (String)in.readObject();
}
public static void main(String[] args) throws Exception {
Main m = new Main();
// 序列化
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File("test")));
out.writeObject(m);
// 反序列化
ObjectInput in = new ObjectInputStream(new FileInputStream(new File("test")));
m = (Main) in.readObject();
System.out.println(m.msg);
out.close();
in.close();
}
}
当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口
当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化