>>分享Java编程技术,对《Java面向对象编程》等书籍提供技术支持 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 30507 个阅读者 刷新本主题
 * 贴子主题:  java序列化用法以及理论 回复文章 点赞(0)  收藏  
作者:日月光华    发表时间:2019-09-28 17:32:31     消息  查看  搜索  好友  邮件  复制  引用

              java序列化用法以及理论
博客分类: java基础
java序列化  java反序列化

基本概念

Java的jdk中自带了一个序列化框架:可以将对象编码成字节流,并可以从字节流编码中重新构建出新对象。这里的“将一个对象编码成一个字节流”,称之为“对象序列化”;相反的” 从字节流编码中重新构建出新对象”,称之为“对象反序列化”。

为什么要对对象进行“序列化”和“反序列化”呢?主要是用于在不对的jvm服务器之间进行传输(如RPC调用),还可有实现从硬盘存取对象。一旦对象被序列化后,他的编码是计算机都可以识别的二进制字节序列,它可以在网络中传输,也可以存储到磁盘,再需要使用这些对象时候,可以通过反序列化创建这些对象(新对象)。

实现Serializable接口

想要一个类的实例可以被序列化,可以简单的实现Serializable接口即可。但需要非常谨慎,一旦实现了该接口,并对外发布(比如发布一个RPC接口),就需要对该类后续维护负责,服务端对该类的任何修改,都有可能影响到客户端。根据以前分享的java序列化和反序列化原理,可以看出如果类的描述信息改变,但是字节序列还是以前的,就会导致反序列化失败。

为了保持兼容,我们可以添加一个流的唯一标识符serialVersionUID,其值为任意的long型。如果不显示的指定serialVersionUID的值,系统会根据这个类的信息调用一个复杂的运行获取(消耗性能):根据类的路径、成员变量信息等。在类中做了任何修改,都会导致InvalidClassException序列化失败。

不管选择哪种序列化形式,为自己编写的每个可序列化类声明一个显示的序列版本是非常必要的。不仅可以提升一点性能,还可以一定程度上保持对老版本的兼容。一般可以通过IDE工具自动生成serialVersionUID,但其实任意的一个非0 long型值都可以,比如1L,-1L,2L等等。当类发生改变时,如果希望兼容以前的版本,则不改变serialVersionUID的值,否则改为任意其他long型值即可。

1、第一个测试,我们还是使用以前User类的例子,但是我们在User类中新增了一个address字段:
Java代码  
package com.sky.serial;  
  
import java.io.*;  
  
/**
* Created by gantianxing on 2017/5/26.
*/
  
public class User implements Serializable {  
  
    //可以用eclipse生成, 也可以随意指定一个非0的值  
    private static final long serialVersionUID = 1L;  
  
    private final String name;//姓名  
  
    private final int sex;//性别0-女 1-男  
  
private String phoneNum;//手机号  
  
private String address;//地址  
  
    public User(String name,int sex){  
        this.name = name;  
        this.sex = sex;  
}  
  
public String getAddress() {  
        return address;  
    }  
  
    public void setAddress(String address) {  
        this.address = address;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public int getSex() {  
        return sex;  
    }  
  
    public String getPhoneNum() {  
        return phoneNum;  
    }  
  
    public void setPhoneNum(String phoneNum) {  
        this.phoneNum = phoneNum;  
    }  
  
    @Override  
    public String toString(){  
        return  
"user info: name="+name+",sex="+sex+",phoneNum="+phoneNum+",address="+address;  
    }  
}  

  UserDemo类中的反序列化使用的字节数组还是以前的内容(不包含address描述信息)。

Java代码  


package com.sky.serial;  
  
import java.io.ByteArrayInputStream;  
import java.io.FileInputStream;  
import java.io.InputStream;  
import java.io.ObjectInputStream;  
  
/**
* Created by gantianxing on 2017/5/29.
*/
  
public class UserDemo {  
  
    private static final byte[] serialByteArray = new byte[]{  
            (byte)0xAC,(byte)0xED,0x00,0x05,0x73,0x72,0x00,0x13,0x63,0x6F,0x6D,0x2E,0x73,0x6B,0x79,0x2E  
            ,0x73,0x65,0x72,0x69,0x61,0x6C,0x2E,0x55,0x73,0x65,0x72,0x00,0x00,0x00,0x00,0x00  
            ,0x00,0x00,0x01,0x02,0x00,0x03,0x49,0x00,0x03,0x73,0x65,0x78,0x4C,0x00,0x04,0x6E  
            ,0x61,0x6D,0x65,0x74,0x00,0x12,0x4C,0x6A,0x61,0x76,0x61,0x2F,0x6C,0x61,0x6E,0x67  
            ,0x2F,0x53,0x74,0x72,0x69,0x6E,0x67,0x3B,0x4C,0x00,0x08,0x70,0x68,0x6F,0x6E,0x65  
            ,0x4E,0x75,0x6D,0x71,0x00,0x7E,0x00,0x01,0x78,0x70,0x00,0x00,0x00,0x01,0x74,0x00  
            ,0x09,0x7A,0x68,0x61,0x6E,0x67,0x20,0x73,0x61,0x6E,0x74,0x00,0x0B,0x31,0x33,0x38  
            ,0x38,0x38,0x38,0x38,0x38,0x38,0x38,0x38  
    };  
  
  
    public static void main(String[] args) throws Exception{  
  
        //拼装字节序列  
        InputStream is = new ByteArrayInputStream(serialByteArray);  
        ObjectInputStream in = new ObjectInputStream(is);  
  
        //反序列化  
        Object newObj = in.readObject();  
        System.out.println(newObj.getClass());  
  
        //判断是否为User类型  
        if(newObj instanceof User){  
            User newUser = (User)newObj;  
            System.out.println("user name:" + newUser.getName());  
            System.out.println(newUser);  
        }  
    }  
}  

  本次我们模拟的是,User类新增了address成员 但suid不变,字节数组采用User类变更前的内容(不含address成员信息),执行UserDemo的main方法,结果如下:
Java代码  
class com.sky.serial.User  
user name:zhang san  
user info: name=zhang san,sex=1,phoneNum=13888888888,address=null  


我们可以发现是兼容成功的。

2、第二测试,我们将User类中的suid注释掉,其他内容保持不变,重新执行执行UserDemo的main方法,结果如下:
Java代码  


Exception in thread "main" java.io.InvalidClassException: com.sky.serial.User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = -8432599453169852227  
         at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)  
         at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)  
         at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)  
         at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)  
         at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)  
         at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)  
         at com.sky.serial.UserDemo.main(UserDemo.java:32)  
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)  
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)  
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)  
         at java.lang.reflect.Method.invoke(Method.java:497)  
         at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)  

  可以发现,如果User类中没有显示的定义suid,系统自动生成了一个suid=-8432599453169852227,但与我们字符数组中的suid不匹配,抛出InvalidClassException异常。

3、第三个测试,将User类中的suid改为2L 即:private static final long serialVersionUID = 2L; 重新执行其他内容保持不变,重新执行执行UserDemo的main方法,结果如下:
Java代码  


Exception in thread "main" java.io.InvalidClassException: com.sky.serial.User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2  
         at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)  
         at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)  
         at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)  
         at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)  
         at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)  
         at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)  
         at com.sky.serial.UserDemo.main(UserDemo.java:32)  
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)  
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)  
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)  
         at java.lang.reflect.Method.invoke(Method.java:497)  
         at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)  

可见版本号变更,会导致不新老版本不兼容,抛出InvalidClassException异常。当然这也有可能是根据业务故意为之。

通过上述3个测试,说明在类内部成员发生改变后(注意不是类的类型改变),suid可以根据业务来选择是否需要兼容老版本。

自定义序列化:writeObject和readObject方法


通过前两篇关于java序列化和反序列化的实现过程,我们看到java默认的序列化过程是一个相对比较复杂的过程:在序列化过程中会解析对象的这个关系拓扑图逐一进行序列化;同样反序列化也需要从字节流中解析出整个关系拓扑图逐一进行反序列化。实际上默认的序列化描述了该类内部所有包含的数据,以及每个可以从这个对象到达的其他对象的内部数据结构。

Jdk的程序员们为了大家能以最简单的方式实现序列化化,他们确实做到了--直接实现Serializable接口即可,但由于每个待序列化类的情况不同,要覆盖各种情况并且保证没有问题,相对复杂一些也在所难免。

对于一些简单的类,采用默认的序列化方式也是推荐的,因为它没有复杂的对象关系拓扑结构。

在阅读jdk源码时,我们可以看到java大神们在需要对一个复杂类做序列化处理时,都是通过编写writeObject和readObject方法来实现自定义序列化规则(ArrayList Hashmap HashSet等等),而不是采用默认的序列化方法。这样做可以降低性能消耗的同时,还可以减少序列化字节流的大小,从而减少网络开销(RPC框架中)。

这里我们还是以jdk中的HashSet为列对自定义序列化进行讲解(关于HashSet可以看我以前这篇文章),我们把HashSet中与序列化不相关的内容都去掉,最终如下:
Java代码  


public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {  
  
    static final long serialVersionUID = -5024744406713321676L;  
    private transient HashMap<E, Object> map;  
    private static final Object PRESENT = new Object();  
  
    private void writeObject(ObjectOutputStream var1) throws IOException {  
        var1.defaultWriteObject();  
        var1.writeInt(this.map.capacity());  
        var1.writeFloat(this.map.loadFactor());  
        var1.writeInt(this.map.size());  
        Iterator var2 = this.map.keySet().iterator();  
  
        while(var2.hasNext()) {  
            Object var3 = var2.next();  
            var1.writeObject(var3);  
        }  
  
    }  
  
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {  
        var1.defaultReadObject();  
        int var2 = var1.readInt();  
        if(var2 < 0) {  
            throw new InvalidObjectException("Illegal capacity: " + var2);  
        } else {  
            float var3 = var1.readFloat();  
            if(var3 > 0.0F && !Float.isNaN(var3)) {  
                int var4 = var1.readInt();  
                if(var4 < 0) {  
                    throw new InvalidObjectException("Illegal size: " + var4);  
                } else {  
                    var2 = (int)Math.min((float)var4 * Math.min(1.0F / var3, 4.0F), 1.07374182E9F);  
                    this.map = (HashMap)(this instanceof LinkedHashSet ?new LinkedHashMap(var2, var3):new HashMap(var2, var3));  
  
                    for(int var5 = 0; var5 < var4; ++var5) {  
                        Object var6 = var1.readObject();  
                        this.map.put(var6, PRESENT);  
                    }  
  
                }  
            } else {  
                throw new InvalidObjectException("Illegal load factor: " + var3);  
            }  
        }  
    }  
}  

  可以看到有三个成员变量
1、Object PRESENT:这个成员是被static修饰的,属于类,不属于对象,是不会被序列化的。这里谈的是序列化,直接忽略不做讨论。
2、serialVersionUID:jdk为了保证版本兼容,各个版本的HashSet的serialVersionUID应该都是相同的。关于suid上一节已经讲过,这里不在累述。
3、HashMap<E, Object> map:该成员是被transient修饰的,被transient关键字修饰的变量不再能被序列化,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。也可以认为在将持久化的对象反序列化后,被transient修饰的变量将按照普通类成员变量一样被初始化。

问题来了,HashSet实现了Serializable接口,Josh Bloch大神是希望它能被序列化的,但是现在他的三个成员,只有HashMap类型的成员是应该被序列化的,但是确被transient修饰,也就是说HashSet里已经没有成员可以被序列化了(所谓的序列化 都是针对非静态非transient成员变量,不针对方法)。

详细大家也看到了,HashMap类型的成员变量,其实是在其定义writeObject和readObject方法中实现的。也就是说被transient修饰的成员,只是不能被默认的序列化方法序列化(从源码中也可以看到),但却可以被自定义的序列化方法序列化。

writeObject方法流程:不是直接序列化的map对象,而只是序列化了map对象的三个关键成员:容量cap、增长因子factor、map大小size,然后对HashMap中的每个成员对象进行序列化化。可以看到通过这种序列化方法,只对HashMap的逻辑数据进行了序列化,相对于默认的序列化过程,该过程不再维护HashSet的整个对象关系。那这个关系由谁来维护呢?答案就是readObject方法。

readObject方法流程:按顺序读取HashMap对象的cap、factor、size,通过这三个参数就可以调用HashMap的构造方法生成一个新的map对象(这里的readObject方法又重新计算了cap),然后再逐一读取字节流中的对象,并放入到新的map中。可以看到这个过程比默认的反序列化过程更简单高效。

从这里也可以看到:
1、对于自定义序列化在保证业务正常的情况下 不必序列化对象的完整关系,通过writeObject和readObject两个方法的默契配合来完成这种关系。这个关系只有开发这个序列化类的程序员自己最清楚,由我们自己维护(jdk的大神只能维护一个大而全的处理逻辑)。

2、对于非关键成员,不必一成不变的还原,比如这里的cap容量,在readObject方法就进行了重新调整,重新计算一个合适的值。

3、对希望采用自定义序列化的字段用transient修饰,然后在先调用writeObject和readObject方法中对transient修饰的字段进行序列化,并在方法最开始调用defaultReadObject和defaultReadObject方法对其他字段采用默认序列化方式。这样的好处是方便兼容,比如HastSet在以后的版本中加入某个新成员,也可以被默认序列化方法序列化。

关于注解和命名模式

注解:简单的将就是类似@Override这种,如果方法定义写错,直接编译就无法通过。
命名模式:通过约定的规则命名,否则框架无法识别。编译期也不会报错,只有在运行时才能发现。Effecttive java建议注解优先于命名模式

不幸的是writeObject和readObject是一种命名模式,而非@Override注解。必须以固定的形式定义(如下),否则序列化框架无法识别,导致自定义序列化失败(尤其注意riteObject和readObject的大小写)。
Java代码  


private void writeObject(ObjectOutputStream var1) throws IOException{}  
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException{}

    也许你还会问这是私有方法,是怎么被访问到的。答案是反射,反射可以绕开权限限制,这两个方法设置为私有,其实是专门为序列化框架使用的。

自定义序列化:实现Externalizable接口

还有一种自定义序列化方式,就是实现Externalizable接口,Externalizable接口本质上是继承的Serializable接口,只是多加了两个方法,我们先看下Externalizable接口定义:
Java代码  


public interface Externalizable extends java.io.Serializable {  
    
void writeExternal(ObjectOutput out) throws IOException;  
  
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;  
}  

  相对于定义writeObject和readObject方法,这种方式实现自定义序列化有一个好处就是 必须强制实现writeExternal和readExternal方法,而且是基于@Override注解的,防止出现写错的问题。但他存在局限,后面我们再说,先看一个列子:
Java代码  


public class UserExternal implements Externalizable{  
  
    //可以用eclipse生成, 也可以随意指定一个非0的值  
    private static final long serialVersionUID = 2L;  
  
    private String name;//姓名  
  
    private int sex;//性别0-女 1-男  
  
    private String phoneNum;//手机号  
  
  
    public UserExternal(String name, int sex){  
        this.name = name;  
        this.sex = sex;  
    }  
  
    public UserExternal(){  
        System.out.println("默认构造方法被调用");  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public int getSex() {  
        return sex;  
    }  
  
    public String getPhoneNum() {  
        return phoneNum;  
    }  
  
    public void setPhoneNum(String phoneNum) {  
        this.phoneNum = phoneNum;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public void setSex(int sex) {  
        this.sex = sex;  
    }  
  
    @Override  
    public String toString(){  
        return "user info: name="+name+",sex="+sex+",phoneNum="+phoneNum;  
    }  
  
    @Override  
    public void writeExternal(ObjectOutput out) throws IOException {  
        out.writeObject(getName());  
        out.writeInt(getSex());  
        out.writeObject(getPhoneNum());  
    }  
  
    @Override  
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
        setName((String)in.readObject());  
        setSex(in.readInt());  
        setPhoneNum((String)in.readObject());  
    }  
  
    public static void main(String[] args) throws Exception{  
        //step 1 序列化  
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D://user.txt"));  
        UserExternal user = new UserExternal("zhang san",1);  
        user.setPhoneNum("13888888888");  
        out.writeObject(user);  
  
        //step2 反序列化  
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D://user.txt"));  
        Object newUser = in.readObject();  
        System.out.println(newUser);  
    }  
}  

  重写writeExternal(readExternal)方法,相对于定义writeObject(readObject)相差不大,但也有细微的差别,前者的参数是ObjectOutPut(ObjectInPut),而后者是ObjectOutputStream(ObjectInputStream)。也就是前者不能调用默认的序列化方法,需要进行序列化的字必须在writeExternal(readExternal)方法中进行自定义序列化,否则对应的成员变量就不会被序列化化。可以做个测试,把writeExternal、readExternal方法内容清空,执行以上程序打印的信息为:user info: name=null,sex=0,phoneNum=null。说明成员变量没有被序列化。

从另一个角度讲,在实现Externalizable接口的成员变量里用transient修饰,已经失去了意义,加不加transient没有任何变化(对于没有在writeExternal、readExternal方法序列化的成员其实就想到于transient了)。

简单的说实现writeExternal接口是全自定义序列化,定义writeObject(readObject)方法可以实现半自定义化,具有更好的扩展性。比如jdk源码中基本都是使用定义writeObject(readObject)实现自定义序列化。

在来看下实现writeExternal接口的局限:
1、实现writeExternal接口的类,必须包含默认的构造函数,可以在反序列化的源码可以看到,实现writeExternal接口的反序列化是通过调用对象的默认构造方法创建的对象。这里可以做个测试,把UserExternal类的默认构造方法注释掉,重新执行main方法会报InvalidClassException no valid constructor异常。

2、成员变量不能是final且没有初始值(必须在构造方法中赋值,但默认构造方法是无参的,无法为final变量动态赋值)。

3、也就是上面提到的 实现writeExternal接口后 就无法使用默认的序列化方法。

4、会导致定义writeObject和readObject方法失效,看下序列化的源码:

Java代码  
序列化逻辑  
if (desc.isExternalizable() && !desc.isProxy()) {  
                writeExternalData((Externalizable) obj); // Externalizable执行逻辑  
            } else {  
                writeSerialData(obj, desc); //默认以及writeObject和readObject方法执行逻辑  
            }  
反序列化逻辑  
if (desc.isExternalizable()) {  
            readExternalData((Externalizable) obj, desc); // Externalizable执行逻辑  
        } else {  
            readSerialData(obj, desc); //默认以及writeObject和readObject方法执行逻辑  
        }  


可以看到只要判断是Externalizable,就不会再执行默认以及writeObject和readObject方法执行逻辑。

当然第四点也不能完全说是局限,也是为了性能考虑,去掉不必要的开销。




程序猿的技术大观园:www.javathinker.net
  Java面向对象编程-->内部类
  JavaWeb开发-->使用过滤器
  JSP与Hibernate开发-->持久化层的映射类型
  Java网络编程-->Java反射机制
  精通Spring-->通过Vuex进行状态管理
  Vue3开发-->绑定CSS样式
  java的三种随机数生成方式
  Java函数式接口和Stream流
  NIO模式的IO多路复用底层原理
  从实战角度解读JVM:类加载机制+JVM调优实战+代码优化!
  Java如何遍历Enumeration
  Java关键字final、static使用总结
  Eclipse和MyEclipse的区别
  Java注解的定义和使用
  Synchronized与ReentrantLock区别总结
  使用javaNIO实现C/S模式的通信
  redis持久化问题处理
  Java入门实用代码:获取当前线程名称
  Java入门实用代码:自定义异常
  Java入门实用代码:字符串优化测试
  Java线程实现龟兔赛跑
  更多...
 IPIP: 已设置保密
楼主      
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。