Serializable的那些事

1.Serializable是什么

serializable是一个对象序列化的接口,一个类只有实现了Serializable接口,那么由它所创建的对象才可以被序列化。那什么是序列化呢?

通常我们对于多种复合的属性会用一个对象去包装,就是一个对象会拥有多种成员属性,而这些属性可能是基本数据类型,也可能是其他的对象。而我们在应用内存中操作这些属性的时候,可以直接通过这个对象去获取,因为这些是在虚拟机堆中给我们分配了存储空间。但当我们想把这些属性进行网络传输,或者进行本地持久化存储到磁盘时,我们肯定是不能直接用这些对象去进行传输,就像文件上传下载是通过数据流操作一样,对象的存储也要通过流。而存储完之后去重新获取并重新解析成我们所需要的对象结构,这个就需要通过序列化。
对于系统底层,数据的传输是通过字节序的形式传递,序列化可以认为是把对象转化成字节序的过程,反序列化则是把字节序转化成对象。也可以认为序列化反序列化是把数据结构对象转化成二进制串的的正反过程

序列化的常用的有两种SerializableParcelable。其中Parcelable是android独有的,android系统的跨进程通信或者说底层Binder的通信就是依赖于Parcelable数据的传输。而Serializable则是Java中的。

序列化只针对于类中的成员变量,不能序列化方法

2.Serializable的使用

Serializable使用很简单,只需要声明的类实现Serializable接口就可以了,其中Serializable接口是一个空接口。Serializable相当于对类进行标记,被标记的类会被系统认为是可以被序列化的。

Serializable序列化的类中如果有非基本数据类型的类成员变量时,如果要使用到这个成员变量的属性,那么这个成员变量所对应的类也需要实现Serializable接口,否则会抛出NotSerializableException异常。如果不关心这个属性,则可以声明这个成员变量为transient,就是这个变量不参与序列化,我们反序列化去获取时这个值就为null

静态变量是无法被序列化的

如果一个类可以被序列化,那么它的子类也是可以被序列化的(不包含子类的非可序列化成员变量)

3.序列化的基本操作

序列化做持久化存储或者传输,都需要先转成byte[]数据,常用的对象输入输出流是ObjectInputStreamObjectOutputStream
比如定义一对简单的对象转换操作可以这么写

  public static byte[] serialData(Object obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            return bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static <T> T decSerialData(byte[] data, Class<T> type) {
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(data);
            ObjectInputStream ois = new ObjectInputStream(bis);
            Object object = ois.readObject();
            return (T) object;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

然后我们新建一个测试的类

class B implements Serializable {
    private  A a;
    private String str;
    public B(String str, A a) {
        this(str);
        this.a = a;
    }
}

class A implements Serializable {
    private String str;
    public A(String str) {
        this.str = str;
    }
}


    public static void main(String[] args) {
        B b = new B("BName", new A("AinnerName"));
        byte[] data = serialData(b);
        B res = decSerialData(data, B.class);
        System.out.println(res);
	}

这里的toString方法没有粘出来。输出结果是

B{a=A{str='AinnerName'}, str='BName'}

这个就是一个基本的序列化和反序列化操作了,这里用的是内存的方式模拟,也可以用FileOutputStream把字节流转成文件保存在本地,然后再去读取,结果是一样的

4.serialVersionUID的作用

当我们定义一个类是可序列化的类后,也就是实现Serializable接口后,一般我们会选择性的分配一个serialVersionUID,那么这个id有什么作用呢,不写有没有问题 ?

其实这个id和Serializable其实差不多,都是起到一个标示的作用,可以理解成和数据库的版本号类似。如果你的数据操作不涉及本地持久化存储和网络数据传输,而仅仅是单纯定义的用来应用内传输的,那么可以不声明这个值,因为这个系统会根据当期的类的数据结构自动生成一个serialVersionUID。
而当我们的数据结构发生改变,系统生成的这个id的值可能也会跟着发生变化,那么序列化过程就会id不匹配导致序列化失败,这也是因为安全性的考量,就像数据库升级,结构变化,原来的数据库可能就不能使用了,需要做数据库迁移,而序列化没有迁移这么一说,失败就是失败。

serialVersionUID没有特别的要求,不一定非要根据插件随机生成的值,你声明成1也没什么问题,因为这个就是一个标示的作用,但最好各个序列化声明的类的id保持不一样

很简单验证方法,我们定义一对磁盘的写入和读取的方法

String path = "xxx磁盘位置";
 public static void writeObjToPc(Object object) {
        try {
            File file = new File(path);
            byte[] data = serialData(object);
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(data);
            fos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static <T> T readObjectFromPc(Class<T> type) {
        try {
            FileInputStream fileInputStream = new FileInputStream(path);
            ObjectInputStream ois = new ObjectInputStream(fileInputStream);
            Object object = ois.readObject();
            return (T) object;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

然后给上面的对象添加一个 serialVersionUID = 1 ,然后把这个对象写入磁盘。接着把这个serialVersionUID改成2。然后使用流读取出来进行。我们会发现抛出了一个异常

java.io.InvalidClassException: com.xx.xxx.B; local class incompatible: 
				stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

说明不同serialVersionUID版本的类是不能被序列化进行转换的

5.ObjectStream的一点思考

ObjectStream支持多个属性同时写入,读取的时候严格按照顺序读取就可以获取到正确的属性值,比如

  public static void testMulWrite1() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeInt(1);
            oos.writeBoolean(true);
            oos.writeFloat(1.5f);
            oos.flush();

            byte[] data = bos.toByteArray();

            ByteArrayInputStream bis = new ByteArrayInputStream(data);
            ObjectInputStream ois = new ObjectInputStream(bis);
            int v1 = ois.readInt();
            boolean v2 = ois.readBoolean();
            float v3 = ois.readFloat();
            
            System.out.println("values ->> " + v1 + " , " + v2 + " , " + v3);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

分别按顺序写入int,boolean和float 。读取时候按照写入的顺序读取,输出结果是

values ->> 1 , true , 1.5

那么对于对象呢,如果我们同时写入多个同一个对象,然后按照顺序读取是否是同一个对象呢

  private static void testMulWrite() {
        try {
            B b = new B("BName", new A("AinnerName"));

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            
            oos.writeObject(b);
            oos.writeObject(b);

            byte[] data = bos.toByteArray();

            ByteArrayInputStream bis = new ByteArrayInputStream(data);
            ObjectInputStream ois = new ObjectInputStream(bis);

            Object object = ois.readObject();
            Object object1 = ois.readObject();
            System.out.println("obj origin 1   " + object.toString());
            System.out.println("obj origin 2   " + object1.toString());
            System.out.println("result matched  " + (object == object1));
            System.out.println("origin matched  " + (b == object1));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这里我连续写入了两次同一个对象,然后打印读取时候每个对象的内容,以及读取后两个对象的地址是否相同和原始对象是否是同一个等,输出结果是

obj origin 1   B{a=A{str='AinnerName'}, str='BName'}
obj origin 2   B{a=A{str='AinnerName'}, str='BName'}
result matched  true
origin matched  false

可以看出,连续写入同一个对象,然后再依次读取,这两个对象的地址是相同的。而且很显然和原始的对象是不相干的两个对象。
或者这个不太明显,我们再把得到的结果进行处理一下

	 B b1 = (B) object;
	 b1.setStr("666");
    System.out.println("b1 result 1   " + b1.toString());

	B b2 = (B) object1;
	System.out.println("b2 result 2   " + b2.toString());

把上面读取的两个对象分别转型,然后给第一个对象修改下参数,第二个对象保持不变,然后再分别打印此时的内容,输出结果是

b1 result 1   B{a=A{str='AinnerName'}, str='666'}
b2 result 2   B{a=A{str='AinnerName'}, str='666'}

可以看出第二个对象的值也变了,也就是这个读取的两个对象其实是同一个

如果我们在write第二个对象前修改下原始数据再写入,就是上面插入一行代码

   ObjectOutputStream oos = new ObjectOutputStream(bos);	
   ......	
   oos.writeObject(b);
   b.setStr("777");
   oos.writeObject(b);
   ......

两次之间插入一条修改数据,写入的对象仍然是同一个

obj origin 1   B{a=A{str='AinnerName'}, str='BName'}
obj origin 2   B{a=A{str='AinnerName'}, str='BName'}
result matched  true
origin matched  false
b1 result 1   B{a=A{str='AinnerName'}, str='BName'}
b2 result 2   B{a=A{str='AinnerName'}, str='BName'}

可以看出这里的修改并没有生效,也就是只有第一次的修改是有效的,那么该如何解决这个问题呢
对应ObjectStream这里有两种解决方式
第一种

   ObjectOutputStream oos = new ObjectOutputStream(bos);
   ......
   oos.writeObject(b);
   b.setStr("777");
   oos.writeUnshared(b);
   ......

第一种是使用writeUnshared方法写入该对象,表示非共享写入,也就是说新写入的对象并不指向原来的对象地址,或者可能是原来对象的拷贝后新建的一个全新的对象,也就是会写入两个独立的对象,当然后面获取的时候因为非共享写入,指向的对象也就不是同一个了;

obj origin 1   B{a=A{str='AinnerName'}, str='BName'}
obj origin 2   B{a=A{str='AinnerName'}, str='777'}
result matched  false
origin matched  false
b1 result 1   B{a=A{str='AinnerName'}, str='BName'}
b2 result 2   B{a=A{str='AinnerName'}, str='777'}

第二种

   ObjectOutputStream oos = new ObjectOutputStream(bos);
   ......
   oos.writeObject(b);
   b.setStr("777");
   oos.reset();
   oos.writeObject(b);
   ......

很显然这是一个流的重置,会刷新流中的数据状态,也就是说如果写入用到了一些缓存内存指,这个会失效,每次都是最新的 (这是我个人的理解,可能不对)

我这里比较一下最终写入到ByteArrayOutputStream中最终转成的byte[]的长度大小。
也就是上面三个修改方法修改同一个类对象然后多次写入同一个类对象的最终生成的字节长度。
1 . writeObject 这个方法修改属性无效,字节长度是171
2 . writeUnshared 这个是非共享写入,字节长度是 183
3 . reset 这个是重置流后写入,字节长度是327

可以看出2和3方法虽然最终都实现了效果,但3最终生成的字节长度远大于2的大小;而方法2和方法1中的大小相差不大。那么可以猜想,writeUnshared方法可能用到了原数据中的部分缓存或者同数据缓存,只是生成的的地址指向不一样,为了验证这个我们再使用writeUnshared方法,然后打印上面内部类的指向地址,增加这么一个打印

   B b1 = (B) object;
   System.out.println("b1 result 1   " + b1.toString());

   B b2 = (B) object1;
  System.out.println("b2 result 2   " + b2.toString());

  System.out.println("b3 result 2   " + (b1.getA() == b2.getA()));

打印读取后的两个类的内部类,输出结果是

b1 result 1   B{a=A{str='AinnerName'}, str='BName'}
b2 result 2   B{a=A{str='AinnerName'}, str='777'}
b3 result 2   true

也就是说这个内部类居然指向同一个地址,那么我们再修改写入的方法,修改内部类的方法

  ObjectOutputStream oos = new ObjectOutputStream(bos);
  ......
  oos.writeObject(b);
  b.setStr("777");
  b.getA().str = "666";
  oos.writeUnshared(b);
  ......

输出结果是

b1 result 1   B{a=A{str='AinnerName'}, str='BName'}
b2 result 2   B{a=A{str='AinnerName'}, str='777'}
b3 result 2   true

发现对内部类的属性修改也是失效的,而我们换用reset方法修改则是成功的,这个就不贴结果了

所以对应同一对象的多次修改写入,如果是针对基本数据类型的,那么使用writeUnshared是最优解。如果涉及到内部类的修改,那么则使用reset方法可以实现

6.Externalizable

当Serializable不满足我们的需求,或者说我们相对序列化进行一些拓展时,可以使用Externalizable

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

Externalizable继承了Serializable接口,并且定义两个读写的方法,使用方法很简单

class C implements Externalizable {
    private String str;
    private int intValue;
    private A a;
    public C() {
    }
    public C(String str, int intValue, A a) {
        this.str = str;
        this.intValue = intValue;
        this.a = a;
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(str);
        out.writeInt(intValue);
        out.writeObject(a);
    }
    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        this.str = (String) in.readObject();
        this.intValue = in.readInt();
        this.a = (A) in.readObject();
    }
}

使用方法很简单,只要重写两个方法,按顺序存放和读取属性就可以了,这里就不多说了
注意的是,实现Externalizable的类必须定一个无参的构造方法,否则会抛出异常

java.io.InvalidClassException: xx.xxx.YourClass; no valid constructor

ObjectStreamClass有这么一个方法

 private static Constructor<?> getExternalizableConstructor(Class<?> cl) {
        try {
            Constructor<?> cons = cl.getDeclaredConstructor((Class<?>[]) null);
            cons.setAccessible(true);
            return ((cons.getModifiers() & Modifier.PUBLIC) != 0) ?
                cons : null;
        } catch (NoSuchMethodException ex) {
            return null;
        }
    }

而在ObjectOutputStream中会去调用这个方法获取实现Externalizable的类的空构造方法,没有就抛出异常,也就是说异常在写入阶段就抛出了

7.Serializable的扩展

通常我们对于Serializable只需要实现就可以了,但这里有几不为人知个隐藏的方法
比如我 定义一个类

class D implements Serializable {

    private String strValue;
    private int intValue;

    public D(String strValue, int intValue) {
        this.strValue = strValue;
        this.intValue = intValue;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("write Object self");
        oos.writeObject(strValue);
        oos.writeInt(intValue);
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("read Object self");
        this.strValue = (String) ois.readObject();
        this.intValue = ois.readInt();
    }

    private Object readResolve() {
        System.out.println("readResolve Object self");
        this.intValue = 666;
        return this;
    }

    private Object writeReplace() {
        System.out.println("writeReplace Object self");
        this.strValue = "DName replaced";
        return this;
    }
}

注意这些方法都是没有@Override标示,的完全私有的,可以认为是凭空定义的和类无关的方法,然后调用测试方法

public static void main(String[] args){
 	 D d = new D("DName", 233);
  	 byte[] data = serialData(d);
 	 D res = decSerialData(data, D.class);
     System.out.println(res);
  }

输出结果是

writeReplace Object self
write Object self
read Object self
readResolve Object self
D{strValue='DName replaced', intValue=666}

我们会发现这些方法居然凭空调用了,而且我们序列化初始传入的值也失效了,取而代之的是我们定义的方法中的值,是不是感觉很神奇。
而且这些方法的执行顺序是
1.写入writeReplace ->>> writeObject
2.读取 readObject ->>>> readResolve

writeObjectreadObject中有参数ObjectOutputStreamObjectInputStream
writeReplacereadResolve则是返回了Object对象,我这里直接修改了成员变量属性并返回了this,也就是当前对象。
也就是说这个模式很像hook,直接修改完就能生效

其实这里在源码中也是有迹可循的,比如在ObjectInputStream中读取object的方法中

  private Object readOrdinaryObject(boolean unshared) throws IOException{
     	......
		Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
		......
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }
	   .....
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod()) {
            Object rep = desc.invokeReadResolve(obj);
          	......
            handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

这里有一个invokeReadResolve的方法,执行时间在readSerialData之后,也就是先读取并序列化转成对象完成后调用这个方法,然后通过反射设置成这个方法修改之后的对象输出出去。
而在ObjectStreamClass的构造方法中

private ObjectStreamClass(final Class<?> cl) {
        this.cl = cl;
	    ......
        if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                   ......
                    if (externalizable) {
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                    return null;
                }
            });
        } else {
            suid = Long.valueOf(0);
            fields = NO_FIELDS;
        }
		......
        if (deserializeEx == null) {
            if (isEnum) {
                deserializeEx = new ExceptionInfo(name, "enum type");
            } else if (cons == null) {
                deserializeEx = new ExceptionInfo(name, "no valid constructor");
            }
        }
        for (int i = 0; i < fields.length; i++) {
            if (fields[i].getField() == null) {
                defaultSerializeEx = new ExceptionInfo(
                    name, "unmatched serializable field(s) declared");
            }
        }
        initialized = true;
    }

在这里获取了类对象中的名为readResolve的方法,保存下来,而invokeReadResolve方法就是反射invoke调用这个方法。writeReplace也是在这里定义的,原理是一样的。

那么问题来了,单例的序列化怎么保持不变?

很简单,在readResolve方法中返回这个单例对象就可以了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值