MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java I/O的对象序列化与反序列化

2023-11-144.3k 阅读

Java I/O 的对象序列化与反序列化

在 Java 编程中,对象序列化与反序列化是 I/O 操作中的重要概念,它们允许我们将对象转换为字节流以便在网络上传输或持久化存储到文件中,之后又能从字节流重新构建出原始对象。这对于实现分布式系统、数据存储与恢复等场景至关重要。

序列化基础概念

序列化,简单来说,就是将对象的状态信息转换为字节序列的过程。这些字节序列可以被存储到文件、数据库,或者通过网络进行传输。当需要使用对象时,反序列化则将这些字节序列重新恢复为原来的对象。

Java 提供了 java.io.Serializable 接口来支持对象的序列化。一个类只要实现了 java.io.Serializable 接口,就表明该类的对象可以被序列化。这个接口没有任何方法,它只是一个标记接口,用于告知 Java 虚拟机这个类可以被序列化。

实现对象序列化

下面通过一个简单的示例来展示如何实现对象序列化。假设我们有一个 Person 类,我们希望将 Person 对象进行序列化。

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

在上述代码中,Person 类实现了 Serializable 接口,这使得 Person 对象可以被序列化。接下来,我们编写代码将 Person 对象序列化到文件中。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializePerson {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("Person object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

SerializePerson 类中,我们创建了一个 Person 对象,并使用 ObjectOutputStream 将其写入到名为 person.ser 的文件中。ObjectOutputStream 是 Java 提供的用于将对象写入输出流的类。它负责将对象转换为字节序列并写入到指定的输出流。

实现对象反序列化

反序列化是序列化的逆过程,它将字节序列转换回原始对象。下面是反序列化 person.ser 文件中 Person 对象的代码示例。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializePerson {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person person = (Person) ois.readObject();
            System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

DeserializePerson 类中,我们使用 ObjectInputStreamperson.ser 文件中读取字节序列,并将其转换回 Person 对象。ObjectInputStreamreadObject 方法负责从输入流中读取字节序列并重新构建对象。如果在反序列化过程中找不到对应的类定义,会抛出 ClassNotFoundException

序列化版本号(serialVersionUID)

在序列化过程中,serialVersionUID 是一个非常重要的概念。它是一个类的标识符,用于验证序列化对象的类版本是否与反序列化时的类版本一致。如果在序列化和反序列化过程中,类的 serialVersionUID 不一致,会抛出 InvalidClassException

Java 会自动为实现了 Serializable 接口的类生成一个默认的 serialVersionUID,它是根据类的结构和成员信息计算得出的。然而,在实际开发中,建议手动指定 serialVersionUID,这样可以确保在类的结构发生变化时,仍然能够正确地进行反序列化。

以下是为 Person 类手动指定 serialVersionUID 的示例。

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

在上述代码中,我们手动指定了 serialVersionUID1L。这样,即使类的结构发生了一些变化,只要 serialVersionUID 不变,就可以成功进行反序列化。

transient 关键字

有时候,我们可能不希望类中的某些字段被序列化。例如,某个字段可能包含敏感信息(如密码),或者是在运行时动态生成且反序列化后可以重新计算的信息。在这种情况下,可以使用 transient 关键字来修饰这些字段。

下面是一个包含 transient 字段的示例。

import java.io.Serializable;

public class Account implements Serializable {
    private String username;
    private transient String password;

    public Account(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }
}

Account 类中,password 字段被声明为 transient。这意味着在序列化 Account 对象时,password 字段的值不会被包含在字节序列中。当反序列化 Account 对象时,password 字段将被初始化为 null

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeAccount {
    public static void main(String[] args) {
        Account account = new Account("admin", "secret");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("account.ser"))) {
            oos.writeObject(account);
            System.out.println("Account object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeAccount {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("account.ser"))) {
            Account account = (Account) ois.readObject();
            System.out.println("Username: " + account.getUsername() + ", Password: " + account.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行上述代码,会发现反序列化后的 passwordnull

自定义序列化与反序列化方法

除了使用默认的序列化机制,Java 还允许我们自定义序列化与反序列化的行为。通过在类中定义 writeObjectreadObject 方法,可以实现对序列化和反序列化过程的精细控制。

下面是一个自定义序列化与反序列化的示例。

import java.io.*;

public class CustomSerializable implements Serializable {
    private int value;

    public CustomSerializable(int value) {
        this.value = value;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.writeInt(value * 2);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        value = ois.readInt() / 2;
    }

    public int getValue() {
        return value;
    }
}

CustomSerializable 类中,我们定义了 writeObject 方法,在序列化时,将 value 乘以 2 后写入输出流。在 readObject 方法中,从输入流读取数据后再除以 2 来恢复 value 的原始值。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeCustom {
    public static void main(String[] args) {
        CustomSerializable custom = new CustomSerializable(10);
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("custom.ser"))) {
            oos.writeObject(custom);
            System.out.println("CustomSerializable object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeCustom {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("custom.ser"))) {
            CustomSerializable custom = (CustomSerializable) ois.readObject();
            System.out.println("Value: " + custom.getValue());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

通过自定义序列化与反序列化方法,我们可以实现一些特殊的需求,如数据加密、压缩等。

序列化中的继承与多态

当一个类继承自另一个实现了 Serializable 接口的类时,子类也自动支持序列化。如果父类没有实现 Serializable 接口,那么在序列化子类对象时,父类的非 transient 字段将不会被序列化。

下面是一个关于继承与序列化的示例。

import java.io.Serializable;

class Animal implements Serializable {
    private String species;

    public Animal(String species) {
        this.species = species;
    }

    public String getSpecies() {
        return species;
    }
}

class Dog extends Animal {
    private String name;

    public Dog(String species, String name) {
        super(species);
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

在上述代码中,Animal 类实现了 Serializable 接口,Dog 类继承自 Animal 类,因此 Dog 类的对象也可以被序列化。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeDog {
    public static void main(String[] args) {
        Dog dog = new Dog("Canine", "Buddy");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dog.ser"))) {
            oos.writeObject(dog);
            System.out.println("Dog object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeDog {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dog.ser"))) {
            Dog dog = (Dog) ois.readObject();
            System.out.println("Species: " + dog.getSpecies() + ", Name: " + dog.getName());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

对于多态的情况,在反序列化时,Java 会根据字节序列中的信息来正确地还原出具体的对象类型。例如,如果我们序列化了一个 Dog 对象,并将其赋值给 Animal 类型的变量,在反序列化时,仍然可以还原出 Dog 对象的具体属性。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeAnimal {
    public static void main(String[] args) {
        Animal animal = new Dog("Canine", "Buddy");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("animal.ser"))) {
            oos.writeObject(animal);
            System.out.println("Animal object serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeAnimal {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("animal.ser"))) {
            Animal animal = (Animal) ois.readObject();
            if (animal instanceof Dog) {
                Dog dog = (Dog) animal;
                System.out.println("Species: " + dog.getSpecies() + ", Name: " + dog.getName());
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

序列化与集合

在 Java 中,集合类(如 ArrayListHashMap 等)也实现了 Serializable 接口,因此可以对包含对象的集合进行序列化与反序列化。

下面是一个序列化和反序列化 ArrayList 的示例。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;

public class SerializeArrayList {
    public static void main(String[] args) {
        ArrayList<Person> personList = new ArrayList<>();
        personList.add(new Person("Bob", 25));
        personList.add(new Person("Charlie", 35));

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("personList.ser"))) {
            oos.writeObject(personList);
            System.out.println("ArrayList serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;

public class DeserializeArrayList {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("personList.ser"))) {
            ArrayList<Person> personList = (ArrayList<Person>) ois.readObject();
            for (Person person : personList) {
                System.out.println("Name: " + person.getName() + ", Age: " + person.getAge());
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,我们创建了一个 ArrayList 并添加了两个 Person 对象,然后将 ArrayList 序列化到文件中。在反序列化时,从文件中读取字节序列并恢复出 ArrayList,同时也恢复了其中的 Person 对象。

序列化在网络编程中的应用

对象序列化在网络编程中有着广泛的应用。例如,在客户端 - 服务器架构中,客户端可能需要将对象发送到服务器进行处理,服务器处理完后再将结果对象返回给客户端。通过对象序列化与反序列化,可以方便地实现这种数据传输。

下面是一个简单的基于套接字的网络通信示例,展示如何在客户端和服务器之间传输序列化对象。

服务器端代码:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(12345)) {
            System.out.println("Server is listening on port 12345...");
            try (Socket socket = serverSocket.accept();
                 ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
                Person person = (Person) ois.readObject();
                System.out.println("Received person: Name: " + person.getName() + ", Age: " + person.getAge());
                // 处理对象,这里简单地将年龄加 1
                person = new Person(person.getName(), person.getAge() + 1);
                oos.writeObject(person);
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

客户端代码:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

public class Client {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 12345);
             ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
             ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) {
            Person person = new Person("David", 28);
            oos.writeObject(person);
            Person updatedPerson = (Person) ois.readObject();
            System.out.println("Received updated person: Name: " + updatedPerson.getName() + ", Age: " + updatedPerson.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,客户端将 Person 对象序列化后发送到服务器,服务器接收并处理该对象,然后将处理后的 Person 对象序列化返回给客户端。客户端再反序列化得到处理后的对象。

序列化的性能优化

在实际应用中,序列化的性能可能会成为一个关键问题,特别是在处理大量对象或频繁进行序列化与反序列化操作时。以下是一些性能优化的建议:

  1. 减少不必要的字段序列化:尽可能使用 transient 关键字标记不需要序列化的字段,这样可以减少序列化的数据量。
  2. 使用更高效的序列化库:除了 Java 自带的序列化机制,还有一些第三方库(如 Kryo、Protostuff 等)提供了更高效的序列化方式。这些库通常在序列化速度和生成的字节序列大小方面都有更好的表现。
  3. 批量处理:如果需要序列化多个对象,可以考虑将它们批量处理,而不是逐个进行序列化。这样可以减少序列化操作的开销。

序列化的安全性考虑

在使用对象序列化时,还需要注意安全性问题。由于反序列化过程会根据字节序列重新构建对象,恶意用户可能通过构造恶意的字节序列来执行任意代码。为了防止这种攻击,可以采取以下措施:

  1. 验证反序列化的输入:在反序列化之前,对输入的字节序列进行验证,确保其来源可靠。
  2. 限制反序列化的类:可以通过自定义 ObjectInputStreamresolveClass 方法来限制能够被反序列化的类。只允许特定的类进行反序列化,拒绝其他未知类。
  3. 更新 Java 版本:及时更新 Java 版本,以获取最新的安全补丁,修复已知的序列化安全漏洞。

通过以上对 Java I/O 中对象序列化与反序列化的详细介绍,我们了解了其基本概念、实现方法、各种特性以及在实际应用中的注意事项。合理使用对象序列化与反序列化可以为我们的程序开发带来很大的便利,特别是在分布式系统和数据持久化等领域。同时,也要注意性能优化和安全性问题,确保程序的高效运行和数据的安全。