Serializable 与 Externalizable序列化

发布于:2022-08-09 ⋅ 阅读:(226) ⋅ 点赞:(0)

什么是序列化和反序列化

Java序列化是指把Java对象转为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程

序列化

对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,一遍在网络上传输或者保存在本地文件中,序列化后的字节流保存了Java对象的状态以及相关的描述信息,序列化机制的核心作用就是对象状态的保存与重建

反序列化

客户端从文件中或网络上获取序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重新构成对象,恢复Java文件

本质上讲,序列化是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重新构建对象,恢复对象状态

为什么需要序列化和反序列化

我们知道,当两个进程远程通信时,可以互相发送各种类型的数据,包括文本,图片,音频,视频等,而这些数据都会以二进制序列的方式在网络中传输

那么当两个Java进程进行通信时,能否实现进程间的对象传输呢?答案是可以的!如何做到呢?这就需要Java序列化和发序列化了!

换句话说,一方面,发送方需要把这个Java对象换为字节序列,然后在网上传输;另一方面,接收方需要从字节序列中恢复出Java对象

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然的回想Java序列化的好处,其好处一实现了数据的持久化,通过序列化可以把数据永久的保存到磁盘中(通常存放在文件里),二是:利用序列化实现远程通信,即在网络上传输对象的字节序列

总结:

  • 将对象实例相关的类元数据输出
  • 递归的输出类的超类描述知道不在有超类
  • 类元数据完了以后,开从最顶层的超类开始输出对象实例的实际数据值
  • 从上至下递归输出实例的数据

作用

  • 实现对象状态的保存到本地,以便下一次启动虚拟机的时候直接读取保存的序列化字节生成对象
  • 实现对象的网络传输(RMI分布对象)
  • 是对象的深拷贝

Java如何实现序列化和反序了化

JDK类库中序列化和反序了化API

Java.io.ObjectOutputStream : 表示对象输出流;
writeObject(Object obj);方法可以对参数指定的obj对象进行反序列化,把得到的自己序列写到一目录输出流中

Java.io.ObjectInputStream :表示对象输入流;
readObject();方法源输入流中读取字节序列,再把它们发序列化成一个对象,并将其返回

实现序列化的要求

是要实现了Serializable 或者 Externalizable 接口的类的对象才能被序列化,否则抛出异常

实现Java对象序列化与反序列化的方法

假如有个User 类,他的对象需要序列化,可以三种方法

若User类仅仅实现类Serializable接口,则可以按照下方式进行序列化和发序列化

ObjectOutputStream 采用默认的序列化方法,对User对象的非transient的实例变量进行序列化

ObjectInputStream 采用默认的发序列化方式,对User对象的非transient的实例变量进行反序列化

transient 关键字,变量修饰符,如果用transient声明的实例变量当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字 修饰的成员变量不参与序列化过程

若User类仅仅实现Serialization接口,并且还定义了 readObject(ObjectInputStream in)
writeObject(ObjectOutputStream out)
则采用下面方式进行序列化和反序列化

ObjectOutputStream 调用了User对象的 writeObject(ObjectOutputStream out) 方式进行序列化

ObjectInputStream 调用了User对象的 readObject(ObjectInputStream in) 方法进行反序列化

若User实例实现了 Externalizable 接口,且User类必须实现
writeExternal(ObjectOutput out);
readExternal(ObjectInput in);
则按照以下方式进行序列化和反序列化

ObjectOutputStream调用User对象的writeExternal(ObjectOutput out))的方法
进行序列化。

ObjectInputStream会调用User对象的readExternal(ObjectInput in)的方法进行反序列化。

实现Serializable 方式序列化

@Data
@ToString
public class User implements Serializable {
    private Integer id;
    private String name;

    public static void main(String[] args) throws Exception {
        User user = new User();
        // 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new                     FileOutputStream("D:\\object.out"));
        user.setId(1);
        user.setName("掌声");
        // 序列化
        objectOutputStream.writeObject(user);
        // 创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
        ObjectInputStream objectInputStream = new ObjectInputStream(new                         FileInputStream(new File("D:\\object.out")));
        // 发序列化
        User users = (User) objectInputStream.readObject();
        System.out.println(users);  // User(id=1, name=掌声)
    }

}

序列化后的样子
在这里插入图片描述
内容为:

  • 对象的类型描述
  • 对象描述类型描述
  • 对象属性值

实现Externalizable 方式进行序列化

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User implements Externalizable {
    private Integer id;
    private String name;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(this);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        User user = (User) in.readObject();
        System.out.println(user);
    }

    /**
     * 序列化
     * @param fileName
     * @throws IOException
     */
    public static void serialize(String fileName) throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new                   FileOutputStream(fileName));

        objectOutputStream.writeObject("序列化的日期:");
        objectOutputStream.writeObject(new Date());
        User user = new User(1,"哈哈");
        objectOutputStream.writeObject(user);

        objectOutputStream.close();
    }
    /**
     * 反序列化
     * @param fileName
     * @throws IOException
     */
    public static void deserialize(String fileName) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
        String str = (String) objectInputStream.readObject();
        Date date=(Date) objectInputStream.readObject();//日期对象
        User userInfo=(User) objectInputStream.readObject();//会员对象
        System.out.println(str);
        System.out.println(date);
        System.out.println(userInfo);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serialize("text");
        deserialize("text");
    }

}

Externalizable接口继承自Serializable接口,如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为,Externalizable接口声明两个方法:
进行序列化
public void writeExternal(ObjectOutput out) ;
进行反序列化
public void readExternal(ObjectInput in) ;

在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的无参构造方法,如果把类的无参构造方法删除了,或者该把无参构造方法设置private,默认或者protected级别,会抛出 java.io.InvalidException: no valid constructor异常

序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相管理,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类,为它赋予明确的值,显示的定义 serialVersionUID有两种用途:

  • 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的 serialVersionUID;
  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的 serialVersionUID;

Externalizable 与 Serializable 的区别

  • 实现Serializable接口是默认序列化所以属性,如果有不需要序列化的属性使用transient修饰
    Externalizable 接口是 继承 Serializable接口,实现这个接口需要重写 writeExternal和readExternal方法,指定对象序列化的对象和从序列化文件中读取对象属性的行为

  • 实现 Serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久状态,在将这个状态赋值给该类的另一个变量
    Externalizable 接口的对象序列文件进行反序列化先走构造方法得到对象,然后调用readExternal方法读取序列化文件中内存给对应的属性赋值

  • 静态属性和transient修饰的不会序列化

  • 静态成员属于类级别的,所以不能序列化,这里的不能序列化的意思,是序列化信息中不包含这个静态成员域

如果一个类没有实现Serializable接口,但是它的基类实现了,这个类可不可以序列化

一个类实现了 Seriallizable接口,那么他的所以子类都间接实现了此接口,所以它可以被序列化

如果一个类实现了Serializable接口,但是它的父类没有实现 ,这个类可不可以序列化

即父类没有实现Serializable接口时,但其子类实现 了此接口,那么 这个子类是可以序列化的,但是在反序列化的过程 中会调用 父类 的无参构造函数,上面异常抛出的原因就是因为我们在Book类中没有一个无参的构造函数。好,那我们下面就为Book类添加一个默认的构造函数。

如果父类没有实现Serializable接口,但其子类实现 了此接口,那么 这个子类是可以序列化的,但是在反序列化的过程 中会调用 父类 的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。

如果将一个对象写入某文件(比如是a),那么之后对这个对象进行一些修改,然后把修改的对象再写入文件a,那么文件a中会包含该对象的两个 版本吗

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
	private NewBook newBook;
	private String name;

	public static void main(String[] args) {
		new Student().go();
	}
	private void go(){
		try {
			ObjectOutputStream out  = new ObjectOutputStream(new FileOutputStream("seria"));
			Student student1 = new Student(new NewBook(2011,"moree"),"kevin");
			out.writeObject(student1); //
			student1.setName("Jordan");
			out.writeObject(student1);
			student1.setName("Paul");
			out.writeObject(student1);
			System.out.println("object has been written..");
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		try{
			ObjectInputStream in = new ObjectInputStream(new FileInputStream("seria"));
			Student s1 = (Student)in.readObject();
			Student s2 = (Student)in.readObject();
			Student s3 = (Student)in.readObject();
			System.out.println("Objects read here: ");
			System.out.println("Student1's name: "+s1.getName());
			System.out.println("Student2's name: "+s2.getName());
			System.out.println("Student3's name: "+s3.getName());
		}catch(FileNotFoundException e){
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
class NewBook implements Serializable{
	private Integer id;
	private String name;
}
输出
object has been written..
Objects read here: 
Student1's name: kevin
Student2's name: kevin
Student3's name: kevin

它输出了三个kevin,这证明 我们对student名字的修改并没有被写入。原因是序列化输出过程跟踪写入流的对象,试图将同一个对象写入流时,不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。

怎么避免呢?

方法是在writeObject()之前调用out.reset()方法,这个方法的作用是清除流中保存的写入对象的记录。我们还是通过代码来看下效果。

try {
			ObjectOutputStream out  = new ObjectOutputStream(new FileOutputStream("seria"));
			Student student1 = new Student(new NewBook(2011,"moree"),"kevin");
			out.writeObject(student1); //
    		out.reset();
			student1.setName("Jordan");
			out.writeObject(student1);
			out.reset();
            student1.setName("Paul");
			out.writeObject(student1);
			System.out.println("object has been written..");
			out.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
输出结果
object has been written..
Objects read here: 
Student1's name: kevin
Student2's name: Jordan
Student3's name: Paul

总结!

  • 序列化时,只对对象的状态进行保存,而不管对象的方法;

  • 当一个父类实现序列化,子类自动实现序列化,不需要显示实现Serializable接口;

  • 当一个对象的实例变量引用其他对象,序列化该对象是也把引用对象进行序列化;

  • 并非所有的对象都可以序列化,至于为什么不可以,有很多原因,
    比如:

    安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行,RMI传输等等,在序列化进行传输的过程中,这个对象的private等域不受保护的

    资源分批方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新资源分批,而且,也是没有必要这样实现;

  • 声明为static和transient类型的成员数据不能被序列化,因为static代表的雷达状态,transient代表对象的临时数据

  • 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

    在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

    在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID;

  • Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;

  • 如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;


网站公告

今日签到

点亮在社区的每一天
去签到