目录
transient writeObject()与readObject(),在上述Student类中均已体现
对象序列化的作用
1、Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能
2、使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量
3、除了在持久化对象时会用到对象序列化之外,当使用RPC(包括Java标准RMI远程方法调用),或在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制
序列化接口
1、在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化
2、java.io.Serializable是一个标识接口,即意味着它仅仅是为了说明类的可序列化属性,接口没有包含任何需要子类实现的抽象方法
对象序列化与反序列化
1、将对象的状态信息保存到流中的操作,称为序列化,可以使用Java提供的工具ObjectOutputStream . writeObject (Serializable obj )来完成
2、从流中读取对状态信息的操作称为反序列化,可以使用Java提供的工具ObjectInputStream.readObject ()来完成
3、为什么一个类实现了Serializable接口,它就可以被序列化呢?从ObjectOutputStream类的源代码片段可以看出它是如何来使用这个标识接口的(源代码1177行)
import java.io.Serializable;
/**
* 要序列化Person创建的对象,Person要实现Serializable
*/
public class Person implements Serializable {
private String name;
private int age;
private String address;
public Person() {
}
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
import java.io.*;
public class SerializableTest {
public static void main(String[] args) {
//把Person对象序列化的ByteArrayOutputStream字节数组输入流中
//对象序列化ObjectOutputStream
ObjectOutputStream oos = null;
Person p = null;
byte[] buffer = null;
try {
//创建ByteArrayOutputStream对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//创建ObjectOutputStream对象
oos = new ObjectOutputStream(baos);
//创建Person对象
p = new Person("jack", 18, "beijing");
//ObjectOutputStream对象writeObject方法把对象序列化到流中
oos.writeObject(p);
//ByteArrayOutputStream对象toByteArray方法把流中的数据返回数组buffer变量中
buffer = baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//ByteArrayInputStream字节数组输入流中反序列化乘Person对象
//反序列化Person对象使用ObjectInputStream流
ObjectInputStream ois = null;
Person p1 = null;
try {
//创建ByteArrayInputStream对象(字数组buffer变量作为构造方法的参数)
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
//创建ObjectInputStream对象
ois = new ObjectInputStream(bais);
//ObjectInputStream对象readObject方法反序列化对象(Person)
p1 = (Person) ois.readObject();
//打印Person对象
System.out.println(p1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println(p1 == p);
}
}
4、上例是一个简单的序列化程序,它先将一个Person对象保存到缓存中,然后再从该缓存中读出被存储的Person对象,并打印该对象
5、从上例的运行结果可以看出的要点:
-
对于Serializable反序列化后的对象,不需要调用构造方法重新构造,对象完全以它存储的二进制位作为基础来构造,而不调用构造方法
6、对象序列化过程不仅仅保存单个对象,还能追踪对象内所包含的所有引用,并保存那些对象(这些对象也需实现了Serializable接口)
7、序列前的对象与序列化后的对象是深复制,反序列化还原后的对象地址与原来的的地址不同,但是内容是一样的,而且对象中包含的引用也相同。换句话说,通过序列化操作,我们可以实现对任何可Serializable对象的”深度复制“,这意味着复制的是整个对象网,而不仅仅是基本对象及其引用。对于同一流的对象,他们的地址是相同,说明他们是同一个对象,但是与其他流的对象地址却不相同。也就说,只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个
8、上面提到,如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。
9、使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Student implements Serializable {
//反序列化过程中,会检查student.dat文件中数据反序列化的对象serialVersionUID和新定义的Student的值是否一致
//不一致,反序列化抛异常,一致,正常反序列化操作
//所以在项目中,如果对象对象序列化,一般自己主动serialVersionUID字段并赋值,防止serialVersionUID的值变化
private static final long serialVersionUID = 5158299539631804615L;
private String no;
private double score;
//private Person person;//自定义对象
private transient Person person;//序列化过程中忽略transient修饰的字段
private String sex;//项目完成后,序列化的文件student.dat已经存在,根据需求添加sex字段
public Student(String no, double score, Person person) {
this.no = no;
this.score = score;
this.person = person;
}
@Override
public String toString() {
return "Student{" +
"no='" + no + '\'' +
", score=" + score +
", person=" + person +
", sex='" + sex + '\'' +
'}';
}
//程序通过反射调用writeObject和readObject私有方法
//序列化Student对象时,会调用此方法,不是程序员调用的,是序列化过程中程序调用的
private void writeObject(ObjectOutputStream out) throws IOException {
//默认的序列化方法
out.defaultWriteObject();
//主动把Person对象序列化
out.writeObject(person);
}
//反序列化Student对象时,会调用此方法,不是程序员调用的,是序列化过程中程序调用的
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
person = (Person) in.readObject();//主动反序列化Person对象,赋值成员变量person
}
}
import java.io.*;
public class StudentSerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeObjToFile();
readObjToFile();
}
//把对象序列化到文件中
private static void writeObjToFile() throws IOException {
//创建FileOutputStream文件字节输出流
FileOutputStream fos = new FileOutputStream("student.dat");
//创建ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(fos);
//创建Person对象
Person p = new Person("王君一", 18, "天津");
//创建Student对象
Student stu = new Student("2022001", 98.0, p);
//ObjectOutputStream对象writeObject方法序列化对象
oos.writeObject(stu);
//关闭流
if (oos != null) {
oos.close();
}
if (fos != null) {
fos.close();
}
}
//从文件中反序列化对象
private static void readObjToFile() throws IOException, ClassNotFoundException {
//创建FileInputStream文件字节输入流
FileInputStream fis = new FileInputStream("student.dat");
//创建ObjectInputStream对象输入流
ObjectInputStream ois = new ObjectInputStream(fis);
//ObjectInputStream对象输入流的readObject方法反序列化对象
Student stu = (Student) ois.readObject();
//打印
System.out.println(stu);
//关闭流
if (ois != null) {
ois.close();
}
if (fis != null) {
fis.close();
}
}
}
10、在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程
11、当某个字段被声明为transient后,默认序列化机制就会忽略该字段
12、对于上述已被声明为transient的字段address,除了将transient关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在Person类中添加两个方法:writeObject()与readObject()
transient writeObject()与readObject(),在上述Student类中均已体现
13、注意:刚才说的是添加方法而不是“覆盖”或者“实现”,因为这两个方法不是基类Object也不是接口Serializable中的方法)
一旦对象被序列化或者反序列还原,就会自动地分别调用者两个方法。也就是说,只要我们提供了这两个方法,就会使用它们而不是默认的序列化机制,这个两个方法必须在类内部自己实现。大家应该注意到这两个方法其实是private类型。也就是说这两个方法仅能被这个类的其他成员调用,但其实我们没有在这个类的其他的方法中调用这两个方法。那么到底是谁调用这两个方法呢?是ObjectOutputStream和ObjectInputStream对象的writeObject和readObject()方法分别调用者两个方法(通过过反射机制来访问类的私有方法),在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,利用反射来搜索是否有writeObject()方法。如果有,就会跳过正常的序列化过程,转而调用这个它的writeObject()方法,readObject方法处理方式也一样writeObject()内部可以通过ObjectOutputStream.defaultWriteObject()来执行默认的writeObject()(非transient字段由这个方法保存),同样的,在类readObject内部,可以通过ObjectInputStream.defalutReadObject()来执行默认的readObject()方法
14、在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:
static final long serialVersionUID=-12345678L;
idea一键生成 serialVersionUID
在实现Serializable接口的类上alt+enter就可以一键生成serialVersionUID
15、如果我们不显式提供serialVersionUID的值,则Java会根据以下几个属性 进行自动计算:
类的名字
属性字段的名字
方法的名字
已实现的接口
改动上述任意一项内容(无论是增加或删除),都会引起编码值变化,从而引起类似的异常警报。这个数字序列称为“串行化版本统一标识符”(serial version universal identifier),简称UID。解决这个问题的办法是在类里面新增一个域serialVersionUID,强制类仍旧使用原来的UID
16、无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化
17、JDK中还提供了另一个序列化接口:Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效,对象将按照我们自定义的方式进行序列化或反序列化,这对于一些信息敏感应用或对序列化反序列化性能要求较高来说非常重要
18、Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由我们自己完成,另外使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person3 implements Externalizable {
private static final long serialVersionUID = -1613378716199647113L;
private String name;
private int age;
private String address;
//无参构造一定要有
public Person3() {
}
public Person3(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
/**
* 对象序列化的时,被调用
*
* @param out
* @throws IOException
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//自定义逻辑序列化字段
out.writeUTF(name);
out.writeInt(age);
// out.writeUTF(address);
}
/**
* 对象反序列化时,被调用
*
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//自定义逻辑序列化字段
//字段的顺序跟序列化一致
name = in.readUTF() + "你好";
age = in.readInt() + 100;
// address = "中国" + in.readUTF();
}
}
import java.io.*;
public class Person3ExternalizableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeObjToFile();
readObjToFile();
}
//把对象序列化到文件中
private static void writeObjToFile() throws IOException {
//创建FileOutputStream文件字节输出流
FileOutputStream fos = new FileOutputStream("person.data");
//创建ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(fos);
//创建Person对象
Person3 p = new Person3("王君一", 18, "天津");
//ObjectOutputStream对象writeObject方法序列化对象
oos.writeObject(p);
//关闭流
if (oos != null) {
oos.close();
}
if (fos != null) {
fos.close();
}
}
//从文件中反序列化对象
private static void readObjToFile() throws IOException, ClassNotFoundException {
//创建FileInputStream文件字节输入流
FileInputStream fis = new FileInputStream("person.data");
//创建ObjectInputStream对象输入流
ObjectInputStream ois = new ObjectInputStream(fis);
//ObjectInputStream对象输入流的readObject方法反序列化对象
Person3 p = (Person3) ois.readObject();
//打印
System.out.println(p);
//关闭流
if (ois != null) {
ois.close();
}
if (fis != null) {
fis.close();
}
}
}