Code前端首页关于Code前端联系我们

Java 序列化机制:示例、代码和继承

terry 2年前 (2023-09-25) 阅读数 50 #后端开发

1。 Java序列化简介

序列化是指对象通过写入描述其状态的值来注册自身的过程,即对象被表示为有序的字节,Java提供了将对象写入流以及从流中恢复对象的方法溪流。对象可以包含其他对象,其他对象也可以包含其他对象。 Java序列化可以自动处理嵌套对象。对于对象中的简单字段,writeObject() 将其值直接写入流。当找到对象字段时,writeObject() 会被再次调用。如果该对象嵌入到另一个对象中,则再次调用 writeObject() 直到该对象可以直接写入流。 程序员 需要做的就是将对象传递给 ObjectOutputStream 的 writeObject() 方法,系统将自动完成剩下的工作。

想要实现序列化的类必须实现 java.io.Serialized 或 java.io.Externalizable 接口,否则会产生 NotSerializedException。该接口内部没有任何方法,它只是一个“标记接口”,只是将其对象“标记”为特殊类型。类通过实现 java.io.Serialized 接口来启用其序列化功能。未实现此接口的类无法序列化或反序列化其状态。可序列化类的所有子类型本身都是可序列化的。 Serialization 接口没有方法或字段,仅用于标识可序列化语义。 Java的“对象序列化”允许你将实现了可序列化接口的对象转换为一组字节,这样如果将来使用该对象,就可以恢复字节数据并相应地重建对象。

2。序列化的必要性和目的

在Java中,一切都是对象。在分布式环境中,通常需要将对象从网络或设备的一端传输到另一端。这就需要一个可以在两端传输数据的协议。 Java的序列化机制就是为了解决这个问题而创建的。

Java 序列化支持的两个主要功能:

  • Java RMI 允许原本存在于其他计算机上的对象表现得就像它们位于本地计算机上一样。
  • 向远程对象发送消息时,参数和返回值必须通过对象序列化的方式传递

Java序列化的目的(目前为止我所理解的):

  • 支持不同版本不同虚拟机二- 类之间的双向通信;
  • 提供持久性和 RMI 的排名;

3。一些序列化示例

我们通过一个简单的例子来看看Java中默认支持的序列化。我们首先定义一个类,然后将其编组到文件中,最后读取文件来重建对象。序列化对象时需要考虑以下几点:

  • 如果对象是序列的序列,则只有对象的非静态成员变量会被序列化,成员方法和静态成员变量不能被序列化。
  • 如果对象的成员变量是一个对象,那么这个对象的数据成员也会被存储。
  • 如果可序列化对象包含对不可序列化对象的引用,则整个序列化操作将失败并抛出NotSerializedException。您可以将此引用标记为瞬态,以便该对象仍然可以序列化。对于一些不想序列化的敏感数据,也可以使用这个标识符进行修改。
    我们通过一个简单的例子来看看Java内置的序列化过程。
class SuperClass implements Serializable{
    private String name;
    private int age;
    private String email;
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public String getEmail() {
    	return email;
    }
    
    public SuperClass(String name,int age,String email) {
    	this.name=name;
    	this.age=age;
    	this.email=email;
    }
}
复制代码

我们看一下main方法的序列化过程。代码如下:

public static void main(String[] args) throws IOException,ClassNotFoundException {
    	System.out.println("序列化对象开始!");
    	SuperClass superClass=new SuperClass("gong",27, "1301334028@qq.com");
    	File rootfile=new File("C:/data");
    	if(!rootfile.exists()) {
    		rootfile.mkdirs();
    	}
    	File file=new File("C:/data/data.txt");
    	if(!file.exists()) {
    		file.createNewFile();
    	}
    	FileOutputStream fileOutputStream=new FileOutputStream(file);
    	ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
    	objectOutputStream.writeObject(superClass);
    	objectOutputStream.flush();
    	objectOutputStream.close();
    	System.out.println("序列化对象完成!");
    	
    	System.out.println("反序列化对象开始!");
    	FileInputStream fileInputStream=new FileInputStream(new File("C:\\data\\data.txt"));
    	ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
    	SuperClass getObject=(SuperClass) objectInputStream.readObject();
    	System.out.println("反序列化对象数据:");
    	
    	System.out.println("name:"+getObject.getName()+"\nage:"+getObject.getAge()+"\nemail:"+getObject.getEmail());
}
复制代码

代码运行结果如下:

序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com
复制代码

通过上面的例子我们可以看到,Java默认提供了序列化和反序列化机制。对于一个实体类来说,整个过程是自动完成的,无需程序员进一步干预。如果我们想从序列化过程中排除某些关键字段怎么办? Java提供了方法,然后继续阅读。

Transient 关键字与序列化

现在如果我们想让上面的 SuperClass 类使用age和email不参与序列化过程,我们只需要在这个定义前面添加transient关键字即可:

private transient int age;
private transient String email;
复制代码

这样,在序列化时,我们不包含年龄和电子邮件数据的字节流,并且这两个变量在反序列化时被赋予默认值。我们现在只会继续该项目。目前,我们的成果如下:

序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:0
email:null
复制代码

序列化流程的定制

如果默认的序列化流程不能满足需求,我们还可以定制整个序列化流程。现在我们只需要在需要序列化的类中定义writeObject方法和readObject方法即可。我们以 SuperClass 为例。现在让我们添加一个自定义序列化过程。瞬态关键字允许Java的内置序列化过程忽略修改的变量。我们仍然通过自定义序列化过程来序列化年龄和电子邮件地址。让我们来看看。查看更改后的结果:

private String name;
private transient int age;
private transient String email;

public String getName() {
	return name;
}

public int getAge() {
	return age;
}

public String getEmail() {
	return email;
}

public SuperClass(String name,int age,String email) {
	this.name=name;
	this.age=age;
	this.email=email;
}

private void writeObject(ObjectOutputStream objectOutputStream) 
		throws IOException {
	objectOutputStream.defaultWriteObject();
	objectOutputStream.writeInt(age);
	objectOutputStream.writeObject(email);
}


private void readObject(ObjectInputStream objectInputStream) 
		throws ClassNotFoundException,IOException {
	objectInputStream.defaultReadObject();
	age=objectInputStream.readInt();
	email=(String)objectInputStream.readObject();
}
复制代码

执行结果如下:

反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com
复制代码

可以看到执行结果与默认结果一致。我们通过调整序列化机制来改变默认的序列化流程(让transient关键字失去作用)。
注意。
细心的同学可能已经注意到,我们在自定义序列化过程中调用了defaultWriteObject()和defaultReadObject()方法。这两个方法是序列化过程中默认调用的方法。如果我们的自定义序列化过程只调用这两个方法而不需要任何额外的步骤,那么它实际上与默认的序列化过程没有什么不同。你可以试试。 ?父类属性无法序列化(没有错误,数据丢失),但子类属性仍然可以正确序列化。
如果我们想在序列化过程中存储超类字段,我们必须在子类实例序列中显式存储基类状态。我们稍微修改一下前面的例子:

    class SuperClass{
    protected String name;
    protected int age;
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public SuperClass(String name,int age) {
    	this.name=name;
    	this.age=age;
    }
    }
    
    class DeriveClass extends SuperClass implements Serializable{
    private String email;
    private String address;
    
    public DeriveClass(String name,int age,String email,String address) {
    	super(name,age);
    	this.email=email;
    	this.address=address;
    }
    
    public String getEmail() {
    	return email;
    }
    
    public String getAddress() {
    	return address;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {  
        out.defaultWriteObject();  
        out.writeObject(name);
        out.writeInt(age);
    }  
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        name=(String)in.readObject();
        age=in.readInt();
    }   
    
    @Override
    public String toString() {
    	return "name:"+getName()+"\nage:"+getAge()+"\nemail:"+getEmail()+"\naddress"+getAddress();
    }
}
复制代码

我们可以改变main方法来序列化子类的对象:

DeriveClass superClass=new DeriveClass("gong",27,"1301334028@qq.com","NJ");
DeriveClass getObject=(DeriveClass) objectInputStream.readObject();
System.out.println("反序列化对象数据:");
System.out.println(getObject);
复制代码

当我们运行代码时,发现报错了。错误如下:

Exception in thread "main" java.io.InvalidClassException: com.learn.example.DeriveClass; no valid constructor
	at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
	at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.readObject(Unknown Source)
	at com.learn.example.RunMain.main(RunMain.java:88)
复制代码

我们来仔细分析一下为什么会这样。 DeriveClass支持序列化,但其父类不支持,因此子类在序列化时必须额外序列化父类的字段(如果需要)。那么,反序列化的时候,构造DeriveClass的实例时,必须先调用基类的构造函数,然后再调用自己的构造函数。反序列化时,我们只能调用父类的无参构造函数作为默认父对象来构造父对象。所以,如果我们取父对象中某个变量的值,那么它的值就是调用父类的无参构造函数后的值。价值。如果考虑这种序列化,请在基类的无参数构造函数中初始化变量。或者在readObject方法中设置值。我们只需要在SuperClass中添加一个空的构造函数即可:

public SuperClass() {}
复制代码

超类支持序列化

这样的话,子类也支持序列化。正常情况下,不需要采取特殊措施。

5。序列化和serialVersionUID

在上面的示例中,我们没有看到serialVersionUID。为什么我们可以正常序列化和反序列化呢?这是因为 Eclipse 默认为我们创建一个序列化 ID。
Eclipse提供了两种生成策略,一种是固定1L,另一种是随机生成不重复的long类型数据(实际上是JDK工具生成的)。这是一个建议。如果没有特殊需要,就使用默认值1L即可,这样可以保证代码一致的情况下反序列化会成功。
注意。虚拟机是否允许反序列化不仅仅取决于类路径和函数代码是否一致。两个类的序列化ID保持一致(即private Final long serialVersionUID = 1L)非常重要。虽然两个类的功能代码完全相同,但是序列化ID不同。它们不能相互序列化和反序列化(这种情况在网络传输后远程创建对象时必须注意)

6。序列化存储

通过前面的例子,我们将数据序列化到data.txt文件中。接下来我们使用二进制查看器工具来看看Java序列化字节流是如何存储在文件中的,以及它的格式是什么。喜欢?我们来转换一下上面的SuperClass类:

class SuperClass implements Serializable{
	
	private static final int serialVersionUID=1;
	
	protected String name;
	protected int age;
	
	public SuperClass() {}
	
	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}
	
	public SuperClass(String name,int age) {
		this.name=name;
		this.age=age;
	}
	
	 @Override
    public String toString() {
    	return "name:"+getName()+"\nage:"+getAge();
    }
}
复制代码

写入的数据如下:

SuperClass superClass=new SuperClass("gong",27);
复制代码

我们打开data.txt文件,可以看到录制的内容: 具体录制内容如图: Java序列化机制:例子、代码与继承

开始吧如下详细解释每一步。

1。部分是序列化文件头。 第 2 部分 该部分是可序列化类的描述,这里是 SerializedObject 类

  • 72:TC_CLASSDESC 语句在这里开始一个新的类
  • 00 1C:28 为十进制,表示类名的长度为28 个字节 63 6F 6D .. .61 73 73:这代表字符串“com.learn.example.SuperClass”。你可以认为它是28字节。
  • 00 00 00 00 00 00 00 01:SerialVersion,该类中设置的值为1。如果我们不设置,Eclipse会自动为我们设置。
  • 02:确认对象支持序列化的标记号
  • 00 02:该类包含的字段数量为 2

3。 Part 是对象中各个属性项的描述:4C:字符“L”,表示该属性是对象类型,而不是基本类型

  • 00 03 十进制 3 表示属性名称的长度
  • 61 67 65:字符串“age”,属性名称
  • 4C:字符“L”,表示该属性是对象类型,不是基本类型
  • 00 04 十进制 4,表示属性名称的长度
  • 6E 61 6D 65:字符串“name”,属性名称
  • 74:TC_STRING,代表一个新字符串,使用字符串
  • 4 来引用该对象。部分是对象的父类信息。如果没有高级班,这部分就不会来。父类与第2部分几乎相同

    • 00 12:18位小数,表示基类的长度
    • 4C 6A 61 ... 6E 67 3B:“L/java/lang/String;”表示父类类属性
    • 78:TC_ENDBLOCKDATA,对象块结束符
    • 70:TC_NULL,表示没有其他超类的字符

    如果属性项是对象,则第 5 部分输出实际值 Object 属性项对象,这个对象也会在这里被序列化。规则与第 2 部分相同

      • 00 00 00 1B:属性值age=27
      • 74:TC_STRING 表示新字符串,使用字符串
      • 00 04(十进制 4)来引用对象、属性name length
      • 67 6F 6E 67 Name 属性值 gong
        通过上面序列化二进制文件的分析,我们可以得出以下主要结论:
    • 1 ,序列化后保存的是对象信息
    • 2 。声明为瞬态的属性不会被序列化。这就是transient关键字
    • 3的作用。声明为静态的属性不会被序列化。 ,这个问题可以理解为序列化保存了对象的状态,但是静态修改的变量属于类而不是对象,所以序列化的时候并没有被序列化。

    版权声明

    本文仅代表作者观点,不代表Code前端网立场。
    本文系作者Code前端网发表,如需转载,请注明页面地址。

    发表评论:

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

    热门