java序列化与反序列化

序列化的意义

1.永久存储某个jvm中运行时的对象。
2.对象可以网络传输
3.rmi调用都是以序列化的方式传输参数

基本知识

1.在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
2.通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化

3.虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
4.序列化并不保存静态变量。
5.要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
6.Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
7.服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。(自定义实现readObject和writeObject)

一些杂事

1.将统一个对象连续多次写入一个文件,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。

    ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream("result.obj"));
    Test test = new Test();
    //试图将对象两次写入文件
    out.writeObject(test);
    out.flush();
    System.out.println(new File("result.obj").length());
    out.writeObject(test);
    out.close();
    System.out.println(new File("result.obj").length());

    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
            "result.obj"));
    //从文件依次读出两个文件
    Test t1 = (Test) oin.readObject();
    Test t2 = (Test) oin.readObject();
    oin.close();
            
    //判断两个引用是否指向同一个对象
    System.out.println(t1 == t2);

2.arraylist源码,elementData为什么是transient的

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;
}

ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。

3.自定义序列化方式writeObject,readObject
以arraylist的readObject为例

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject(); //如果不自己定义readObject方法,默认就是调用ObjectInputStream的defaultReadObject方法。

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

Serializable的几个自定义神器方法

一. 解决反序列化异常的神器——Serializable接口的readObjectNoData方法

  • 方法定义:private void readObjectNoData() throws ObjectStreamException
  • 这个方法可能会让你摸不着头脑,测试的时候怎么都不会被执行?,哈哈哈!这里是真相:如果有一个类A正在被反序列化,S是它现在的父类,readObjectNoData方法也应该存在与S类中。当发生以下条件时,将触发这个readObjectNoData方法被执行:
    1.S类直接或间接实现了Serializable接口
    2.S类必须定义实现readObjectNoData方法
    3.当前待被反序列化的stream在被序列化时A类还没有进程S类。
    The readObjectNoData() method is analogous to the class-defined readObject() method, except that (if defined) it is called in cases where the class descriptor for a superclass of the object being deserialized (and hence the object data described by that class descriptor) is not present in the serialization stream.
    Note that readObjectNoData() is never invoked in cases where a class-defined readObject() method could be called, though serializable class implementors can call readObjectNoData() from within readObject() as a means of consolidating initialization code.

二. writeReplace,如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中,例如:

class Person implements Serializable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return name + "(" + age + ")";
    }
    
    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
    
}

在这里就将该对象直接替换成了一个list保存;

  • 实现writeReplace就不要实现writeObject了,实现了也不会被调用;
  • writeReplace的返回值(对象)必须是可序列话的,如果是Java自己的基础类或者类型那就不用说了;
  • writeReplace替换后在反序列化时就无法被恢复了,即对象被彻底替换了!也就是说使用ObjectInputStream读取的对象只能是被替换后的对象,只能在读取后自己手动恢复了,接着上例的演示:
class Person implements Serializable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return name + "(" + age + ")";
    }
    
    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

public class Test {
    
    public static void print(String s) {
        System.out.println(s);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
            Person p = new Person("lala", 33);
            oos.writeObject(p);
            oos.close();
        }
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
            print(((ArrayList)ois.readObject()).toString());
        }
    }
}

  • 使用writeReplace替换写入后也不能通过实现readObject来实现自动恢复了,即下面的代码是错误的!因为默认已经被彻底替换了,就不存在自定义反序列化的问题了,直接自动反序列化成ArrayList了,该方法即使实现了也不会调用!

class Person implements Serializable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return name + "(" + age + ")";
    }
    
    private Object writeReplace() throws ObjectStreamException {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // 错!写了也没用
        ArrayList<Object> list = (ArrayList)in.readObject();
        this.name = (String)list.get(0);
        this.age = (int)list.get(1);
    }
}

所以,writeObject只和readObject配合使用,一旦实现了writeReplace在写入时进行替换就不再需要writeObject和readObject了!因为替换就已经是彻底的自定义了,比writeObject/readObject更彻底!

三. 保护性恢复对象(同时也可以替换对象)——readResolve:

  • readResolve会在readObject调用之后自动调用,它最主要的目的就是让恢复的对象变个样,比如readObject已经反序列化好了一个Person对象,那么就可以在readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为ObjectInputStream的readObject的返回结果;
  • 可以返回一个非原类型的对象,也就是说可以彻底替换对象
class Person implements Serializable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return name + "(" + age + ")";
    }
    
    private Object readResolve() throws ObjectStreamException { // 直接替换成一个int的1返回
        return 1;
    }
}
  • 其最重要的应用就是保护性恢复单例、枚举类型的对象!

枚举问题示例:

class Brand implements Serializable {  
    private int val;  
    private Brand(int val) {  
        this.val = val;  
    }  
      
    // 两个枚举值  
    public static final Brand NIKE = new Brand(0);  
    public static final Brand ADDIDAS = new Brand(1);  
}  
  
public class Test {  
      
    public static void print(String s) {  
        System.out.println(s);  
    }  
  
    public static void main(String[] args) throws IOException, ClassNotFoundException {  
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {  
            oos.writeObject(Brand.NIKE);  
            oos.close();  
        }  
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {  
            Brand b = (Brand)ois.readObject();  
            print("" + (b == Brand.NIKE)); // 答案显然是false  !!!!!!!!!因为Brand.NIKE是程序中创建的对象,而b是从磁盘中读取并恢复过来的对象,两者明显来源不同,因此必然内存空间是不同的,引用(地址)显然也是不同的
        }  
    }  
}  

使用readResolve解决这个问题:

class Brand implements Serializable {  
    private int val;  
    private Brand(int val) {  
        this.val = val;  
    }  
      
    // 两个枚举值  
    public static final Brand NIKE = new Brand(0);  
    public static final Brand ADDIDAS = new Brand(1);  
    
    //这样就能保证反序列化回来的枚举能相等。  
    private Object readResolve() throws ObjectStreamException {  
        if (val == 0) {  
            return NIKE;  
        }  
        if (val == 1) {  
            return ADDIDAS;  
        }  
        return null;  
    }  
}  

四. 强制自己实现串行化和反串行化——Externalizable接口

  • 不像Serializable接口只是一个标记接口,里面的接口方法都是可选的(可实现可不实现,如果不实现则启用其自动序列化功能),而Externalizable接口不是一个标记接口,它强制你自己动手实现串行化和反串行化算法:
    
 //可以看到该接口直接继承了Serializable接口,其两个方法其实就对应了Serializable的writeObject和readObject方法,实现方式也是一模一样;  
 public interface Externalizable extends java.io.Serializable {  
        void writeExternal(ObjectOutput out) throws IOException; // 串行化  
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; // 反串行化  
}  

  • ObjectOutput和ObjectInput的使用方法和ObjectOutputStream和ObjectInputStream一模一样(readObject、readInt之类的,完全一模一样),因此Externalizable就是强制实现版的Serializable罢了;
class Person implements Externalizable {  
    private String name;  
    private int age;  
    public Person(String name, int age) {  
        super();  
        this.name = name;  
        this.age = age;  
    }  
    @Override  
    public void writeExternal(ObjectOutput out) throws IOException {  
        // TODO Auto-generated method stub  
        out.writeObject(new StringBuffer(name).reverse());  
        out.writeInt(age);  
    }  
    @Override  
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
        // TODO Auto-generated method stub  
        this.name = ((StringBuffer)in.readObject()).reverse().toString();  
        this.age = in.readInt();  
    }  
      
}  
  • 和Serializable的主要区别:
    1.效率比Serializable高
    2.但Serializable更加灵活,其最重要的特色就是可以自动序列化,这也是其应用更广泛的原因
    3.使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。因此,必须提供一个无参构造器,访问权限为public;否则会抛出java.io.InvalidClassException 异常;Serializable序列化时不会调用默认的构造器
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 什么是序列化 (1)序列化是将对象转变为字节序列的过程,反序列化则是将字节序列恢复为对象的过程。 (2)对象序列化...
    伊凡的一天阅读 1,612评论 0 4
  • 一、 序列化和反序列化概念 Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化de...
    步积阅读 1,439评论 0 10
  • 序列化和反序列化的概念 序列化:把java对象转换为字节序列的过程称为对象的序列化,这些字节序列可以被保存在磁盘上...
    snoweek阅读 698评论 0 3
  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,842评论 0 24
  • 简介 对于一个存在于Java虚拟机中的对象来说,其内部的状态只保持在内存中。JVM停止之后,这些状态就丢失了。在很...
    FX_SKY阅读 787评论 0 0