1. 程式人生 > >thinking in java (二十二) ----- IO之序列化

thinking in java (二十二) ----- IO之序列化

序列化的作用和用途

序列化,就是為了保持物件的狀態,而與之對應的反序列化,則可以把物件的狀態再讀取出來,

簡而言之:序列化/反序列化,是JAVA提供的一種專門用於儲存/恢復物件狀態的機制

一般在以下幾種情況我們會使用序列化:

1.當你想把記憶體中的物件狀態儲存到一個檔案或者資料庫中時

2,當你想用套接字在網路上傳送物件的時候

3,當你想用RMI傳輸物件的時候

演示程式1

/**
 * 序列化的演示測試程式
 *
 * @author skywang
 */

import java.io.FileInputStream;   
import java.io.FileOutputStream;   
import java.io.ObjectInputStream;   
import java.io.ObjectOutputStream;   
import java.io.Serializable;   
  
public class SerialTest1 { 
    private static final String TMP_FILE = "serialtest1.txt";
  
    public static void main(String[] args) {   
        // 將“物件”通過序列化儲存
        testWrite();
        // 將序列化的“物件”讀出來
        testRead();
    }
  

    /**
     * 將Box物件通過序列化,儲存到檔案中
     */
    private static void testWrite() {   
        try {
            // 獲取檔案TMP_FILE對應的物件輸出流。
            // ObjectOutputStream中,只能寫入“基本資料”或“支援序列化的物件”
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream(TMP_FILE));
            // 建立Box物件,Box實現了Serializable序列化介面
            Box box = new Box("desk", 80, 48);
            // 將box物件寫入到物件輸出流out中,即相當於將物件儲存到檔案TMP_FILE中
            out.writeObject(box);
            // 列印“Box物件”
            System.out.println("testWrite box: " + box);

            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 
    /**
     * 從檔案中讀取出“序列化的Box物件”
     */
    private static void testRead() {
        try {
            // 獲取檔案TMP_FILE對應的物件輸入流。
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream(TMP_FILE));
            // 從物件輸入流中,讀取先前儲存的box物件。
            Box box = (Box) in.readObject();
            // 列印“Box物件”
            System.out.println("testRead  box: " + box);
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


/**
 * Box類“支援序列化”。因為Box實現了Serializable介面。
 *
 * 實際上,一個類只需要實現Serializable即可實現序列化,而不需要實現任何函式。
 */
class Box implements Serializable {
    private int width;   
    private int height; 
    private String name;   

    public Box(String name, int width, int height) {
        this.name = name;
        this.width = width;
        this.height = height;
    }

    @Override
    public String toString() {
        return "["+name+": ("+width+", "+height+") ]";
    }
}

執行結果:

testWrite box: [desk: (80, 48) ] testRead  box: [desk: (80, 48) ]

說明:

  • 程式的作用就是:將BOX物件,通過物件輸出流儲存到檔案中,之後再通過物件輸入流將BOX物件讀取出來
  • BOX實現了Serializable介面,因此支援序列化操作,即:支援通過ObjectOutputStream去寫入到輸出流中,並且支援ObjectInputStream從輸入流中讀取出來
  • testWrite()方法說明,作用是建立一個BOX物件,然後將該物件寫入檔案中。   首先新建檔案TMP_FILE的檔案輸出流物件(即FileOutStream),再建立該檔案的物件輸出流
    。最後通過out.wrtieObject(box)將box寫入物件輸出輸出流中,實際上是相當於將box寫入到檔案中
  • testRead()方法說明,作用是從檔案中讀取出box物件。首先建立檔案TMP_FILE的檔案輸入流物件,再建立該檔案的物件輸入流物件,通過in.readObject()從物件中讀取出box物件,實際上相當於從檔案中讀取出box物件

通過上面的例項我們可以知道,我們可以自定義一個類實現序列化介面,從而能夠支援物件的儲存和恢復。

演示例項2

package io;

/**
 * “基本型別” 和 “java自帶的實現Serializable介面的類” 對序列化的支援
 *
 * @author skywang
 */

import java.io.FileInputStream;   
import java.io.FileOutputStream;   
import java.io.ObjectInputStream;   
import java.io.ObjectOutputStream;   
import java.io.Serializable;   
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
  
public class SerialTest2 { 
    private static final String TMP_FILE = "serialabletest2.txt";
  
    public static void main(String[] args) {   
        testWrite();
        testRead();
    }
  
    /**
     * ObjectOutputStream 測試函式
     */
    private static void testWrite() {   
        try {
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream(TMP_FILE));
            out.writeBoolean(true);    // 寫入Boolean值
            out.writeByte((byte)65);// 寫入Byte值
            out.writeChar('a');     // 寫入Char值
            out.writeInt(20131015); // 寫入Int值
            out.writeFloat(3.14F);  // 寫入Float值
            out.writeDouble(1.414D);// 寫入Double值
            // 寫入HashMap物件
            HashMap map = new HashMap();
            map.put("one", "red");
            map.put("two", "green");
            map.put("three", "blue");
            out.writeObject(map);

            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 
    /**
     * ObjectInputStream 測試函式
     */
    private static void testRead() {
        try {
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream(TMP_FILE));
            System.out.printf("boolean:%b\n" , in.readBoolean());
            System.out.printf("byte:%d\n" , (in.readByte()&0xff));
            System.out.printf("char:%c\n" , in.readChar());
            System.out.printf("int:%d\n" , in.readInt());
            System.out.printf("float:%f\n" , in.readFloat());
            System.out.printf("double:%f\n" , in.readDouble());
            // 讀取HashMap物件
            HashMap map = (HashMap) in.readObject();
            Iterator iter = map.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry entry = (Map.Entry)iter.next();
                System.out.printf("%-6s -- %s\n" , entry.getKey(), entry.getValue());
            }

            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 程式就是將基本型別和HashMap物件,通過物件輸出流儲存到檔案中,再通過物件輸入流將儲存的資料取出來

在前面我們說過,要支援序列化,必須實現Serializable介面。因此“基礎型別”和java自帶的實現了Serializable介面的類,都支援序列化。我們看HashMap的原始碼,可以發現是實現了序列化介面的。

至此,我們對序列化的認識:知道了序列化的作用和用法,也知道了“基本型別”,java自帶的支援Serializable介面的類和自定義的實現了Serializable介面的 都支援序列化。

我們在介紹序列化定義時,說序列化/反序列化,是專門用於儲存和恢復物件狀態的機制,即支援儲存/恢復類的成員變數,但是不支援類的成員方法,但是序列化是不是對類的所有成員變數的狀態都能儲存呢?

答案是否定

打個比方,如果一個使用者有一些敏感資訊(如密碼,銀行卡號等),為了安全起見,不希望在網路操作(主要涉及到序列化操作,本地序列化快取也適用)中被傳輸,這些資訊對應的變數就可以加上transient關鍵字。換句話說,這個欄位的生命週期僅存於呼叫者的記憶體中而不會寫到磁盤裡持久化。

  1. 序列化對static和transient變數,是不會自動進行狀態儲存的 ,transient關鍵字的作用就是:宣告的變數不會被序列化()
  2. 對於Socket,Thread類,不支援序列化,如果實現序列化的介面中,有Thread成員,在對該類進行序列化操作的時候是回報錯的

下面我們通過示例檢視“序列化對static和transient的處理”

package io;

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;

/**
 * @description 使用transient關鍵字不序列化某個變數
 *        注意讀取的時候,讀取資料的順序一定要和存放資料的順序保持一致
 *        
 * @author Alexia
 * @date  2013-10-15
 */
public class TransientTest {
    
    public static void main(String[] args) {
        
        User user = new User();
        user.setUsername("Alexia");
        user.setPasswd("123456");
        
        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());
        
        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("user.txt"));
            os.writeObject(user); // 將User物件寫進檔案
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            // 在反序列化之前改變username的值
//            User.username = "jmwang";
            
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "user.txt"));
            user = (User) is.readObject(); // 從流中讀取User的資料
            is.close();
            
            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;  
    
    public static String username;
    private transient String passwd;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPasswd() {
        return passwd;
    }
    
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

}

結果:

read before Serializable: 
username: Alexia
password: 123456

read after Serializable: 
username: Alexia
password: null

密碼欄位為null,說明反序列化時根本沒有從檔案中獲取到資訊。

總結

  1. 一旦變數被transient修飾,該成員變數就不再是物件持久化的一部分,系列化以後無法獲得訪問
  2. transient只能修飾變數,不能修飾方法
  3. 被transient關鍵字修飾的變數不再能被序列化,一個靜態變數不管是否被transient修飾,均不能被序列化。

有人會比較迷惑,因為static修飾的的變數在序列化以後仍然是可以訪問的,實際上第三點是沒有錯的,我們使用下面的程式來驗證

package io;
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;

/**
 * @description 使用transient關鍵字不序列化某個變數
 *        注意讀取的時候,讀取資料的順序一定要和存放資料的順序保持一致
 *        
 * @author Alexia
 * @date  2013-10-15
 */
public class TransientTest {
    
    public static void main(String[] args) {
        
        User user = new User();
        user.setUsername("Alexia");
        user.setPasswd("123456");
        
        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());
        
        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("C:/user.txt"));
            os.writeObject(user); // 將User物件寫進檔案
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            // 在反序列化之前改變username的值
            User.username = "jmwang";
            
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "C:/user.txt"));
            user = (User) is.readObject(); // 從流中讀取User的資料
            is.close();
            
            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;  
    
    public static String username;
    private transient String passwd;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPasswd() {
        return passwd;
    }
    
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

}

結果:

read before Serializable: 
username: Alexia
password: 123456

read after Serializable: 
username: jmwang
password: null

說明反序列化後類中static型變數username的值為當前JVM中對應static變數的值,為修改後jmwang,而不是序列化時的值Alexia

但是,若我們想要儲存static或transient變數,能不能辦到呢? 當然可以!我們在類中重寫兩個方法writeObject()和readObject()即可。下面程式演示瞭如何手動儲存static和transient變數。

package io;

import java.io.FileInputStream;   
import java.io.FileOutputStream;   
import java.io.ObjectInputStream;   
import java.io.ObjectOutputStream;   
import java.io.Serializable;   
import java.lang.Thread;
import java.io.IOException;   
import java.lang.ClassNotFoundException;   
  
public class ExternalizableTest { 
    private static final String TMP_FILE = "serialtest7.txt";
  
    public static void main(String[] args) {   
        // 將“物件”通過序列化儲存
        testWrite();
        // 將序列化的“物件”讀出來
        testRead();
    }
  

    /**
     * 將Box物件通過序列化,儲存到檔案中
     */
    private static void testWrite() {   
        try {
            // 獲取檔案TMP_FILE對應的物件輸出流。
            // ObjectOutputStream中,只能寫入“基本資料”或“支援序列化的物件”
            ObjectOutputStream out = new ObjectOutputStream(
                    new FileOutputStream(TMP_FILE));
            // 建立Box物件,Box實現了Serializable序列化介面
            Box box = new Box("desk", 80, 48);
            // 將box物件寫入到物件輸出流out中,即相當於將物件儲存到檔案TMP_FILE中
            out.writeObject(box);
            // 列印“Box物件”
            System.out.println("testWrite box: " + box);
            // 修改box的值
            box = new Box("room", 100, 50);

            out.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
 
    /**
     * 從檔案中讀取出“序列化的Box物件”
     */
    private static void testRead() {
        try {
            // 獲取檔案TMP_FILE對應的物件輸入流。
            ObjectInputStream in = new ObjectInputStream(
                    new FileInputStream(TMP_FILE));
            // 從物件輸入流中,讀取先前儲存的box物件。
            Box box = (Box) in.readObject();
            // 列印“Box物件”
            System.out.println("testRead  box: " + box);
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


/**
 * Box類“支援序列化”。因為Box實現了Serializable介面。
 *
 * 實際上,一個類只需要實現Serializable即可實現序列化,而不需要實現任何函式。
 */
class Box implements Serializable {
    private static int width;   
    private transient int height; 
    private String name;   
    private transient Thread thread = new Thread() {
        @Override
        public void run() {
            System.out.println("Serializable thread");
        }
    };

    public Box(String name, int width, int height) {
        this.name = name;
        this.width = width;
        this.height = height;
    }

    private void writeObject(ObjectOutputStream out) throws IOException{ 
        out.defaultWriteObject();//使定製的writeObject()方法可以利用自動序列化中內建的邏輯。 
        out.writeInt(height); 
        out.writeInt(width); 
        //System.out.println("Box--writeObject width="+width+", height="+height);
    }

    private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{ 
        in.defaultReadObject();//defaultReadObject()補充自動序列化 
        height = in.readInt(); 
        width = in.readInt(); 
        //System.out.println("Box---readObject width="+width+", height="+height);
    }

    @Override
    public String toString() {
        return "["+name+": ("+width+", "+height+") ]";
    }
}

執行結果:

testWrite box: [desk: (80, 48) ]
testRead  box: [desk: (80, 48) ]

程式說明

“序列化不會自動儲存static和transient變數”,因此我們若要儲存它們,則需要通過writeObject()和readObject()去手動讀寫。 (01) 通過writeObject()方法,寫入要儲存的變數。writeObject的原始定義是在ObjectOutputStream.java中,我們按照如下示例覆蓋即可:

private void writeObject(ObjectOutputStream out) throws IOException{ 
    out.defaultWriteObject();// 使定製的writeObject()方法可以利用自動序列化中內建的邏輯。 
    out.writeInt(ival);      // 若要儲存“int型別的值”,則使用writeInt()
    out.writeObject(obj);    // 若要儲存“Object物件”,則使用writeObject()
}

(02) 通過readObject()方法,讀取之前儲存的變數。readObject的原始定義是在ObjectInputStream.java中,我們按照如下示例覆蓋即可:

private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{ 
    in.defaultReadObject();       // 使定製的readObject()方法可以利用自動序列化中內建的邏輯。 
    int ival = in.readInt();      // 若要讀取“int型別的值”,則使用readInt()
    Object obj = in.readObject(); // 若要讀取“Object物件”,則使用readObject()
}