何为序列化
序列化与反序列化对应。
序列化:将一个对象转为字节流
反序列化: 将字节流转为对象
通过序列化继续,可以将对象编码之后,通过网络传输到其他应用中,再反序列化,实现远程交互调用。
如何序列化
要想使一个类的实例可以实例化,只需要实现 Serializable 即可。
正因为实现序列化太简单,很容易给程序员一种误解:序列化很容易,不需要考虑太多。
实际的情形却比较复杂。
标记类可实例化很简单,执行实例化动作的直接开销也很低。
序列化实例
我们先创建一个Car类
@Data
@Accessors(chain = true)
public class Car implements Serializable {
private static final Long serialVersionUID = 1L;
private String id;
private Integer age;
private String name;
}
创建序列化和反序列化方法
public class Client {
public static void main(String[] args) throws Exception {
// ser();
dser();
}
/**
* 序列化
* @throws Exception
*/
static void ser() throws Exception {
//age会序列化默认值null
Car car = new Car()
.setId("13")
.setName("小红");
FileOutputStream fileOut =new FileOutputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(car);
out.close();
fileOut.close();
System.out.println("--------序列化完成----------");
}
/**
* 反序列化
* @throws Exception
*/
static void dser() throws Exception {
FileInputStream fileIn = new FileInputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Car car = (Car) in.readObject();
in.close();
fileIn.close();
System.out.println("---------反序列化完成-------");
System.out.println(car);
}
如果需要特殊定制序列化, 这种情况,只有指定的字段被处理,其他的字段丢失。 注意,读写的顺序对应,否则值就串了
我们在Car类添加两个方法
@Data
@Accessors(chain = true)
public class Car implements Serializable {
private static final Long serialVersionUID = 1L;
private String id;
private Integer age;
private String name;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("writeObject");
out.writeObject(id);
out.writeInt(age == null? 1:age);
out.writeObject(name);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject");
this.id= in.readObject().toString();
this.age = in.readInt();
this.name =in.readObject().toString();
}
}
序列化的代价
1、一旦确认了序列化的形式。后续任何变动,都可能导致使用这个格式进行反序列化的程序发生错误。
2、默认的序列化会导出私有字段这一点与封装特性相违背
程序不同版本之间,因为优化或者修复 bug ,改变内部的私有字段非常常见
这些本来应该被隐藏在具体实现中,调用者不需要也不应该关心,但默认的序列化会导出这些数据
3、序列化是另一种创建对象实例的机制(clone同样也是绕过了构造器)
因为序列化绕过了构造器很容易发生“在构造器中做了安全前提检查,但因为序列化根本不走构造器,所以绕过了这种安全检查”
自定义序列化形式
假设,有个类 Cat 实现了序列化,并且将数据序列化,保存到硬盘或某个位置
之后,Cat 考虑应该继承一个 Animal 父类。并且,Animal 的信息自然也应该属于序列化的一部分
但注意:我们序列化的时候可没有保存 Animal 信息
这时候怎么办?
java 给出的方案就是 readObjectNoData
也就是,没有流,给一个默认的恢复方案。java 会调用 Animal 的 readObjectNoData
当然,还有一种类似的情况。就是本来就继承 Animal ,但Animal 忘了声明序列化。
在保存数据之后想起来了,加上了 Serializable ,但因为之前的数据中也没有 序列化父类信息
所以恢复的时候也应该从 readObjectNoData设置
总结: 此方法用在序列化的父类中。当子类序列化时候,父类未参与。
但后续反序列化的时候根据新规则,父类需要参与反序列化,可实际信息中,父类并没有,所以,需要用 readObjectNoData
回到刚才的话题,为什么需要书写 readObjectNoData?
因为,如果我们去掉这个方法,父类就默认不处理数据了,这时候可能处于错误的状态中
当然,如果父类不面临这种问题,就不需要处理
例子:
假设现在Car需要继承Animal
@Data
@Accessors(chain = true)
public class Animal{
public int pid;
}
@Data
@Accessors(chain = true)
public class Car extends Animal implements Serializable {
private static final Long serialVersionUID = 1L;
private String id;
private Integer age;
private String name;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("writeObject");
out.writeObject(id);
out.writeInt(age == null? 1:age);
out.writeObject(name);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject");
this.id= in.readObject().toString();
this.age = in.readInt();
this.name =in.readObject().toString();
}
}
由于之前序列化的时候没有继承Animal,所以反序列化的时候,获取Animal的字段均为默认值,这个时候如果要自定义给Animal赋值就需要这样做
@Data
@Accessors(chain = true)
public class Animal implements Serializable {
private static final Long serialVersionUID = 1L;
public int pid;
private void readObjectNoData() throws IOException, ClassNotFoundException {
System.out.println("readObjectNoData");
this.pid = 1;
}
}
我们给父类的pid赋值1 ,然后调用反序列化方法
public class Client {
public static void main(String[] args) throws Exception {
// ser();
dser();
}
/**
* 序列化
* @throws Exception
*/
static void ser() throws Exception {
//age会序列化默认值null
Car car = new Car()
.setId("13")
.setName("小红");
FileOutputStream fileOut =new FileOutputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(car);
out.close();
fileOut.close();
System.out.println("--------序列化完成----------");
}
/**
* 反序列化
* @throws Exception
*/
static void dser() throws Exception {
FileInputStream fileIn = new FileInputStream("C:\\Users\\admin\\Desktop\\file_upload\\car.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Car car = (Car) in.readObject();
in.close();
fileIn.close();
System.out.println("---------反序列化完成-------");
System.out.println(car);
System.out.println(car.pid);
}
}
可以看到,父类的pid字段被成功赋值
readObjectNoData
readObject
---------反序列化完成-------
Car(id=13, age=1, name=小红)
1
补充说明
以上的情况是出于谨慎考虑,实际上, 现在很多框架都不会明确依赖序列化
更多的是Object - json 的路线或者使用 rest api
现在的 SpringBoot 或 SpringCloud 项目,一般不会被序列化问题所困扰