程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 深入分析Java的序列化與反序列化,深入分析java序列化

深入分析Java的序列化與反序列化,深入分析java序列化

編輯:JAVA綜合教程

深入分析Java的序列化與反序列化,深入分析java序列化


序列化是一種對象持久化的手段。普遍應用在網絡傳輸、RMI等場景中。本文通過分析ArrayList的序列化來介紹Java序列化的相關內容。主要涉及到以下幾個問題:

怎麼實現Java的序列化

為什麼實現了java.io.Serializable接口才能被序列化

transient的作用是什麼

怎麼自定義序列化策略

自定義的序列化策略是如何被調用的

ArrayList對序列化的實現有什麼好處

Java對象的序列化

Java平台允許我們在內存中創建可復用的Java對象,但一般情況下,只有當JVM處於運行時,這些對象才可能存在,即,這些對象的生命周期不會比JVM的生命周期更長。但在現實應用中,就可能要求在JVM停止運行之後能夠保存(持久化)指定的對象,並在將來重新讀取被保存的對象。Java對象序列化就能夠幫助我們實現該功能。

使用Java對象序列化,在保存對象時,會把其狀態保存為一組字節,在未來,再將這些字節組裝成對象。必須注意地是,對象序列化保存的是對象的”狀態”,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量

除了在持久化對象時會用到對象序列化之外,當使用RMI(遠程方法調用),或在網絡中傳遞對象時,都會用到對象序列化。Java序列化API為處理對象序列化提供了一個標准機制,該API簡單易用。

如何對Java對象進行序列化與反序列化

在Java中,只要一個類實現了java.io.Serializable接口,那麼它就可以被序列化。這裡先來一段代碼:

code 1 創建一個User類,用於序列化及反序列化

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.hollis; import java.io.Serializable; import java.util.Date;   /**  * Created by hollis on 16/2/2.  */ public class User implements Serializable{     private String name;     private int age;     private Date birthday;     private transient String gender;     private static final long serialVersionUID = -6849794470754667710L;       public String getName() {         return name;     }       public void setName(String name) {         this.name = name;     }       public int getAge() {         return age;     }       public void setAge(int age) {         this.age = age;     }       public Date getBirthday() {         return birthday;     }       public void setBirthday(Date birthday) {         this.birthday = birthday;     }       public String getGender() {         return gender;     }       public void setGender(String gender) {         this.gender = gender;     }       @Override     public String toString() {         return "User{" +                 "name='" + name + '\'' +                 ", age=" + age +                 ", gender=" + gender +                 ", birthday=" + birthday +                 '}';     } }

code 2 對User進行序列化及反序列化的Demo

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.hollis; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import java.io.*; import java.util.Date;   /**  * Created by hollis on 16/2/2.  */ public class SerializableDemo {       public static void main(String[] args) {         //Initializes The Object         User user = new User();         user.setName("hollis");         user.setGender("male");         user.setAge(23);         user.setBirthday(new Date());         System.out.println(user);           //Write Obj to File         ObjectOutputStream oos = null;         try {             oos = new ObjectOutputStream(new FileOutputStream("tempFile"));             oos.writeObject(user);         } catch (IOException e) {             e.printStackTrace();         } finally {             IOUtils.closeQuietly(oos);         }           //Read Obj from File         File file = new File("tempFile");         ObjectInputStream ois = null;         try {             ois = new ObjectInputStream(new FileInputStream(file));             User newUser = (User) ois.readObject();             System.out.println(newUser);         } catch (IOException e) {             e.printStackTrace();         } catch (ClassNotFoundException e) {             e.printStackTrace();         } finally {             IOUtils.closeQuietly(ois);             try {                 FileUtils.forceDelete(file);             } catch (IOException e) {                 e.printStackTrace();             }         }       } } //output //User{name='hollis', age=23, gender=male, birthday=Tue Feb 02 17:37:38 CST 2016} //User{name='hollis', age=23, gender=null, birthday=Tue Feb 02 17:37:38 CST 2016}

序列化及反序列化相關知識

1、在Java中,只要一個類實現了java.io.Serializable接口,那麼它就可以被序列化。

2、通過ObjectOutputStreamObjectInputStream對對象進行序列化及反序列化

3、虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID

4、序列化並不保存靜態變量。

5、要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。

6、Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。

7、服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。

ArrayList的序列化

在介紹ArrayList序列化之前,先來考慮一個問題:

如何自定義的序列化和反序列化策略

帶著這個問題,我們來看java.util.ArrayList的源碼

code 3

1 2 3 4 5 6 7 public class ArrayList<E> extends AbstractList<E>         implements List<E>, RandomAccess, Cloneable, java.io.Serializable {     private static final long serialVersionUID = 8683452581122892189L;     transient Object[] elementData; // non-private to simplify nested class access     private int size; }

筆者省略了其他成員變量,從上面的代碼中可以知道ArrayList實現了java.io.Serializable接口,那麼我們就可以對它進行序列化及反序列化。因為elementData是transient的,所以我們認為這個成員變量不會被序列化而保留下來。我們寫一個Demo,驗證一下我們的想法:

code 4

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main(String[] args) throws IOException, ClassNotFoundException {         List<String> stringList = new ArrayList<String>();         stringList.add("hello");         stringList.add("world");         stringList.add("hollis");         stringList.add("chuang");         System.out.println("init StringList" + stringList);         ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));         objectOutputStream.writeObject(stringList);           IOUtils.close(objectOutputStream);         File file = new File("stringlist");         ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));         List<String> newStringList = (List<String>)objectInputStream.readObject();         IOUtils.close(objectInputStream);         if(file.exists()){             file.delete();         }         System.out.println("new StringList" + newStringList);     } //init StringList[hello, world, hollis, chuang] //new StringList[hello, world, hollis, chuang]

了解ArrayList的人都知道,ArrayList底層是通過數組實現的。那麼數組elementData其實就是用來保存列表中的元素的。通過該屬性的聲明方式我們知道,他是無法通過序列化持久化下來的。那麼為什麼code 4的結果卻通過序列化和反序列化把List中的元素保留下來了呢?

writeObject和readObject方法

在ArrayList中定義了來個方法: writeObjectreadObject

這裡先給出結論:

在序列化過程中,如果被序列化的類中定義了writeObject 和 readObject 方法,虛擬機會試圖調用對象類裡的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化。

如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。

來看一下這兩個方法的具體實現:

code 5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject(java.io.ObjectInputStream s)         throws java.io.IOException, ClassNotFoundException {         elementData = EMPTY_ELEMENTDATA;           // Read in size, and any hidden stuff         s.defaultReadObject();           // Read in capacity         s.readInt(); // ignored           if (size > 0) {             // be like clone(), allocate array based upon size not capacity             ensureCapacityInternal(size);               Object[] a = elementData;             // Read in all elements in the proper order.             for (int i=0; i<size; i++) {                 a[i] = s.readObject();             }         }     }

code 6

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void writeObject(java.io.ObjectOutputStream s)         throws java.io.IOException{         // Write out element count, and any hidden stuff         int expectedModCount = modCount;         s.defaultWriteObject();           // Write out size as capacity for behavioural compatibility with clone()         s.writeInt(size);           // Write out all elements in the proper order.         for (int i=0; i<size; i++) {             s.writeObject(elementData[i]);         }           if (modCount != expectedModCount) {             throw new ConcurrentModificationException();         }     }

那麼為什麼ArrayList要用這種方式來實現序列化呢?

why transient

ArrayList實際上是動態數組,每次在放滿以後自動增長設定的長度值,如果數組自動增長長度設為100,而實際只放了一個元素,那就會序列化99個null元素。為了保證在序列化的時候不會將這麼多null同時進行序列化,ArrayList把元素數組設置為transient。

why writeObject and readObject

前面說過,為了防止一個包含大量空對象的數組被序列化,為了優化存儲,所以,ArrayList使用transient來聲明elementData。 但是,作為一個集合,在序列化過程中還必須保證其中的元素可以被持久化下來,所以,通過重寫writeObject 和 readObject方法的方式把其中的元素保留下來。

writeObject方法把elementData數組中的元素遍歷的保存到輸出流(ObjectOutputStream)中。

readObject方法從輸入流(ObjectInputStream)中讀出對象並保存賦值到elementData數組中。

至此,我們先試著來回答剛剛提出的問題:

如何自定義的序列化和反序列化策略

答:可以通過在被序列化的類中增加writeObject 和 readObject方法。那麼問題又來了:

雖然ArrayList中寫了writeObject 和 readObject 方法,但是這兩個方法並沒有顯示的被調用啊。

那麼如果一個類中包含writeObject 和 readObject 方法,那麼這兩個方法是怎麼被調用的呢?

ObjectOutputStream

從code 4中,我們可以看出,對象的序列化過程通過ObjectOutputStream和ObjectInputputStream來實現的,那麼帶著剛剛的問題,我們來分析一下ArrayList中的writeObject 和 readObject 方法到底是如何被調用的呢?

為了節省篇幅,這裡給出ObjectOutputStream的writeObject的調用棧:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

這裡看一下invokeWriteObject:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void invokeWriteObject(Object obj, ObjectOutputStream out)         throws IOException, UnsupportedOperationException     {         if (writeObjectMethod != null) {             try {                 writeObjectMethod.invoke(obj, new Object[]{ out });             } catch (InvocationTargetException ex) {                 Throwable th = ex.getTargetException();                 if (th instanceof IOException) {                     throw (IOException) th;                 } else {                     throwMiscException(th);                 }             } catch (IllegalAccessException ex) {                 // should not occur, as access checks have been suppressed                 throw new InternalError(ex);             }         } else {             throw new UnsupportedOperationException();         }     }

其中writeObjectMethod.invoke(obj, new Object[]{ out });是關鍵,通過反射的方式調用writeObjectMethod方法。官方是這麼解釋這個writeObjectMethod的:

class-defined writeObject method, or null if none

在我們的例子中,這個方法就是我們在ArrayList中定義的writeObject方法。通過反射的方式被調用了。

至此,我們先試著來回答剛剛提出的問題:

如果一個類中包含writeObject 和 readObject 方法,那麼這兩個方法是怎麼被調用的?

答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法時,會通過反射的方式調用。


至此,我們已經介紹完了ArrayList的序列化方式。那麼,不知道有沒有人提出這樣的疑問:

 

Serializable明明就是一個空的接口,它是怎麼保證只有實現了該接口的方法才能進行序列化與反序列化的呢?

Serializable接口的定義:

1 2 public interface Serializable { }

讀者可以嘗試把code 1中的繼承Serializable的代碼去掉,再執行code 2,會拋出java.io.NotSerializableException

其實這個問題也很好回答,我們再回到剛剛ObjectOutputStream的writeObject的調用棧:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

writeObject0方法中有這麼一段代碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 if (obj instanceof String) {                 writeString((String) obj, unshared);             } else if (cl.isArray()) {                 writeArray(obj, desc, unshared);             } else if (obj instanceof Enum) {                 writeEnum((Enum<?>) obj, desc, unshared);             } else if (obj instanceof Serializable) {                 writeOrdinaryObject(obj, desc, unshared);             } else {                 if (extendedDebugInfo) {                     throw new NotSerializableException(                         cl.getName() + "\n" + debugInfoStack.toString());                 } else {                     throw new NotSerializableException(cl.getName());                 }             }

在進行序列化操作時,會判斷要被序列化的類是否是Enum、Array和Serializable類型,如果不是則直接拋出NotSerializableException

總結

1、如果一個類想被序列化,需要實現Serializable接口。否則將拋出NotSerializableException異常,這是因為,在序列化操作過程中會對類型進行檢查,要求被序列化的類必須屬於Enum、Array和Serializable類型其中的任何一種。

2、在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中。

3、在類中增加writeObject 和 readObject 方法可以實現自定義序列化策略

 

問啊-一鍵呼叫程序員答題神器,牛人一對一服務,開發者編程必備官方網站:www.wenaaa.com

QQ群290551701 聚集很多互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved