序列化与反序列化

JDK版本为1.8

序列化

序列化:把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

反序列化:把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

public interface Serializable {
}

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

序列化Java对象

新建一个User

public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;

    // 省略get、set方法

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

新建测试类

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serialize();
        deserialize();
    }
    // 反序列化
    public static void deserialize(  ) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream =
                new ObjectInputStream( new FileInputStream( new File("user.txt")));
        User user = (User) objectInputStream.readObject();
        objectInputStream.close();
        System.out.println("反序列化结果为:");
        System.out.println(user.toString());
    }
    // 序列化
    public static void serialize() throws IOException {
        User user = new User();
        user.setId(1);
        user.setUsername("lzc");
        user.setPassword("123456");
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream( new FileOutputStream( new File("user.txt") ) );
        objectOutputStream.writeObject(user);
        objectOutputStream.close();
        System.out.println("序列化成功!");
    }
}

Serializable接口作用

如果需要序列化的Java对象没有实现Serializable接口,在序列化对象的时候会抛出java.io.NotSerializableException异常。查看源码ObjectOutputStream.writeObject(Object obj)方法

// java.io.ObjectOutputStream
public final void writeObject(Object obj) throws IOException {
    if (enableOverride) {
        writeObjectOverride(obj);
        return;
    }
    try {
        // 会运行到这里,主要看这个方法
        writeObject0(obj, false);
    } catch (IOException ex) {
        if (depth == 0) {
            writeFatalException(ex);
        }
        throw ex;
    }
}

private void writeObject0(Object obj, boolean unshared) throws IOException {
    boolean oldMode = bout.setBlockDataMode(false);
    depth++;
    try {
        // 省略......
        // 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) { // Serializable类型
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } finally {
        depth--;
        bout.setBlockDataMode(oldMode);
    }
}

从上面的方法可以看出,只有字符串类型数组类型枚举类型Serializable类型的对象才能够进行序列化操作,否则会抛出NotSerializableException异常

serialVersionUID

一般实现了Serializable接口的对象都会定义一个serialVersionUID字段,该字段可以用编辑器自动生成。

private static final long serialVersionUID = -8838781226739307504L;

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException

如果不指定serialVersionUID字段,编译器会为它自动声明一个。

假设User类中没有定义serialVersionUID字段,创建一个User对象保存到user.txt文件中。然后在User类中新增一个字段,此时去读取user.txt文件进行反序列化,这个时候就会反序列化失败,报InvalidClassException异常。

如果在User类中定义了serialVersionUID字段,并且保证序列化与反序列化时对象的serialVersionUID一致,反序列化操作才能成功。

特殊字段

  1. 除了serialVersionUID字段,凡是被static修饰的字段都不会被序列化。序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
  2. transient关键字修饰的字段也不会被序列化,如下所示。
private transient String password;

反序列化破坏单例模式

新建UserSingleton

public class UserSingleton implements Serializable {
    private static final long serialVersionUID = -1123030495398145165L;
    private UserSingleton() {
    }
    // 使用静态内部类实现线程安全的单例模式
    private static class SingletonHolder {
        private static final UserSingleton singleton = new UserSingleton();
    }
    public static UserSingleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

新建测试类

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream( new FileOutputStream( new File("UserSingleton.txt") ) );
        objectOutputStream.writeObject(UserSingleton.getSingleton());
        objectOutputStream.close();
        System.out.println("序列化成功!");

        ObjectInputStream objectInputStream1 =
                new ObjectInputStream( new FileInputStream( new File("UserSingleton.txt")));
        UserSingleton userSingleton1 = (UserSingleton) objectInputStream1.readObject();
        objectInputStream1.close();

        UserSingleton userSingleton2 = UserSingleton.getSingleton();

        System.out.println(userSingleton1 == userSingleton2);
    }
}

通过打印结果可以发现,反序列化的对象与原对象并不相等

解决办法:在单例类中编写readResolve()方法

public class UserSingleton implements Serializable {
    private static final long serialVersionUID = -1123030495398145165L;
    private UserSingleton() {
    }
    // 使用静态内部类实现线程安全的单例模式
    private static class SingletonHolder {
        private static final UserSingleton singleton = new UserSingleton();
    }
    public static UserSingleton getSingleton() {
        return SingletonHolder.singleton;
    }
    // 当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象
    private Object readResolve() {
        return SingletonHolder.singleton;
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!