java序列化详解

什么是序列化

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。

对于一个实体类,不想将所有的属性都进行序列化,有专门的关键字 transient:

private transient String name;

当对该类序列化时,会自动忽略被 transient 修饰的属性。

serialVersionUID

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

具体的序列化过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

serialVersionUID有两种显示的生成方式:
一是默认的1L(即Eclipse自动生成的),比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;

当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

如下:

package serializable;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable{


    /**
     * serialVersionUID,序列化版本,当不指定时,会自动生成,当文件不发生变化(类名,方法名,空格,换行,注释)
     * 其值也不会变,在读取序列化对象时会比较序列化id是否一致,不一致时会出错
     */
    private static final long serialVersionUID = 123456789L;
    private int id;
    private String name;
    public String toString() {
        return "Person: " + id + " " + name;
    }
}

单个对象的序列化和反序列化

序列化单个对象

 public void outPut(Person person) {
    try {

        System.out.println("Person Serial" + person);
        FileOutputStream fos = new FileOutputStream("Person.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(person);
        oos.flush();
        oos.close();
    } catch (Exception e) {
       e.printStackTrace();
       log.error("单个对象序列化错误",e);
    }
    }

    /***
     * 序列化单个对象
     */
    @Test
    public void outPutObject() {
    Person person = new Person(1234, "wang");
    outPut(person);
    }

反序列化单个对象

 /**
     * 反序列化单个对象
     */
    @Test
    public void inPutobject() {
    try {
        FileInputStream fis = new FileInputStream("Person.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        System.out.println(fis.available()+"==="+fis.getChannel().position());
        Person person = (Person) ois.readObject();
        System.out.println(fis.available()+"==="+fis.getChannel().position());
        ois.close();
        System.out.println(person);
    } catch (Exception e) {
        e.printStackTrace();
    }

    }

多个对象的序列化和反序列化

想要实现多个对象的序列化有两种方法,但是想要理解这两种方法,我们首先要了解java的ObjectOutputStream和ObjectInputStream源码来帮助理解

public ObjectOutputStream(OutputStream out) throws IOException {  
        verifySubclass();  
        bout = new BlockDataOutputStream(out);  
        handles = new HandleTable(10, (float) 3.00);  
        subs = new ReplaceTable(10, (float) 3.00);  
        enableOverride = false;  
        writeStreamHeader();  
        bout.setBlockDataMode(true);  
        if (extendedDebugInfo) {  
            debugInfoStack = new DebugTraceInfoStack();  
        } else {  
            debugInfoStack = null;  
        }  
    }  
public ObjectInputStream(InputStream in) throws IOException {  
        verifySubclass();  
        bin = new BlockDataInputStream(in);  
        handles = new HandleTable(10);  
        vlist = new ValidationList();  
        enableOverride = false;  
        readStreamHeader();  
        bin.setBlockDataMode(true);  
    }  

不难发现在序列化new的时候调用了writeStreamHeader()方法写入了4个字节的StreamHeader

但是呢在读取的时候只有读取一次StreamHeader的,所以在多个对象的序列化与反序列化时如果直接进行肯定会报错
《java序列化详解》
如上图所示,如果我们在序列化时不做任何处理就直接写入文件会形成第一种结果,文件内容由一段4个字节StreamHeader和对象的字节码交替组成,那么我们在读取时可以跳过对4个字节StreamHeader的读取而直接读取对象的字节码即可,这是一种解决方法

还有一种解决方法就是在写入对象时丢掉4个字节StreamHeader,直接写入对象的字节码,文件的内容就会如上图的第二种结果

多个对象的序列化方法1:在反序列化时解决

  /**
     * 直接存储多个对象
     * @param persons
     */
    public void outPutss(List<Person> persons) {
    FileOutputStream fo=null;
    ObjectOutputStream oos = null;
    File file = new File("Personss.txt");
    boolean isexist = false;// 定义一个用来判断文件是否需要截掉头aced 0005的
    try {
        if (file.exists()) { // 文件是否存在
        isexist = true;
        }
        for (Person person : persons) {
        long pos = 0;
        if (isexist) {
            // true表示是是否追加
            fo = new FileOutputStream(file, true);
            oos = new ObjectOutputStream(fo);
            oos.writeObject(person);// 进行序列化

        } else {
            // 文件不存在
            file.createNewFile();
            isexist = true;
            fo = new FileOutputStream(file);
            oos = new ObjectOutputStream(fo);
            oos.writeObject(person);// 进行序列化
            System.out.println("首次对象序列化成功!");
        }

        }
        oos.flush();
        oos.close();
        fo.close();


    } catch (Exception e) {
        e.printStackTrace();
    }
    }

    /**
     * 序列化多个对象
     */
    @Test
    public void outPutObjectss() {
    List<Person> persons = new ArrayList<Person>();
    persons.add(new Person(5678, "wang") );
    persons.add( new Person(4567, "li"));
    persons.add(new Person(1234, "zhang"));


    outPutss(persons);

    }

直接进行读取会报以下错误:

 wang
java.io.StreamCorruptedException: invalid type code: AC
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1601)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
    at serializable.PersonTest.inPutObjects(PersonTest.java:134)

解决办法:不读取每个对象的4个字节的StreamHeader,跳过4个字节,直接读取对象

/**
 * 反序列化多个对象
     */
    @Test
    public void inPutObjectss() {
    File file = new File("Personss.txt");
    if (file.exists()) {
        ObjectInputStream ois;
        try {
        FileInputStream fn = new FileInputStream(file);
        ois = new ObjectInputStream(fn);
        while (fn.available() > 0) {// 代表文件还有内容
            Person p = (Person) ois.readObject();// 从流中读取对象
            fn.getChannel().position(fn.getChannel().position()+4);
            System.out.println(p.getName());
        }

        ois.close();
        fn.close();
        // 注意在循环外面关闭
        } catch (Exception e1) {
        e1.printStackTrace();
        }
    }
    }

多个对象的序列化方法2:在序列化时解决

序列化时跳过4个字节的StreamHeader

public void outPuts(List<Person> persons) {
    FileOutputStream fo=null;
    ObjectOutputStream oos = null;
    File file = new File("Persons.txt");
    boolean isexist = false;// 定义一个用来判断文件是否需要截掉头aced 0005的
    try {
        if (file.exists()) { // 文件是否存在
        isexist = true;
        }
        for (Person person : persons) {
        long pos = 0;
        if (isexist) {
            // true表示是是否追加
            fo = new FileOutputStream(file, true);
            oos = new ObjectOutputStream(fo);
            pos = fo.getChannel().position() - 4;// 追加的时候去掉头部aced 0005的    
            fo.getChannel().truncate(pos);
            oos.writeObject(person);// 进行序列化

        } else {
            // 文件不存在
            file.createNewFile();
            isexist = true;
            fo = new FileOutputStream(file);
            oos = new ObjectOutputStream(fo);
            oos.writeObject(person);// 进行序列化
            System.out.println("首次对象序列化成功!");
        }

        }
        oos.flush();
        oos.close();
        fo.close();


    } catch (Exception e) {
        e.printStackTrace();
    }
    }

    /**
     * 序列化多个对象
     */
    @Test
    public void outPutObjects() {
    List<Person> persons = new ArrayList<Person>();
    persons.add(new Person(5678, "wang") );
    persons.add( new Person(4567, "li"));
    persons.add(new Person(1234, "zhang"));


    outPuts(persons);

    }

反序列化

 /**
     * 反序列化多个对象
     */
    @Test
    public void inPutObjects() {
    File file = new File("Persons.txt");
    if (file.exists()) {
        ObjectInputStream ois;
        try {
        FileInputStream fn = new FileInputStream(file);
        ois = new ObjectInputStream(fn);
        while (fn.available() > 0) {// 代表文件还有内容
            Person p = (Person) ois.readObject();// 从流中读取对象
            System.out.println(p.getName());
        }

        ois.close();
        fn.close();
        // 注意在循环外面关闭
        } catch (Exception e1) {
        e1.printStackTrace();
        }
    }
    }

闲言碎语

java的序列化远不止如此,本章篇幅有限就先写到这里,下一篇文章我们将更深入的研究它

参考

java类中serialversionuid 作用 是什么?举个例子说明

Java对象序列化文件追加对象的问题,以及Java的读取多个对象的问题解决方法。

多对象序列化为什么-4个字节

Java中对象序列化与反序列化

点赞

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注