JavaSE笔记(2.6)Java基础 -序列化

前言

记录学习过程
学的越多越感觉渺小,序列化是一个深坑

目录

  1. 概念
  2. Java的序列化机制
    2.1 Serializable接口
    2.2 Externalizable接口
    2.3 区别
  3. 序列化与构造方法
  4. serialVersionUID的作用
  5. 静态变量的序列化问题
  6. transient 关键字的使用
  7. 自定义序列化
  8. 更多操作
  9. 总结

序列化

概念

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,
这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,
就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能

Java序列化是指把Java对象转换为字节序列的过程,即是把对象写入IO流中

Java序列化的常见运用场景:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;

很明显,序列化的好处就在与永久的保存数据和实现远程通信

TCP/IP 远程传送数据:
在这里插入图片描述

Java的序列化机制

Java中实现序列化有两个接口:Serializable、Externalizable

Serializable接口

可以看到Serializable接口是属于IO包,即IO输入输出流

Serializable接口是一个标记接口,不用实现任何方法,实现该接口就可以完成序列化

import java.io.Serializable;

可以看看jdk文档的Serializable注释
在这里插入图片描述

使用Serializable实现序列化

创建一个User类:

package com.company.NewEntity;

import java.io.Serializable;

public class User implements Serializable {
    private int id;
    private String name;
    //序列化对象版本控制
    private static final long serialVersionUID = 1L;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

测试类TestUser实现序列化、反序列化(创建一个文件提供写入读出)

序列化步骤:

  1. 创建对象
  2. 创建文件字节输出流
  3. 对象的序列化输出流
  4. 写入文件
  5. 关闭序列化输出流

反序列化步骤与序列化相似,将输出改为输入

package com.company.NewEntity;

import java.io.*;

public class TestUser {
        public static void main(String[] args) throws Exception, IOException {
            SerializeUser();
            DeSerializeUser();
        }
        //序列化方法
        private static void SerializeUser() throws IOException {
            User user=new User();
            user.setId(1);
            user.setName("zhangsan");
            //序列化对象到文档
            
            try {
                //文件字节输出流
                FileOutputStream fileOutputStream=new FileOutputStream("UserFile.txt");
                //对象的序列化流:把对象转成字节数据的输出到文件中保存
                ObjectOutputStream outputStream=new ObjectOutputStream(fileOutputStream);
                //写入
                outputStream.writeObject(user);
                //关闭对象的序列化流
                outputStream.close();
                System.out.println("写入成功");
            }
            catch (FileNotFoundException e){
                System.out.println("文件找不到");
            }
            catch (IOException io){
                System.out.println("输出流失败");
            }

        }
        //反序列化方法
        private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
            //创建文件对象,得到对应地址的文件
            File file=new File("UserFile.txt");
            //文件字节输入流
            FileInputStream fileInputStream=new FileInputStream(file);
            //对象的反序列化输入流
            ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
            //读出对象
            User user= (User) objectInputStream.readObject();
            System.out.println(user.toString());
            objectInputStream.close();
        }


}

在这里插入图片描述

完成简单的序列化和反序列化

使用Externalizable实现序列化

Externalizable的特点

  1. Externalizable是Serializable的子类
    在这里插入图片描述
  2. 继承Externalizable实现序列化需要重写readExternal和writeExternal方法
  3. 实现Externalizable接口的类必须要提供一个public的无参的构造器
    反序列化时会默认调用无参构造实例化对象,如果没有此无参构造,则运行时将会出现异常
    在这里插入图片描述

改变类User

package com.company.NewEntity;

import java.io.*;

public class User implements Externalizable {
    private int id;
    private String name;
    //序列化对象版本控制
    private static final long serialVersionUID = 1L;

    public User() {
    }

	//省略set、get方法
	//省略toString方法

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

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        id=  in.readInt();
        name= (String) in.readObject();
    }
}

在User对象中重写方法,如果不重写方法得不到对象
在这里插入图片描述
重写完后继续运行上面的测试类:
在这里插入图片描述

需要注意:反序列化获得的对象不是通过构造方法实例的,而是JVM自己生成的对象
可以试着去验证一下序列化与构造方法

Externalizable接口、Serializable接口的区别

实现Serializable接口

  1. 系统自动存储必要的信息
  2. Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持
  3. 性能略差

实现Externalizable接口

  1. 程序员决定存储哪些信息
  2. 必须实现接口内的两个方法
  3. 性能略好

序列化与构造方法

验证一下序列化与构造法的关系:
修改一下User类
在这里插入图片描述

运行一下测试类:
在这里插入图片描述

可以看出,序列化和反序列化都与带参构造方法无关
序列化会调用无参构造方法
而反序列不会调用构造方法,是JVM生成的对象
关于父类、子类Java序列化与空参构造方法的关系详解

serialVersionUID的作用

随着项目的升级,class文件肯定会发生改变,而反序列必须拥有class文件,就必须讨论版本兼容问题

serialVersionUID的作用:序列化对象版本控制,有关各版本反序列化时是否兼容

兼容问题就像其他东西的版本兼容:

  1. 新版本修改了属性,旧版本肯定不兼容,如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量

例:前面已经序列化输出对象了,接下来我们把User中id的类型改为String,再反序列化得到对象
就会发现已经不兼容了
在这里插入图片描述
这个时候就需要手动修改serialVersionUID版本号,然后运行序列化、反序列化,将旧版本的对象更新

  1. 修改serialVersionUID版本号,会造成反序列失败

例:前面已经序列化输出对象,把版本号从1改为12,再反序列
在这里插入图片描述

  1. 如果只是修改了方法,反序列化不容影响,则无需修改版本号

例:前面序列化了对象,关闭输出流后修改getId方法

    public int getId() {
        System.out.println("getId");
        return id;
    }

然后在反序列化方法中运行getId方法
在这里插入图片描述
确实,修改方法不会影响反序列化

  1. 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号(因为这两个变量根本不会进入序列化)

serialVersionUID有两种显示的生成方式:
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;
不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化

静态变量的序列化问题

静态变量不会被序列化
为什么?
静态变量在全局区,本来流里面就没有写入静态变量,打印静态变量当然会去全局区查找,当write read 同时使用时,内存中的静态变量变了,所以打印出来的也变了

package com.company.NewEntity;

import java.io.*;

public class TestUser {
        public static void main(String[] args) throws Exception, IOException {
            SerializeUser();
            DeSerializeUser();
        }
        //序列化方法
        private static void SerializeUser() throws IOException {
            User user=new User(1,"zhangsan");
            //序列化对象到文档
            try {
                //文件字节输出流
                FileOutputStream fileOutputStream=new FileOutputStream("UserFile.txt");
                //对象的序列化流:把对象转成字节数据的输出到文件中保存
                ObjectOutputStream outputStream=new ObjectOutputStream(fileOutputStream);
                //写入
                outputStream.writeObject(user);
                //关闭对象的序列化流
                outputStream.close();
                user.setId(3);
                System.out.println("写入成功");
            }
            catch (FileNotFoundException e){
                System.out.println("文件找不到");
            }
            catch (IOException io){
                System.out.println("输出流失败");
            }

        }
        //反序列化方法
        private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
            //创建文件对象,得到对应地址的文件
            File file=new File("UserFile.txt");
            //文件字节输入流
            FileInputStream fileInputStream=new FileInputStream(file);
            //对象的反序列化输入流
            ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
            //读出对象
            User user= (User) objectInputStream.readObject();
            System.out.println(user.toString());
            objectInputStream.close();
        }


}

对于序列化的方法,在输出流关闭后,通过setId()方法把user的id改为3
如果id序列化成功了,id应该是不变的,以为反序列化只是将文件里的对象取出
在这里插入图片描述

事实是id改变了,也就是说静态变量不能序列化,静态变量是在全局区里改变、查找的

transient 关键字的使用

transient 关键字的作用是控制变量的序列化
在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

 private transient  int id;

注意:不是import java.beans.Transient;

添加了transient关键字后,id被阻止序列化到文件了
在这里插入图片描述

transient是控制变量的序列化,将属性完全隔离在了序列化外

自定义序列化

Java提供了自定义序列化,可以进行控制序列化的方式,或者对序列化数据进行编码加密

  1. Serializable通过重写writeObject、readObject方法实现自定义序列化
package com.company.NewEntity;

import java.beans.Transient;
import java.io.*;

public class User implements Serializable{
    private  int id;
    private String name;
    //序列化对象版本控制
    private static final long serialVersionUID = 12L;

    public User() {

    }

    public User(int id, String name) {

        this.id = id;
        this.name = name;
    }

	//省略toString方法
	//省略set、get方法

    //在序列化时调用
    private void writeObject(ObjectOutputStream out) throws IOException {

        out.writeObject(name);
        System.out.println("writeObject");
    }


    //在反序列化时调用
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

        name= (String) in.readObject();
        System.out.println("readObject");
    }
}

注意:writeObject、readObject是包私有private 方法

注意:readObject()的顺序要和writeObject()的顺序一致,即在readObject中属性顺序为 id ,name ,writeObject也要是 id ,name

如果没有重写,在序列化、反序列化时使用默认的方法,重写了就会使用我们重写的方法

上面重写的是将name写入序列化,而id没写入
在这里插入图片描述

  1. 编码加密

可以再属性上加密:name上小小的加密(还有很多加密方式)

    //在序列化时调用
    private void writeObject(ObjectOutputStream out) throws IOException {
        //加密
        out.writeObject(new StringBuffer(name).reverse());
        System.out.println("writeObject");
    }

如果不解密的话
在这里插入图片描述
再在readObject解密:

  //在反序列化时调用
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        //readObject返回的是object,要类型转换
        //反序列化解码
        StringBuffer nameSB= (StringBuffer) in.readObject();
        name=nameSB.reverse().toString();
        System.out.println("readObject");
    }

在这里插入图片描述

  1. 偷梁换柱writeReplace()方法 -将输入的对象替换
    将上面的writeObject、readObject方法注释,写上writeReplace()方法
    private Object writeReplace(){
        return new String("ok");
    }

将User对象换成String对象“ok”
当然反序列化要改一下得到的对象类型

        //反序列化方法
        private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
            //创建文件对象,得到对应地址的文件
            File file=new File("UserFile.txt");
            //文件字节输入流
            FileInputStream fileInputStream=new FileInputStream(file);
            //对象的反序列化输入流
            ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
            //读出对象
           /* User user= (User) objectInputStream.readObject();*/
            String str=(String)objectInputStream.readObject();
            System.out.println(str.toString());
            objectInputStream.close();
        }

在这里插入图片描述
User:明明先来的是我!!

  1. 偷梁换柱readResolve()方法 - 将输出的对象替换
    反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用
    private Object readResolve() throws ObjectStreamException{
        return new Integer(3);
    }

再改一下readObject改一下:

 //反序列化方法
        private static void DeSerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
            //创建文件对象,得到对应地址的文件
            File file=new File("UserFile.txt");
            //文件字节输入流
            FileInputStream fileInputStream=new FileInputStream(file);
            //对象的反序列化输入流
            ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
            //读出对象
            Integer integer= (Integer) objectInputStream.readObject();
            System.out.println(integer.toString());
            objectInputStream.close();
        }

在这里插入图片描述

读出Integer对象 3

readResolve常用来反序列单例类,保证单例类的唯一性

  1. Externalizable自定义序列化
    Externalizable本来就是自定义的序列化,前面已经知道继承Externalizable接口就必须重新两个方法writeExternal,readExternal
 @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        System.out.println("writeExternal");
    }

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

在这里插入图片描述

操作和Serializable一样

更多操作

  • 使用序列化实现深度克隆
  • readObjectNoData()方法可以用来正确地初始化反序列化的对象

涉及序列化的技术
在这里插入图片描述

总结

  • 序列化是为了保存对象或者远程传输对象
  • Java有两个序列化接口,各有特点
  • 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量、静态变量都不会被序列化
  • 序列化、反序列化与带参构造方法无关,序列化会调用无参构造方法,而反序列不会调用构造方法,是JVM生成的对象
  • 序列化时,如果第一个非可序列化超类非Object类,那么一定要预留一个空参构造方法,否则就会序列化失败(关于父子类的序列化问题)
  • transient修饰的变量会被阻止进入序列化
  • 可以通过自定义序列化进行控制序列化或者编码加密
  • 偷梁换柱writeReplace、readResolve方法
  • 序列化是个需要深入的知识
发布了49 篇原创文章 · 获赞 0 · 访问量 1230

猜你喜欢

转载自blog.csdn.net/key_768/article/details/104250500