java类序列化和反序列化分析

一、序列化与反序列化疑惑

我们都知道序列化的特点:

  1. 如果某个类能够被序列化,其子类也可以被序列化。
  2. 声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态, transient代表对象的临时数据。

我们也知道平时写类涉及到交互的时候,基本上都需要实现Serializable接口。
如下图所示:
显示类
可是为什么要这么写,我们好像并未太过于关注这点。当我们开始疑惑这点的时候,我们点到Serializable 接口里面的时候,却发现是下图这样的。
空的
不知道你是不是开始时候也和我一样疑惑呢?空的接口,为什么空的就能实现序列化呢?
莫慌莫慌,我们在这个接口的注释上面可以找到对应的答案。
注释很长:具体说的是什么,我这里先跳过,感兴趣的小伙伴们可以自行读一下,读不懂的也可以直接翻译。上图中截图有介绍序列化id的作用。大家也可以读下。下文中会介绍这个接口的作用。
先看一个序列化demo吧,我们就序列化上图中的Studen类
先编写一个测试方法。

    @Test
    public void testObjectOutputSerializable() throws Exception {
        Student liLei = new Student(1, "李雷", 20);
        FileOutputStream liLeiOutputStream = new FileOutputStream("D:\\serializable\\liLei.txt");
        ObjectOutputStream liLeiOs = new ObjectOutputStream(liLeiOutputStream);
        //写入本地磁盘
        liLeiOs.writeObject(liLei);
        //关闭流
        liLeiOs.close();
        liLeiOutputStream.close();
    }

这里运行一下,查看本地磁盘,已经把文件写入磁盘了。
我的图片
我们再编写另一个测试方法,把李雷创建回来如下:

    @Test
    public void testObjectInputSerializable() throws Exception {
        FileInputStream liLieInputStream = new FileInputStream("D:\\serializable\\liLei.txt");
        ObjectInputStream in = new ObjectInputStream(liLieInputStream);
        Student liLei = (Student) in.readObject();
        in.close();
        liLieInputStream.close();
        System.out.println(liLei);
    }

这时候我们看到控制台已经打印出我刚才创建的李雷了。
反序列化
这里没有任何问题。这里稍微提一下,有时候大家面试会碰见java中创建对象的几种方式。

  1. new 一个对象,可能会考察jvm内存开辟
  2. 反射,考察反射基本知识。
  3. 反序列化, 就是本文中要讲到的注意事项。
  4. clone ,这个一般考察深克隆和浅克隆之前的关系与实现

现在继续回来分析序列化和反序列化。
这时候,我们把Student 类中的 implements Serializable 去掉。执行第一个测试方法。
序列化出错
我们发现报错了。还记得上文中我们看过源码,Serializable接口中是空方法。为什么加上就能序列化到本地磁盘,不加就不能序列化到本地磁盘呢?

再看下一个例子。
我们先把Student类 implements Serializable 然后再添加一个序列化id
如下图:
条件序列化id
我们先执行序列化测试方法,再把Student 中的序列化id去掉。然后执行反序列化方法。
反序列化错误
反序列化失败。 同样只要修改了序列化id后都会报这个错误。

序列化源码查看

通过测试方法中我们可以看出序列化用的是 ObjectOutputStream 类writeObject()方法序列化到本地中去的。我们就追一下。

第一步

我们通过debug 可以发现序列化进入了 writeObject0() 方法中了。
序列化第一步

第二步

我们进去 writeObject0() 方法,会发现一堆验证,我们一步一步的往下走。
序列化数据
在序列化中
java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>) 方法 其中一个方法是获取类的中序列化,如果没有,这里就生成一个默认的。
在这里插入图片描述
在java.io.ObjectStreamClass#getDefaultSerialFields中提现了转换字段。

    private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
        Field[] clFields = cl.getDeclaredFields();
        ArrayList<ObjectStreamField> list = new ArrayList<>();
        //当字段static 或者 TRANSIENT 的时候对应字段不序列化,这就是为什么用transient 修饰字段为什么不序列化的原因、
        int mask = Modifier.STATIC | Modifier.TRANSIENT;

        for (int i = 0; i < clFields.length; i++) {
            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]);
    }

跳出这个方法回到 writeObject0 方法突然发现一个比较重要的东西。如下图:
序列化第二步
是不是突然恍然大悟。为什么 Serializable 中的方法为空,原来只是起个标识作用。 至此我你们的疑问已经解开。我们就继续把代码追完吧。

第三步,

我们进入writeOrdinaryObject 方法,接着执行 writeSerialData() 方法,在writeSerialData()方法中调用defaultWriteFields方法 如下图:
第三步序列化2
里面又进行数据处理。然后最后执行
bout.write 把数据写入磁盘
111
这样一个对象就被序列化到磁盘中去了。

注意:网上好多博客没有追源码,序列化顺序把 slotDesc.invokeWriteObject(obj, this); 这个方法也加入到了类的序列化中其实是错误的。类序列化不会走这个方法。走的defaultWriteFields(obj, slotDesc)这个方法,集合类型才会走这个方法。

反序列化源码查看

同样在反序列化中核心方法在java.io.ObjectInputStream#readObject()方法中。我们开始调试一下。

分析

同序列化方法入口重要的方法也是readObject0() 方法。
反序列化第一步
通过调试,我们进了readOrdinaryObject方法
在readOrdinaryObject 方法中,调用入下图所示:
newInstance方法
在readOrdinaryObject 中

 private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }
		//这个方法中 switch 语句块匹配 TC_CLASSDESC 调用readNonProxyDesc
        ObjectStreamClass desc = readClassDesc(false);
    
        desc.checkDeserialize();
        ........
    }

java.io.ObjectInputStream#readNonProxyDesc 中判断序列化id。
序列化id验证

接着回到 readOrdinaryObject 方法中java.io.ObjectInputStream#readSerialData方法执行数据填充。

小结

这里只是简单介绍序列化和反序列化的流程。源码上的逻辑还是挺多,这里需要大家慢慢的去品读。
在结束的时候,我来把上次反序列化方式破坏单例,再处理一下。
通过反序列化源码我们可以知道:序列化会通过反射调用无参数的构造方法创建一个新的对象。
这时候就破坏了单例。
我们再源码中找到了如下方法:
判断对象中有没有这个方法
如果以上的的判断为false 就不会执行下面的代码,就会通过反射生成新的对象。

Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
            //这里通过反射生成对象。
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

反射的方法可以调试一下,在这里赋值的。同时对象里面需要加上public Object readResolve() 方法
反射调用的方法
可知只需要在单例模式上添加一段代码即可,这里就不做详细的测试了,大家可以自行测试。

public class DoubleCheckSingleton {
    public DoubleCheckSingleton() {

    }

   public class DoubleCheckVolatileSingleton {
    public DoubleCheckVolatileSingleton() {

    }

    public static volatile DoubleCheckVolatileSingleton singleton = null;

    public static DoubleCheckVolatileSingleton getInstance() {
        if (null == singleton) {
            synchronized (DoubleCheckVolatileSingleton.class) {
                if (null == singleton) {
                    singleton = new DoubleCheckVolatileSingleton();
                }
            }
        }
        return singleton;
    }
    //防止反序列化破坏单例
    public Object readResolve() {
        return singleton;
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值