Java序列化和反序列化
序列化的概念
序列化(Serialization)是将对象的状态转换为可以存储或者传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
也就是说,程序运行的时候,会产生很多对象,而对象信息也只是在程序运行的时候才在内存中保持其状态,一旦程序停止,内存释放,对象也就不存在了。怎么能让对象永久的保存下来呢?就需要用到对象序列化。
基础部分
在Java的 I/O 类库中,专门给开发人员提供了两个类用于对象的序列化和反序列化操作的流类 ObjectOutputStream 和 ObjectInputStream。有了这两个类的帮助,再依照流的操作步骤一步两步,简单的对象的序列化和反序列化就真的很简单。
代码示例
基础类
import java.util.Date;
public class Sheep implements Serializable {
private String name;
private String color;
private Integer age;
private Date date = new Date();
public Sheep() {
}
public Sheep(String name, String color, Integer age) {
this.name = name;
this.color = color;
this.age = age;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", age=" + age +
", 出生时间=" + date +
'}';
}
// getter 和 setter方法 ...
}
序列化把实体类转换为文件
public class Test {
public static void main(String[] args) throws InterruptedException {
Sheep sheep = new Sheep("Tom", "white", 3);
try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("E:"+File.separator+"user"))) {
os.writeObject(sheep);
} catch (Exception e) {
e.printStackTrace();
}
}
}
序列化基本步骤:
① 对象实体类实现Serializable 标记接口
② 创建序列化输出流对象ObjectOutputStream,该对象的创建依赖于其它输出流对象,通常我们将对象序列化为文件存储,所以这里用文件相关的输出流对象 FileOutputStream
③ 通过ObjectOutputStream 的 writeObject()方法将对象序列化为文件
④ 关闭流 这里采用1.7开始的新语法try-with-resources
而不用自己控制流的关闭
使用反序列化把刚才的文件重新转换为对象
public class Test {
public static void main(String[] args) throws InterruptedException {
try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("E:"+File.separator+"user"))) {
Sheep o = (Sheep) is.readObject();
System.out.println(o);
System.out.println("反序列化时间:"+new Date());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
反序列化基本步骤:
① 创建输入流对象ObjectOutputStream。同样依赖于其它输入流对象,这里是文件输入流 FileInputStream
② 通过 ObjectInputStream 的 readObject()方法,将文件中的对象读取到内存
③ 关闭流 同序列化部分
关键字transient
这个关键字用来执行非序列化信息,把上面的代码改一下:
private transient String color;
序列化一下,之后再反序列化查看结果。
提高部分
上面只是很简单的基础部分,实际开发中我们还要面对很多复杂的业务场景。比如模型对象持有其它对象的引用怎么处理,如果引用类型是复杂些的集合类型怎么处理?提高的部分,一起来探索一下。
关于第一个问题,其实仔细分析上面的基础示例已经很明显了,我们Sheep类中本来就持有Date,String类的引用,不是一样的被序列化和反序列化了吗?如果是我们自己定义的类,那效果是怎么样的呢?给小羊添加出生(Born)类来尝试一下。
public class Born implements Serializable {
private Integer id;
private String country;
private String location;
public Born() { }
public Born(Integer id, String country, String location) {
this.id = id;
this.country = country;
this.location = location;
}
@Override
public String toString() {
return "Born{" +
"id=" + id +
", country='" + country + '\'' +
", location='" + location + '\'' +
'}';
}
// getter 和 setter 方法 ...
}
public class Sheep implements Serializable {
private String name;
private transient String color;
private Integer age;
private Date date = new Date();
private Born born;
public Sheep() {}
public Sheep(String name, String color, Integer age) {
this.name = name;
this.color = color;
this.age = age;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", age=" + age +
", 时间=" + date +
", 出生信息=" + born +
'}';
}
// getter 和 setter 省略...
}
测试样例:
public class Test {
public static void main(String[] args) throws InterruptedException {
Sheep sheep = new Sheep("Tom", "white", 3);
Born born = new Born(1,"澳洲","xx牧场");
sheep.setBorn(born);
try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("F:"+File.separator+"sheep"))) {
os.writeObject(sheep);
System.out.println("序列化时间" + new Date());
} catch (Exception e) {
e.printStackTrace();
}
//先睡5秒
TimeUnit.SECONDS.sleep(5);
try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("F:"+File.separator+"sheep"))) {
Sheep o = (Sheep) is.readObject();
System.out.println(o);
System.out.println("反序列化时间:"+new Date());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 执行结果:
序列化时间Wed Aug 05 18:22:01 CST 2020
Sheep{name='Tom', color='null', age=3, 时间=Wed Aug 05 18:22:01 CST 2020, 出生信息=Born{id=1, country='澳洲', location='xx牧场'}}
反序列化时间:Wed Aug 05 18:22:06 CST 2020
那么假如说现在有个类内对象是集合呢??
我们的小羊每个季度都是需要健康检查的,那么新增一个类。
public class Health implements Serializable {
private Integer id;
private Date date;
private String healthStatus;
public Health() { }
public Health(Integer id, Date date, String healthStatus) {
this.id = id;
this.date = date;
this.healthStatus = healthStatus;
}
@Override
public String toString() {
return "Health{" +
"id=" + id +
", date=" + date +
", healthStatus='" + healthStatus + '\'' +
'}';
}
// getter and setter
}
// Sheep类修改
public class Test {
public static void main(String[] args) throws InterruptedException {
Sheep sheep = new Sheep("Tom", "white", 3);
Born born = new Born(1,"澳洲","xx牧场");
List<Health> checks = new ArrayList<>();
checks.add(new Health(1,new Date(), "健康"));
checks.add(new Health(2,new Date(), "胃结石"));
checks.add(new Health(3,new Date(), "健康"));
sheep.setBorn(born);
sheep.setChecks(checks);
try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("F:"+File.separator+"sheep"))) {
os.writeObject(sheep);
System.out.println("序列化时间" + new Date());
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.SECONDS.sleep(5);
try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("F:"+File.separator+"sheep"))) {
Sheep o = (Sheep) is.readObject();
System.out.println(o);
System.out.println("反序列化时间:"+new Date());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// 输出结果
序列化时间Wed Aug 05 18:37:17 CST 2020
Sheep{name='Tom', color='null', age=3, date=Wed Aug 05 18:37:17 CST 2020,
born=Born{id=1, country='澳洲', location='xx牧场'},
checks=[
Health{id=1, date=Wed Aug 05 18:37:17 CST 2020, healthStatus='健康'},
Health{id=2, date=Wed Aug 05 18:37:17 CST 2020, healthStatus='胃结石'},
Health{id=3, date=Wed Aug 05 18:37:17 CST 2020, healthStatus='健康'}]}
反序列化时间:Wed Aug 05 18:37:22 CST 2020
关于transient的争议
transient是干什么的??是隐藏字段的,是防止序列化的,但是看一下ArrayList里面:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
这个object数组是隐藏起来的??那刚才反序列化的结果的的确确是有的呀,其实对于集合类这些,都是有单独实现序列化的方法的,而不是使用虚拟机默认的方式。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList的序列化和反序列化主要思路就是根据集合中实际存储的元素个数来进行操作,也就是说有一个对象就序列化一个,而不是把所有的空间都序列化,这样做估计是为了避免不必要的空间浪费(因为ArrayList的扩容机制决定了,集合中实际存储的元素个数肯定比集合的可容量要小)。
自定义序列化方法
public class CustomSheepSerial implements Serializable {
private String name;
private transient String color;
private Integer age;
private Date date = new Date();
public CustomSheepSerial() {}
public CustomSheepSerial(String name, String color, Integer age) {
this.name = name;
this.color = color;
this.age = age;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", age=" + age +
", date=" + date +
'}';
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.writeObject(name);
s.writeObject(color);
s.writeObject(age);
s.writeObject(date);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
name = (String) s.readObject();
color = (String) s.readObject();
age = (Integer) s.readObject();
date = (Date) s.readObject();
}
}
在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
序列化和反序列化的一些问题
实现 serializable 接口必须写 serialVersionUID 吗???
官方文档当中的解释
如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。
不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。
因此,为保证 serialVersionUID 值跨不同java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类
-- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
所以说尽量还是显式声明出来,这样序列化的类即使有字段的修改,因为serialVersionUID
的存在,也能保证反序列化成功。
序列化只有上面的这种方式吗???
其实不是的。根据序列化的定义,不管通过什么方式,只要你能把内存中的对象转换成能存储或传输的方式,又能反过来恢复它,其实都可以称为序列化。因此,我们常用的 Fastjson、Jackson等第三方类库将对象转成Json格式文件,也可以算是一种序列化,用JAXB实现XML格式文件输出文件输出,也可以算是序列化。
序列化和反序列化就都这了,谢谢你的观看!!!