Java 基础-- 序列化与反序列化

java 中的序列化


java序列化图4.png
java中的序列化( Serialization )可以让我们把一个对象转换为流( stream ),这样我们才能把它通过网络发送出去、以文件的方式存储到本地、或者存到数据库里,顾名思义,反序列化( Deserialization ) 就是将流再转换为 java 对象 序列化看起来非常简单,但是在实际应用中,也会面临一些问题,本文的后半部分会详细讨论。

本文代码下载 导入 eclipse 或者 idea 中可直接运行

Serializable 接口

在 java 中,一个类想要被序列化,必需要实现 java.io.Serializable 接口,具体的序列化操作被封装在 ObjectInputStreamObjectOutputStream 中,所以我们不用关注具体 序列化 & 反序列化 的细节,只用这两个类,就能完成我们的工作。

下面是一个简单的例子。

User.java

package com.serializable;

import java.io.Serializable;

public class User implements Serializable {
    // private static final long serialVersionUID = -7470090944414208496L;

    private String name;
    private int id;

    // private double salary;
    transient private String gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    /*
     * public double getSalary() { return salary; } public void setSalary(double
     * salary) { this.salary = salary; }
     */
    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{name=" + name + ",id=" + id + ",gender=" + gender + "}";
    }

}

User 类是一个简单的 JavaBean ,如果类中的某个变量不希望被序列化,可以使用 transient 关键字。另外,静态变量也不会被序列化。

下面我们将user对象序列化,保存到文件中,然后读取这个文件,再将其反序列化。

SerializationUtil.java

package com.serializable;

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

public class SerializationUtil {

    // 将文件反序列化为对象
    public static Object deserialize(String fileName) throws IOException,
            ClassNotFoundException {
        FileInputStream fis = new FileInputStream(fileName);
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object obj = ois.readObject();
        ois.close();
        return obj;
    }

    // 将对象序列化并存到文件中
    public static void serialize(Object obj, String fileName)
            throws IOException {
        FileOutputStream fos = new FileOutputStream(fileName);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);

        fos.close();
    }

    // 测试
    public static void main(String[] args) {
        String fileName = "User.ser";
        User user = new User();
        user.setId(123456);
        user.setName("张三");
        user.setGender("男");

        // 序列化
        try {
            SerializationUtil.serialize(user, fileName);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        // 反序列化
        User userNew = null;
        try {
            userNew = (User) SerializationUtil.deserialize(fileName);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }

        System.out.println("user Object::" + user);
        System.out.println("userNew Object::" + userNew);
    }
}

运行 Main 方法,因为变量 gender 前加了关键字 transient ,所以没有被序列化到文件中。

输出结果:

user Object::User{name=张三,id=123456,gender=男}
userNew Object::User{name=张三,id=123456,gender=null}

变量 serialVersionUID

细心的读者可能会发现,在 User 类的定义中有一个被注释掉的变量 serialVersionUID ,实际上,如果类中没有定义 serialVersionUID ,编译器也会为你的类自动生成一个 serialVersionUID ,serialVersionUID 的值是根据类中的变量、方法名、包名等相关信息计算出来的,如果你对类的定义做了小小的改动,比如在类中新增一个变量,serialVersionUID 的值也会随之改变。在我将变量 serialVersionUID 注释掉后,我的 IDE 还显示了以下warning信息:

The serializable class User does not declare a static final serialVersionUID field of type long

因此,强烈建议在需要序列化的类中,定义 serialVersionUID 这个变量,否则,可能会造成反序列化失败。还以 user 为例, 上一步已经将 user 对象序列化为 文件 user.ser,然后我们在 User.java 上做一些改动,将变量 salary 还有它的 gettser-setter方法取消注释。然后我们再来尝试将上一步生成的 user.ser 反序列化为 user对象。

DeserializationTest.java

package com.serializable;

import java.io.IOException;

public class DeserializationTest {

    public static void main(String[] args) {
        String fileName = "user.ser";
        User userNew = null;

        try {
            userNew = (User) SerializationUtil.deserialize(fileName);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }

        System.out.println("user Object  " + userNew);
    }

}

运行 main 方法,输出结果:

java.io.InvalidClassException: com.User; local class incompatible: stream classdesc serialVersionUID = -1009812510484884867, local class serialVersionUID = 2307692425311469795
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.SerializationUtil.deserialize(SerializationUtil.java:16)
at com.DeserializationTest.main(DeserializationTest.java:12)
user Object::null

反序列化失败,原因是序列化 user 对象时 编译器计算出来一个 serialVersionUID (假设值为A),中间User类经历了一次改动,在反序列化的时候,User 类的 serialVersionUID 又变为另一个值(假设值为B),程序一检查发现,之前的 A 和现在的 B 不相等( 因为User类被改动了 ),立刻抛出异常。

为了避免这个问题,我们在类中显式的声明这个变量。这样无论类的定义如何变化,serialVersionUID 的值都不会变。

如果您时间有限,或者只是做一些简单的测试,看到这里就够了。

与 Serialization 接口相关的一些方法

上面讲到,在 Java 中,序列化无需过多的操作,我们需要做的就是让类继承 Serialization 接口,然后调用 ObjectInputStream 和 ObjectOutputStream 类来进行相关操作,整个序列化的细节都被封装起来了。但是为了处理一些更细节的东西, Serialization 也提供了以下四个方法。

  • readObject(ObjectInputStream ois)
  • writeObject(ObjectOutputStream oos)
  • Object writeReplace()
  • Object readResolve()

下面来用具体的例子,来说明它们的作用,通常为了安全性,以上几个方法都应该被声明为私有方法。

自定义的序列化

readObject() 和 writeObject() 这两个方法允许用户自定义序列化和反序列化的过程。在序列化过程中,jvm 会试图调用对象类里的 writeObject 和 readObject ,如果类中没有定义 这两个方法,则默认调用 defaultWriteObject 方法来执行序列化。来看一个例子:

Account.java

package com.customize;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Account implements Serializable {

    private static final long serialVersionUID = 1L;
    private String userName;
    private String password;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void writeObject(ObjectOutputStream aOutputStream)
            throws IOException {
        aOutputStream.writeUTF(userName + "***");
        aOutputStream.writeUTF(password);
    }

    private void readObject(ObjectInputStream aInputStream)
            throws ClassNotFoundException, IOException {

        userName = aInputStream.readUTF();
        if (!userName.endsWith("***")) {
            throw new IOException("数据错误");
        }
        userName = userName.substring(0, userName.length() - 3);

        password = aInputStream.readUTF();

    }

    public String toString() {
        return "Account{userName=" + userName + ",password=" + password + "}";
    }

}

注意,writeObject 中变量写入的顺序,必须与 readObject中 读取的顺序一致。

TestCustomSerialization.java

 package com.customize;

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

public class TestCustomize {

    public static void main(String[] args) throws IOException,
            ClassNotFoundException {
        Account account = new Account();
        account.setUserName("张三");
        account.setPassword("123");

        // 序列化
        FileOutputStream fileOut = new FileOutputStream("account.ser");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);
        out.writeObject(account);
        out.close();
        fileOut.close();

        // 反序列化
        Account accountNew = null;

        FileInputStream fileIn = new FileInputStream("account.ser");
        ObjectInputStream in = new ObjectInputStream(fileIn);
        accountNew = (Account) in.readObject();
        in.close();
        fileIn.close();

        System.out.println(accountNew);

    }
}

运行main方法,输出结果:

Account{userName=张三,password=123}

注意 Account 类中的 writeObject 和 readObject 方法,变量 userName 序列化之前,在后面加了个后缀"***", 反序列化后,检查了一下 userName 是不是带有这个后缀,如果没有,说明数据不完整,需要抛出异常。

序列化与继承

假设一个子类继承了某个第三方的类,子类需要被序列化,但是它的父类并没有实现序列化接口,那么子类能够执行序列化吗?当然可以,假设 SubClass 继承了SuperClass。

SuperClass.java

package com.inheritance;

public class SuperClass {

    private int id;
    private String value;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

SubClass.java

package com.inheritance;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SubClass extends SuperClass implements Serializable,ObjectInputValidation {

    private static final long serialVersionUID = -1322322333926390329L;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "SubClass{id=" + getId() + ",value=" + getValue() + ",name="+ getName() + "}";
    }

    private void readObject(ObjectInputStream ois)
            throws ClassNotFoundException, IOException {
        ois.defaultReadObject();

        // 注意set和write的顺序要一致
        setId(ois.readInt());
        setValue((String) ois.readObject());
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();

        // 注意set和write的顺序要一致
        oos.writeInt(getId());
        oos.writeObject(getValue());
    }

    @Override
    public void validateObject() throws InvalidObjectException {
        // 验证对象
        if (getId() <= 0)
            throw new InvalidObjectException("ID 必须大于0");
    }
}

需要注意的有两点

  • 在序列化中,writeObject 调用了 defaultWriteObject 来处理父类相关的信息,与 writeObject() 类似,在反序列化中 readObject() 中调用了 defaultReadObject()。
  • SubClass 实现了 ObjectInputValidation 接口,并重写了 validateObject() 方法,在 validateObject() 中,用户可以加入自己的业务逻辑,来验证反序列化后对象的完整性。
package com.inheritance;

import java.io.IOException;

import com.serializable.SerializationUtil;

public class TestInheritance {

    public static void main(String[] args) {
        String fileName = "subclass.ser";

        SubClass subClass = new SubClass();
        subClass.setId(10);
        subClass.setValue("123");
        subClass.setName("张三");

        try {
            SerializationUtil.serialize(subClass, fileName);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        try {
            SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
            System.out.println("SubClass  = " + subNew);
        } catch (ClassNotFoundException | IOException e) {
            e.printStackTrace();
        }
    }
}

运行main 方法,输出结果:

SubClass = SubClass{id=10,value=123,name=张三}

java 序列化代理模式 ( Serialization Proxy Pattern )

序列化代理模式用到了两个关键的方法:

  • writeReplace()
    该方法返回一个 Object ,如果类中定义了 writeReplace() ,序列化进程就会将 writeReplace() 的返回值作为序列化的对象。也就是说,用 writeReplace() 返回的对象,来代替本来要序列化的对象。(所以这个方法名带了个replace)。

  • readResolve()
    与 writeReplace 类似,readResolve() 也返回一个 Object,如果类中定义了 readResolve() ,反序列化进程会调用 readResolve() ,最终由 readResolve() 将序列化后的对象返回给调用者。

上面的描述可能过于抽象,下面结合一个例子来说明它们的用法。假设Data类为一个普通的类,我们为Data加入一个代理类 DataProxy。

package com.proxypattern;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Data implements Serializable {

    private static final long serialVersionUID = 2287363337312358459L;

    private String data;

    public Data(String d) {
        this.data = d;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "Data{data=" + data + "}";
    }

    // 代理类
    private static class DataProxy implements Serializable {

        private static final long serialVersionUID = 1233455273185436744L;

        private String dataProxy;
        private static final String PREFIX = "ABC";
        private static final String SUFFIX = "DEFG";

        public DataProxy(Data d) {
            // 数据混淆加密
            this.dataProxy = PREFIX + d.data + SUFFIX;
        }

        private Object readResolve() throws InvalidObjectException {

            if (dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)) {
                return new Data(dataProxy.substring(3, dataProxy.length() - 4));
            } else
                throw new InvalidObjectException("数据被损坏");
        }

    }

    // 用 DataProxy 代替 Data
    private Object writeReplace() {
        return new DataProxy(this);
    }

    private void readObject(ObjectInputStream ois)
            throws InvalidObjectException {
        throw new InvalidObjectException("不允许直接反序列化,只能通过反序列化代理实例化");
    }
}
  • Data 和 DataProxy 都实现了 Serializable 接口。

  • DataProxy 是一个内部私有静态类,所以只有 Data 类可以访问它。

  • DataProxy 的构造函数接收一个 Data 对象。

  • Data 类定义了 writeReplace() 方法,该方法返回一个 DataProxy 的实例. 所以在序列化Data对象的时,实际上被序列化的是 DataProxy 对象。

  • 在反序列化时,实际上是先将 DataProxy 反序列化,然后自动调用 readResolve() ,由 readResolve() 将 Data 对象返回给调用者。

  • readObject 接口拒绝直接序列化,让序列化只能通过代理实现。

    使用代理模式可以大大减少序列化过程中的安全问题,防止数据被篡改,虽然在 readObject 中也可以做一些简单的验证,但是作用很有限。所以如果您的类是可序列化的,强烈建议您使用这种模式。

    package com.proxypattern;
    

import java.io.IOException;

import com.serializable.SerializationUtil;

public class TestProxyPattern {

public static void main(String[] args) {
    String fileName = "data.ser";

    Data data = new Data("张三");

    try {
        SerializationUtil.serialize(data, fileName);
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        Data newData = (Data) SerializationUtil.deserialize(fileName);
        System.out.println(newData);
    } catch (ClassNotFoundException | IOException e) {
        e.printStackTrace();
    }
}

}

运行main方法,输出结果:
> Data{data=张三}

## 接口 Externalizable 
Externalizable 其实功能和 Serializable 差不多,网络上也有很多关于 Serializable 和 Externalizable 的讲解和对比,但是笔者认为Externalizable已经过时,在著名问答网站上,有这么一段回答,翻译成中文大概意思是:
>通过实现 java.io.Serializable 你的类就自动成为了可序列化的,用户无需再进行其他操作,java 会运用反射机制,自动组装和拆解对象,在早期的java版本,反射是非常慢的,所以需一旦遇上大量的体积庞大对象,就会造成性能问题。为了解这个问题,Externalizable应运而生。但是自动java1.3以后,反射机制比以前快的多,所以也就不再有那么多的性能问题,再加上 Externalizable 使用起来比较麻烦,所以Externalizable几乎已经没什么用处了。总的来说,Externalizable 是java1.1 遗留的产物,现在已经没有必要再使用了。

[查看原文出处](https://stackoverflow.com/questions/817853/what-is-the-difference-between-serializable-and-externalizable-in-java)

本着科普的精神,我们简单的说一下Externalizable的用法
 Externalizable 继承自 Serializable,也是用于序列化的接口,在使用上稍有不同——让类继承 *java.io.Externalizable* 接口,然后重写 writeExternal() 和 readExternal() 方法,writeExternal() 用于序列化, *readExternal()* 用于反序列化。以Dog类为例:

**Dog.java**
```java
package com.externalizable;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Dog implements Externalizable {

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(id);
        out.writeObject(name + "123");

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        id = in.readInt();
        name = (String) in.readObject();

        if (!name.endsWith("123"))
            throw new IOException("数据错误");
        name = name.substring(0, name.length() - 3);
    }

    @Override
    public String toString() {
        return "Dog{id=" + id + ",name=" + name + "}";
    }

}

ExternalizationTest.java

package com.externalizable;

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

public class ExternalizationTest {

    public static void main(String[] args) {
        String fileName = "dog.ser";
        Dog dog = new Dog();
        dog.setId(123);
        dog.setName("doggie");

        try {
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(dog);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        FileInputStream fis;
        try {
            fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            Dog dogNew = (Dog) ois.readObject();
            ois.close();
            System.out.println("读取Dog对象:   " + dogNew);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

注意,使用 Externalizable 必须重写 writeExternal() 和 readExternal()
运行main方法,输出结果:

读取Dog对象: Dog{id=123,name=doggie}

全部评论
...
点赞 回复 分享
发布于 2020-05-07 16:09

相关推荐

10-29 15:51
嘉应学院 Java
后端转测开第一人:你把简历的学历改成北京交通大学 去海投1000份发现基本还是没面试
点赞 评论 收藏
分享
10-16 15:48
算法工程师
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务