首頁 > 軟體

Java基礎入門總結之序列化和反序列化

2022-02-28 10:00:38

基本概念

  • Java中建立物件時,一旦程式終止,建立的物件可能就不存在.要想使得物件能夠在程式不執行的狀態下依然能夠儲存物件的資訊,這時就需要用到序列化機制
  • 序列化機制:
    • 一個物件可以被表示為一個位元組序列,包括:
      • 物件的資料
      • 物件的型別資訊
      • 儲存在物件中的資料型別
    • 將可序列化物件寫入檔案後,可以從檔案中讀取出來,根據物件的各種資訊在記憶體中建立該物件. 這裡的讀取並建立物件的過程就是反序列化
    • 序列化和反序列化的整個過程都是JVM獨立的.也就是說,在一個JVM中的序列化物件可以在另一個完全不同的JVM中反序列化物件
    • 一般情況下,序列化需要實現java.io.Serializable介面,使用ObjectInputStreamObjectOutputStream進行物件的讀寫操作
    • 還可以實現java.io.Externalizable介面,進行標準的序列化或者自定義的二進位制格式.用來滿足不同場景下的需求
  • Java序列化場景:
    • Java物件的位元組序列持久化到硬碟中
    • 在網路上傳輸物件的位元組序列
    • 進行遠端方法呼叫RMI(Remote Method Invocation)
    • JVM執行結束時,還需要使用建立的物件
    • 需要將建立的物件儲存下來以便後續的傳輸
    • 使得舊JVM建立的物件能夠在一個新的JVM中執行
  • Java序列化注意點:
    • 物件的序列化儲存的是物件成員變數物件,物件的序列化不會關注類中的靜態變數
    • 類的序列化要保證類的所有屬性都是可以序列化的,如果想要某個屬性不被序列化.可以宣告為瞬時態transient

序列化

  • Java物件序列化:
    • 使得可序列化的物件實現Serializable介面
    • 建立一個ObjectOutputStream輸出流
    • 呼叫ObjectOutputStream物件的writeObject() 方法進行輸出可序列化物件即可
  • 序列化範例:
public class Person implements Serializable {
	private String name;
	private int age;

	public Person() {
		System.out.println("無參構造...");
	}

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
		System.out.println("有參構造...");
	}

	@Override
	public String toString() {
		return "Person{" +
				"name='" + name + "'" +
				", age='" + age + "'"
				"}";
	}
}
public class SerializableTest {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Person person = new Person("Lily", 20);

		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Person.txt"));
		oos.writeObject(person);
		oos.close();
	}
}

反序列化

  • Java物件反序列化:
    • 建立一個ObjectInputStream輸入流
    • 呼叫ObjectInputStream物件的readObject() 方法得到序列化物件
public class  DeserializableTest {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Person.txt"));
		Pesron person = (Person)ois.readObject(ois);
		System.out.println(person);
	}
}
  • 反序列化的物件是由JVM生成的物件,而不是通過類別建構函式生成的:
    • 反序列化物件時 ,JVM中要存在物件對應的類,否則會丟擲ClassNotFoundException異常
    • 如果一個可序列化的類的成員不是基本型別,而是一個參照型別時,那麼這個參照型別必須實現Serializable介面,否則會丟擲NotSerializableException異常

序列化和反序列化總結

  • 實現Serializable介面就可以進行序列化的原因:
    • writeObject():
      • 首先會處理之前被編寫的以及不可替換的物件
      • 如果有的物件被替換了,則檢查被替換的物件
      • 最後如果物件都被替換了,則進行原始的檢查
      • 原始的檢查即檢查被替換的物件型別是否為String型別,陣列型別 ,Enum型別或者實現了Serializable介面,符合條件就可以對檢查的物件執行相應的序列化操作,否則將會丟擲NotSerializableException異常
    • 對於序列化的機制來說,如果對同一個物件執行多次序列化操作時,不會得到多個物件
      • 儲存到磁碟的物件都有一個序列化編號,當程式試圖進行序列化時,會檢查該物件是否已經序列化
      • 只有物件從未被序列化過時,才會將此物件序列化為位元組序列,如果物件已經序列化過,那麼直接輸出序列化編號
      • Java序列化機制不會重複序列化同一個物件,會記錄已經序列化物件的編號,此時如果序列化了一個可變物件後,如果更改了物件的內容會再次進行序列化.如果沒有更改內容,則不會將此物件轉換為位元組序列,只會儲存序列化編號
  • 實現Serializable介面時可以重寫writeObject() 方法和readObject() 方法:
    • 重寫writeObject() 方法和readObject() 方法後,物件進行序列化和反序列化時,就會自動呼叫重寫的writeObject() 方法和readObject() 方法
  • 實現Externalizable介面時可以重寫writeExternal() 方法和readExternal() 方法:
    • 重寫writeExternal() 方法和readExternal() 方法後,物件進行序列化和反序列化時,就會自動呼叫重寫的writeExternal() 方法和readExternal() 方法

自定義序列化策略

Externalizable

  • 如果需要使得物件的一部分可以被序列化,另一部分資料不被序列化,此時可以自定義實現Externalizable介面,並且實現writeExternal()readExternal() 方法,可以在序列化和反序列化過程中自動呼叫來執行一些特殊的操作
  • 注意點:
    • Serializable介面實現的物件是與二進位制的構建有關的,不會呼叫構造器
    • Externalizable介面實現的物件的所有建構函式都會被呼叫,所以要編寫出類的無參和有參建構函式
  • 使用Externalizable自定義序列化範例:
public class CustomExternal implements Externalizable {
	
	private String name;
	
	private int code;

	public CustomExternal() {
		System.out.println("無參構造...");
	}
	
	public CustomExternal(String name, int code) {
		this.name = name;
		this.code = code;
		System.out.println("有參構造...");
	}

	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("執行writeExternal()方法...");
		out.writeObject(name);
		out.writeInt(code);
	} 

	@Override
	public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
		System.out.println("執行readExternal()方法...");
		name = (String) in.readObject();
		code = in.readInt();
	}

	@Override
	public String toString() {
		return "類:" + name + code;
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		CustomExternal custom = new CustomeExternal("oxford", 666);
		System.out.println(custom);
		
		// 序列化
		ObjectOutputStream out = new ObjectOutputStream(new FileInputStream("oxford.txt"));
		System.out.println("序列化物件...");
		out.writeObject(custom);
		out.close();

		// 反序列化
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("oxford.txt"));
		System.out.println("反序列化物件...");
		custom = (CustomExternal) in.readObject();
		System.out.println(custom);
		in.close();
	}
}

有參構造...
類:oxford666
序列化物件...
執行writeExternal()方法...
反序列化物件...
無參構造...
執行readExternal()方法...
類:oxford666 

  • 使用Externalizable自定義序列化時,為了保證序列化和反序列化的正確性,需要在writeExternal() 方法中將資訊寫入,並且在readExternal() 方法中恢復資料

transient

  • 可以使用transient關鍵字設定一些重要的資訊比如密碼等不進行序列化
    • transient關鍵字修飾的屬性不會參與到序列化過程中
    • transient關鍵字修飾的屬性在反序列化過程中,如果是參照資料型別,則返回null. 如果是基本資料型別,則返回預設值.不一定是基本資料型別序列化之前的值
  • 因為實現Externalizable介面的物件預設情況下不會儲存任何欄位,所以transient關鍵字只能和Serializable物件一起使用
  • transient關鍵字的使用場景:
    • 伺服器端給使用者端傳送序列化物件資料時,物件中存在敏感資料
    • 比如密碼字串,在序列化時進行加密,使用者端擁有解密的金鑰,只有在使用者端進行反序列化時,才會對密碼進行讀取
    • 這時就可以對密碼字串物件使用transient修飾,這樣可以一定程度上保證序列化物件的資料安全

靜態變數

  • 序列化時不會序列化靜態變數
    • 靜態變數屬於類的狀態,序列化中儲存的是物件,也就是類的範例的狀態
    • 序列化操作的是序列化中物件,也就是類的範例的狀態,靜態變數屬於類的狀態.所以序列化時不會對靜態變數進行序列化

序列化ID

  • Java虛擬機器器進行反序列化:
    • 兩個類的類路徑和功能程式碼一致
    • 兩個類的序列化ID,也就是serialVersionUID一致
  • 功能程式碼一致:
    • 序列化的類和反序列化的類所實現的功能和功能相關的程式碼是一樣的
    • 範例:
      • 使用者端A將類物件序列化使用者端B, 使用者端B進行反序列化
      • 這時要求AB都有這樣的一個類檔案,功能程式碼一致,並且都實現了Serializable介面
  • serialVersionUID的兩種生成方式:
    • 預設值 1L
    • 通過類名,介面名和方法名以及屬性隨機生成的一個不重複的long型別的值
  • 在序列化ID,serialVersionUID相同的情況下,即使序列化物件的序列化屬性修改,序列化物件也可以進行反序列化.因此如果只是修改了方法或者修改了靜態變數或transient變數,只要不修改序列化ID, 那麼反序列化就不會受到影響
  • 顯式宣告序列化ID,serialVersionUID的場景:
    • 如果需要類的不同版本對序列化相容,要確保類的不同版本具有相同的serialVersionUID
    • 如果不需要類的不同版本對序列化相容,要確保類的不同版本具有不同的serialVersionUID
    • 序列化一個類的範例後,如果修改一個欄位或者增加一個欄位,如果沒有設定類的serialVersionUID, 就會導致無法反序列化舊的範例,會在反序列化時丟擲異常
    • 序列化類新增SerialVersionUID後,如果修改一個欄位或者增加一個欄位,反序列化舊的範例時,修改的或者增加的欄位的值會設定為初始化的值

破壞單例

  • 除了反射可以破壞單例模式外,序列化和反序列化後會得到一個新的物件,也可以破壞單例模式
  • 序列化和反序列化破壞單例模式:
    • 反序列化時,使用ObjectInputStream物件中的readObject() 方法
    • readObject() 的方法中呼叫readObject0() 方法
        public final Object readObject()
          throws IOException, ClassNotFoundException
      {
          if (enableOverride) {
              return readObjectOverride();
          }
          
          int outerHandle = passHandle;
          try {
              Object obj = readObject0(false);
              handles.markDependency(outerHandle, passHandle);
              ClassNotFoundException ex = handles.lookupException(passHandle);
              if (ex != null) {
                  throw ex;
              }
              if (depth == 0) {
                  vlist.doCallbacks();
              }
              return obj;
          } finally {
              passHandle = outerHandle;
              if (closed && depth == 0) {
                  clear();
              }
          }
      }
    • readObject0() 方法中會返回一個checkResolve(readOrdinaryObject(unshared))
        private Object readObject0(boolean unshared) throws IOException {
          boolean oldMode = bin.getBlockDataMode();
          if (oldMode) {
              int remain = bin.currentBlockRemaining();
              if (remain > 0) {
                  throw new OptionalDataException(remain);
              } else if (defaultDataEnd) {
                  throw new OptionalDataException(true);
              }
              bin.setBlockDataMode(false);
          }
    
          byte tc;
          while ((tc = bin.peekByte()) == TC_RESET) {
              bin.readByte();
              handleReset();
          }
    
          depth++;
          totalObjectRefs++;
          try {
              switch (tc) {
                  case TC_NULL:
                      return readNull();
    
                  case TC_REFERENCE:
                      return readHandle(unshared);
    
                  case TC_CLASS:
                      return readClass(unshared);
    
                  case TC_CLASSDESC:
                  case TC_PROXYCLASSDESC:
                      return readClassDesc(unshared);
    
                  case TC_STRING:
                  case TC_LONGSTRING:
                      return checkResolve(readString(unshared));
    
                  case TC_ARRAY:
                      return checkResolve(readArray(unshared));
    
                  case TC_ENUM:
                      return checkResolve(readEnum(unshared));
    
                  case TC_OBJECT:
                      return checkResolve(readOrdinaryObject(unshared));
    
                  case TC_EXCEPTION:
                      IOException ex = readFatalException();
                      throw new WriteAbortedException("writing aborted", ex);
    
                  case TC_BLOCKDATA:
                  case TC_BLOCKDATALONG:
                      if (oldMode) {
                          bin.setBlockDataMode(true);
                          bin.peek();             
                          throw new OptionalDataException(
                              bin.currentBlockRemaining());
                      } else {
                          throw new StreamCorruptedException(
                              "unexpected block data");
                      }
    
                  case TC_ENDBLOCKDATA:
                      if (oldMode) {
                          throw new OptionalDataException(true);
                      } else {
                          throw new StreamCorruptedException(
                              "unexpected end of block data");
                      }
    
                  default:
                      throw new StreamCorruptedException(
                          String.format("invalid type code: %02X", tc));
              }
          } finally {
              depth--;
              bin.setBlockDataMode(oldMode);
          }
      }
    • readOrdinaryObject() 方法用於讀取並返回普通物件. 這裡的普通物件不包括String, Class, ObjectStreamClass, 陣列或者列舉常數這些物件
    private Object readOrdinaryObject(boolean unshared)
          throws IOException
      {
          if (bin.readByte() != TC_OBJECT) {
              throw new InternalError();
          }
    
          ObjectStreamClass desc = readClassDesc(false);
          desc.checkDeserialize();
    
          Class<?> cl = desc.forClass();
          if (cl == String.class || cl == Class.class
                  || cl == ObjectStreamClass.class) {
              throw new InvalidClassException("invalid class descriptor");
          }
    
          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);
          }
    
          passHandle = handles.assign(unshared ? unsharedMarker : obj);
          ClassNotFoundException resolveEx = desc.getResolveException();
          if (resolveEx != null) {
              handles.markException(passHandle, resolveEx);
          }
    
          if (desc.isExternalizable()) {
              readExternalData((Externalizable) obj, desc);
          } else {
              readSerialData(obj, desc);
          }
    
          handles.finish(passHandle);
    
          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);
              }
          }
    
          return obj;
      }
    • isInstantiable() 方法表示如果一個實現了Serializable介面或者Externalizable介面的類可以在執行時範例化,那麼該方法就返回true
    • 如果可以在執行時序列化,就會呼叫desc.newInstance() 方法使用反射的方式呼叫無參構造方法新建一個物件,建立一個類的新的範例
      • 如果類實現的是Serializable介面,就呼叫第一個不可進行序列化超類的無參構造方法數建立新的範例
      • 如果類實現的Externalizable介面,就呼叫公共的無參構造方法建立新的範例
  • 因為序列化過程中會通過反射呼叫無參建構函式建立一個新的物件,所以序列化會破壞單例模式
  • 為了防止序列化破壞單例模式,可以在Singleton.java中新增readResolve() 方法並且指定要返回的物件的生成策略
    • 因為readOrdinaryObject() 方法原始碼中的hasResolveMethod() 表示如果實現了Serializable或者Externalizable介面的類中包含readResolve() 方法就返回true
    • invokeReadResolve() 方法會通過反射的方式呼叫要被反序列化的類的readResolve() 方法

總結

到此這篇關於Java基礎入門之序列化和反序列化的文章就介紹到這了,更多相關Java序列化和反序列化內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com