JDK1.8 java.io.Serializable接口详解

java.io.Serializable接口是一个标志性接口,在接口内部没有定义任何属性与方法。只是用于标识此接口的实现类可以被序列化与反序列化。但是它的奥秘并非像它表现的这样简单。现在从以下几个问题入手来考虑。

  1. 希望对象的某些属性不参与序列化应该怎么处理?

  2. 对象序列化之后,如果类的属性发生了增减那么反序列化时会有什么影响呢?
  3. 如果父类没有实现java.io.Serializable接口,子类实现了此接口,那么父类中的属性能被序列化吗?
  4. serialVersionUID属性是做什么用的?必须申明此属性吗?如果不申明此属性会有什么影响?如果此属性的值发生了变化会有什么影响?
  5. 能干预对象的序列化与反序列化过程吗?

在解决这些问题之前,先来看一看如何进行对象的序列化与反序列化。定义一个Animal类,并实现java.io.Serializable接口。如下代码所示把Animal实例序列化为文件保存在硬盘中。

class Animal implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private String[] alias;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String[] getAlias() {
        return alias;
    }
    public void setAlias(String[] alias) {
        this.alias = alias;
    }
}

对Animal对象进行序列化与反序列化的代码如下所示:

// 反序列化
static void unserializable() throws FileNotFoundException, IOException, ClassNotFoundException{
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("D://animal.dat"));
        Animal animal = (Animal) ois.readObject();
        System.out.println(animal);
    } finally {
        if( null != ois ){
            ois.close();
        }
    }

}
// 序列化
static void serializable() throws FileNotFoundException, IOException{
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream("D://animal.dat"));
        Animal animal = new Animal();
        animal.setName("Dog");
        animal.setColor("Black");
        animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
        oos.writeObject(animal);
        oos.flush();
    } finally {
        if(null != oos){
            oos.close();
        }
    }
}

现在利用以上序列化与反序列化Animal对象的例子来逐步回答本文开始时提出的几个问题。

一、如何让某些属性不参与序列化与反序列化的过程?

假定在Animal对象中,我们希望alias属性不能被序列化。这个问题非常容易解决,只需要使用transient关键定修饰此属性就可以了。对Animal类的简单修改如下所示:

class Animal implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;

如果一个属性被transient关键字修饰,那么此属性就不会参与对象序列化与反序列化的过程。

二、类的属性发生了增减那么反序列化时会有什么影响?

假定在设计Animal类的时候由于考虑不周全而需要添加age属性,那么如果在添加此之前Animal对象已序列化为animal.dat文件,那么在添加age属性之后,还能不能成功的反序列化呢?新的Animal类的片段如下所示:

class Animal implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;

再次调用反序列化的方法,使用添加age属性之前的animal.dat文件进行反序列化,运行结果表明还是能正常的反序列化,只是新添加的属性为默认值。

反过来考虑,如果把animal.dat文件中存在的name属性删除,那么还能使用animal.dat文件进行反序列化吗?修改之后的Animal类如下所示:

class Animal implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 8822818790694831649L;
//    private String name;
    private String color;
    private transient String[] alias;
    private int age;

调用反序列化的方法,使用删除name属性之前的animal.dat文件进行反序列化,运行结果表时还是能正常的反序列化。由此可知,类的属性的增删并不能对对象的反序列化造成影响。

三、继承关系在序列化过程中的影响?

假定有父类Living没有实现java.io.Serializable接口,子类Human实现了java.io.Serializable接口,那么在序列化子类时父类中的属性能被序列化吗?先给出Living与Human类的定义如下所示:

class Living{
    private String environment;

    public String getEnvironment() {
        return environment;
    }

    public void setEnvironment(String environment) {
        this.environment = environment;
    }
}

class Human extends Living implements Serializable{

    /**
     *
     */
    private static final long serialVersionUID = -4389621464687273122L;

    private String name;
    private double weight;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getWeight() {
        return weight;
    }
    public void setWeight(double weight) {
        this.weight = weight;
    }
    @Override
    public String toString() {
        return getEnvironment() + " : " + name + ", " + weight;
    }
}

通过代码序列化Human对象得到human.dat文件,再从此文件中进行反序列化得出结果为:

null : Wg, 130.0

也可以使用文件编辑工具Notepad++,查看human.dat文件如下所示:

在这个文件中看不到任何与父类中的environment属性相同的内容,说明这个属性并没有被序列化。

修改父类Living,使之实现java.io.Serialazable接口,父类修改之后代码片段如下所示:

class Living implements Serializable{

序列化Human对象再次得到human.dat文件,再从此文件中反序列化得出结果为:

human environment : Wg, 130.0

再次通过Notepad++,查看human.dat文件如下所示:

从这个文件中也可以清楚的看到父类Living中的environment属性被成功的序列化。

由此可得出结论在继承关系中如果父类没有实现java.io.Serializable接口,那么在序列化子类时即使子类实现了java.io.Serializable接口也不能把父类中的属性序列化。

四、serialVersionUID属性

在使用Eclipse之类的IDE开发工具时,如果类实现了java.io.Serializable接口,那么IDE会警告让生成如下属性:

private static final long serialVersionUID = 8822818790694831649L;

这个属性必须被申明为static的,最好是final不可修改的。此属性被用于序列化与反序列化过程中的类信息校验,如果此属性的值在序列化之后发生了变化,那么可序列化的文件就不能再反序列化,会抛出InvalidClassException异常。如下所示,在序列化之生修改此属性,运行代码的结果:

// 序列化之生手动修改了serialVersionUID属性
    private static final long serialVersionUID = 1822818790694831649L;
//    private static final long serialVersionUID = 8822818790694831649L;

这时反序列化会出现如下的异常信息:

java.io.InvalidClassException: j2se.Animal; local class incompatible: stream classdesc serialVersionUID = 8822818790694831649, local class serialVersionUID = 1822818790694831649
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at j2se.SerializableTest.unserializable(SerializableTest.java:58)
    at j2se.SerializableTest.animalUnSerializable(SerializableTest.java:50)
    at j2se.SerializableTest.main(SerializableTest.java:26)

由此可见,如果序列化之后修改了serialVersionUID属性,那么序列化的文件就不能再成功的反序列化。

当然,在工作中也见过很多程序员并不在意IDE警告,不会为类申明serialVersionUID属性,因为这个属性也不是必须的。通过把类的serialVersionUID属性删除也可以成功的序列化与反序列化,如果类没有显式的申明serialVersionUID属性,那么JVM会依据类的各方面信息自动生成serialVersionUID属性值,但是由于不同的JVM生成serialVersionUID的原理存在差异。所以强烈建议程序员显式申明serialVersionUID属性,并强烈建议使用private static final修饰此属性。

五、如果干预对象的序列化与反序列化过程?

在上面例子中的Animal类中定义了一个由transient关键字修饰的alias变量,由于被transient修饰所以它不会被序列化。但是希望在序列化的过程中把alias数组的各个元素序列化,并在反序列化过程把数组中的元素还原到alias数组中。java.io.Serializable接口虽然没有定义任何方法,但是可以通过在要序列化的类中的申明如下准确签名的方法:

   /**
     * 序列化对象时调用此方法完成序列化过程
     * @param o
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream o) throws IOException{

    }
    /**
     * 反序列化对象时调用此方法完成反序列化过程
     * @param o
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{

    }
    /**
     * 反序列化的过程中如果没有数据时调用此方法
     * @throws ObjectStreamException
     */
    private void readObjectNoData() throws ObjectStreamException{

    }

在Animal类中可以申明以上的方法,如下所示:

class Animal implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;

    /**
     * 序列化对象时调用此方法完成序列化过程
     * @param o
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream o) throws IOException{
        o.defaultWriteObject(); // 默认写入对象的信息
        o.writeInt(alias.length);// 写入alias元素的个数
        for(int i=0;i<alias.length;i++){
            o.writeObject(alias[i]);// 写入alias数组中的每一个元素
        }
    }
    /**
     * 反序列化对象时调用此方法完成反序列化过程
     * @param o
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{
        // 读取顺序与写入顺序一致
        o.defaultReadObject(); // 默认读取对象的信息
        int length = o.readInt(); // 读取alias元素的个数
        alias = new String[length];
        for(int i=0;i<length;i++){
            alias[i] = o.readObject().toString(); // 读取元素存入数组
        }
    }

到目前为止,我们已可以自定义对象的序列化与反序列化的过程。比如通过以下程序序列化对象,得到animal.dat文件。

static void animalSerializable(){
        Animal animal = new Animal();
        animal.setName("Dog");
        animal.setColor("Black");
        animal.setAge(100);
        animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
        serializable(animal, "D://animal.dat");
    }

通过Notepad++打开animal.dat文件如下图所示:

可以从上图中发现,实际上可以序列化的文件中找到部分对象信息。现在我们希望能把信息加密之后再序列化,并在反序列化时自动解密。在java.io.Serializable接口的实现类中还可以定义如下的方法,用于替换序列化过程中的对象与解析反序列化过程中的对象。

/**
     * 在writeObject方法之前调用,通过此方法替换序列化过程中需要替换的内部。
     * @return
     * @throws ObjectStreamException
     */
    Object writeReplace() throws ObjectStreamException{

    }

    /**
     * 在readObject方法之前调用,用于把writeReplace方法中替换的对象还原
     * @return
     * @throws ObjectStreamException
     */
    Object readResolve() throws ObjectStreamException{

    }

在Animal对象的序列化与反序列化的过程中可以利用以上的两个方法进行加密与解密,如下所示:

/**
     * 在writeObject方法之前调用,通过此方法替换序列化过程中需要替换的内部。
     * @return
     * @throws ObjectStreamException
     */
    Object writeReplace() throws ObjectStreamException{
        try {
            Animal animal = new Animal();
            String key = String.valueOf(serialVersionUID); // 简单使用erialVersionUID做为对称算法的密钥
            animal.setAge(getAge() << 2); // 对于整数就简单的处理为向左移动两位
            animal.setName(DesUtil.encrypt(getName(), key)); // 加密
            animal.setColor(DesUtil.encrypt(getColor(), key));
            String[] as = new String[getAlias().length];
            for(int i=0;i<as.length;i++){
                as[i] = DesUtil.encrypt(getAlias()[i], key);
            }
            animal.setAlias(as);
            return animal;
        } catch (Exception e) {
            throw new InvalidObjectException(e.getMessage());
        }
    }

    /**
     * 在readObject方法之前调用,用于把writeReplace方法中替换的对象还原
     * @return
     * @throws ObjectStreamException
     */
    Object readResolve() throws ObjectStreamException{
        try {
            Animal animal = new Animal();
            String key = String.valueOf(serialVersionUID);
            animal.setAge(getAge() >> 2);
            animal.setName(DesUtil.decrypt(getName(), key)); // 解密
            animal.setColor(DesUtil.decrypt(getColor(), key));
            String[] as = new String[getAlias().length];
            for(int i=0;i<as.length;i++){
                as[i] = DesUtil.decrypt(getAlias()[i], key);
            }
            animal.setAlias(as);
            return animal;
        } catch (Exception e) {
            throw new InvalidObjectException(e.getMessage());
        }
    }

再次使用Notepad++打开animal.dat文件如下图所示,在其中就不会再存在Animal对象的信息。

所以综上所述,对象的序列化与反序列化过程是完全可控的,利用writeReplace与writeObject方法控制序列化过程,readResolve与readObject方法控制反序列化过程。在序列化过程中与反序列化过程中方法的调用顺序如下所示:

序列化过程:writeReplace –> writeObject

反序列化过程:readObject –> readResolve

时间: 2024-12-26 18:01:16

JDK1.8 java.io.Serializable接口详解的相关文章

JDK源码阅读(五)java.io.Serializable接口

package java.io; public interface Serializable { } (1)实现Serializable接口的类,将会被提示提供一个 serialVersionUID 注意点一.序列化时为了保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性. 有两种生成方式:              ①一个是默认的1L,比如:private static final long serialVersionUID = 1L;              ②一个是根据类名.接口

java 锁 Lock接口详解

一:java.util.concurrent.locks包下常用的类与接口(lock是jdk 1.5后新增的) (1)Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock. Lock 接口支持那些语义不同(重入.公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则.主要的实现是 Ree

java Io 流类详解

关于java  流类的复习:习惯性的复习按照图结构一层层往下深入去了解去复习,最后通过代码来实现感觉印象会更深刻一些: 关于 I/O流:IO可以理解为JAVA用来传递数据的管道,创建一个IO,就相当于将管道与某个数据源连接到一起了. 字节流:数据流中最小的数据单元是字节.  字节:字节是一个八位的二进制数是一个很具体的存储空间: 字符流:数据流中最小的数据单元是字符:  字符:是一个抽象的符号,例如 1.2.3 .人  等   (并不是说直接传输字符,而是将字符按照指定的编码规则,转成对应的字节

java抽象类和接口详解

接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 抽象类与接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的面向对象的能力.他们两者之间对抽象概念的支持有很大的相似,甚至可以互换,但是也有区别. 一.抽象类 我们都知道在面向对象的领域一切都是对象,同时所有的对象都是通过类来描述的,但是并不是所有的类都是来描述对象的.如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类.比如new Animal

Java中的接口详解

接口 是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量.构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9). 接口的定义: 它与定义类方式相似,但是使用 interface 关键字.它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型. 引用数据类型:数组,类,接口. 接口的使用: 它不能创建对象,但是可以被实现(implements ,类似于被继承)

Java之嵌套接口详解(附源码)

示例源码 接口可以嵌套在类或其他接口中.这揭示了许多非常有趣的特性: package com.mufeng.theninthchapter; class A { interface B { void f(); } public class BImp implements B { @Override public void f() { // TODO Auto-generated method stub } } private class BImp2 implements B { @Overrid

java抽象类与接口 详解

在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类. 抽象类往往用来表征我们在对问题领域进行分析. 设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象,我们不能把它们实例化(拿不出一个具体的东西)所以称之为抽象. 比如:我们要描述“水果”,它就是一个抽象,它有质量.体积等一些共性(水果有质量),但又缺乏特性(苹果.橘子都是水果,它们有自己的特性),我们拿不出

转发 java数据结构之hashMap详解

概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMap数据结构第3部分 HashMap源码解析(基于JDK1.6.0_45)第3.1部分 HashMap的“拉链法”相关内容第3.2部分 HashMap的构造函数第3.3部分 HashMap的主要对外接口第3.4部分 HashMap实现的Cloneable接口第3.5部分 HashMap实现的Seria

java.io.Serializable

Java API中java.io.Serializable接口源码: 1 public interface Serializable { 2 } 类通过实现java.io.Serializable接口可以启用其序列化功能.未实现次接口的类无法使其任何状态序列化或反序列化.可序列化类的所有子类型本身都是可序列化的.序列化接口没有方法或字段,仅用于标识可序列化的语义. Java的"对象序列化"能让你将一个实现了Serializable接口的对象转换成byte流,这样日后要用这个对象时候,你