Java对象序列化与反序列化全解析
立即解锁
发布时间: 2025-08-19 00:11:31 阅读量: 8 订阅数: 27 


Java编程基础与Android开发入门
### Java对象序列化与反序列化全解析
在Java编程中,对象的序列化与反序列化是一项重要的技术,它允许我们将对象的状态保存到文件或网络流中,并且在需要的时候重新恢复对象。下面将详细介绍Java中对象序列化与反序列化的相关知识。
#### 1. 基本概念
对象的状态由存储基本类型值和/或对其他对象引用的实例字段组成。当一个对象被序列化时,作为其状态一部分的对象也会被序列化(除非阻止它们被序列化),依此类推。Java支持默认序列化和反序列化、自定义序列化和反序列化以及外部化。
#### 2. 默认序列化和反序列化
默认序列化和反序列化是最容易使用的形式,但对对象的序列化和反序列化方式的控制较少。虽然Java会代你完成大部分工作,但你仍需要完成以下两个任务:
- **实现`Serializable`接口**:要序列化的对象的类必须直接或通过其超类间接实现`java.io.Serializable`接口。实现`Serializable`接口的目的是避免无限制的序列化。`Serializable`是一个空标记接口(没有要实现的方法),类实现该接口是为了告诉虚拟机可以序列化该类的对象。当序列化机制遇到其类未实现`Serializable`的对象时,会抛出`java.io.NotSerializableException`类的实例。
Java不支持无限制序列化的原因如下:
- **安全性**:如果Java自动序列化包含敏感信息(如密码或信用卡号)的对象,黑客很容易发现这些信息并造成破坏。因此,最好让开发人员有选择地防止这种情况发生。
- **性能**:序列化利用反射API,这往往会降低应用程序的性能。无限制序列化可能会严重损害应用程序的性能。
- **不适合序列化的对象**:有些对象仅在正在运行的应用程序的上下文中存在,序列化它们没有意义。例如,反序列化的文件流对象不再表示与文件的连接。
以下是一个实现`Serializable`接口的`Employee`类的示例:
```java
import java.io.Serializable;
public class Employee implements Serializable
{
private String name;
private int age;
public Employee(String name, int age)
{
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
```
由于`Employee`实现了`Serializable`接口,序列化机制在序列化`Employee`对象时不会抛出`NotSerializableException`实例。不仅`Employee`类实现了`Serializable`接口,`String`类也实现了该接口。
- **使用`ObjectOutputStream`和`ObjectInputStream`**:使用`ObjectOutputStream`类及其`writeObject()`方法来序列化对象,使用`ObjectInputStream`类及其`readObject()`方法来反序列化对象。
`ObjectOutputStream`用于将对象的状态序列化到对象输出流中。它的构造函数`ObjectOutputStream(OutputStream out)`将对象输出流链接到`out`指定的输出流。当你将输出流引用传递给`out`时,该构造函数会尝试向该输出流写入序列化头。如果`out`为`null`,会抛出`NullPointerException`;如果I/O错误阻止写入该头,会抛出`IOException`。
`ObjectOutputStream`通过其`void writeObject(Object obj)`方法序列化对象。该方法会尝试将关于`obj`类的信息以及`obj`实例字段的值写入底层输出流。`writeObject()`方法不会序列化静态字段的内容,相反,它会序列化所有未明确使用`transient`保留字前缀的实例字段的内容。例如:
```java
public transient char[] password;
```
上述声明指定了`transient`以避免序列化密码,防止黑客获取。虚拟机的序列化机制会忽略任何标记为`transient`的实例字段。
`ObjectInputStream`用于从对象输入流中反序列化对象。它的构造函数`ObjectInputStream(InputStream in)`将对象输入流链接到`in`指定的输入流。当你将输入流引用传递给`in`时,该构造函数会尝试从该输入流中读取序列化头。如果`in`为`null`,会抛出`NullPointerException`;如果I/O错误阻止读取该头,会抛出`IOException`;如果流头不正确,会抛出`java.io.StreamCorruptedException`。
`ObjectInputStream`通过其`Object readObject()`方法反序列化对象。该方法会尝试从底层输入流中读取关于`obj`类的信息以及`obj`实例字段的值。当出现问题时,`readObject()`方法会抛出`java.lang.ClassNotFoundException`、`IOException`或`IOException`子类的实例。
以下是一个使用这些类对`Employee`对象进行序列化和反序列化的示例:
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializationDemo
{
final static String FILENAME = "employee.dat";
public static void main(String[] args)
{
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try
{
FileOutputStream fos = new FileOutputStream(FILENAME);
oos = new ObjectOutputStream(fos);
Employee emp = new Employee("John Doe", 36);
oos.writeObject(emp);
oos.close();
oos = null;
FileInputStream fis = new FileInputStream(FILENAME);
ois = new ObjectInputStream(fis);
emp = (Employee) ois.readObject(); // (Employee) cast is necessary.
ois.close();
System.out.println(emp.getName());
System.out.println(emp.getAge());
}
catch (ClassNotFoundException cnfe)
{
System.err.println(cnfe.getMessage());
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage());
}
finally
{
if (oos != null)
try
{
oos.close();
}
catch (IOException ioe)
{
assert false; // shouldn't happen in this context
}
if (ois != null)
try
{
ois.close();
}
catch (IOException ioe)
{
assert false; // shouldn't happen in this context
}
}
}
}
```
运行这个应用程序时,除了生成`employee.dat`文件外,还会输出以下内容:
```
John Doe
36
```
在反序列化序列化对象时,不能保证相同的类仍然存在(可能某个实例字段已被删除)。在反序列化过程中,当该机制检测到反序列化对象与其类之间存在差异时,会导致`readObject()`方法抛出`java.io.InvalidClassException`(`IOException`类的间接子类)。
每个序列化对象都有一个标识符。反序列化机制会将正在反序列化的对象的标识符与其类的序列化标识符进行比较(所有可序列化类都会自动获得唯一标识符,除非它们明确指定自己的标识符),当检测到不匹配时,会抛出`InvalidClassException`。
你可以通过向类中添加`static final long serialVersionUID = long integer value;`声明来避免抛出`InvalidClassException`实例。长整数值必须唯一,称为流唯一标识符(SUID)。在反序列化过程中,虚拟机将比较反序列化对象的SUID与其类的SUID。如果它们匹配,当`readObject()`方法遇到兼容的类更改(例如添加实例字段)时,不会抛出`InvalidClassException`。但是,当遇到不兼容的类更改(例如更改实例字段的名称或类型)时,仍然会抛出此异常。
JDK提供了`serialver`工具来计算SUID。例如,要为上述`Employee`类生成SUID,切换到包含`Employee.class`的目录并执行`serialver Employee`。`serialver`会生成以下输出,你将其(除了`Employee:`)粘贴到`Employee.java`中:
```
Employee: static final long serialVersionUID = 1517331364702470316L;
```
Windows版本的`serialver`还提供了一个图形用户界面,你可能会发现使用起来更方便。要访问此界面,请指定`serialver -show`。当`serialver`窗口出现时,在“Full Class Name”文本字段中输入`Employee`,然后单击“Show”按钮。
#### 3. 自定义序列化和反序列化
虽然前面讨论了默认序列化和反序列化,但在某些情况下,你需要自定义这些任务。例如,假设你要序列化一个未实现`Serializable`接口的类的实例。作为一种解决方法,你可以对该类进行子类化,让子类实现`Serializable`接口,并将子类构造函数调用转发给超类。
然而,当超类没有声明无参数构造函数时,你无法反序列化这些序列化的对象,因为反序列化机制需要无参数构造函数。以下是一个示例:
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class Employee
{
private String name;
Employee(String name)
{
this.name = name;
}
@Override
public String toString()
{
return name;
}
}
class SerEmployee extends Employee implements Serializable
{
SerEmployee(String name)
{
super(name);
}
}
public class SerializationDemo
{
public static void main(String[] args)
{
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try
{
oos = new ObjectOutputStream(new FileOutputStream("employee.dat"));
SerEmployee se = new S
```
0
0
复制全文
相关推荐










