Serializable两三事

starlin 1,125 2019-08-16

简介

Serializable在java语言中的作用就是用来序列化,通过查看源码,发现只是一个空的接口,竟然能够实现对象的序列化和反序列化?其接口定义如下:

public interface Serializable {
}

实践

先来创建一个类用于序列化和反序列化

public class User implements Serializable {

    private static final long serialVersionUID = 8329386886941302966L;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    private String name;
    private int age;

}

在创建一个测试类,通过ObjectOutputStream和ObjectInputStream进行序列化和反序列化,代码如下:

public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("wang wu");

        // 通过ObjectOutputStream将该对象写入到文件中,实际上就是一种序列化的过程
        // 在通过ObjectInputStream将该对象从文件中读出来,实际上就是一种反序列化的过程
        // 把对象写入文件
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test"));
            outputStream.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 从文件中读出对象
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test"));
            User user1 = (User) inputStream.readObject();
            System.out.println(user1);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上面代码中,看看序列化的方法调用顺序

writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()

反序列化的方法调用

readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()

那么,看到这里,是不是能够理解了Serializable接口之所以定义为空,是因为它只是起到了一个标识作用,真正的序列化和反序列化的操作并不是由它去完成的

注意事项

static 和 transient修饰的字段是不会被序列化的

来验证一下,在原先的User类上加两个实例,代码如下:

public class User implements Serializable {

    private static final long serialVersionUID = 8329386886941302966L;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public static String getClassName() {
        return className;
    }

    public static void setClassName(String className) {
        User.className = className;
    }

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String studentName) {
        this.studentName = studentName;
    }

    private String name;
    private int age;
    public static String className = "测试-序列化前";
    private transient String studentName = "学生姓名-序列化前";

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", className=" + className +
                ", studentName='" + studentName + '\'' +
                '}';
    }
}

对应的测试类为:

public class Test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("wang wu");
        user.setStudentName("学生姓名-序列化后");
        User.className = "测试-序列化后";

        // 通过ObjectOutputStream将该对象写入到文件中,实际上就是一种序列化的过程
        // 在通过ObjectInputStream将该对象从文件中读出来,实际上就是一种反序列化的过程
        // 把对象写入文件
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test"));
            outputStream.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 从文件中读出对象
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test"));
            User user1 = (User) inputStream.readObject();
            System.out.println(user1);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

返回结果:

User{name='wang wu', age=18, className=测试-序列化后, studentName='null'}

可以看出

  1. 通过static修饰的对象className反序列化后的值变了,为什么了,因为序列化保存的是对象的状态,而 static 修饰的字段属于类的状态,因此可以证明序列化并不保存 static 修饰的字段

  2. 通过transient修饰的对象,反序列化之后就变成null(如果是换成int类型,则默认输出的是0),transient的意思是临时的意思,在反序列化后,transient修饰的字段值为初始值,在ObjectStreamClass类中发现如下代码:

    /**
     * Returns array of ObjectStreamFields corresponding to all non-static
     * non-transient fields declared by given class.  Each ObjectStreamField
     * contains a Field object for the field it represents.  If no default
     * serializable fields exist, NO_FIELDS is returned.
     */
    private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            if ((clFields[i].getModifiers() & mask) == 0) {
                list.add(new ObjectStreamField(clFields[i], false, true));
            }
        }
        int size = list.size();
        return (size == 0) ? NO_FIELDS :
            list.toArray(new ObjectStreamField[size]);
    }

是否可以自定义序列化过程, 或者是否可以覆盖 Java 中的默认序列化过程?

答案是肯定的, 你可以。我们都知道,对于序列化一个对象需调用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 读取对象, 但 Java 虚拟机为你提供的还有一件事, 是定义这两个方法。如果在类中定义这两种方法, 则 JVM 将调用这两种方法, 而不是应用默认序列化机制。你可以在此处通过执行任何类型的预处理或后处理任务来自定义对象序列化和反序列化的行为。
需要注意的重要一点是要声明这些方法为私有方法, 以避免被继承、重写或重载。由于只有 Java 虚拟机可以调用类的私有方法, 你的类的完整性会得到保留, 并且 Java 序列化将正常工作。在我看来, 这是在任何 Java 序列化面试中可以问的最好问题之一, 一个很好的后续问题是, 为什么要为你的对象提供自定义序列化表单?

在 Java 中的序列化和反序列化过程中使用哪些方法?

这是很常见的面试问题, 在序列化基本上面试官试图知道: 你是否熟悉 readObject() 的用法、writeObject()、readExternal() 和 writeExternal()。Java 序列化由java.io.ObjectOutputStream类完成。该类是一个筛选器流, 它封装在较低级别的字节流中, 以处理序列化机制。要通过序列化机制存储任何对象, 我们调用 ObjectOutputStream.writeObject(savethisobject), 并反序列化该对象, 我们称之为 ObjectInputStream.readObject()方法。调用以 writeObject() 方法在 java 中触发序列化过程。关于 readObject() 方法, 需要注意的一点很重要一点是, 它用于从持久性读取字节, 并从这些字节创建对象, 并返回一个对象, 该对象需要类型强制转换为正确的类型。

什么是 serialVersionUID ?如果你不定义这个, 会发生什么?

serialVersionUID 是一个 private static final long 型 ID, 当它被印在对象上时, 它通常是对象的哈希码,你可以使用 serialver 这个 JDK 工具来查看序列化对象的 serialVersionUID。SerialVerionUID 用于对象的版本控制。也可以在类文件中指定 serialVersionUID。不指定 serialVersionUID的后果是,当你添加或修改类中的任何字段时, 则已序列化类将无法恢复, 因为为新类和旧序列化对象生成的 serialVersionUID 将有所不s同。Java 序列化过程依赖于正确的序列化对象恢复状态的, ,并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常,类似如下报错:

java.io.InvalidClassException: local classincompatible: stream classdesc serialVersionUID = -2095916884810199532, local classserialVersionUID = -3818877437117647968 at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521) at com.xx.xx.xx.Test.main(Test.java:27)

另一种序列化Externalizable

在java语言中,还提供了一种序列化接口Externalizable,该接口提供了2个方法,

void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

和之前的实体测试类相比,这里我们多指定一个构造方法:

public class UserExternalizable implements Externalizable {

    private static final long serialVersionUID = 8329386886941302966L;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    private String name;
    private int age;

    // 指定了构造方法
    public UserExternalizable(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }
}

测试类如下:

public class TestExternalizable {
    public static void main(String[] args) {
        UserExternalizable user = new UserExternalizable("wang wu",18);
        //user.setAge(18);
        //user.setName("wang wu");

        // 通过ObjectOutputStream将该对象写入到文件中,实际上就是一种序列化的过程
        // 在通过ObjectInputStream将该对象从文件中读出来,实际上就是一种反序列化的过程
        // 把对象写入文件
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("testExternalizable"));
            outputStream.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 从文件中读出对象
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("testExternalizable"));
            UserExternalizable user1 = (UserExternalizable) inputStream.readObject();
            System.out.println(user1);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

测试结果,居然报错了,错误信息如下:

java.io.InvalidClassException: cn.lxh.java.SerializableDemo.UserExternalizable; no valid constructor
	at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
	at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
	at cn.lxh.java.SerializableDemo.TestExternalizable.main(TestExternalizable.java:27)

加上无参构造后,正常了,说明用此序列化方式需要默认加载无参构造

在UserExternalizable类中,加上无参构造后,运行结构如下:

User{name='null', age=0}

为什么反序列化失败了?原因是新增的两个方法,我们需要重写,修改后如下:

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(age);
    }

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

再次执行后能够正常进行序列化和反序列化,结果为:

User{name='wang wu', age=18}

# Java基础