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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章