深入理解——————序列化与反序列化


最近在做超市管理项目,通过序列化将数据存储在Redis数据库中。在实现用户信息的修改功能时,为了方便将Date数据类型修改为String类型结果报错了,打开控制台发现出现下面的错误。

java.io.InvalidClassException: com.li.pojo.SmbmsUser; 
local class incompatible: stream classdesc serialVersionUID = 2416888619525883151, 
local class serialVersionUID = 516098953370879925

错误原因:序列化与反序列化前后ID不一致导致报错。
既然出现了错误就要解决。之前对于序列化、反序列化的理解只是停留在会用的基础上
今天就深入了解。

1.首先知道什么是序列化与反序列化

学习新的知识点的时候我们要时刻抱有疑问:是什么、为什么、怎么做、三个方面去完成这件事。这样才会清晰明了,记忆也更加深刻。

  • 什么是序列化与反序列化呢?

    序列化:就是将对象转化为字符序列
    反序列化:就是将字符序列恢复为原来的对象

  • 为什么要序列化、反序列化呢?
    前面提到在做ssm项目的时候使用了序列化将对象转化为了字符序列存储在了Redis数据库中,
    如果不使用序列化我们应该怎么做,使用map(key-value)存储对象,单表数据不是特别多的时候可以去使用,数据量大会很麻烦。所以建议数据量特别大的时候序列化对象存储在Redis中
    一般序列化用于被写入数据库、文件、用于网络传输。

  • 怎么做,才能序列化呢?

实现Serializable接口

public interface Serializable {
}

Serializable 分明就是一个空接口,其实这个接口的作用就是告诉JVM我要序列化、反序列化。
具体操作如下
准备实体类

public class Student {
    private String name;
    private int age;

    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;
    }

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

测试:

 //初始化
        Student student = new Student();
        student.setName("张三");
        student.setAge(12);
        System.out.println(student.toString());
        //序列化
        try {
            //把对象写入文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
            oos.writeObject(student);
            System.out.println("序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        //反序列化
        try {
            //从文件中读出对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
            Student stu = (Student) ois.readObject();
            System.out.println("反序列化成功!");
            System.out.println(stu.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }

没有实现Serializable接口报错。

java.io.NotSerializableException: com.li.pojo.Student
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.li.test.SerializableTest.serializable(SerializableTest.java:25)
	at com.li.test.SerializableTest.main(SerializableTest.java:14)

实现Serializable接口

public class Student implements Serializable{
	//省略属性
	//省略setter、getter方法
	//省略toString方法
}

序列化之前打印对象 :

Student{name=‘张三’, age=12}

序列化成功! 并且b.txt 有一些内容

在这里插入图片描述
接着调用反序列化,并且输出对象
反序列化成功!

Student{name=‘张三’, age=12}

反序列化以后发现与序列化之前的对象保持一致!

2.序列化与反序列化的具体操作过程

具体序列化和反序列化是怎么执行的呢?

  • 具体序列化执行流程如下(参数列表省略):

ObjectOutputStream->writeObject() -> writeObject0() -> writeOrdinaryObject() -> writeSerialData() -> defaultWriteFields()
其中writeObject0方法进行了判断
是否是String类型、枚举类型、序列化对象,如果都不是将抛出异常
NotSerializableException这个异常是不是很眼熟前面我们没有实现Serializable接口抛出的异常就是从这来的。

			 // remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
  • 具体反序列化执行流程如下(参数列表省略):
    ObjectInputStream -> readObject() -> readObject0() -> readOrdinaryObject() -> readSerialData()-> defaultReadFields()
3.transient和static修饰的属性为什么不能序列化

测试两个关键字:

  • static 静态的
  • transient 临时的
	//新增加两个字段
    public static String addRess="宁夏";
    private transient String carName="捷豹";
     @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", addRess='" + addRess + '\'' +
                ", carName='" + carName + '\''+
                '}';
    }
 		//初始化
        Student student = new Student();
        student.setName("张三");
        student.setAge(12);
        System.out.println(student.toString());
        //序列化
        try {
            //把对象写入文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
            oos.writeObject(student);
            System.out.println("序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        student.setAddRess("陕西");

        //反序列化
        try {
            //从文件中读出对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
            Student stu = (Student) ois.readObject();
            System.out.println("反序列化成功!");
            System.out.println(stu.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }

输出打印结果:

//序列化前的对象
Student{name='张三', age=12, addRess='宁夏', carName='捷豹'}
序列化成功!
反序列化成功!
//反序列化后的对象
Student{name='张三', age=12, addRess='陕西', carName='null'}

从结果中我们可以对比来看:
序列化前:addRess字段的值为'宁夏',序列化后修改字段值为'陕西',反序列化后字段值是'陕西',而不是反序列化前的状态
为什么呢?原来static修饰的字段是属于类状态的,序列化是针对对象状态,所以static关键字修饰的关键字不保存序列化前的状态
序列化前carName字段的值为 ''捷豹,序列化后字段值为null,这又是为什么呢?
原来transient(临时的)关键字修饰的字段它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null。

如果要探究其实源码中也有体现

/**
     * Returns array of ObjectStreamFields corresponding to all non-static
     * non-transient fields declared by given class.  Each ObjectStreamField
     * contains a Field object for the field it represents.  If no default
     * serializable fields exist, NO_FIELDS is returned.
     */
 private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

注释中说明了:Modifier.STATIC | Modifier.TRANSIENT静态的、临时的字段都不会序列化

4.序列化ID的作用是什么

不知道小伙伴们有没有发现,凡是实现了Serializable接口的类中都会有下面这行代码
private static final long serialVersionUID = 5791892247841471821L

serialVersionUID:是序列化ID的意思
那么这个序列化ID有什么作用?
其实他是为了确保反序列化能够成功的因子
如果在实现了序列化接口后不设置序列化ID java会自作主张的给你生成一个序列化ID (根据字段属性类型数目生成)
但是如果你在序列化后 增加某一个字段就会报错
下面就来验证一下:

	//准备两个字段
     private String name;
     private int age;
     //提供setter、getter方法 

测试:
序列化

    //初始化
        Student student = new Student();
        student.setName("张三");
        student.setAge(12);
        System.out.println(student.toString());
        //序列化
        try {
            //把对象写入文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
            oos.writeObject(student);
            System.out.println("序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //反序列化
        try {
            //从文件中读出对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\imag\\b.txt"));
            Student stu = (Student) ois.readObject();
            System.out.println("反序列化成功!");
            System.out.println(stu.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }

打印结果:

Student{name=‘张三’, age=12}
序列化成功!
反序列化成功!
Student{name=‘张三’, age=12}

修改字段age的数据类型为String类型
这次测试我们先序列化

//初始化
		Student student = new Student();
        student.setName("张三");
        student.setAge(12);
        System.out.println(student.toString());
        //序列化
        try {
            //把对象写入文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\imag\\b.txt"));
            oos.writeObject(student);
            System.out.println("序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

然后将实体类的字段修改为String类型 ,注释序列化代码,执行反序列化
结果报错:

java.io.InvalidClassException: com.li.pojo.Student; local class incompatible: stream classdesc serialVersionUID = -5791892247841471821, local class serialVersionUID = -3882745368779827342
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)

这个错误就是和我一开始说的错误是一样的序列化ID和反序列化ID不一致导致的错误
所以一般我们只要实现了序列化接口就采用默认的序列化 ID(1L)就可以
private static final long serialVersionUID = 1L

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值