Java基础 - Serializable序列化简介

参考:https://blog.csdn.net/u011607686/article/details/78933856
https://www.ibm.com/developerworks/cn/java/j-5things1/
https://baijiahao.baidu.com/s?id=1633305649182361563&wfr=spider&for=pc

Serializable序列化简介

什么是序列化

序列化:将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。

反序列化:把字节序列恢复为对象的过程。

实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。

一个简单的序列化/反序列化过程:

@Data
public class SerialVO implements Serializable {
    private String str1;
    private String str2;
}
public class SerializableTest {

    /**
     * 序列化
     */
    private static void serialize() throws Exception {
        SerialVO serialVO = new SerialVO();
        serialVO.setStr1("string 1");
        serialVO.setStr2("string 2");
        //ObjectOutputStream对象输出流,将serialVO对象存储到文件中,完成对serialVO对象的序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/Users/iyoukeji/Desktop/test.txt")));
        oos.writeObject(serialVO);
        System.out.println("对象序列化成功!");
        oos.close();
    }

    /**
     * 反序列化
     */
    private static void deserialize() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("/Users/iyoukeji/Desktop/test.txt")));
        SerialVO serialVO = (SerialVO) ois.readObject();
        System.out.println("对象反序列化成功!");
        System.out.println(serialVO.toString());
    }

    public static void main(String[] args) throws Exception {
        serialize();
        deserialize();
    }
}

transient和static

transient:意为“临时的”。被transient修饰的变量在序列化的时候不会被保存到文件中,通过反序列化读取这个变量时不会有值。

static:被static修饰的变量也是不会被序列化的,因为只有堆内存会被序列化。所以静态变量会天生不会被序列化。

修改SerialVO如下:

@Data
public class SerialVO implements Serializable {
    private String str1;
    private String str2;
    private static int num1 = 123;
    private transient String transientStr = "transient";
}

执行序列化/反序列化方法之后打印:

对象序列化成功!
SerialVO(str1=string 1, str2=string 2, transientStr=transient)
对象反序列化成功!
SerialVO(str1=string 1, str2=string 2, transientStr=null)

serialVersionUID

serialVersionUID字段表示类的序列化版本,用于反序列化时校验。

在序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前的文件的serialVersionUID是否一致,如果一致则反序列化成功。否则就说明当前类跟序列化后的类发生了变化,在反序列化时就会发生crash,并且会报出InvalidClassException。

如果类中没有这个字段,那么在运行时JVM会通过类名/方法名/属性等诸多因素计算一个值。

推荐用户给每个需要序列化的类明确指定一个serialVersionUID。因为默认的计算方式严重依赖于编译器的实现,可能导致反序列化的时候抛出InvalidClassException异常。

案例重现:

(1)先执行 serialize() 方法,再将SerialVO修改为如下:

@Data
public class SerialVO implements Serializable {
    private String str1;
    private String str2;
    private String str3;
}

最后执行 deserialize() 方法,发现程序报InvalidClassException异常。

(2)先将SerialVO修改为如下:

@Data
public class SerialVO implements Serializable {

    private static final long serialVersionUID = 1L;

    private String str1;
    private String str2;
}

再执行 serialize() 方法。然后将SerialVO修改为如下:

@Data
public class SerialVO implements Serializable {

    private static final long serialVersionUID = 1L;

    private String str1;
    private String str2;
    private String str3;
}

最后执行 deserialize() 方法,反序列化成功。

序列化并不安全

如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而反序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()。

让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。

这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。

幸运的是,序列化允许“hook”序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。

修改SerialVO如下:

@Data
public class SerialVO implements Serializable {

    private static final long serialVersionUID = 1L;

    private int num1;
    private String str1;
    private String str2;

    private void writeObject(java.io.ObjectOutputStream stream)
            throws java.io.IOException {
        // "Encrypt"/obscure the sensitive data
        num1 = num1 << 2;
        stream.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream stream)
            throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();

        // "Decrypt"/de-obscure the sensitive data
        num1 = num1 >> 2;
    }
}

为了“hook”序列化过程,我们在SerialVO上实现一个writeObject方法;为了“hook”反序列化过程,我们在同一个类上实现一个readObject方法。

posted @ 2019-07-16 20:17  Helios_Fz  阅读(232)  评论(0编辑  收藏  举报