深入解析序列化与反序列化:对象状态的保存与重建,进程间通信应用

[id_1311514384]

深入解析序列化与反序列化:对象状态的保存与重建,进程间通信应用

序列化与反序列化

序列化:

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

反序列化:

客户端从文件中获得序列化后的对象字节流,客户端从网络上获得序列化后的对象字节流,客户端根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

序列化是将实体对象状态依照一定格式写入有序字节流,反序列化是从有序字节流重建对象,进而恢复对象状态。

简单来说,进程间通信能够把图片、视频、音频等信息以二进制方式进行传输,然而进程间的对象却无法这样做。

比如,我创建了一个User,名为u1,它具有属性1,值为“a”,还有属性100,值为100;

我要将它传给另一个软件(进程),

进程间的对象想要传输就需要序列化和反序列化。

它能够被序列化成二进制数据,这些数据能够永久存于硬盘之中,还能够用于网络传输。

三

实现java序列化和反序列化

下面嫌太长了可以直接看例子。

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

java.io.ObjectOutputStream:

表示对象输出流;

它的writeObject(Object obj)方法,能够对参数指定的obj对象开展序列化操作,会将得到的字节序列写入到一个目标输出流里 ;

java.io.ObjectInputStream:

表示对象输入流,它的readObject()方法从源输入流中读取字节序列,然后把这些字节序列反序列化成为一个对象,最后将这个对象返回。

实现序列化的要求

只有实现了Serializable接口的类的对象能被序列化,只有实现了Externalizable接口的类的对象能被序列化,否则抛出异常!

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

若User类只实现了Serializable接口,那么可依照以下方式来进行序列化,还可依照以下方式来进行反序列化:

ObjectOutputStream采用默认的序列化方式,它会对User对象进行序列化,且序列化的是User对象非transient的实例变量。

ObjcetInputStream采用默认的反序列化方式,对User对象进行反序列化,反序列化的是其非transient的实例变量。

若User类仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)方法和writeObject(ObjectOutputSteam out)方法,那么采用以下方式进行序列化与反序列化:

ObjectOutputStream进行序列化时,会调用User对象的writeObject(ObjectOutputStream out)方法 。

ObjectInputStream会调用方法进行反序列化,该方法是User对象的readObject(ObjectInputStream in) 。

若User类实现了Externalnalizable接口,User类必须实现readExternal(ObjectInput in)方法,User类必须实现writeExternal(ObjectOutput out)方法,那么按照以下方式进行序列化与反序列化:

ObjectOutputStream通过调用User对象的writeExternal(ObjectOutput out))方法来进行序列化,ObjectInputStream会借助调用User对象的readExternal(ObjectInput in)方法来进行反序列化。

实例

使用的是上述第一种方式,因此User要实现Serializable ,该方式的对象是user 。

import java.io.Serializable; public class User implements Serializable {    int id;    String name;    String phone;      #一些get set 构造参数,这里就不列举了}

序列化与反序列化

import java.io.*; public class userDemo {    public static void main(String[] args) throws输入输出异常,类未找到异常{        //创建对象        User u1 = new User(1,"AAAAAAA","110"); //被序列化的对象        User u2; //反序列化的对象         //序列化        getSerial(u1);         //反序列化        u2 = backSerial();获取变量u2的名字,然后将其输出显示    }     //序列化    static void getSerial(User u1) throws IOException {        FileOutputStream fos = new FileOutputStream("obj.out");        ObjectOutputStream oos = new ObjectOutputStream(fos);        oos.writeObject(u1);        oos.flush();        oos.close();    }     //反序列化    static User backSerial() throws IOException, ClassNotFoundException {        FileInputStream fis = new FileInputStream("obj.out");        ObjectInputStream ois = new ObjectInputStream(fis);将对象ois读取的对象强制转换为User类型并赋值给u1 ,        return u1;    }}

四

序列化底层分析

ObjdectOutputStream对象的初始化

bout是数据输出流的底层

writeStreamHeader将文件头写入文件

这里根据序列化的文件分析

所以这里是写入文件头,用于声明使用序列化协议,还能说明序列化版本。

初始化完毕,文件存在且写入了文件头。

开始序列化写入文件

writeObject(u1);

向下调用write0ject0();这个方法,其内容比较长。重要点在于,意思是要按照不同类型的方法去写入序列化数据,还可以看上面实现Java对象序列化与反序列化的方法。

我们的实例实现了Serializable,因此执行writeOrdinaryObject方法。

bout.writeByte(TC_OBJECT);

写入了0x73

调用 writeClassDesc(desc, false),进行跟进:

这里isProxy是判断类是否是动态代理模式。

具体情况你可以自己去了解,我并不清楚。由于我们实例的类并非动态代理,因此跟进writeNonProxyDesc();

先写入了描述符号0x72

下面进行判断,跟进两个参数,其中一个参数的值是1,另一个参数的值是2 。随后跟进writeClassDescriptor(desc);

和true执行同一个方法:

在开发过程中,我们常常会碰到需要借助for循环来判定循环体里是否含有某一元素的情况,此时我们也经常运用一个boolean值来参与判断。而“|=”能够让我们轻松地实现这一点。

布尔值 flag 初始化为 false 。在一个循环体里,执行 flag |= (c==e) 。若一直不相等,那么 flag 始终为 false 。一旦有一个相等, flag 就变为 true ;

out.writeUTF(name);

写入类名

out.writeLong,获取序列化版本号并写入

写入序列化uid

再往下一堆if判断接口的实现方式,将标志位写入

out.writeByte(flags);

我们使用serializable,所以应该写入0x02

所以从0x000B - 0x0013 都是序列化uid

然后调用writeShort写入两个字节的域长度,例如存在3个变量的情况,此时就写入00 03 。

实例中有三个参数

接下来就是循环写入变量名和变量类型。

每轮循环:

writeByte用于写入一个字节的变量类型,writeUTF()用于写入变量名,要判断是否为原始类型,也就是判断是否为对象。若不是原始类型(基本类型),则调用writeTypeString() 。

这个函数,如果传入的是字符串,就会调用另一个函数 。而这个writeString()通常是这样来写的,当字符串长度(并非大小)小于两个字节时,会先写入一个字节的TC_STRING(16进制74),接着调用writeUTF(),再写入一个signature,这似乎与jvm相关。

最后通常写的是类似下面这串74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67,“翻译”过来就是,它属于字符串类型,其长度占18个字节,变量名是 Ljava/lang/string;

红色 id参数 int 类型

绿色name参数为string,由于String是引用数据类型,所以调用了writeTypeString(),写入了Ljava/lang/string;

黄色 phone 参数 string

这里第一次看会有个疑问,phone参数是string类型,然而却没有“Ljava/lang/string;”这一串内容,后来又增加了一个string类型的参数,要确定同一种引用数据类型是否只写入一次。

循环执行完毕后,返回到writeNonProxyDesc方法,写入结束标志位0x78 。

bout写入字节,该字节为TC_ENDBLOCKDATA 。

准备开始写入序列化数据,回到writeOrdinaryObject()方法,通过writeSerialData(obj, desc);方法来写入序列化数据

这里是依据使用方式进行判断的,因此调用了defaultWriteFields();

第二个if用于判断是否为基本数据类型,若为基本数据类型,便会直接写入序列化数据,若不是基本数据类型,则向下到for循环附近。获取变量数,接着循环调用writeObject0()来写入

循环结束,直到所有运行完成,回到主函数。

反序列化就不写了,反反过来推一遍就成。

五

java反射机制

反射机制可让程序在运行期间,借助Reflection API获取任何类的内部信息,还能够直接操控任意类和对象的所有属性与方法。

要使用一个类,需先将其加载到虚拟机中。加载完类之后,堆内存的方法区中会产生一个Class类型的对象,一个类仅有一个class对象。这个对象包含完整的类的结构信息。我们能够通过这个对象看到类的结构。这个对象如同镜子一般,透过它可看到类的结构。所以形象地将其称之为:反射。

实例:

import java.lang.reflect.Method; public class test {    public static void main(String[] args) throws Exception {        a1Class a1 = new a1Class();        //通过运行时的对象调用getClass();        Class c = a1.getClass();        try {            //getMethod(方法名,参数类型)            getMethod的第一个参数为方法名,第二个参数是该方法的参数类型 。            存在同方法名不同参数的情况,只有同时指定方法名,并且指定参数类型,才能唯一确定一个方法。            Method m1 = c.getMethod("print", int.class, int.class);            用m1对象进行方法调用,这是一种反射操作,其效果和r1.print(1, 2);方法调用完全相同。            使用r1调用m1获得对象,该对象声明了公开方法print,将int类型的1、2作为参数传入print方法中。            Object i = m1.invoke(a1,1,1);        }catch (Exception e){            e.printStackTrace();        }    }    static class a1Class {        public void print(int a, int b) {            System.out.println(a + b);        }    }}

尝试简化上面的代码

创建另一个文件

public class testMiao {    public static void maio(){        System.out.println("miao!");    }}

使用反射来执行miao();

public class test {    public static void main(String[] args) throws Exception {        try {            Object s = Class.forName("testMiao").getMethod("maio").invoke(null);         }catch (Exception e){            e.printStackTrace();        }    }}

尝试添加参数简化

public class testMiao {    public static void maio(String s){        System.out.println("miao!"+s);    }}

反射

public class test {    public static void main(String[] args) throws Exception {        try {            Class.forName("testMiao").getMethod("maio"你提供的内容似乎不完整且存在错误表述,无法准确按照要求进行改写,请补充完整且正确的内容。"testMiao"),"aaa");        }catch (Exception e){            e.printStackTrace();        }    }}

六

java执行命令

在java里,能够借助Runtime.getRuntime.exec();去执行系统命令 。

如:

尝试使用反射来执行

Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(打开,系统,应用程序,计算器应用 。);

这样会报错,报错的信息:是对象不是声明类的实例

说明exec只能是通过getRuntime来执行

import java.lang.reflect.Method; public class test {    public static void main(String[] args) throws Exception {        Object o = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);        Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(o,打开,/System/Applications/Calculator.app,换行);    }}

这样会成功,原理跟随反射实例第一个实例来理解。

现在可以打开计算器,明白什么是序列与反序列化了。

关于cc1的链,以后再撰写,能够去看bilibili白日梦组长的分析思路,我个人认为他的思路真的极为出色。

看雪ID:p1yang

*本文由看雪论坛 p1yang原创,转载请注明来自看雪社区

#往期推荐

1.

2.

3.

4.

5.

6.

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读