详解Java 对象序列化和反序列化

之前的文章中我们介绍过有关字节流字符流的使用,当时我们对于将一个对象输出到流中的操作,使用DataOutputStream流将该对象中的每个属性值逐个输出到流中,读出时相反。在我们看来这种行为实在是繁琐,尤其是在这个对象中属性值很多的时候。基于此,Java中对象的序列化机制就可以很好的解决这种操作。本篇就简单的介绍Java对象序列化,主要内容如下:

  1. 简洁的代码实现
  2. 序列化实现的基本算法
  3. 两种特殊的情况
  4. 自定义序列化机制
  5. 序列化的版本控制

一、简洁的代码实现

在介绍对象序列化的使用方法之前,先看看我们之前是怎么存储一个对象类型的数据的。

//简单定义一个Student类
public class Student {

 private String name;
 private int age;

 public Student(){}
 public Student(String name,int age){
 this.name = name;
 this.age=age;
 }

 public void setName(String name){
 this.name = name;
 }
 public void setAge(int age){
 this.age = age;
 }
 public String getName(){
 return this.name;
 }
 public int getAge(){
 return this.age;
 }
 //重写toString
 @Override
 public String toString(){
 return ("my name is:"+this.name+" age is:"+this.age);
 }
}
//main方法实现了将对象写入文件并读取出来
public static void main(String[] args) throws IOException{

 DataOutputStream dot = new DataOutputStream(new FileOutputStream("hello.txt"));
 Student stuW = new Student("walker",21);
 //将此对象写入到文件中
 dot.writeUTF(stuW.getName());
 dot.writeInt(stuW.getAge());
 dot.close();

 //将对象从文件中读出
 DataInputStream din = new DataInputStream(new FileInputStream("hello.txt"));
 Student stuR = new Student();
 stuR.setName(din.readUTF());
 stuR.setAge(din.readInt());
 din.close();

 System.out.println(stuR);
 }

输出结果:my name is:walker age is:21

显然这种代码书写是繁琐的,接下来我们看看,如何使用序列化来完成保存对象的信息。

public static void main(String[] args) throws IOException, ClassNotFoundException {

 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
 Student stuW = new Student("walker",21);
 oos.writeObject(stuW);
 oos.close();

 //从文件中读取该对象返回
 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
 Student stuR = (Student)ois.readObject();
 System.out.println(stuR);
 }

写入文件时,只用了一条语句就是writeObject,读取时也是只用了一条语句readObject。并且Student中的那些set,get方法都用不到了。是不是很简洁呢?接下来介绍实现细节。

二、实现序列化的基本算法

在这种机制中,每个对象都是对应着唯一的一个序列号,而每个对象在被保存的时候也是根据这个序列号来对应着每个不同的对象,对象序列化就是指利用了每个对象的序列号进行保存和读取的。首先以写对象到流中为例,对于每个对象,第一次遇到的时候会将这个对象的基本信息保存到流中,如果当前遇到的对象已经被保存过了,就不会再次保存这些信息,转而记录此对象的序列号(因为数据没必要重复保存)。对于读的情况,从流中遇到的每个对象,如果第一次遇到,直接输出,如果读取到的是某个对象的序列号,就会找到相关联的对象,输出。

说明几点,一个对象要想是可序列化的,就必须实现接口 java.io.Serializable;,这是一个标记接口,不用实现任何的方法。而我们的ObjectOutputStream流,就是一个可以将对象信息转为字节的流,构造函数如下:

public ObjectOutputStream(OutputStream out)

也就是所有字节流都可以作为参数传入,兼容一切字节操作。在这个流中定义了writeObject和readObject方法,实现了序列化对象和反序列化对象。当然,我们也是可以通过在类中实现这两个方法来自定义序列化机制,具体的后文介绍。此处我们只需要了解整个序列化机制,所有的对象数据只会保存一份,至于相同的对象再次出现,只保存对应的序列号。下面,通过两个特殊的情况直观的感受下他的这个基本算法。

三、两个特殊的实例

先看第一个实例:

public class Student implements Serializable {

 String name;
 int age;
 Teacher t; //另外一个对象类型

 public Student(){}
 public Student(String name,int age,Teacher t){
 this.name = name;
 this.age=age;
 this.t = t;
 }

 public void setName(String name){this.name = name;}
 public void setAge(int age){this.age = age;}
 public void setT(Teacher t){this.t = t;}
 public String getName(){return this.name;}
 public int getAge(){return this.age;}
 public Teacher getT(){return this.t;}
}

public class Teacher implements Serializable {
 String name;

 public Teacher(String name){
 this.name = name;
 }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {

 Teacher t = new Teacher("li");
 Student stu1 = new Student("walker",21,t);
 Student stu2 = new Student("yam",22,t);
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
 oos.writeObject(stu1);
 oos.writeObject(stu2);

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
 Student stuR1 = (Student)ois.readObject();
 Student stuR2 = (Student)ois.readObject();

 if (stuR1.getT() == stuR2.getT())
  System.out.println("相同对象");
 }

结果是很显而易见的,输出了相同对象。我们在main函数中定义了两个student类型对象,他们却都引用的同一个teacher对象在内部。完成序列化之后,反序列化出来两个对象,通过比较他们内部的teacher对象是否是同一个实例,可以看出来,在序列化第一个student对象的时候t是被写入流中的,但是在遇到第二个student对象的teacher对象实例时,发现前面已经写过了,于是不再写入流中,只保存对应的序列号作为引用。当然在反序列化的时候,原理类似。这和我们上面介绍的基本算法是一样的。

下面看第二个特殊实例:

public class Student implements Serializable {

 String name;
 Teacher t;

}

public class Teacher implements Serializable {
 String name;
 Student stu;

}

public static void main(String[] args) throws IOException, ClassNotFoundException {

 Teacher t = new Teacher();
 Student s =new Student();
 t.name = "walker";
 t.stu = s;
 s.name = "yam";
 s.t = t;

 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
 oos.writeObject(t);
 oos.writeObject(s);
 oos.close();

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
 Teacher tR = (Teacher)ois.readObject();
 Student sR = (Student)ois.readObject();
 if(tR == sR.t && sR == tR.stu)System.out.println("ok");

 }

输出的结果是ok,这个例子可以叫做:循环引用。从结果我们可以看出来,序列化之前两个对象存在的相互的引用关系,经过序列化之后,两者之间的这种引用关系是依然存在的。其实按照我们之前介绍的判断算法来看,首先我们先序列化了teacher对象,因为他内部引用了student的对象,两者都是第一次遇到,所以将两者序列化到流中,然后我们去序列化student对象,发现这个对象以及内部的teacher对象都已经被序列化了,于是只保存对应的序列号。读取的时候根据序列号恢复对象。

四、自定义序列化机制

综上,我们已经介绍完了基本的序列化与反序列化的知识。但是往往我们会有一些特殊的要求,这种默认的序列化机制虽然已经很完善了,但是有些时候还是不能满足我们的需求。所以我们看看如何自定义序列化机制。自定义序列化机制中,我们会使用到一个关键字,它也是我们之前在看源码的时候经常遇到的,transient。将字段声明transient,等于是告诉默认的序列化机制,这个字段你不要给我写到流中去,我会自己处理的。

public class Student implements Serializable {

 String name;
 transient int age;

 public String toString(){
 return this.name + ":" + this.age;
 }
}

public static void main(String[] args) throws IOException, ClassNotFoundException {

 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
 Student stu = new Student();
 stu.name = "walker";stu.age = 21;
 oos.writeObject(stu);

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
 Student stuR = (Student)ois.readObject();

 System.out.println(stuR);
 }

输出结果:walker:0

我们不是给age字段赋初始值了么,怎么会是0呢?正如我们上文所说的一样,被transient修饰的字段不会被写入流中,自然读取出来就没有值,默认是0。下面看看我们怎么自己来序列化这个age。

//改动过的student类,main方法没有改动,大家可以往上看
public class Student implements Serializable {

 String name;
 transient int age;

 private void writeObject(ObjectOutputStream oos) throws IOException {
 oos.defaultWriteObject();

 oos.writeInt(25);
 }

 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 ois.defaultReadObject();

 age = ois.readInt();
 }

 public String toString(){
 return this.name + ":" + this.age;
 }
}

输出结果:walker:25

结果既不是我么初始化的21,也不是0,而是我们在writeObject方法中写的25。现在我们一点一点看看每个步骤的意义。首先,要想要实现自定义序列化,就需要在该对象定义的类中实现两个方法,writeObject和readObject,而且格式必须和上面贴出来的一样,笔者试过改动方法修饰符,结果导致不能成功序列化。这是因为,Java采用反射机制,检查该对象所在的类中有没有实现这两个方法,没有的话就使用默认的ObjectOutputStream中的这个方法序列化所有字段,如果有的话就执行你自己实现的这个方法。

接下来,看看这两个方法实现的细节,先看writeObject方法,参数是ObjectOutputStream 类型的,这个拿到的是我们在main方法中定义的ObjectOutputStream 对象,要不然它怎么知道该把对象写到那个地方去呢?第一行我们调用的是oos.defaultWriteObject();这个方法实现的功能是,将当前对象中所有没有被transient修饰的字段写入流中,第二条语句我们显式的调用了writeInt方法将age的值写入流中。读取的方法类似,此处不再赘述。

五、版本控制

最后我们来看看,序列化过程的的版本控制问题。在我们将一个对象序列化到流中之后,该对象对应的类的结构改变了,如果此时我们再次从流中将之前保存的对象读取出来,会发生什么?这要分情况来说,如果原类中的字段被删除了,那从流中输出的对应的字段将会被忽略。如果原类中增加了某个字段,那新增的字段的值就是默认值。如果字段的类型发生了改变,抛出异常。在Java中每个类都会有一个记录版本号的变量:static final serivalVersionUID = 115616165165L,此处的值只用于演示并不对应任意某个类。这个版本号是根据该类中的字段等一些属性信息计算出来的,唯一性较高。每次读出的时候都会去比较之前和现在的版本号确认是否发生版本不一致情况,如果版本不一致,就会按照上述的情形分别做处理。

对象的序列化就写完了,如果有什么内容不妥的地方,希望大家指出!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java 序列化和反序列化实例详解

    Java 序列化和反序列化实例详解 在分布式应用中,对象只有经过序列化才能在各个分布式组件之间传输,这就涉及到两个方面的技术-发送者将对象序列化,接受者将对象反序列化,下面就是一个很好的例子! 1.实体-Employee import java.io.Serializable; public class Employee implements Serializable{ /** * */ private static final long serialVersionUID = 1L; publi

  • 浅谈java中为什么实体类需要实现序列化

    当客户端访问某个能开启会话功能的资源,web服务器就会创建一个HTTPSession对象,每个HTTPSession对象都会占用一定的内存,如果在同一个时间段内访问的用户太多,就会消耗大量的服务器内存,为了解决这个问题我们使用一种技术:session的持久化. 什么是session的持久化? web服务器会把暂时不活动的并且没有失效的HTTPSession对象转移到文件系统或数据库中储存,服务器要用时在把他们转载到内存. 把Session对象转移到文件系统或数据库中储存就需要用到序列化: jav

  • java 中Spark中将对象序列化存储到hdfs

    java 中Spark中将对象序列化存储到hdfs 摘要: Spark应用中经常会遇到这样一个需求: 需要将JAVA对象序列化并存储到HDFS, 尤其是利用MLlib计算出来的一些模型, 存储到hdfs以便模型可以反复利用. 下面的例子演示了Spark环境下从Hbase读取数据, 生成一个word2vec模型, 存储到hdfs. 废话不多说, 直接贴代码了. spark1.4 + hbase0.98 import org.apache.spark.storage.StorageLevel imp

  • Java 序列化详解及简单实现实例

    一.序列化 序列化定义:序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数据. 目的: 以某种存储形式使自定义对象持久化 将对象从一个地方传递到另一个地方 二.Java序列化 一个对象能够序列化的前提是实现Serializable接口.Serializable接口没有方法,更像是个标记.有了这个标记的Class就能被序列化机制处理.如下: class myPoint implements Serializabl

  • 序列化版本号serialVersionUID的作用_动力节点Java学院整理

    Java序列化是将一个对象编码成一个字节流,反序列化将字节流编码转换成一个对象. 序列化是Java中实现持久化存储的一种方法:为数据传输提供了线路级对象表示法. Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的.在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常. Eclipse中The

  • 浅谈Java序列化和hessian序列化的差异

    在远程调用中,需要把参数和返回值通过网络传输,这个使用就要用到序列化将对象转变成字节流,从一端到另一端之后再反序列化回来变成对象. 既然前面有一篇提到了hessian,这里就简单讲讲Java序列化和hessian序列化的区别. 首先,hessian序列化比Java序列化高效很多,而且生成的字节流也要短很多.但相对来说没有Java序列化可靠,而且也不如Java序列化支持的全面.而之所以会出现这样的区别,则要从它们的实现方式来看. 先说Java序列化,具体工作原理就不说了,Java序列化会把要序列化

  • 详解Java 对象序列化和反序列化

    之前的文章中我们介绍过有关字节流字符流的使用,当时我们对于将一个对象输出到流中的操作,使用DataOutputStream流将该对象中的每个属性值逐个输出到流中,读出时相反.在我们看来这种行为实在是繁琐,尤其是在这个对象中属性值很多的时候.基于此,Java中对象的序列化机制就可以很好的解决这种操作.本篇就简单的介绍Java对象序列化,主要内容如下: 简洁的代码实现 序列化实现的基本算法 两种特殊的情况 自定义序列化机制 序列化的版本控制 一.简洁的代码实现 在介绍对象序列化的使用方法之前,先看看

  • 详解Java对象序列化为什么要使用SerialversionUID

    1.首先谈谈为什么要序列化对象 - 把对象转换为字节序列的过程称为对象的序列化. - 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中: 2) 在网络上传送对象的字节序列. 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存.比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器

  • 详解Java对象的强、软、弱和虚引用+ReferenceQueue

    详解Java对象的强.软.弱和虚引用+ReferenceQueue 一.强引用(StrongReference) 强引用是使用最普遍的引用.如果一个对象具有强引用,那垃圾回收器绝不会回收它.当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题. 二.软引用(SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它:如果内存空间不足了,就会回收这些对象的内存.只要垃圾回

  • 详解java 对象锁与类锁

    一.什么是对象锁 对象锁也叫方法锁,是针对一个对象实例的,它只在该对象的某个内存位置声明一个标识该对象是否拥有锁,所有它只会锁住当前的对象,而并不会对其他对象实例的锁产生任何影响,不同对象访问同一个被synchronized修饰的方法的时候不会阻塞, 例如: public class MyObject { private synchronized void method1(){ try { System.out.println(Thread.currentThread().getName());

  • 详解Java对象的内存布局

    前言 今天来讲些抽象的东西 -- 对象头,因为我在学习的过程中发现很多地方都关联到了对象头的知识点,例如JDK中的 synchronized锁优化 和 JVM 中对象年龄升级等等.要深入理解这些知识的原理,了解对象头的概念很有必要,而且可以为后面分享 synchronized 原理和 JVM 知识的时候做准备. 对象内存构成 Java 中通过 new 关键字创建一个类的实例对象,对象存于内存的堆中并给其分配一个内存地址,那么是否想过如下这些问题: 这个实例对象是以怎样的形态存在内存中的? 一个O

  • 详解Java对象创建的过程及内存布局

    一.对象的内存布局 对象头 对象头主要保存对象自身的运行时数据和用于指定该对象属于哪个类的类型指针. 实例数据 保存对象的有效数据,例如对象的字段信息,其中包括从父类继承下来的. 对齐填充 对齐填充不是必须存在的,没有特别的含义,只起到一个占位符的作用. 二.对象的创建过程 实例化一个类的对象的过程是一个典型的递归过程. 在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类. 此时,首先实例化Object类

  • java对象序列化与反序列化原理解析

    这篇文章主要介绍了java对象序列化与反序列化原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.序列化和反序列化的概念 对象转换为字节序列的过程称为对象的序列化.把字节序列恢复为对象的过程称为对象的反序列化. 二.序列化和反序列化的作用 对象的序列化主要有两种用途: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中. 在网络上传送对象的字节序列.网络上传输的都是二进制序列. 在很多应用中,需要对某些对象进行序列化,让它们离开内

  • 详解Java对象转换神器MapStruct库的使用

    目录 前言 MapStruct简介 MapStruct入门 1. 引入依赖 2. 需要转换的对象 3. 创建转换器 4. 验证 5. 自动生成的实现类 MapStruct进阶 场景1:属性名称不同.(基本)类型不同 场景2:统一映射不同类型 场景3:固定值.忽略某个属性.时间转字符串格式 场景4:为某个属性指定转换方法 场景5:多个参数合并为一个对象 场景6:已有目标对象,将源对象属性覆盖到目标对象 场景7:源对象两个属性合并为一个属性 小结 前言 在我们日常开发的程序中,为了各层之间解耦,一般

  • 详解JavaScript对象序列化

    一.什么是对象序列化? 对象序列化是指将对象的状态转换为字符串(来自我这菜鸟的理解,好像有些书上也是这么说的,浅显易懂!): 序列化(Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程(来自"百度百科-序列化",学术性强,略显高端): 二.为什么会有对象序列化? 世间万物,都有其存在的原因.为什么会有对象序列化呢?因为程序猿们需要它.既然是对象序列化,那我们就先从一个对象说起: var obj = {x:1, y:2}; 当这句代码运行时,对象obj的内容会

  • 详解Java对象结构与对象锁的升级

    目录 1.Java对象结构 2.MarkWord的结构信息 3.无锁.偏向锁.轻量级锁和重量级锁 总结 1. Java对象结构 Java对象结构包括三部分:对象头.对象体和填充字节,如图所示: 对象头又包括三个字段: 第一个字段叫作Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位.哈希码.锁状态等信息. 第二个字段叫作Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例. 第三个字段叫作Array Le

随机推荐