java序列化与反序列化

1.什么是序列化和反序列化?

  • 序列化: 将java对象转换成字节序列的过程
  • 反序列化:将在序列化过程中所生成的字节序列转换成java对象的过程

在实际生活中,需要将对象持久化,需要的时候再重新读取出来,通过对象序列化,可以将对象的状态保存为字节数组,需要的时候再将字节数组反序列化为对象。此外,两个进程行行远程通信时,需要互相发送各种类型的数据,比如图片、视频、音频、文字等等,这些数据都是以二进制序列的形式在网络上传送。那java进程之间的通信也是如此。通过java序列化和反序列化来实现。即把这个java对象转换为字节序列,然后在网络上传送,接收方再把字节序列恢复为java对象的过程。

2.如何实现Java序列化与反序列化?

前提:实现了Serializable或Externalizable接口的类的对象才能被序列化。否则抛出NotSerializableException异常。

有以下三种方式:

  1. 实现了Serializable接口。使用ObjectOutputStream和ObjcetInputStream对对象的非transient实例变量进行序列化和反序列化。

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.io.Serializable;  
/**  
 * @author 旺仔牛奶 
 * 2017年8月30日  
 */  
public class ApplyTransient {  
  
    public static void main(String[] args) {  
        User user = new User(); 
        user.setUserPhone("1234567891"); 
        user.setUserName("旺仔牛奶");  
        user.setPasswd("caka");  
          
        System.out.println("Before Serializable: ");  
        System.out.println("username: " + user.getUserName());
        System.out.println("userPhone: " + user.getUserPhone());
        System.err.println("password: " + user.getPasswd());  
        //序列化操作  
        try {  
           ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("E:/wangzai.txt"));  
            os.writeObject(user); // 将User对象写进文件  
            os.flush();//刷新流到文件  
            os.close();//关闭流  
        } catch (FileNotFoundException e) {  
            e.printStackTrace();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        try {  
            //反序列化操作  
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("E:/wangzai.txt"));  
            user = (User) is.readObject(); // 从流中读取User的数据  
            is.close();//关闭流  
              
            System.out.println("\nAfter Serializable: ");  
            System.out.println("username: " + user.getUserName()); 
            System.out.println("userPhone: " + user.getPhone());
            System.err.println("password: " + user.getPasswd());  
              
        } catch (FileNotFoundException e) {  
            e.printStackTrace();  
        } catch (IOException e) {  
            e.printStackTrace();  
        } catch (ClassNotFoundException e) {  
            e.printStackTrace();  
        }  
    }  
}  
//User实体类  
class User implements Serializable {  
    private static final long serialVersionUID = 8294180014912103005L;    
      
    public String userPhone;
    public static String userName;  
    private transient String passwd;  
      
    public String getUserName() {  
        return userName;  
    }  
      
    public void setUserName(String userName) {  
        this.userName = userName;  
    }  
    
    public String getUserPhone() {  
        return userPhone;  
    }  
      
    public void setUserPhone(String userPhone) {  
        this.userPhone = userPhone;  
    } 
    
    public String getPasswd() {  
        return passwd;  
    }  
      
    public void setPasswd(String passwd) {  
        this.passwd = passwd;  
    }  
}  

执行结果:

Before Serializable: 
username: 旺仔牛奶
userPhone: 1234567891
password: caka

After Serializable: 
username: 旺仔牛奶的牛奶
userPhone: 1234567891
password: null
问题:password反序列化的值为什么为空?username反序列化后的值为什么被临时改变的值替换?

上面两个问题涉及到java序列化需要注意的两个点:

  • 静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也就是它的成员变量,因此序列化不会关注静态变量)
  • transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null)

既然静态成员变量不属于对象,那为什么还能在反序列化之后得到它的值?

答案是因为username取出的值不是反序列化得到的而是JVM中得到的,正如上例,如果是反序列化得到的,那么临时修改username的值是不会影响反序列化取得的值的。但username的值却被临时改变的值替换掉了,所以并不与第一个注意点冲突,username的值能被取出是因为在JVM中存放。

     2.实现了Serializable接口,并且还定义了private类型的writeObject()和readObject()。

  • 如果需要默认序列化方法或者默认反序列化方法,则可以在这两个方法中调用ObjectOutputStream的defaultWriteObject()方法或者ObjectOutputStream的defaultReadObject()方法。也就是上列中默认的序列化方法。
  • 如果需要用户自定义序列化方式,从而控制序列化的行为。则使用ObjectOutputStream调用对象的writeObject(ObjectOutputStream out)方法进行序列化。用ObjectInputStream调用对象的readObject(ObjectInputStream in)进行反序列化。

   例子如下:

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
/** 
 * @description 
 * @author 旺仔牛奶 
 * 2017年8月30日  
 */  
public class ApplyTransient implements Serializable {  
    /**
     * 生成序列号标识
     */
    private static final long serialVersionUID = -4083503801443301445L;
    private int id;
    private String name;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    /**
     * 序列化时,
     * 首先系统会先调用writeReplace方法,在这个阶段,
     * 可以进行自己操作,将需要进行序列化的对象换成我们指定的对象.
     * 一般很少重写该方法
     * @return
     * @throws ObjectStreamException
     */
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace invoked");
        return this;
    }
    /**
     *接着系统将调用writeObject方法,
     * 来将对象中的属性一个个进行序列化,
     * 我们可以在这个方法中控制住哪些属性需要序列化.
     * 这里只序列化name属性
     * @param out
     * @throws IOException
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        System.out.println("writeObject invoked");
        out.writeObject(this.name == null ? "旺仔牛奶" : this.name);
    }
    /**
     * 反序列化时,系统会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,
     * 反序列化回来.然后通过readResolve方法,我们也可以指定系统返回给我们特定的对象
     * 可以不是writeReplace序列化时的对象,可以指定其他对象.
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException {
        System.out.println("readObject invoked");
        this.name = (String) in.readObject();
        System.out.println("got name:" + name);
    }
    /**
     * 通过readResolve方法,我们也可以指定系统返回给我们特定的对象
     * 可以不是writeReplace序列化时的对象,可以指定其他对象.
     * 一般很少重写该方法
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException {
        System.out.println("readResolve invoked");
        return this;
    }
}  

3.实现了Externalnalizable接口,且必须实现public类型的readExternal()和writeExternal()方法。

ObjectOutputStream调用对象的writeExternal(ObjectOutput out))的方法进行序列化。ObjectInputStream会调用对象的readExternal(ObjectInput in)的方法进行反序列化。

举例如下:

import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

/** 
 * @author 旺仔牛奶 
 * 2017年8月30日  
 */  
public class ApplyTransient implements Externalizable {  
        private static final long serialVersionUID = -4083503801443301445L;
        
        private transient String content = "旺仔牛奶";
        
        @Override public void writeExternal(ObjectOutput out) throws IOException {
            
            out.writeObject(content);
        } 
        
        @Override public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException { 
            content = (String) in.readObject(); 
        } 
        
        public static void main(String[] args) throws Exception {
            
            ApplyTransient applyTransientt = new ApplyTransient(); 
            //序列化 
            ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File("wangzai"))); 
            out.writeObject(applyTransientt); 
            //反序列化
            ObjectInput in = new ObjectInputStream(new FileInputStream(new File("wangzai"))); 
            applyTransientt = (ApplyTransient) in.readObject(); 
            System.out.println(applyTransientt.content); 
            out.close(); in.close(); 
     } 
}
注意:

上面的三个例子中都显式的申明了serialVersionUID(唯一的序列版本号),每个可序列化的类都有一个唯一标识号与其相关联。
如果没有显式的指定私有静态final的long类型的serialVersionUID,系统会自动生成一个标识号(会受类名、接口名、成员的影响而发生变化)。如果一旦改变了这些信息,自动生成的序列版本会发生变化。因此如果没有显示声明,兼容性会遭到破坏,导致运行时出现InvalidClassException异常。所以,强烈建议所有可序列化类都显式声明serialVersionUID

显式地定义serialVersionUID有两种用途:

1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

2)在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID

以上三种例子可见,对象序列化和反序列化的步骤如下:
对象序列化步骤:
1.创建一个对象输出流
2.通过对象输出流的writeObject(Objectobj)方法写对象
如:
ObjectOutputStream out = newObjectOutputStream(new FileOutputStream(filePath));//filePath:文件路径
out.writeObject(obj);//obj:要写入文件的对象

对象反序列化步骤:
1.创建一个对象输入流
2.通过对象输入流的readObject(j)方法读取对象.(读取对象时是按照对列方式进行的,既先进先出)
如:
ObjectInputStream  in = new ObjectInputStream (newFileInputStream(filePath));
in.readObject();//读取对象

4.Java实现几种序列化方式

Java实现几种序列化方式包括Java原生以流的方法进行的序列化、Json序列化、FastJson序列化、Protobuff序列化。

  • Java原生序列化方法即通过Java原生流(InputStream和OutputStream之间的转化)的方式进行转化。需要注意的是JavaBean实体类必须实现Serializable接口,否则无法序列化。比如上面的第一个例子。
  • Json序列化一般会使用jackson包,通过ObjectMapper类来进行一些操作,比如将对象转化为byte数组或者将json串转化为对象。比如调用一个服务器接口,通常的请求为xxx.json?a=xxx&b=xxx的形式。
  • fastjson序列化是由阿里开发的一个性能很好的Java 语言实现的 Json解析器和生成器。完全支持java bean、集合、Map、日期、Enum,支持范型和自省。无依赖,使用时候需引入FastJson第三方jar包。
  • ProtocolBuffer序列化是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化。适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。




发布了33 篇原创文章 · 获赞 51 · 访问量 9万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章