Java序列化

什么是序列化

所谓的序列化,即把java对象以二进制形式保存到内存、文件或者进行网络传输。从二进制的形式恢复成为java对象,即反序列化。

通过序列化可以将对象持久化,或者从一个地方传输到另一个地方。这方面的应用有RMI,远程方法调用。

java中实现序列化有两种方式,实现Serializable接口或者Externalizable接口。这篇总结只讨论Serializable的情况。

public class SerializeTest implements Serializable{
        private static final long serialVersionUID = 1L;
        public String str;
        public SerializeTest(String str) {
               this.str = str;
        }
        
        public static void main(String[] args) throws IOException, ClassNotFoundException {
               SerializeTest test = new SerializeTest("hello");
               ByteArrayOutputStream oStream = new ByteArrayOutputStream();
               ObjectOutputStream objectOutputStream = new ObjectOutputStream(oStream);
               objectOutputStream.writeObject(test);//序列化
               
               ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(oStream.toByteArray()));
               SerializeTest obj = (SerializeTest) inputStream.readObject();//反序列化
               System.out.println(obj.str);
        }
}

上面的代码演示了类SerializeTest实现序列化和反序列化的过程。

所有的序列化和反序列化过程都是java默认实现的,你只需要实现接口Serializable,就能得到一个实现了序列化的类。
通过ObjectOutputStream和ObjectInputStream分别将序列化对象输出或者写入到某个流当中。流的目的地可以是内存字节数组(上例)、文件、或者网络。

下面研究一下序列化过程中的几个问题:

静态变量如何序列化

public class SeriaUtil {
        ByteArrayInputStream bInputStream;
        ByteArrayOutputStream byOutputStream;
        ObjectOutputStream outputStream ;
        ObjectInputStream inputStream;
        
        public void seria(Object test) throws IOException {
            if (byOutputStream == null) {
                byOutputStream = new ByteArrayOutputStream();
            }
            if (outputStream == null) {
                outputStream = new ObjectOutputStream(byOutputStream);
            }
            outputStream.writeObject(test);
//         System.out.println(byOutputStream.toByteArray().length); 
        }
        
        public Object  reSeria() throws IOException, ClassNotFoundException {
                if (bInputStream == null) {
                      bInputStream = new ByteArrayInputStream(byOutputStream.toByteArray());
                }
                if (inputStream == null) {
                    inputStream = new ObjectInputStream(bInputStream);
                }
                Object obj =  inputStream.readObject();
                return obj;
        }
}

public class StaticTest implements Serializable{
    private static final long serialVersionUID = 1L;
    public static int A = 0;
    public static String B = "hello";
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
            //先序列化类,此时 A=0 B = hello
            SeriaUtil seriaUtil = new SeriaUtil();
            StaticTest test = new StaticTest();
            seriaUtil.seria(test);
            //修改 静态变量的值
            StaticTest.A = 1;
            StaticTest.B = "world";
            
            StaticTest obj = (StaticTest) seriaUtil.reSeria();
            //输出 A = 1 B = world
            System.out.println(obj.A);
            System.out.println(obj.B);
    }
}

上面的代码说明了,静态变量不会被序列化。

序列化StaticTest实例test时,静态变量 A=0 B="hello",序列化之后,修改StaticTest类的静态变量值,A=1 B="world",
此时反序列化得到之前序列化的实例对象赋给obj,发现obj的静态变量变为A=1 B="world",说明静态变量并未序列化成功。

事实上,在序列化对象时,会忽略对象中的静态变量。很好理解,静态变量是属于类的,而不是某个对象的状态。我们序列化面向的是对象,是想要将对象的状态保存下来,所以
静态变量不会被序列化。反序列化得到的对象中的静态变量的值是当前jvm中静态变量的值。静态变量对于同一个jvm中同一个类加载器加载的类来说,是一样的。对于同一个静态变量,不会存在同一个类的不同实例拥有不同的值。

同一对象序列化两次,反序列化后得到的两个对象是否相等

这个问题提到的相等,是指是否为同一对象,即==关系

在某些情况下,确保这种关系是很重要的。比如王经理和李经理拥有同一个办公室,即存在引用关系:

public class Manager{
    Room room;
    public Manager(Room r){
        room = r;
    }
}

public class Room{}

public class APP{
    public void main(String args[]){
        Room room = new Room();
        Manager wang = new Manager(room);
        Manager li = new Manager(room);
    }
}

反序列化之后,wang,li,room的这种引用关系不应该发生变化。通过代码验证一下:

public class ReferenceTest implements Serializable{
        public String a;
        public ReferenceTest() {
            a = "hah";
        }
        public static void main(String[] args) throws Exception {
              System.out.println("构造对象********************");
              ReferenceTest test = new ReferenceTest();
              System.out.println("序列化**********************");
              SeriaUtil util = new SeriaUtil();
              util.seria(test);
              util.seria(test);//第二次序列化该对象
              System.out.println("反序列化**********************");
              ReferenceTest obj = (ReferenceTest) util.reSeria();
              ReferenceTest obj1 = (ReferenceTest) util.reSeria();
              System.out.println(obj == obj1);//true
              System.out.println(obj == test);//false
        }
}

上面的例子证明(System.out.println(obj == obj1);//true),同一对象序列化多次之后,反序列化得到的多个对象相等,即内存地址一致。

使用同一个ObjectOutputStream对象序列化某个实例时,如果该实例还没有被序列化过,则序列化,若之前已经序列化过,则不再进行序列化,只是做一个标记而已。
所以在反序列化时,可以保持原有的引用关系。

System.out.println(obj == test);//false 也可以理解,反序列化之后重建了该对象,内存地址必然是新分配的,故obj != test

父类没有实现Serializable,父类中的变量如何序列化

public class SuperTest{
       public String superB;
       public SuperTest() {
           superB = "hehe";
           System.out.println("super 无参构造函数");
       }
       
       public SuperTest(String b){
           System.out.println("super 有参构造函数");
           superB = b;
       }
        public static void main(String[] args) throws IOException, ClassNotFoundException {
              System.out.println("构造对象*******************");
              SonTest sonTest = new SonTest("son", "super");
              System.out.println("序列化*********************");
              SeriaUtil  seriaUtil = new SeriaUtil();
              seriaUtil.seria(sonTest);
              System.out.println("反序列化******************");
              SonTest obj = (SonTest) seriaUtil.reSeria();
              System.out.println(obj.sonA);
              System.out.println(obj.superB);
        }
}

class SonTest extends SuperTest  implements Serializable{
     private static final long serialVersionUID = 1L;
     public String sonA;
     public SonTest() {
         System.out.println("son 无参构造函数");
     }
     
     public SonTest(String a, String b) {
         super(b);
         System.out.println("son 有参构造函数");
         sonA = a;
     }
}

输出:

构造对象*******************
super 有参构造函数
son 有参构造函数
序列化*********************
反序列化******************
super 无参构造函数
son
hehe

通过上面的代码可以看出,父类如果没有实现serializable,反序列化时会调用父类的无参构造函数初始化父类当中的变量。

所以,我们可以通过显示声明父类的无参构造函数,并在其中初始化变量值来控制反序列化后父类变量的值。

transient使用

实现了serializable接口的类在序列化时默认会将所有的非静态变量进行序列化。我们可以控制某些字段不被默认的序列化机制序列化。

比如,有些字段是跟当前系统环境相关的或者涉及到隐私的,需要保密的。这些字段是不可以被序列化到文件中或者通过网络传输的。我们可以通过为这些字段声明
transient关键字,保证其不被序列化。

被关键字transient声明的变量不会被序列化,反序列化时该变量会被自动填充为null(int 为0)。我们也可以为这些字段实现自己的序列化机制。

public class TransientTest implements Serializable{
       private static final long serialVersionUID = 1L;
        public transient String  str;
        public TransientTest() {
            str = "hello";
        }
        
        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            String encryption = "key" + str;
            out.writeObject(encryption);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
           String encryption =  (String) in.readObject();
           str = encryption.substring("key".length(), encryption.length());
        }
        
        public static void main(String[] args) throws IOException, ClassNotFoundException {
              TransientTest test = new TransientTest();
              SeriaUtil util = new SeriaUtil();
              util.seria(test);
              TransientTest reSeria = (TransientTest) util.reSeria();
              System.out.println(reSeria.str);//hello
        }
}

通过实现writeObject和readObject实现自己的序列化机制。上面的代码模拟了一个加密的序列化过程。

成员变量没有实现序列化

序列化某个实例时,如果这个实例含有对象类型的成员变量,那么同时会触发该变量的序列化机制。这时就要求这个成员变量也实现Serializable接口,如果没有实现该接口,抛出异常。

public class VariableTest implements Serializable{
    Variable variable ;
    public VariableTest() {
        variable = new Variable();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
            System.out.println("构造对象*************");
            VariableTest variableTest = new VariableTest();
            System.out.println("序列化**************");
            SeriaUtil util = new SeriaUtil();
            //抛出异常:java.io.NotSerializableException
            util.seria(variableTest);
            System.out.println("反序列化****************");
            VariableTest obj = (VariableTest) util.reSeria();
            System.out.println(obj.variable.a);//抛出异常:Exception in thread "main" java.io.NotSerializableException: space.kyu.Variable
    }
}
class Variable {
    public String a;
    public Variable(){
        a = "hehe";
    }
}

单例模式下的序列化

public class SingleTest implements Serializable{
    public static SingleTest instance = new SingleTest();
    private SingleTest(){}
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingleTest test = SingleTest.instance;
        SeriaUtil util = new SeriaUtil();
        util.seria(test);
        SingleTest reSeria = (SingleTest) util.reSeria();
        System.out.println(reSeria == SingleTest.instance);//false
    }
}

由上面的代码可以看出,有两个SingleTest实例同时存在,通过反序列化破坏了单例模式。反序列化时会开辟新的内存空间重新实例化对象,所以单例模式被破坏。

为了解决这种问题,可以实现readResolve()方法。

public class SingleTest implements Serializable{
    public static SingleTest instance = new SingleTest();
    private SingleTest(){}
    private Object readResolve() {
        return SingleTest.instance;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingleTest test = SingleTest.instance;
        SeriaUtil util = new SeriaUtil();
        util.seria(test);
        SingleTest reSeria = (SingleTest) util.reSeria();
        System.out.println(reSeria == SingleTest.instance);//true
    }
}

序列化版本

代码是在不断的演化的。1.1版本的类可以读取1.0版本的序列化文件吗?这就涉及到序列化的版本管理。

每个序列化版本都有其唯一的ID,他是数据域类型和方法签名的指纹。当类的定义产生变化时,他的指纹也会跟着产生变化,对象流将拒绝读入具有不同指纹的对象。
如果想保持早期版本的兼容,首先要获取这个类早期版本的指纹。

我们可以使用 jdk自带的工具 serialver 获得这个指纹:serialver Test

staic final long serialVersionUID = -1423859403827594712L

然后将1.1版本中Test类的serialVersionUID常量定义为上面的值,即可序列化老版本的代码。

如果一个类具有名为serialVersionUID的常量,那么java就不会再主动计算这个值,而是直接将其作为这个版本类的指纹。没有特殊要求的话,一般都显示的声明serialVersionUID:private static final long serialVersionUID = 1L;来保证兼容性

如果对象流中的对象具有在当前版本中没有的数据域,那么对象流会忽略这些数据;如果当前版本具有对象流中所没有的数据域,那么这些新加的域将被设为默认值。

序列化与克隆

反序列化重新构建对象的机制提供了一种克隆对象的简便途径,只要对应的类可序列化即可。

做法很简单:直接将对象序列化到输出流当中,然后将其读回。这样产生的对象是对现有对象的一个深拷贝。

public class CloneTest implements Serializable, Cloneable {
    public String str;
    public CloneTest(String str) {
        this.str =str;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        SeriaUtil util = new SeriaUtil();
        try {
            util.seria(this);
            CloneTest reSeria = (CloneTest) util.reSeria();
            return reSeria;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } 
        
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest test = new CloneTest("hi");
        CloneTest clone = (CloneTest) test.clone();
        System.out.println(clone.str);//hi
        System.out.println(clone == test);//false
    }
}

这样克隆对象的优点是简单,缺点是比普通的克隆实现要慢的多。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 如果你只知道实现 Serializable 接口的对象,可以序列化为本地文件。那你最好再阅读该篇文章,文章对序列化...
    jiangmo阅读 456评论 0 2
  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,834评论 0 24
  • 序列化是什么 信息的传递、交换支撑整个互联网产业,那么信息的交流的过程中遵循着什么样的标准。常见的网络传输协议有 ...
    非典型程序员阅读 2,276评论 0 5
  • 你在想什么 世界那么大 只存你一念之间 我能装得下 你梦里的蓝天 你亲吻过的风 还有你心中 倒映着月光的无尽波澜 ...
    树大叔阅读 237评论 0 0
  • 最大的悲哀莫过于长大,从此,笑不再纯粹,哭不再彻底,宁愿像个孩子,不肯看太多的事,听太多的不是,单纯一辈子。开心了...
    邂逅相遇太早阅读 464评论 0 0