深入理解Java序列化

目录

    • 一、概念
    • 二、Serializable接口
    • 三、源码分析

一、概念

序列化:把Java对象转化为二进制字节码的过程。
反序列化:将在序列化过程中所生成的二进制字节码转换成Java对象的过程。

为什么需要序列化呢?
主要有两个作用:

  • 持久化,通过序列化把Java对象转化为二进制字节码,然后可以将其保存在文件中,在合适的时候再反序列化恢复为一个对象。
  • 传输,网络上传输的数据都是二进制的形式,再网络上传输一个Java对象需要先序列化为二进制数据,然后在网络的另一端通过反序列化将接收到的二进制数据转为Java对象。

二、Serializable接口

public interface Serializable {
}

Serializable 接口本身没有任何方法和属性,只是一个类可以进行序列化的标志。在对对象进行序列化时如果该类型没有实现Serializable接口就会抛出异常,实现了Serializable接口,才会对这个对象进行序列化。

使用
基本使用如下:

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
 public static void main(String[] args) throws Exception {
        Person person1=new Person("小明",23);
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("text.out"));
        oos.writeObject(person1);
        System.out.println(person1);//Person{name='小明', age=23}

        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("text.out"));
        Person person2= (Person) ois.readObject();
        System.out.println(person2);//Person{name='小明', age=23}

        System.out.println("person1和person2是同一个对象吗?"+(person1==person2));//false
    }

注意:反序列化获取的对象和原来的对象不是同一个。

借助Serializable关键字对Java对象进行序列化,反序列化非常的简单,基本的用法就不在复述,这里主要记录几个可能遗漏的知识点。

  • 1、如果某个类实现序列化,它的成员有引用类型,但是这个引用类型没有实现序列化。那么这个类在进行序列化时会报错
public class Person implements Serializable {
    private String name;
    private int age;
    private Cat cat;
    //...
}
//Cat.java
public class Cat{
}

如上代码,在Person类中加入Cat成员,但是Cat没有实现Serializable接口。在对Person进行序列化时会如何呢?
深入理解Java序列化_第1张图片
在对Java对象进行序列化的时候会对其每个成员进行序列化,如果这个成员是引用类型的,又没有实现Serializable接口就会抛出异常。后面源码分析的时候会讲到。
深入理解Java序列化_第2张图片

  • 2、子类实现序列化接口,父类没实现序列化接口,此时父类要有一个无参数构造器,否则会报错。
public class Person extends Base implements Serializable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        super(name);
        this.name = name;
        this.age = age;
    }
}

//Base.java
public class Base {
    private String desc;
    //无参的构造器
    public Base() {
    }

    public Base(String desc) {
        this.desc = desc;
    }
}

如果把这个无参的构造器去掉,在反序列化Person 的时候就会报错。
深入理解Java序列化_第3张图片

  • 3、writeObject和readObject方法
    这两个方法不是Serializable 接口的,但它们确实可以参与序列化的过程,当我们给需要序列化的类加上了这两个方法,那么Java在序列化对象的时候就不会走默认的序列化写入和默认的序列化读取了(defaultWriteObject\defaultReadObject),而是走我们自己定义的writeObject和readObject方法。
public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("writeObject");
        oos.writeUTF(name);
        oos.writeInt(age);
    }

    private void readObject(ObjectInputStream ois) throws IOException {
        System.out.println("readObject");
        this.name=ois.readUTF();
        this.age=ois.readInt();
    }

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

执行结果:

writeObject
person1=Person{name='小明', age=23}
readObject
person2=Person{name='小明', age=23}

Java程序在对这个对象进行序列化的时候会通过反射尝试拿到Person 类的writeObject、readObject方法。如果用户给需要序列化的类添加了这两个方法,那么在序列化的时候就会通过反射调用它们,而不再走默认的方法。定义这两个方法可以定制一些特殊的需求,比如给某个字段加密,或者反序列化时根据条件修改某个字段等。

  • 4、readResolve
    在反序列化读恢复对象后,会检查用户是否定义了在readResolve方法,如果定义了,就用readResolve返回的对象来替代反序列化读获取的对象,此时反序列化获取的对象将被丢弃。
public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("writeObject");
        oos.writeUTF(name);
        oos.writeInt(age);
    }

    private void readObject(ObjectInputStream ois) throws IOException {
        System.out.println("readObject");
        this.name=ois.readUTF();
        this.age=ois.readInt();
        System.out.println(toString());
    }

    private Object readResolve(){
        System.out.println("readResolve");
        return new Person("张三",40);
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
 public static void main(String[] args) throws Exception {
        Person person1=new Person("小明",23);
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("text.out"));
        oos.writeObject(person1);

        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("text.out"));
        Person person2= (Person) ois.readObject();
        System.out.println("person2="+person2);

    }

打印结果:

writeObject
readObject
Person{name='小明', age=23}
readResolve
person2=Person{name='张三', age=40}

可以看到最终返回的对象是readResolve的结果,至于readObject反序列化获取的对象就被丢弃了。这里为了方便看执行流程重写了readObject,采用默认的序列化读取结果是相同的,都会被readResolve的结果替代。

那么这个方法有什么应用场景呢?还真有,防止反序列化破坏单例。因为通过反序列化恢复的对象,是不会走构造方法的,是Java内部帮我们构建的,这时如果我们对单例对象进行序列化,然后再反序列化获取,实际上就不在是同一个对象了,这也就破坏了单例模式。此时我们就可以通过readResolve来解决这个问题。

public class DCLSingleton implements Serializable{
	private static volatile DCLSingleton instance;
	private DCLSingleton() {}
	public static DCLSingleton getInstance() {
		if(instance==null) {
			synchronized (DCLSingleton.class) {
				if(instance==null) {
					instance=new DCLSingleton();
				}
			}
		}
		return instance;
	}
	
	//增加readResolve方法
	private Object readResolve() {
		return instance;
	}
}
  • 5、writeReplace
    如果在需要序列化的类中重写了这个方法,那么实际被序列化的对象,将会被替换为这个方法返回的结果。直接看代码。
public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
  
    private Object writeReplace(){
        System.out.println("writeReplace");
        return new Person("小李",26);
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
        Person person1=new Person("小明",23);
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("text.out"));
        oos.writeObject(person1);

        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("text.out"));
        Person person2= (Person) ois.readObject();
        System.out.println("person2="+person2);

执行结果:

writeReplace
person2=Person{name='小李', age=26}

writeReplace会在序列化写入方法之前执行,这个方法的返回值会作为真正的即将被序列化的对象。

  • 6、序列化Id的作用(serialVersionUID)
    java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,会把字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,可以进行反序列化,否则就会报序列化版本不一致的异常。
    如果我们没有手动指定serialVersionUID,Java序列化机制会根据编译时的class自动生成一个serialVersionUID,但是随着时间的推移,如果我们在本地类添加或减少了某个字段,此时本地实体类自动生成的serialVersionUID就会发生变化,进行反序列化的时候会出现serialVersionUID不一致,导致反序列化失败。解决方法就是手动指定一个serialVersionUID,这样即使后续类中新增了字段,serialVersionUID也不会变化。
  • 7、static和transient字段不会被序列化。

三、源码分析

这里对序列化的写入进行分析,读取是类似的。

首先看看ObjectOutputStream是如何构造的。

   public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        //bout 字节数据容器与out进行关联
        bout = new BlockDataOutputStream(out);
        ...
        //往数据容器中写入文件头信息
        writeStreamHeader();
        bout.setBlockDataMode(true);
      ...
    }

writeStreamHeader方法很简单,往字节容器中写入表示序列化的Magic Number以及版本号。

 protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
  }
    
//ObjectStreamConstants.java
public interface ObjectStreamConstants {
    /**
     * Magic number that is written to the stream header.
     */
    final static short STREAM_MAGIC = (short)0xaced;

    /**
     * Version number that is written to the stream header.
     */
    final static short STREAM_VERSION = 5;
    ....
    }

接着,序列化的写入是从ObjectOutputStream的writeObject方法开始的,跟进看看

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {//enableOverride通常是false
            writeObjectOverride(obj);
            return;
        }
        try {
           //通常会执行writeObject0方法
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
            // check for replacement object
            Object orig = obj;
            //即将被序列化的对象的Class对象
            Class<?> cl = obj.getClass();
            //下面创建了ObjectStreamClass对象desc,它用于描述cl这个Class对象
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }
              
            // 判断被序列化对象的类型,调用对应的写入方法
            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 {
            //如果这个对象不是上面几种类型就抛出NotSerializableException异常
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }

可以看出Serializable只是一个标记,引导方法的执行进入writeOrdinaryObject方法。

 private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared) throws IOException {
        try {
            desc.checkSerialize();
            //写入标志位(0x73),表示对象写入的开始
            bout.writeByte(TC_OBJECT);
            //写入类的描述信息(写类元信息的过程是一个递归的过程,先写自己的,在写父类的,直至没有超类)
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                //如果对象实现了Externalizable接口就走writeExternalData
                writeExternalData((Externalizable) obj);
            } else {
                //如果对象实现了Serializable接口就走writeSerialData
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

Externalizable接口也是序列化接口,它继承自Serializable,并且有两个抽象方法writeExternal,readExternal。如果实现Externalizable接口来实现序列化,我们需要重写writeExternal,readExternal,自己进行序列化的写和读。通常我们会实现Serializable接口,因此会进入writeSerialData方法。

 private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
    // 获取表示被序列化对象的数据的布局数组,自上而下,父类排在前面。
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            //如果这个对象有writeObject方法,那么就会通过反射调用writeObject
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;
                ...
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } 
            } else {
            //如果这个对象没有writeObject方法,那么就会调用默认的序列化写入方法defaultWriteFields
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

这里解释了为什么在被序列化对象中添加了writeObject方法,就会走writeObject进行序列化的写入,而不是默认的方法。接下来看看defaultWriteFields方法做了什么(这部分光看代码可能比较难理解,实际上很简单,最好打断点看)。

 private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }

        desc.checkDefaultSerialize();
		//下面几行代码是用来写入obj中的基本数据类型成员的,比如int
        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        // 获取obj中的基本数据类型的数据,并保存在primVals字节数组中
        desc.getPrimFieldValues(obj, primVals);
        //写入基本类型的成员
        bout.write(primVals, 0, primDataSize, false);
		//获取类的所有字段信息(包括基本类型和引用类型)
        ObjectStreamField[] fields = desc.getFields(false);
        //desc.getNumObjFields()获取类中引用类型字段的数量,然后根据这个数量窗口Object数组
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        //获取obj对象中所有的引用类型字段的值,将其保存到objVals中
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {
           ....
            try {
            // 对所有引用类型(Object)的字段的值递归调用writeObject0()方法进行写入
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }

这个方法主要干了两件事,1、将对象中的基本类型字段数据写入数据容器。2、遍历对象中的所用的引用类型成员,调用writeObject0进行引用类型的写入,这是一个递归的过程。
对于上面的defaultWriteFields很多字段不好用语言解释,这里给一个示例,帮助分析。对于下面的这个类:

public class Person implements Serializable {
    private static final long serialVersionUID = 2L;
    private String name;
    private int age;
    private Cat cat;

    public Person(String name, int age,Cat cat) {
        this.name = name;
        this.age = age;
        this.cat=cat;
    }
}

序列化写入走到defaultWriteFields后,断点如下:
深入理解Java序列化_第4张图片
上面说了ObjectStreamClass这个类是用来描述即将被序列化的对象的Class对象的。它里面保存了很多的信息,我们修改下Person 这个类,然后查看下ObjectStreamClass的数据是什么样子的。

//Base.java
public class Base implements Serializable {
    
    private String info;
    public Base(String info) {
        this.info = info;
    }
}

//Person.java
public class Person extends Base {
    private String name;
    private int age;
    private Cat cat;
    //下面这几个字段不会被序列化
    private transient int height;
    public static String desc="人类";
    private static final long serialVersionUID = 2L;

    public Person(String name, int age, Cat cat, int height) {
        super("info");
        this.name = name;
        this.age = age;
        this.cat = cat;
        this.height = height;
    }
}

深入理解Java序列化_第5张图片结合writeSerialData方法以及上面的截图可以知道两点:

  • 在序列化写入类中的字段时,先写父类的,接着写自己的成员字段。
  • 对于被static和transient修饰的字段不会被加入到fileds中,因此不会参与序列化。

那么问题来了?这个fields数组是如何创建的,以及如何排除static和transient修饰的字段的?

ObjectStreamClass这个类是用来描述被序列化的对象的,因此可以查看它的构建过程。
深入理解Java序列化_第6张图片
然后就是通过getDefaultSerialFields获取所有可以被序列化的字段(源码中的注释写的明明白白)。

/**
     * 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++) {
            //如果字段既不是static也不是transient修饰的
            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基础)