JAVA序列化

一、概念

       java对象序列化的意思就是将对象的状态转化成字节流,以后可以通过这些值再生成相同状态的对象。对象序列化是对象持久化的一种实现方法,它是将对象的属性和方法转化为一种序列化的形式用于存储和传输。反序列化就是根据这些保存的信息重建对象的过程。

       序列化:将java对象转化为字节序列的过程。

       反序列化:将字节序列转化为java对象的过程。

 

二、使用对象序列化的原因

 

1、保存(持久化)对象及其状态到内存或者磁盘

  • Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
  • 序列化对象以字节数组保持-静态成员不保存使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。

      举例:在使用tomcat开发JavaEE相关项目的时候,我们关闭tomcat后,相应的session中的对象就存储在了硬盘上,如果我们想要在tomcat重启的时候能够从tomcat上面读取对应session中的内容,那么保存在session中的内容就必须实现相关的序列化操作。

2、序列化用户远程对象传输
   

       除了在持久化对象时会用到对象序列化之外,java对象要在分布式中使用(spark,flink......)、当使用RMI(远程方法调用)、在网络中传递对象时,都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。

三、序列化实现方式

1、Serializable实现序列化

  • 在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
  • 通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化。
// serialization/Worm.java
// Demonstrates object serialization
import java.io.*;
import java.util.*;
class Data implements Serializable {
    private int n;
    Data(int n) { this.n = n; }
    @Override
    public String toString() {
        return Integer.toString(n);
    }
}
public class Worm implements Serializable {
    private static Random rand = new Random(47);
    private Data[] d = {
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10))
    };
    private Worm next;
    private char c;
    // Value of i == number of segments
    public Worm(int i, char x) {
        System.out.println("Worm constructor: " + i);
        c = x;
        if(--i > 0)
            next = new Worm(i, (char)(x + 1));
    }
    public Worm() {
        System.out.println("No-arg constructor");
    }
    @Override
    public String toString() {
        StringBuilder result = new StringBuilder(":");
        result.append(c);
        result.append("(");
        for(Data dat : d)
            result.append(dat);
        result.append(")");
        if(next != null)
            result.append(next);
        return result.toString();
    }
    public static void
    main(String[] args) throws ClassNotFoundException,
            IOException {
        Worm w = new Worm(6, 'a');
        System.out.println("w = " + w);
        try(
                ObjectOutputStream out = new ObjectOutputStream(
                        new FileOutputStream("worm.dat"))
        ) {
            out.writeObject("Worm storage\n");
            out.writeObject(w);
        }
        try(
                ObjectInputStream in = new ObjectInputStream(
                        new FileInputStream("worm.dat"))
        ) {
            String s = (String)in.readObject();
            Worm w2 = (Worm)in.readObject();
            System.out.println(s + "w2 = " + w2);
        }
        try(
                ByteArrayOutputStream bout =
                        new ByteArrayOutputStream();
                ObjectOutputStream out2 =
                        new ObjectOutputStream(bout)
        ) {
            out2.writeObject("Worm storage\n");
            out2.writeObject(w);
            out2.flush();
            try(
                    ObjectInputStream in2 = new ObjectInputStream(
                            new ByteArrayInputStream(
                                    bout.toByteArray()))
            ) {
                String s = (String)in2.readObject();
                Worm w3 = (Worm)in2.readObject();
                System.out.println(s + "w3 = " + w3);
            }
        }
    }
}
输出为:

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)

2、实现接口Externalizable(控制序列化)

      Externalizable 接口继承了 Serializable 接口,同时增添了两个方法:writeExternal() 和 readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。在writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。

     

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author lenovo
 **/
public class User implements Externalizable {
    private static final long serialVersionUID = 1L;
    private String userName;
    private String password;
    private String age;

    public User(String userName, String password, String age) {
        super();
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    public User() {
        super();
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    /**
     * 序列化操作的扩展类
     */
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //增加一个新的对象
        Date date = new Date();
        out.writeObject(userName);
        out.writeObject(password);
        out.writeObject(date);
    }

    /**
     *  反序列化操作的扩展类
     */
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 这里的接受顺序是有限制
        userName=(String) in.readObject();
        password=(String) in.readObject();
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
        Date date=(Date)in.readObject();
        System.out.println("反序列化后的日期为:"+sdf.format(date));

    }

    @Override
    public String toString() {
        //这里的年龄是不会被序列化的,所以在反序列化的时候是读取不到数据的
        return "用户名:"+userName+",密 码:"+password+",年龄:"+age;
    }
}

/**
 * 序列化和反序列化的相关操作类
 */
class Operator{
    /**
     * 序列化方法
     */
    public void serializable(User user) throws FileNotFoundException, IOException{
        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream("hjw.txt"));
        outputStream.writeObject(user);
    }

    /**
     * 反序列化的方法
     */
    public User deSerializable() throws FileNotFoundException, IOException, ClassNotFoundException{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("hjw.txt"));
        return (User) ois.readObject();
    }

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        Operator operate=new Operator();
        User person=new User("hjw","123456","31");
        System.out.println("序列化之前的数据:");
        System.out.println(person.toString());
        operate.serializable(person);
        User newPerson=operate.deSerializable();
        System.out.println("---------------------------------------------");
        System.out.println("序列化之后的数据:");
        System.out.println(person.toString());
    }

}
================================================================
运行结果:
序列化之前的数据:
用户名:hjw,密 码:123456,年龄:31
反序列化后的日期为:2020-12-16
---------------------------------------------
序列化之后的数据:
用户名:hjw,密 码:123456,年龄:31

 

3、使用readResolve增强单例

  但是如果Sinleton类实现了序列化,那么它不再是一个Singleton,无论该类使用了默认的序列化形式,还是自定义的序列化形式,还是是否提供显式的readObject方法都没关系。任何一个readObject方法,不管是显式还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

  readResolve特性允许使用readObject创建实例代替另一个实例,如果一个类定义了readResolve方法,并且具备正确的声明,那么在反序列化的之后,新建的readResolve方法就会被调用,然后返回的对象引用将被返回,取代新建的对象。

public class Singleton implements Serializable { 
    private static Singleton INSTANCE= new Singleton();
    private Singleton(){};
    private Object readResolve(){ 
        return INSTANCE; 
    } 
}

 

4、把对象包装成JSON字符串传输 

     采用JSON格式同时使用采用Google的gson-2.2.4.jar 进行转义

5、采用谷歌的ProtoBuf

  •     protocol buffers 是google内部得一种传输协议,目前项目已经开源(http://code.google.com/p/protobuf/)。它定义了一种紧凑得可扩展得二进制协议格式,适合网络传输,并且针对多个语言有不同得版本可供选择。
  •    proto文件是对数据的一个描述,包括字段名称,类型,字节中的位置。protoc工具读取proto文件生成对应builder代码的类库。protoc xxxxx  --java_out=xxxxxx 生成java类库。builder类根据自己的算法把数据序列化成字节流,或者把字节流根据反射的原理反序列化成对象。官方的示例:https://developers.google.com/protocol-buffers/docs/javatutorial。

 

总结:

  • 五种方式对比传输同样的数据,google protobuf只有53个字节是最少的。结论:
方式优点缺点
JSON

跨语言、格式清晰一目了然

字节数比较大,需要第三方类库
Externalizablejava原生方法不依赖外部类库字节数比较大,不能跨语言
Object Serializejava原生方法不依赖外部类库字节数比较大,不能跨语言
使用readResolve增强单例java原生方法不依赖外部类库,安全性高字节数比较大,不能跨语言
Google protobuf

跨语言、字节数比较少

编写.proto配置用protoc工具生成对应的代码

四、序列化代价

          --参考

1、可能会导致InvalidClassException异常

       如果没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时导致InvalidClassException。比如:增加一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化,则会出现序列版本UID不一致的情况。所以最好还是显式的增加序列版本号UID。

2、增加了出现Bug和安全漏洞的可能性

序列化机制是一种语言之外的对象创建机制,反序列化机制都是一个“隐藏的构造器”,具备与其他构造器相同的特点,正式因为反序列化中没有显式构造器,所以很容易就会忽略:不允许攻击者访问正在构造过程中的对象内部信息。换句话说,序列化后的字节流可以被截取进行伪造,之后利用readObject方法反序列会不符合要求甚至不安全的实例。

    

 

3、随着类发行新的版本,测试负担也会增加。

  一个可序列化的类被修订时,需要检查是否“在新版本中序列化一个实例,可以在旧版本中反序列化”,如果一个实现序列化的类有很多的子类或者是被修改时,就不得不加以测试。

五、序列化的缺陷

1、序列化是保存对象的状态,也就是不会关心static静态域,静态域不会被序列化;

2、在序列化对象时,如果该对象中有引用对象域名,那么也要要求该引用对象是可实例化的;

3、默认序列化的过程可能消耗大量内存空间和时间,甚至可能会引起栈溢出:

      因为第二条的原因,如果一个类中大量存在引用对象域,并且都需要实现序列化,那么整个序列化过程可能会很消耗时间,在通信传输过程中更是如此,同时序列化后的字节流需要足够大的内存。

总结:

  •  java中的序列化时transient变量(这个关键字的作用就是告知JAVA我不可以被序列化)和静态变量不会被序列;
  •  如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象;
  •  实现序列化接口的对象并不强制声明唯一的serialVersionUID,是否声明serialVersionUID对于对象序列化的向上向下的兼容性有很大的影响;
  •      如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。
  • 结合业务场景,从安全性,性能、成本三方面因素选择序列化方式

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值