浅谈Java中的序列化

Java序列化

概念

  1. 什么是序列化和反序列化
    Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是一种将这些字节重建成一个对象的过程

  2. 什么情况下需要序列化
    a)当你想把的内存中的对象保存到一个文件中或者数据库中时候;
    b)当你想用套接字Socket在网络上传送对象的时候;
    c)当你想通过跨进程通信传输对象的时候;
    相当于在两端传输数据的协议,约定好怎么序列化然后怎么正确的反序列化,Java序列化机制就是为了解决这个问题而产生

  3. 如何实现序列化
    将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以序列化。

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

public class MyTest implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name = "LiaBin";
    private transient String home = "China";
    private static int age = 24;
    private static Test test1 = new Test(10);
    private Test test2 = new Test(10);
    public static void main(String[] args) {// 以下代码实现序列化
        MyTest myTest = new MyTest();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt")); // 序列化到硬盘文件
            // 输出流保存的文件名为my.out ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(myTest);
            oos.flush(); // 缓冲流
            oos.close(); // 关闭流

            ByteArrayOutputStream bo = new ByteArrayOutputStream(); //序列化到内存
            ObjectOutputStream oo = new ObjectOutputStream(bo);
            oo.writeObject(new MyTest());
            oo.flush();
            oo.close();
            byte[] bytes = bo.toByteArray();
            for (Byte b : bytes) {
                // 此时跟myout.txt中用16进制格式查看的内容是一样的,只是这里的表示是2进制
                System.out.print(b + " ");
            }
            System.out.println();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        myTest.fan(myTest);// 调用下面的 反序列化 代码
    }
    public void fan(MyTest myTest) { // 反序列的过程
        ObjectInputStream oin = null;// 局部变量必须要初始化
        try {
            oin = new ObjectInputStream(new FileInputStream("myout.txt"));
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        name = "BinJing";
        age = 1;
        test1.setLevel(20);//静态变量生效
        test2.setLevel(20);//无效
        MyTest mts = null;
        try {
            mts = (MyTest) oin.readObject();// 由Object对象向下转型为MyTest对象
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // false,说明是深拷贝,堆中不同对象
        System.out.println("myTest==mts: " + (myTest == mts));
        // name=LiaBin
        System.out.println("name=" + mts.name);
        // home=null transient修饰变量,不序列化,所以是默认值null
        System.out.println("home=" + mts.home);
        // age=1 序列化会忽略静态字段,因为他们不属于对象的任何状态,打印静态变量当然会去静态变量全局区查找,所以此时是修改之后的值1
        System.out.println("age=" + mts.age);
        // test1.level:20 test1是个静态属性不被系列化
        System.out.println("test1.level:" + mts.test1.getLevel());
        // test2.level:10 一起序列化,如果Test没实现Serializable接口,异常退出
        System.out.println("test2.level:" + mts.test2.getLevel());
    }
}
public class Test implements Serializable{
    private int level;
    public Test(int level) {
        super();
        this.level = level;
    }
    public int getLevel() {
        return level;
    }
    public void setLevel(int level) {
        this.level = level;
    }
}

总结一下:

  1. 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
  2. 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化,注意是深拷贝而不是浅拷贝;
  3. static,transient后的变量不能被序列化;

serialVersionUID作用

如果没有设置这个值,你在序列化一个对象之后,改动了该类的字段或者方法名之类的,那如果你再反序列化想取出之前的那个对象时就可能会抛出异常,因为你改动了类中间的信息,serialVersionUID是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,当修改后的类去反序列化的时候发现该类的serialVersionUID值和之前保存在问价中的serialVersionUID值不一致,所以就会抛出异常。
而显示的设置serialVersionUID值就可以保证版本的兼容性,如果你在类中写上了这个值,就算类变动了,它反序列化的时候也能和文件中的原值匹配上。而新增的值则会设置成null,删除的值则不会显示
所以如果没设置serialVersionUID的话,类如果发生变动,那么最新序列化的serialVersionUID该值根据最新属性值计算得来,所以跟文件中的序列化值就对应不上了,反序列化失败。
注意方法的改变不影响,因为序列化只序列化属性,跟方法无关
如果你不在类中声明SerialVersionUID的话,Java会在运行时替你生成一个,不过这个生成的过程会受到类元数据包括字段数,字段类型,字段的访问限制符,类实现的接口等因素的影响.
Java的序列化机制会替你生成一个的。它的生成机制受很多因素的影响,包括类中的字段,还有访问限制符,类实现的接口,甚至是不同的编译器实现,任何类的修改或者使用了不同的编译器生成SerialVersionUID都各不相同,很可能最终导致重新加载序列化的数据中止

Eclipse会根据类元数据计算得出serialVersionUID值

private static final long serialVersionUID = 7539660831609822000L; //默认根据所有的属性值计算出来的,如果没定义serialVersionUID,那么默认使用的就是通过所有属性值计算出来的该值

一般情况下,值直接申明为1L就行了

private static final long serialVersionUID = 1L;

举个例子

public class Test implements Serializable {
    private String home = "China";
    //此时默认的serialVersionUID是-7477377630168477287L
    public static void main(String args[]) {
        Test myTest = new Test();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt"));
            oos.writeObject(myTest);
            oos.flush();
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        fan();
    }
    public static void fan() {
        ObjectInputStream oin = null;
        try {
            oin = new ObjectInputStream(new FileInputStream("myout.txt"));
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        Test mts = null;
        try {
            mts = (Test) oin.readObject();// 由Object对象向下转型为Test对象
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("home=" + mts.home);
    }
}

此时没错,没有定义serialVersionUID值,那么按照默认序列化行为
后续开发中,新添了一个新的age属性,然后直接从文件中反序列化,代码如下

public class Test implements Serializable {
    private String home = "China";
    //此时默认的serialVersionUID是-7477377630168477287L
    private int age = 20;
    //添加age属性之后,默认的serialVersionUID值是-3342077822325805408
    public static void main(String args[]) {
        fan();
    }
    public static void fan() {
        ObjectInputStream oin = null;
        try {
            oin = new ObjectInputStream(new FileInputStream("myout.txt"));
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        Test mts = null;
        try {
            mts = (Test) oin.readObject();// 由Object对象向下转型为Test对象
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("home=" + mts.home);
    }
}

那么此时报异常,因为两者的serialVersionUID不一致

java.io.InvalidClassException: Test; local class incompatible: stream classdesc serialVersionUID = -3342077822325805408, local class serialVersionUID = 7477377630168477287

解决方案:添加如下行即可

private static final long serialVersionUID = 1L;

自定义序列化

当进行序列化的时候:
1. 首先JVM会先调用writeReplace方法,在这个阶段,我们可以进行张冠李戴,将需要进行序列化的对象换成我们指定的对象.
2. 跟着JVM将调用writeObject方法,来将对象中的属性一个个进行序列化,我们可以在这个方法中控制住哪些属性需要序列化.
当反序列化的时候:
1. JVM会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,反序列化回来.
2. 然后在readResolve方法中,我们也可以指定JVM返回我们特定的对象(不是刚刚序列化回来的对象).
注意到在writeReplace和readResolve,我们可以严格控制singleton的对象,在同一个JVM中完完全全只有唯一的对象,控制不让singleton对象产生副本.
注:writeReplace调用在writeObject前;readResolve调用在readObject之后

单例的序列化

为了使一个单例类变成可序列化的,仅仅在声明中添加“implements Serializable”是不够的。因为一个串行化的对象在每次反序列化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入readResolve方法

public class AnoTest implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final AnoTest INSTANCE = new AnoTest();
    private AnoTest() {
    }
    public static AnoTest getInstance() {
        return INSTANCE;
    }
    //单例序列化,定义该方法
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
    public static void main(String args[]) {
        AnoTest anoTest = getInstance();// 此时把堆中的AnoTest对象序列化,但是INSTANCE是个静态变量,所以不会重复序列化
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myout.txt"));
            oos.writeObject(anoTest);
            oos.flush();
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        fan();
    }
    public static void fan() {
        ObjectInputStream oin = null;
        try {
            oin = new ObjectInputStream(new FileInputStream("myout.txt"));
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
        AnoTest mts = null;
        try {
            mts = (AnoTest) oin.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("mts==INSTANCE ? " + (mts == INSTANCE)); //定义readResolve方法,返回true
        //System.out.println("mts==INSTANCE ? " + (mts == INSTANCE)); //注释readResolve方法,返回false
    }
}

这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。

子类/父类序列化行为

参考Java自定义序列化行为解析
一个可序列化的类继承自一个非序列化的有状态超类

class AbstractSerializeDemo {     
    private int x, y;     
    public void init(int x, int y) {     
        this.x = x;     
        this.y = y;     
    }      
    public int getX() {     
        return x;     
    }     
    public int getY() {     
        return y;     
    }     
    public void printXY() {     
        System.out.println("x:" + x + ";y:" + y);     
    }     
}     

public class SerializeDemo extends AbstractSerializeDemo implements Serializable {     
    private int z;     
    public SerializeDemo() {     
        super.init(10, 50);     
        z = 100;     
    }     
    public void printZ() {     
        super.printXY();     
        System.out.println("z:" + z);     
    }     
    public static void main(String[] args) throws IOException, ClassNotFoundException {     
        ByteArrayOutputStream bos = new ByteArrayOutputStream();     
        ObjectOutputStream out = new ObjectOutputStream(bos);     
        SerializeDemo sd = new SerializeDemo();     
        sd.printZ();     
        out.writeObject(sd);     
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));     
        SerializeDemo sd2 = (SerializeDemo) in.readObject();     
        sd2.printZ();     
    }     
}  

输出结果:
x:10;y:50
z:100
x:0;y:0
z:100
子类的值域保留下来了,但是超类的值域丢失了,这对jvm来说是正常的,因为超类不可序列化;
为了解决这个问题,只能自定义序列化行为,具体做法是在SerializeDemo里加入以下代码:

  private void writeObject(ObjectOutputStream os) throws IOException {     
      os.defaultWriteObject();//java对象序列化默认操作     
      os.writeInt(getX());     
      os.writeInt(getY());     
  }     
  private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {     
      is.defaultReadObject();//java对象反序列化默认操作     
      int x=is.readInt();     
      int y=is.readInt();     
      super.init(x,y);     
  }  

writeObject和readObject方法为JVM会在序列化和反序列化java对象时会分别调用的两个方法,修饰符都是private
我们在序列化的默认动作之后将超类里的两个值域x和y也写入object流;与之对应在反序列化的默认操作之后读入x和y两个值,然后调用超类的初始化方法
再次执行程序之后的输出为:
x:10;y:50
z:100
x:10;y:50
z:100

Android中的序列化

Android中的新的序列化机制

在Android系统中,针对内存受限的移动设备,因此对性能要求更高,Android系统采用了新的IPC(进程间通信)机制,要求使用性能更出色的对象传输方式。因此Parcel类被设计出来,其定位就是轻量级的高效的对象序列化和反序列化机制。Parcel的序列化和反序列化的读写全是在内存中进行,所以效率比JAVA序列化中使用外部存储器会高很多
Android Serializable与Parcelable原理与区别 这里可以看到Parcelable的原理,读写全在内存,速度快

Android中启动一个activity都是通过AMS来进行的,我们知道AMS在系统进程中,所以此时跨进程通信的话,如果需要传输对象,那么就需要序列化了

如何选择

  1. Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
    如activity间传输数据
  2. Serializable可将数据持久化方便保存,所以在需要保存在本地硬盘文件或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不能很好的保存数据的持续性在外界有变化的情况下,不推荐使用Parcelable进行数据持久化

你可能感兴趣的:(Java基础)