java序列化/反序列化详解

一、序列化理解

1、什么是序列化?

        主要用于存储对象状态为另一种通用格式,比如存储为二进制、xml、json等等,把对象转换成这种格式就叫序列化,而反序列化通常是从这种格式转换回来。

序列化过程: 是指把一个Java对象变成二进制内容,实质上就是一个byte[]数组。 因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程(IO),这样,就相当于把Java对象存储到文件或者通过网络传输出去了。

反序列化过程: 把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

2、为什么使用序列化?

      使用序列化主要是因为跨平台和对象存储的需求,因为网络上只允许字符串或者二进制格式,而文件需要使用二进制流格式,如果想把一个内存中的对象存储下来就必须使用序列化转换为xml(字符串)、json(字符串)或二进制(流)

以下是一些使用序列化的示例:

  • -以面向对象的方式将数据存储到磁盘上的文件,例如,Redis存储Student对象的列表。
    -将程序的状态保存在磁盘上,例如,保存游戏状态。
    -通过网络以表单对象形式发送数据,例如,在聊天应用程序中以对象形式发送消息。

3、实现两个程序之间内存数据交互|内存共享?

      两个程序在内存空间是完全独立的,默认情况下是互相不能访问的,那么两个程序如何进行复制数据(列表,字典等)的交换?

      因为硬盘上只能存储字符串或二进制形式的数据,不能直接将列表或字典写入硬盘。故一个程序通过序列化将数据变成字符串或二进制写入到硬盘,其他程序通过反序列化将二进制或字符串数据成原来的数据读到内存进行操作。

4、存储函数的执行状态或存储整个类

例如玩游戏时,正常下状态是在内存中动态生成。但是当电脑宕机,开机时会恢复到原来状态。因为游戏会实时将状态保存到硬盘,每隔10秒保存最新状态

5、两个机器进行通信,socket只能传递二进制或字符串,如何将服务器端产生的字典传给客户端?

首先必须将其变成字符串,传到客户端,客户端在反序列化读数据。即在两个独立的服务器之间完成内存的共享例如,nosql。例如快递一样,A包装快递(即序列化),通过车运输(即socket),B收到拆开快递(即反序列化)。

6、json与pickle的区别:

  • pickle只能在python中使用,json是所有语言都支持一种数据接口格式。故多个python程序可以通过pickle共享数据。当一个程序时java,一个程序时python,想要进行内存数据交互只能通过json
  • pickle不仅可以dump常规数据类型(列表,字典),他可以序列化类,对象等。json只能序列化常规的数据类型(列表,字典,集合)。因为不同语言类的语法格式不同。
  • pickle格式化后的数据,人用眼看不出来,json格式化后的数据,人能看出。

json格式,一种标准化的数据格式,将数据json化类似于序列化

二、什么是Java序列化

        Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
        使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。

三、Java序列化的应用场景

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;

在很多应用场景中都需要用到序列化和反序列化,以保存当前对象的状态信息或者用于传输,例如,在常见的web服务中的持久化session就是一个很好的例子,一般session都是入驻内存的,当服务器异常宕机,内存里的session因为掉电而搽除,当我们设置了session持久化特性时,就会把session保存在硬盘上,这就是序列化,等服务器重启后有可以读取硬盘上这个session文件,还原session对象,这就是反序列化。

另一个需要序列化的场景就是进程间的远程通信,远程通信是以二进制字节流传输的,当需要传输对象的时候,首先在发送端需要将对象序列化为二进制形式方便网络传输,然后在接收端将二进制形式反序列化为java对象。比如早期的RMI,甚至现在流行的RPC通信都用到了序列化和反序列化。

/**
    不实现Serializable时
     java.io.NotSerializableException: TEst.Box
     at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
     at TEst.SerializableTest.main(SerializableTest.java:19)
 **/
public class Box implements Serializable {

    private int width;
    private int height;

    public Box(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public String toString() {
        return "Child{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}

public class SerializableTest {

    public static void main(String[] args) {
        try {
            File file = new File("a.out");
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            Box box = new Box(10,20);
            objectOutputStream.writeObject(box);
            objectOutputStream.close();


            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream in = new ObjectInputStream(fis);
            Box newBox = (Box)in.readObject();
            in.close();
            System.out.println(newBox.toString());

            System.out.println(file.getAbsolutePath());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//Child{width=10, height=20}
/Users/lihe/workspace/SpringCrane/a.out
其中new File();表示在该项目的目录下,不是mac目录

四、基本知识点

  • Serializable接口
    对于任何需要被序列化的对象,都必须要实现接口Serializable,它只是一个标识接口,本身没有任何成员,只是用来标识说明当前的实现类的对象可以被序列化.
    如果父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
    如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。
  • 对象的读写
    Java类中对象的序列化工作是通过ObjectOutputStream和ObjectInputStream来完成的
    使用readObject()|writeObject()方法对对象进行读写操作;对于基本类型,可以使用readInt()|writeInt(), readDouble()|writeDouble()等类似的接口进行读写。
  • transient关键字
    如果某实例变量不能或不应该被序列化,就把它标记为transient的变量,这样序列化程序就会把它跳过。
    transient的引用变量会以null返回,基本数据类型会以相应的默认值返回
    (例如:引用类型没有实现Serializable,或者动态数据只可以在执行时求出而不能或不必存储)

五、注意事项

  • 读取对象的顺序必须与写入的顺序相同;
  • 如果有不能被序列化的对象,执行期间就会抛出NotSerializableException异常;
  • 序列化时,只对对象的状态进行保存,而不管对象的方法;
  • 静态变量不会被序列化,因为所有的对象共享同一份静态变量的值;
  • 如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存还原,而且会是递归的方式。 (序列化程序会将对象版图上的所有东西储存下来,这样才能让该对象恢复到原来的状态
  • 如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常;
    因为反序列化时会恢复原有子对象的状态,而父类的成员变量也是原有子对象的一部分。由于父类没有实现序列化接口,即使没有显示调用,也会默认执行父类的无参构造函数使变量初始化;

1、静态变量为什么不能序列化?

静态成员属于类级别的,所以不能序列化,这里的不能序列化的意思,是序列化信息中不包含这个静态成员域

public class Box implements Serializable {

    private int width;
    private int height;
    private static int count = 0;

    public Box(int width, int height) {
        this.width = width;
        this.height = height;
        count++;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public static int getCount() {
        return count;
    }

    public static void setCount(int count) {
        Box.count = count;
    }

    @Override
    public String toString() {
        return "Box{" +
                "width=" + width +
                ", height=" + height +
                ", count=" + count +
                '}';
    }
}
public class SerializableTest {

    public static void main(String[] args) {
        try {
            File file = new File("a.out");
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            Box box = new Box(10,20);
            objectOutputStream.writeObject(box);
            objectOutputStream.close();


            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream in = new ObjectInputStream(fis);
            Box newBox = (Box)in.readObject();
            in.close();
            System.out.println(newBox.toString());

            System.out.println(file.getAbsolutePath());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//Box{width=10, height=20, count=1}
public class TestBox {

    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("a.out");
            ObjectInputStream in = new ObjectInputStream(fis);
            Box newBox = (Box)in.readObject();
            in.close();
            System.out.println(newBox.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
//Box{width=10, height=20, count=0}

2、总结:

      类SerializableTest 的运行结果显示count=1,似乎被序列化了,但是类TestBox的运行结果显示count=0并未被序列化。
       ”序列化保存的是对象的状态,静态变量数以类的状态,因此序列化并不保存静态变量。
      这里的不能序列化的意思,是序列化信息中不包含这个静态成员域
      SerializableTest 测试成功,是因为都在同一个机器(而且是同一个进程),因为这个jvm已经把count加载进来了,所以你获取的是加载好的count,如果你是传到另一台机器或者你关掉程序重写写个程序读入test.obj,此时因为别的机器或新的进程是重新加载count的,所以count信息就是初始时的信息

七、深入理解

1、序列化ID的问题

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

2、序列化存储规则

      Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用;
      序列化到同一个文件时,如第二次修改了相同对象属性值再次保存时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。

你可能感兴趣的:(Java基础详解,java,序列化,反序列化)