Java序列化与反序列化

什么是序列化

内容主要参考 https://blog.csdn.net/xlgen157387/article/details/79840134
Java的序列化(Object serialization)机制,就是将对象编码成一个字节流(序列化serialization),以及从字节流编码中重新构建对象(反序列化deserialization)的过程。一旦将对象序列化后,一方面可以将其持久化到磁盘上,供以后反序列化使用;另一方面在分布式环境中经常将对象从这一端网络传递到另一端,需要一种在两端传输数据的协议,而java序列化机制就提供了这种协议的实现。

  • 序列化
    对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
  • 反序列化
    客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
为什么要序列化

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!

换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

总的来说可以归结为以下几点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;


序列化测试
import java.io.Serializable;

/**
 * @author Twilight
 * @date 18-12-1 下午1:55
 */
public class Test implements Serializable {

    private String name;

    private int age;

    private transient String temporary;

    private static String aekc = "666";
	
	//这里省略了set get方法

    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", temporary='" + temporary + '\'' +
                ", aekc='" + aekc + '\'' +
                '}';
    }
}

测试方法


import java.io.*;

/**
 * @author Twilight
 * @date 18-12-1 下午1:59
 */
public class TestSerializable {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serializeTest();
        Test test = deserializeTest();
        System.out.println(test.toString());
    }

    private static void serializeTest() throws IOException {
        Test test = new Test();
        test.setAge(10);
        test.setName("小明");
        test.setTemporary("序列化测试");
        FileOutputStream fileOutputStream = new FileOutputStream(new File("testSerializable.txt"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(test);
        System.out.println("序列化成功");
        objectOutputStream.close();
    }

    private static Test deserializeTest() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(new File("testSerializable.txt"));
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Test test = (Test) objectInputStream.readObject();
        System.out.println("反序列化成功");
        return test;
    }
}

最后输出如下

序列化成功
反序列化成功
Test{name='小明', age=10, temporary='null', aekc='666'}

可以看到temporary为空,所以声明为transient的成员不能被序列化。transient代表对象的临时数据。
我们序列化后,在反序列化之前修改下静态变量aekc的值

public static void main(String[] args) throws IOException, ClassNotFoundException {
    serializeTest();
    Test.setAekc("888");
    Test test = deserializeTest();
    System.out.println(test.toString());
}

输出如下

序列化成功
反序列化成功
Test{name='小明', age=10, temporary='null', aekc='888'}

可见声明为static类型的成员也不能被序列化。因为static代表类的状态。

如果被序列化的类中的成员变量是一个对象呢?

import java.io.Serializable;

public class Test implements Serializable {

    private String name;

    private int age;

    private transient String temporary;

    private static String aekc = "666";

    private Test2 test2;

	//这里省略了set get方法

    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", temporary='" + temporary + '\'' +
                ", test2=" + test2 +
                ", aekc='" + aekc + '\'' +
                '}';
    }
}

public class Test2 implements Serializable {

    private String name;

    public Test2(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "Test2{" +
                "name='" + name + '\'' +
                '}';
    }
}

对象成员对应的类也要实现Serializable接口,否则就会抛出NotSerializableException异常。如果是父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。


序列化与单例模式
import java.io.Serializable;

public class Singleton implements Serializable {

    private Singleton() {}

    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

测试方法


import java.io.*;

public class TestSerializable {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serializeTest();
        System.out.println(deserializeTest() == Singleton.getInstance());
    }

    private static void serializeTest() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream(new File("testSerializable.txt"));
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(Singleton.getInstance());
        System.out.println("序列化成功");
        objectOutputStream.close();
    }

    private static Singleton deserializeTest() throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(new File("testSerializable.txt"));
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        System.out.println("反序列化成功");
        return (Singleton) objectInputStream.readObject();

    }
}

输出如下

序列化成功
反序列化成功
false

结果是false,说明

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

简单说下why,我们通过debug objectInputStream.readObject()方法,可以找到这个方法readOrdinaryObject。该方法中有一块代码

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);
}

这里isInstantiable表示,如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
所以,我们可以知道为什么序列化可以破坏单例了吧

序列化会通过反射调用无参数的构造方法创建一个新的对象。

如何解决呢?这里需要在Singleton类中定义readResolve就可以解决该问题

import java.io.Serializable;

public class Singleton implements Serializable {

    private Singleton() {}

    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    private Object readResolve() {
        return instance;
    }
}

输出如下

序列化成功
反序列化成功
true

原理同样也在readOrdinaryObject方法下

if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, obj = rep);
        }
    }

hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true。
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。


serialVersionUId

        serialVersionUID是用来辅助序列化和反序列化过程的,序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候系统会检测当前文件中的serialVersionUID是否和当前类的serialVersionUID一致,如果一致这个时候可以反序列化成功,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的。

        如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或减少了某些成员变量,那么系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败。所以当我们手动指定了serialVersionUID的值,就可以很大程度上避免了反序列化的失败。但是如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化还是会失败,因为类结构发生了改变。


其他序列化技术
  • 专门针对Java语言的:基于Protobuf的Kryo,JBoss的Marshalling等等
  • 跨语言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等

下面我们测试下Kryo序列化方式和Java原生序列化方式的时间效率对比

实体类

package com.serializable.test;

import java.io.Serializable;
import java.util.Map;

public class Simple  implements Serializable {
     private static final long serialVersionUID = -4914434736682797743L;  
     private String name;  
     private int age;  
     private Map<String,Integer> map;  
     public Simple(){  
  
     }  
     public Simple(String name,int age,Map<String,Integer> map){  
         this.name = name;  
         this.age = age;  
         this.map = map;  
     }  
  
     public String getName() {  
       return name;  
     }  
  
     public void setName(String name) {  
        this.name = name;  
     }  
  
     public int getAge() {  
        return age;  
     }  
  
     public void setAge(int age) {  
        this.age = age;  
     }  
  
     public Map<String, Integer> getMap() {  
        return map;  
     }  
  
     public void setMap(Map<String, Integer> map) {  
        this.map = map;  
     }  
  
  
}  

Java原生序列化方式

package com.serializable.test;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Map;

public class OriginalSerializable {  
      
    public static void main(String[] args) throws IOException {
        long start =  System.currentTimeMillis();  
        setSerializableObject();  
        System.out.println("java原生序列化时间:" + (System.currentTimeMillis() - start) + " ms" );    
        start =  System.currentTimeMillis();  
        getSerializableObject();  
        System.out.println("java原生反序列化时间:" + (System.currentTimeMillis() - start) + " ms");  
    }  
  
    public static void setSerializableObject() throws IOException{  
  
        FileOutputStream fo = new FileOutputStream("file1.bin");
  
        ObjectOutputStream so = new ObjectOutputStream(fo);  
  
        for (int i = 0; i < 100000; i++) {  
            Map<String,Integer> map = new HashMap<String, Integer>(2);  
            map.put("zhang0", i);  
            map.put("zhang1", i);  
            so.writeObject(new Simple("zhang"+i,(i+1),map));  
        }  
        so.flush();  
        so.close();  
    }  
  
    public static void getSerializableObject(){  
         FileInputStream fi;  
        try {  
            fi = new FileInputStream("file1.bin");
            ObjectInputStream si = new ObjectInputStream(fi);  
  
            Simple simple =null;  
            while((simple=(Simple)si.readObject()) != null){  
                //System.out.println(simple.getAge() + "  " + simple.getName());  
            }  
            fi.close();  
            si.close();  
        } catch (FileNotFoundException e) {  
            e.printStackTrace();  
        } catch (IOException e) {  
            //e.printStackTrace();  
        } catch (ClassNotFoundException e) {  
            e.printStackTrace();  
        }  

    }  
  
}  

kyro序列化方式

package com.serializable.test;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class KyroSerializable {

    public static void main(String[] args) throws IOException {
        long start =  System.currentTimeMillis();
        setSerializableObject();
        System.out.println("Kryo 序列化时间:" + (System.currentTimeMillis() - start) + " ms" );
        start =  System.currentTimeMillis();
        getSerializableObject();
        System.out.println("Kryo 反序列化时间:" + (System.currentTimeMillis() - start) + " ms");

    }

    public static void setSerializableObject() throws FileNotFoundException {
        Kryo kryo = new Kryo();
        kryo.setReferences(false);
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        kryo.register(Simple.class);
        Output output = new Output(new FileOutputStream("file2.bin"));
        for (int i = 0; i < 100000; i++) {
            Map<String,Integer> map = new HashMap<String, Integer>(2);
            map.put("zhang0", i);
            map.put("zhang1", i);
            kryo.writeObject(output, new Simple("zhang"+i,(i+1),map));
        }
        output.flush();
        output.close();
    }


    public static void getSerializableObject() {
        Kryo kryo = new Kryo();
        kryo.setReferences(false);
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        Input input;
        try {
            input = new Input(new FileInputStream("file2.bin"));
            Simple simple = null;
            while((simple = kryo.readObject(input, Simple.class)) != null) {
                //System.out.println(simple.getAge() + "  " + simple.getName() + "  " + simple.getMap().toString());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

最后时间效率对比

java原生序列化时间:8826 ms
java原生反序列化时间:10354 ms
Kryo 序列化时间:730 ms
Kryo 反序列化时间:20 ms

经过对比,可以发现kryo是java原生序列化性能十几倍

序列化后文件大小对比

java原生序列化文件大小:7606k
Kryo序列化文件大小:5337k
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值