1. 程式人生 > >Java I/O (輸入/輸出)

Java I/O (輸入/輸出)


Java I/O (輸入/輸出)

I/O(輸入輸出)
    使用輸入機制,允許程式讀取外部資料(包括來自磁碟、光碟等儲存裝置的資料)、使用者輸入的資料
    使用輸出機制,允許程式記錄執行狀態,將程式資料輸出到磁碟、光碟等儲存裝置中
    
    
Java 的 I/O 通過 java.io 包下的類和介面支援。 java.io 包下主要包括輸入、輸出兩種 IO 流,每種輸入、輸出流又分為位元組流和字元流兩大類:
    位元組流以位元組為單位處理輸入、輸出操作
    字元流以字元來處理輸入、輸出操作
    
Java IO 流採用裝飾器設計模式,將 IO 流分成底層節點流和上層處理流
節點流用於和底層物理儲存節點直接關聯,不同的物理節點獲取節點流的方式可能存在一定差異,但程式可以把不同的物理節點流包裝成統一的處理流,
從而允許程式使用統一的輸入、輸出程式碼來讀寫不同物理儲存節點上的資源

Java 7 在 java.nio 及其子包下提供了一些列全新 API , 這些 API 是對原有 NIO 的升級,因此也被稱為 NIO2 ,通過這些 NIO2 ,程式可以更高效地進行輸入輸出操作


1. File 類
---------------------------------------------------------------------------------------    
File 是 java.io 包下代表與平臺無關的檔案和目錄。
不管是檔案還是目錄都是使用 File 來操作的, File 能新建、刪除、重新命名檔案和目錄
File 不能訪問檔案內容本身,如果需要訪問檔案內容本身,則需要使用輸入輸出流


訪問檔案和目錄
-----------------------------------------------
File 類可以使用檔案路徑字串來建立例項,該檔案路徑字串既可以是絕對路徑,也可以是相對路徑。
預設情況下,系統總是依據使用者的工作路徑來解釋相對路徑,這個路徑由系統屬性 “user.dir” 指定,通常也是執行 Java 虛擬機器所在的路徑。

一旦建立了 File 物件之後,就可以呼叫 File 物件的方法來訪問, File 類提供了很多方法來操作檔案和目錄


檔案過濾器
------------------------------------------------
File 類的
    String[]     list(FilenameFilter filter)
方法接受一個 FilenameFilter 引數,通過該引數可以只列出符合條件的檔案
FilenameFilter 介面包含:
    boolean     accept(File dir, String name)
方法,該方法將依次對指定 File 的所有子目錄或檔案進行迭代,方法返回 true, 則 list() 方法列出該目錄或檔案

例子:
public class FilenameFilterTest
{
    public static void main(String[] args)
    {
        File file = new File(".");
        // 使用Lambda表示式(目標型別為FilenameFilter)實現檔案過濾器。
        // 如果檔名以.java結尾,或者檔案對應一個路徑,返回true
        String[] nameList = file.list((dir, name) -> name.endsWith(".java")
            || new File(name).isDirectory());
        for(String name : nameList)
        {
            System.out.println(name);
        }
    }
}

*
*
*

2. 理解 Java 的 IO 流
--------------------------------------------------------------------------------------------------------------------
Java 的 IO 流是實現輸入輸出的基礎,它可以方便地實現資料的輸入、輸出操作。
在 Java 中把不同的輸入、輸出源(鍵盤、檔案、網路連線等) 抽象表述為 “流” (stream), 通過流的方式允許 Java 程式使用相同的方式來訪問不同的輸入/輸出源
stream 是從源(stream)到接收(sink)的有序資料。



流的分類
---------------------------------------------------------------------------------------------
按照不同的分類方式,可以將流分為不同的型別

    
    1. 輸入流和輸出流
    --------------------------------------------------
    輸入流:只能從中讀取資料,而不能向其寫入資料
    輸出流:只能向其寫入資料,而不能從中讀取資料
        
    這裡的輸入、輸出都是從程式執行所在記憶體的角度來劃分的。輸入、輸出涉及一個方向問題,資料從記憶體到硬碟,通常稱為輸出流;相反,資料從硬碟到記憶體,稱為輸入流。
    
    Java 的輸入流主要由 InputStream 和 Reader 作為基類,而輸出流則主要由 OutputStream 和 Writer 作為基類。

        輸入流基類:            輸出流基類:
        
        InputStream                OutputStream
        Reader                    Writer
        
    2. 位元組流和字元流
    --------------------------------------------------
    位元組流和字元流的用法幾乎完全一樣,區別在於位元組流和字元流所操作的資料單元不同
    位元組流:操作的資料單元是 8 位的位元組
    字元流:操作的資料單元是 16 位的字元
    
        位元組流基類:            字元流基類:
        
        InputStream                Reader
        OutputStream            Writer

    3. 節點流和處理流
    ---------------------------------------------------
    按照流的角色分,可以分為節點流和處理流
    可以從/向一個特定的 IO 裝置(如磁碟、網路)讀寫資料的流,稱為節點流,節點流也被稱為低階流(Low Level Stream)。
    當使用節點流進行輸入/輸出時,程式直接連線到實際的資料來源,和實際的輸入/輸出節點連線。
    
    處理流用於對一個已存在的流進行連線或封裝,通過封裝後的流實現資料讀/寫功能。處理流也被稱為高階流。
    當使用處理流進行輸入/輸出時,程式並不會直接連線到實際的資料來源,沒有和實際的輸入/輸出節點連線。
    使用處理流的一個明顯好處是,只要使用相同的處理流,程式就可以採用完全相同的輸入/輸出程式碼來訪問不同的資料來源,隨著處理流所包裝節點流的變化,程式實際所訪問的資料來源也相應地放生變化。
    
    Java 使用處理流來包裝節點流是一種典型的裝飾器設計模式,通過使用處理流包裝不同的節點流,既可以消除不同節點流的實現差異,也可以提供更方便的方法完成輸入/輸出功能。
    因此,處理流也稱為包裝流。
    
    
    
流的概念模型
-----------------------------------------------------------------------------------------------
Java 的 IO 流共涉及40多個類,都是從如下4個抽象基類派生的
    InputStream/Reader : 所有輸入流的基類,前者是位元組輸入流,後者是字元輸入流
    OutputStream/Writer: 所有輸出流的基類,前者是位元組輸出流,後者是字元輸出流。
    
    輸入流使用隱式的記錄指標來表示當前正準備從哪個單元開始讀取,程式從InputStream/Reader 取出一個或多個單元后,記錄指標自動向後移動。
    當執行輸出時,程式依次把資料單元放入到輸出流的管道中,輸出流同樣採用隱式的記錄指標來標識當前資料單元即將放入的位置,每當程式向OutputStream/Writer裡輸出一個或多個數據單元后,記錄指標自動向後移動。

Java 的處理流模型體現了 Java 輸入輸出流設計的靈活性,功能主要體現在以下兩個方面:
    1. 效能的提高: 主要以增加緩衝的方式來提高輸入/輸出效率
    2. 操作的便捷: 處理流可能提供了一些列便捷的方法來一次輸入/輸出打批量的內容。而不是輸入輸出一個或多個數據單元
    
處理流可以嫁接在任何已存在的流的基礎上,這就允許 Java 應用程式採用相同的程式碼、透明的方式訪問不同的輸入輸出裝置的資料流。
通過使用處理流, Java 程式無須理會輸入/輸出節點是磁碟、網路還是其他的輸入輸出裝置,程式只要將這些節點流包裝成處理流,就可以使用相同的輸入輸出程式碼讀寫不同的輸入輸出裝置的資料。


*
*
*

3. 位元組流和字元流
---------------------------------------------------------------------------------------------------------------------------


InputStream 和 Reader
----------------------------------------------------------------------
InputStream 和 Reader 是所有輸入流的抽象基類,本身並不能建立例項執行輸入,但它們將稱為所有輸入流的模版,它們的方法是所有輸入流都可以使用的方法
    
    InputStream:
        abstract int     read() :從輸入流讀取單個位元組,返回所讀取的位元組資料
        int     read(byte[] b) :從輸入流中最多讀取 b.length 個位元組資料,並將其儲存在位元組陣列 b 中,返回讀實際讀取的位元組數
        int     read(byte[] b, int off, int len):從輸入流中最多讀取 len 個位元組資料,並將其儲存在陣列 b 中,放入陣列 b 中時,並不是從陣列起點開始,而是從 off 位置開始存放,返回實際讀取的位元組數。

    Reader:
        int     read() :從輸入流中讀取單個字元,返回所讀取的字元資料
        int     read(char[] cbuf):從輸入流中最多讀取 cbuf.length 個字元資料,並將其存放在字元陣列 cbuf 中,返回實際讀取的字元數
        abstract int     read(char[] cbuf, int off, int len):從輸入流中最多讀取 len 個字元的資料,並將其儲存在字元陣列 cbuf 中,放入陣列 cbuf 中時,並不是從陣列的起點開始,而是從 off 位置開始,返回實際讀取的字元數。

    
    java.io.InputStream                                        java.io.Reader
        java.io.FileInputStream                                 java.io.InputStreamReader
                                                                        java.io.FileReader

    FileInputStream 和 FileReader:它們都是節點流-----會和指定的檔案關聯
    
例子:
public class FileInputStreamTest
{
    public static void main(String[] args) throws IOException
    {
        // 建立位元組輸入流
        FileInputStream fis = new FileInputStream(
            "FileInputStreamTest.java");
        // 建立一個長度為1024的“竹筒”
        byte[] bbuf = new byte[1024];
        // 用於儲存實際讀取的位元組數
        int hasRead = 0;
        // 使用迴圈來重複“取水”過程
        while ((hasRead = fis.read(bbuf)) > 0 )
        {
            // 取出“竹筒”中水滴(位元組),將位元組陣列轉換成字串輸入!
            System.out.print(new String(bbuf , 0 , hasRead ));
        }
        // 關閉檔案輸入流,放在finally塊裡更安全
        fis.close();
    }
}

例子:
public class FileReaderTest
{
    public static void main(String[] args)
    {
        try(
            // 建立字元輸入流
            FileReader fr = new FileReader("FileReaderTest.java"))
        {
            // 建立一個長度為32的“竹筒”
            char[] cbuf = new char[32];
            // 用於儲存實際讀取的字元數
            int hasRead = 0;
            // 使用迴圈來重複“取水”過程
            while ((hasRead = fr.read(cbuf)) > 0 )
            {
                // 取出“竹筒”中水滴(字元),將字元陣列轉換成字串輸入!
                System.out.print(new String(cbuf , 0 , hasRead));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

OutputStream和Writer
-------------------------------------------------------------------
OutputStream和Writer 也非常相似,兩個流都提供瞭如下三個方法:
        
        abstract void     write(int b)    :將位元組/字元輸出到輸出流,c 既可以代表位元組,也可以代表字元
        void     write(byte[]/char[] b)    :將位元組陣列/字元陣列中的資料輸出到輸出流中
        void     write(byte[]/char[] b, int off, int len) :將位元組陣列/字元陣列中從 off 位置開始,長度為 len 的位元組/字元輸出到輸出流中。
        
        
    Writer 裡還包含如下兩個方法:
        void     write(String str) :將 str 字串裡包含的字元輸出到指定輸出流
        void     write(String str, int off, int len) :將 str 字串裡從 off 位置開始,長度為 len 的字元輸出到指定的輸出流
        
例子:
public class FileOutputStreamTest
{
    public static void main(String[] args)
    {
        try(
            // 建立位元組輸入流
            FileInputStream fis = new FileInputStream(
                "FileOutputStreamTest.java");
            // 建立位元組輸出流
            FileOutputStream fos = new FileOutputStream("newFile.txt"))
        {
            byte[] bbuf = new byte[32];
            int hasRead = 0;
            // 迴圈從輸入流中取出資料
            while ((hasRead = fis.read(bbuf)) > 0 )
            {
                // 每讀取一次,即寫入檔案輸出流,讀了多少,就寫多少。
                fos.write(bbuf , 0 , hasRead);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

public class FileWriterTest
{
    public static void main(String[] args)
    {
        try(
            FileWriter fw = new FileWriter("poem.txt"))
        {
            fw.write("錦瑟 - 李商隱\r\n");
            fw.write("錦瑟無端五十弦,一弦一柱思華年。\r\n");
            fw.write("莊生曉夢迷蝴蝶,望帝春心託杜鵑。\r\n");
            fw.write("滄海月明珠有淚,藍田日暖玉生煙。\r\n");
            fw.write("此情可待成追憶,只是當時已惘然。\r\n");
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

*
*
*

4. 輸入/輸出流體系
------------------------------------------------------------------------------------------------------------------------


處理流的用法
-------------------------------------------------------------------------------------
    處理流的功能,可以隱藏底層裝置上節點流的差異,並對外提供更加方便的輸入/輸出方法, 讓程式設計師只需關心高階流的操作。
    
    使用處理流的典型思路是,使用處理流來包裝節點流,程式通過處理流來執行輸入輸出功能,讓節點流與底層 I/O 裝置、檔案互動
    
    識別處理流:只要流的構造器引數不是一個物理節點,而是已經存在的流,那麼這種流就一定是處理流;而所有節點流都是直接以物理IO節點作為構造器引數的。

    java.io.OutputStream
        java.io.FilterOutputStream
            java.io.PrintStream
            
            
    java.lang.Object
        java.io.Writer
            java.io.PrintWriter
    
例子:
public class PrintStreamTest
{
    public static void main(String[] args)
    {
        try(
            FileOutputStream fos = new FileOutputStream("test.txt");
            PrintStream ps = new PrintStream(fos))
        {
            // 使用PrintStream執行輸出
            ps.println("普通字串");
            // 直接使用PrintStream輸出物件
            ps.println(new PrintStreamTest());
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

由於 PrintStream 類的輸出功能非常強大,通常如果需要輸出文字內容,都應該將輸出包裝成 PrintStream 後進行輸出

程式使用處理流非常簡單,通常只需要在建立處理流時傳入一個節點流作為構造器引數即可,這樣建立的處理流就是包裝了該節點流的處理流。

在使用處理流包裝底層節點流之後,關閉輸入輸出流資源時,只要關閉最上層的處理流即可。關閉最上層的處理流時,系統會自動關閉被該處理流包裝的節點流。

    
輸入/輸出流體系
------------------------------------------------------------------------------------------------------------------------------------


                Java 輸入/輸出流體系中常用的流分類
                
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|
|    分類        |    位元組輸入流            |    位元組輸出流                |    字元輸入流            |    字元輸出流            |
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|
| 抽象基類        | InputStream            | OutputStream                | Reader                | Writer                |
|                |                        |                            |                        |                         |
| 訪問檔案        | FileInputStream        | FileOutputStream            | FileReader            | FileWriter            |
|                |                        |                            |                        |                        |
| 訪問陣列        | ByteArrayInputStream    | ByteArrayOutputStream        | CharArrayReader        | CharArrayWriter        |
|                |                        |                            |                        |                        |
| 訪問管道        | PipedInputStream        | PipedOutputStream            | PipedReader            | PipedWriter            |
|                |                        |                            |                        |                        |
| 訪問字串    |                         |                            | StringReader            | StringWriter            |
|                |                        |                            |                        |                        |
| 緩衝流        | BufferedInputStream    | BufferedOutputStream        | BufferedReader        | BufferedWriter        |
|                |                        |                            |                         |                        |
| 轉換流        |                         |                            | InputStreamReader        | OutputStreamWriter    |
|                |                        |                            |                        |                        |
| 物件流        | ObjectInputStream        | ObjectOutputStream        |                        |                        |
|                |                        |                            |                        |                        |
| 過濾器流        | FilterInputStream        | FilterOutputStream        | FilterReader            | FilterWriter            |
|                |                        |                             |                        |                        |
| 列印流        |                        | PrintStream                |                         | PrintWriter            |
|                |                        |                            |                        |                        |
| 推回輸入流    | PushbackInputStream    |                             | PushbackReader        |                        |
|                |                        |                            |                        |                        |
| 特殊流        | DataInputStream        | DataOutputStream            |                        |                        |
|                |                        |                            |                        |                        |
|---------------|-----------------------|---------------------------|-----------------------|-----------------------|


一般規則: 如果進行輸入輸出的內容是文字內容,應該考慮使用字元流;如果進行輸入輸出的內容是二進位制內容,則應該考慮使用位元組流。

還有一些特殊功能的位元組流位於 JDK 其他包下:
    AudioInputStream:
    CipherInputStream:
    DeflaterInputStream
    ZipInputStream:
    ...
    
以陣列為物理節點的節點流,位元組流以位元組陣列為節點,字元流以字元陣列為節點,這種以陣列為物理節點的節點流除了建立節點流物件時需要傳入一個位元組陣列或者字元陣列之外,用法上與檔案節點流完全相似。
字元流還可以使用字串作為物理節點,用於實現從字串讀取內容,或將內容寫入字串(用 StringBuffer 充當字串)

例子:
public class StringNodeTest
{
    public static void main(String[] args)
    {
        String src = "從明天起,做一個幸福的人\n"
            + "餵馬,劈柴,周遊世界\n"
            + "從明天起,關心糧食和蔬菜\n"
            + "我有一所房子,面朝大海,春暖花開\n"
            + "從明天起,和每一個親人通訊\n"
            + "告訴他們我的幸福\n";
        char[] buffer = new char[32];
        int hasRead = 0;
        try(
            StringReader sr = new StringReader(src))
        {
            // 採用迴圈讀取的訪問讀取字串
            while((hasRead = sr.read(buffer)) > 0)
            {
                System.out.print(new String(buffer ,0 , hasRead));
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
        try(
            // 建立StringWriter時,實際上以一個StringBuffer作為輸出節點
            // 下面指定的20就是StringBuffer的初始長度
            StringWriter sw = new StringWriter())
        {
            // 呼叫StringWriter的方法執行輸出
            sw.write("有一個美麗的新世界,\n");
            sw.write("她在遠方等我,\n");
            sw.write("哪裡有天真的孩子,\n");
            sw.write("還有姑娘的酒窩\n");
            System.out.println("----下面是sw的字串節點裡的內容----");
            // 使用toString()方法返回StringWriter的字串節點的內容
            System.out.println(sw.toString());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}



轉換流
--------------------------------------------------------------------
輸入/輸出體系中提供了兩轉換流,這兩個轉換流用於實現將位元組流轉換成字元流,
    InputStreamReader 將位元組流轉換為字元輸入流
    OutputStreamWriter 將位元組流轉換成字元輸出流
    
Java 使用 System.in 代表標準輸入,即鍵盤輸入,但這個標準輸入流是 InputStream 類的例項,使用不太方便,而且鍵盤輸入的內容都是文字內容,
所以可以使用 InputStreamReader 將其轉換成字元輸入流,普通的 Reader 讀取輸入內容時依然不太方便,可以將普通的 Reader 再次包裝成 BufferReader, 利用 BufferReader 的 readLine() 方法可以一次讀取一行內容

例子:
public class KeyinTest
{
    public static void main(String[] args)
    {
        try(
            // 將Sytem.in物件轉換成Reader物件
            InputStreamReader reader = new InputStreamReader(System.in);
            // 將普通Reader包裝成BufferedReader
            BufferedReader br = new BufferedReader(reader))
        {
            String line = null;
            // 採用迴圈方式來一行一行的讀取
            while ((line = br.readLine()) != null)
            {
                // 如果讀取的字串為"exit",程式退出
                if (line.equals("exit"))
                {
                    System.exit(1);
                }
                // 列印讀取的內容
                System.out.println("輸入內容為:" + line);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}


推回輸入流: PushbackInputStream 和PushbackReader
----------------------------------------------------------------------------------------
它們都提供瞭如下三個方法:
    void     unread(byte[]/char[] cbuf) :將一個位元組/字元陣列內容推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容
    void     unread(byte[]/char[] cbuf, int off, int len) :將一個位元組/字元數組裡從 off 開始,長度為 len 位元組/字元的內容推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容
    void     unread(int c) :將一個位元組/字元推回到推回緩衝區,從而允許重複讀取剛剛讀取的內容。
    
這兩個推回輸入流都帶有一個推回緩衝區,當程式呼叫這兩個推回輸入流的 unread() 方法時,系統將會把指定陣列的內容推回到該緩衝區,
而推回輸入流每次呼叫 read() 方法時總是先從推回緩衝區讀取,只有完全讀取了推回緩衝區的內容後,但還沒有裝滿 read() 所需的陣列時才會從原輸入流讀取。

當程式建立一個 PushbackInputStream 和PushbackReader 時需要指定推回緩衝區的大小,預設緩衝區的長度為 1 。
如果程式中推回到推回緩衝區的內容超出了推回緩衝區的大小,將會引發 Pushback buffer overfloww 的 IOException 異常。

例子:
public class PushbackTest
{
    public static void main(String[] args)
    {
        try(
            // 建立一個PushbackReader物件,指定推回緩衝區的長度為64
            PushbackReader pr = new PushbackReader(new FileReader(
                "PushbackTest.java") , 64))
        {
            char[] buf = new char[32];
            // 用以儲存上次讀取的字串內容
            String lastContent = "";
            int hasRead = 0;
            // 迴圈讀取檔案內容
            while ((hasRead = pr.read(buf)) > 0)
            {
                // 將讀取的內容轉換成字串
                String content = new String(buf , 0 , hasRead);
                int targetIndex = 0;
                // 將上次讀取的字串和本次讀取的字串拼起來,
                // 檢視是否包含目標字串, 如果包含目標字串
                if ((targetIndex = (lastContent + content)
                    .indexOf("new PushbackReader")) > 0)
                {
                    // 將本次內容和上次內容一起推回緩衝區
                    pr.unread((lastContent + content).toCharArray());
                    // 重新定義一個長度為targetIndex的char陣列
                    if(targetIndex > 32)
                    {
                        buf = new char[targetIndex];
                    }
                    // 再次讀取指定長度的內容(就是目標字串之前的內容)
                    pr.read(buf , 0 , targetIndex);
                    // 列印讀取的內容
                    System.out.print(new String(buf , 0 ,targetIndex));
                    System.exit(0);
                }
                else
                {
                    // 列印上次讀取的內容
                    System.out.print(lastContent);
                    // 將本次內容設為上次讀取的內容
                    lastContent = content;
                }
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

*
*
*

5. 重定向標準輸入/輸出
-----------------------------------------------------------------------------------------------------------------------------------
System 類提供瞭如下三個重定向標準輸入/輸出方法
        static void     setErr(PrintStream err)    : 重定向標準錯誤輸出
        static void     setIn(InputStream in)    : 重定向標準輸入流    
        static void     setOut(PrintStream out)    : 重定向標準輸出流

例子:
public class RedirectOut
{
    public static void main(String[] args)
    {
        try(
            // 一次性建立PrintStream輸出流
            PrintStream ps = new PrintStream(new FileOutputStream("out.txt")))
        {
            // 將標準輸出重定向到ps輸出流
            System.setOut(ps);
            // 向標準輸出輸出一個字串
            System.out.println("普通字串");
            // 向標準輸出輸出一個物件
            System.out.println(new RedirectOut());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

例子:
public class RedirectIn
{
    public static void main(String[] args)
    {
        try(
            FileInputStream fis = new FileInputStream("RedirectIn.java"))
        {
            // 將標準輸入重定向到fis輸入流
            System.setIn(fis);
            // 使用System.in建立Scanner物件,用於獲取標準輸入
            Scanner sc = new Scanner(System.in);
            // 增加下面一行將只把回車作為分隔符
            sc.useDelimiter("\n");
            // 判斷是否還有下一個輸入項
            while(sc.hasNext())
            {
                // 輸出輸入項
                System.out.println("鍵盤輸入的內容是:" + sc.next());
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

*
*
*

6. Java 虛擬機器讀寫其他程序的資料
----------------------------------------------------------------------------------------------------------------------------
使用 Runtime 物件的 exec() 方法可以執行平臺上的其他程式,該方法產生一個 Process 物件, Process 物件代表由該 Java 程式啟動的子程序。
Process 類提供瞭如下三個方法,用於讓程式和其子程序進行通訊。

    abstract InputStream     getErrorStream() :獲取子程序的錯誤流
    abstract InputStream     getInputStream() :獲取子程序的輸入流
    abstract OutputStream     getOutputStream():獲取子程序的輸出流
    
    此處的輸入流、輸出流容易混淆,如果試圖讓子程序讀取程式的資料,那麼應該用輸入流還是輸出流?不是輸入流,而是輸出流。
    要站在 Java 程式的角度來看問題,子程序讀取 Java 程式的資料,就是讓 Java 程式把資料輸出到子程序(就像把資料輸出到檔案中一樣,只是現在由子程序節點代替了檔案節點),所以應該使用輸出流。
    
例子:
public class ReadFromProcess
{
    public static void main(String[] args)
        throws IOException
    {
        // 執行javac命令,返回執行該命令的子程序
        Process p = Runtime.getRuntime().exec("javac");
        try(
            // 以p程序的錯誤流建立BufferedReader物件
            // 這個錯誤流對本程式是輸入流,對p程序則是輸出流
            BufferedReader br = new BufferedReader(new
                InputStreamReader(p.getErrorStream())))
        {
            String buff = null;
            // 採取迴圈方式來讀取p程序的錯誤輸出
            while((buff = br.readLine()) != null)
            {
                System.out.println(buff);
            }
        }
    }
}

例子:
public class WriteToProcess
{
    public static void main(String[] args)
        throws IOException
    {
        // 執行java ReadStandard命令,返回執行該命令的子程序
        Process p = Runtime.getRuntime().exec("java ReadStandard");
        try(
            // 以p程序的輸出流建立PrintStream物件
            // 這個輸出流對本程式是輸出流,對p程序則是輸入流
            PrintStream ps = new PrintStream(p.getOutputStream()))
        {
            // 向ReadStandard程式寫入內容,這些內容將被ReadStandard讀取
            ps.println("普通字串");
            ps.println(new WriteToProcess());
        }
    }
}
// 定義一個ReadStandard類,該類可以接受標準輸入,
// 並將標準輸入寫入out.txt檔案。
class ReadStandard
{
    public static void main(String[] args)
    {
        try(
            // 使用System.in建立Scanner物件,用於獲取標準輸入
            Scanner sc = new Scanner(System.in);
            PrintStream ps = new PrintStream(
            new FileOutputStream("out.txt")))
        {
            // 增加下面一行將只把回車作為分隔符
            sc.useDelimiter("\n");
            // 判斷是否還有下一個輸入項
            while(sc.hasNext())
            {
                // 輸出輸入項
                ps.println("鍵盤輸入的內容是:" + sc.next());
            }
        }
        catch(IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

*
*
*

7. java.io.RandomAccessFile
-------------------------------------------------------------------------------------------------------------------------------
RandomAccessFile 是 Java 輸入輸出體系中功能最豐富的檔案內容訪問類,提供了眾多的方法來訪問檔案內容,既可以讀取檔案內容,也可以向檔案輸出資料
RandomAccessFile 支援“隨機訪問” 方式,程式可以直接跳轉到檔案任意地方讀寫資料

侷限性:只能讀寫檔案,不能讀寫其他 IO 節點。

RandomAccessFile 包含如下兩個方法操作檔案記錄指標:
    long     getFilePointer() : 返回檔案記錄指標的當前位置
    void     seek(long pos) : 將檔案記錄指標定位到 pos 位置
    
RandomAccessFile 的含義是可以自由訪問檔案的任意地方(與 InputStream 、 Reader 需要依次向後讀取相區分)。

構造器:
    RandomAccessFile(File file, String mode)
    RandomAccessFile(String name, String mode)
    
    mode 引數指定 RandomAccessFile 的訪問模式,由如下4個值:
        "r"        :以只讀方式開啟指定檔案,如果試圖對該 RandomAccessFile 執行寫入方法,將丟擲 IOException 異常。
        "rw"    : 以讀、寫方式開啟指定檔案。如果該檔案尚不存在,則嘗試建立該檔案。
        "rws"    : 以讀、寫方式開啟指定檔案。相對於 "rw" 模式,還要求對檔案的內容或元資料的每個更新都同步到寫入到底層儲存裝置。
        "rwd"    : 以讀、寫方式開啟指定檔案。相對於 "rw" 模式,還要求對檔案的內容的每個更新都同步到寫入到底層儲存裝置。

例子:
public class RandomAccessFileTest
{
    public static void main(String[] args)
    {
        try(
            RandomAccessFile raf =  new RandomAccessFile(
                "RandomAccessFileTest.java" , "r"))
        {
            // 獲取RandomAccessFile物件檔案指標的位置,初始位置是0
            System.out.println("RandomAccessFile的檔案指標的初始位置:"
                + raf.getFilePointer());
            // 移動raf的檔案記錄指標的位置
            raf.seek(300);
            byte[] bbuf = new byte[1024];
            // 用於儲存實際讀取的位元組數
            int hasRead = 0;
            // 使用迴圈來重複“取水”過程
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 取出“竹筒”中水滴(位元組),將位元組陣列轉換成字串輸入!
                System.out.print(new String(bbuf , 0 , hasRead ));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

例子:
public class AppendContent
{
    public static void main(String[] args)
    {
        try(
            //以讀、寫方式開啟一個RandomAccessFile物件
            RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
        {
            //將記錄指標移動到out.txt檔案的最後
            raf.seek(raf.length());
            raf.write("追加的內容!\r\n".getBytes());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

RandomAccessFile 依然不能向檔案的指定位置插入內容,如果直接將檔案記錄指標移動到中間某位置後開始輸出,則新輸出的內容會覆蓋檔案中原有的內容。
如果需要向指定位置插入內容,程式需要先把插入點後面的內容讀入緩衝區,等把需要插入的資料寫入檔案後,再將緩衝區的內容追加的檔案後面。

例子:
public class InsertContent
{
    public static void insert(String fileName , long pos
        , String insertContent) throws IOException
    {
        File tmp = File.createTempFile("tmp" , null);
        tmp.deleteOnExit();
        try(
            RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
            // 使用臨時檔案來儲存插入點後的資料
            FileOutputStream tmpOut = new FileOutputStream(tmp);
            FileInputStream tmpIn = new FileInputStream(tmp))
        {
            raf.seek(pos);
            // ------下面程式碼將插入點後的內容讀入臨時檔案中儲存------
            byte[] bbuf = new byte[64];
            // 用於儲存實際讀取的位元組數
            int hasRead = 0;
            // 使用迴圈方式讀取插入點後的資料
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 將讀取的資料寫入臨時檔案
                tmpOut.write(bbuf , 0 , hasRead);
            }
            // ----------下面程式碼插入內容----------
            // 把檔案記錄指標重新定位到pos位置
            raf.seek(pos);
            // 追加需要插入的內容
            raf.write(insertContent.getBytes());
            // 追加臨時檔案中的內容
            while ((hasRead = tmpIn.read(bbuf)) > 0 )
            {
                raf.write(bbuf , 0 , hasRead);
            }
        }
    }
    public static void main(String[] args)
        throws IOException
    {
        insert("InsertContent.java" , 45 , "插入的內容\r\n");
    }
}

上面程式中使用 File 的 createTempFile(String prefix, String suffix) 方法建立了一個臨時檔案(該臨時檔案將在 JVM 退出時被刪除),用以儲存被插入檔案的插入點後面的內容。
程式先將檔案插入點後的內容讀入臨時檔案中,然後重新定位到插入點,將需要插入的內容新增到檔案後面,最後將臨時檔案的內容新增到檔案後面。


*
*
*

8. 物件序列化
-------------------------------------------------------------------------------------------------------------------------------------
物件序列化的目標就是將物件儲存到磁碟上,或允許在網路中直接傳輸物件。
物件序列化機制允許把記憶體中的 Java 物件轉換成平臺無關的二進位制流,從而允許把這種二進位制流持久地儲存在磁碟上,通過網路將這種二進位制流傳輸到另一個網路節點。
其他程式一旦獲得了這種二進位制流,就可以將這種二進位制流恢復成原來的 Java 物件。


序列化的含義和意義
-------------------------------------------------------------------------------------------
序列化機制使得物件可以脫離程式的執行而獨立存在。

物件序列化(Serialize)指的是將一個 Java 物件寫入 IO 流中,物件的反序列化 (Deserialize)則是指從 IO 流中恢復該 Java 物件。

如果需要讓某個物件支援序列化機制,則必須讓它的類是可以序列化的 ( serializable ),為了讓某個類是可序列化的,該類必須實現如下兩個介面之一:
    Serializable    : 該介面是一個標記介面,實現該介面無須實現任何方法,它只是表明該類的例項是可序列化的
    Externalizable    
    
    所有可能在網路上傳輸的物件的類都應該是可序列化的,否則程式將會出現異常。比如 RMI 遠端方法呼叫過程中的引數和返回值。
    所有需要儲存到磁碟的物件的類都應該時可序列化的,比如 Web 應用中需要儲存到 HttpSession 或 ServletContex 屬性的 Java 物件。
    
    

使用対向流實現序列化
--------------------------------------------------------------------------------------------
一旦某個類實現了 Serializable 介面,該類的物件就是可序列化的,程式可以通過如下兩個步驟來序列化物件
    1. 建立一個 java.io.ObjectOutputStream ,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎上
        ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream("object.txt"));
    2. 呼叫 ObjectOutputStream 物件的 writeObject(Object obj) 方法,輸出可序列化物件
        oos.writeObject(obj);

例子:
public class WriteObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("object.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 將per物件寫入輸出流
            oos.writeObject(per);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

從二進位制流中恢復 Java 物件,需要反序列化,反序列化步驟如下:
    1. 建立一個 java.io.ObjectInputStream 輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎之上。
        ObjectInputStream ois = new ObjectInputStream( new FileInputStream("object.txt"));
            
    2. 呼叫 ObjectInputStream 物件的 readObject() 方法讀取流中的物件,該方法返回一個 Object 型別的 Java 物件,如果程式知道該 Java 物件的型別,則可以將該物件強制型別轉換成真實的型別。
        Person p = (Person)ois.readObject();
        
例子:
public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
            Person p = (Person)ois.readObject();
            System.out.println("名字為:" + p.getName()
                + "\n年齡為:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

必須指出的是,反序列化讀取的僅僅是 Java 物件的資料,而不是 Java 類,因此採用反序列化恢復 Java 物件時,必須提供該 Java 物件所屬類的 class 檔案,否則將引發 ClassNotFoundException 異常。
還有一點指出,反序列化機制無須通過構造器來初始化 Java 物件。

如果使用序列化機制向檔案中寫入了多個 Java 物件,使用反序列化機制恢復物件時必須按實際寫入的順序讀取。
當一個可序列化類由多個父類時(包括直接父類和間接父類),這些父類要麼有無引數的構造器,要麼也是可序列化的,否則反序列化時丟擲 InvalidClassException 異常。
如果父類是不可序列化的,只是帶有無引數構造器,則該父類中定義的成員變數值不會序列化到二進位制流中。


物件引用的序列化
-----------------------------------------------------------------------------------
如果某個類的成員變數的型別不是基本型別或 String 型別,而是另一個引用型別,那麼這個引用類必須是可序列化的,否則擁有該型別成員變數的類也是不可序列化的。

如 Teacher 類持有一個 Person 類的引用,只有 Person 類是可序列化的, Teacher 類才是可序列化的。如果 Person 類不可序列化,則無論 Teacher 類是否實現 Serializable 、Externalizable 介面,則 Teacher 類都是不可序列化的。

Java 序列化機制採用一種特殊的序列化演算法,內容如下:
    1. 所有儲存到磁碟中的物件都有一個序列化編號。
    2. 當程式試圖序列化一個物件時,程式將先檢查該物件是否已經被序列化過,只有該物件從未被序列化過(在本次虛擬機器中),系統才會將該物件轉換成位元組序列輸出。
    3. 如果某個物件已經序列化過,程式將只是直接輸出一個序列化編號,而不是再次重新序列化該物件
    
由於 Java 序列化機制使然,如果多次序列化同一個 Java 物件,只有第一次序列化時才會被該 Java 物件轉換成位元組序列輸出,這樣可能引起一個潛在問題-----當程式序列化一個可變物件時,
只有第一次使用 writeObject() 方法輸出時才會將該物件轉換成位元組序列輸出,當程式再次呼叫 writeObject() 方法時,程式只是輸出前面的序列化編號,即使後面該物件的例項變數已被改變,改變的例項變數值也不會輸出。


自定義序列化
--------------------------------------------------------------------------------------
在一些特殊場景下,如果一個類裡包含的某些例項變數是敏感資訊,如銀行賬戶資訊等,這時不希望系統將該例項變數值進行序列化,
或者某個例項變數的型別是不可序列化的,因此不希望對該例項變數進行遞迴序列化,以避免引發 java.io.NotSerializableException 異常。

通過在例項變數前使用 transient 關鍵字修飾,可以指定 Java 序列化時無須理會該例項變數。
transient 關鍵字只能用來修飾例項變數,不能用來修飾程式的其他部分。

例子:
public class Person
    implements java.io.Serializable
{
    private String name;
    private transient int age;
    // 注意此處沒有提供無引數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有引數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }
}

使用 transient 關鍵字修飾例項變數雖然簡單、方便,但被 transient 修飾的例項變數將被完全隔離在序列化機制之外,這樣導致在飯序列化恢復 Java 物件時無法取得該例項變數的值。
Java 提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程式控制如何序列化各例項變數,甚至完全不序列化某些例項變數(與 transient 關鍵字效果相同)
在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊方法用以實現自定義序列化:
    private void writeObject(java.io.ObjectOutputStream out) throws IOException :
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException:
    private void readObjectNoData() throws ObjectStreamException :
    
    writeObject() 方法負責寫入特定類的例項狀態,以便相應的 readObject() 方法可以恢復它。通過重寫該方法,程式設計師可以完全獲得對序列化機制的控制,可以自主決定哪些例項變數需要序列化,需要怎樣序列化。
            在預設情況下,該方法會呼叫 out.defaultWriteObject() 來儲存 Java 物件的各例項變數,從而可以實現序列化 Java 物件狀態的目的。

    readObject() 方法負責從流中讀取並恢復物件例項變數,通過重寫該方法,程式設計師可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些例項變數,以及如何進行反序列化。
            在預設情況下,該方法會呼叫 in.defaultReadObject() 來恢復 Java 物件的非 transient 例項變數。通常情況下, readObject() 方法與 writeObject() 方法對應,
            如果 writeObject() 方法中對 Java 物件的例項進行了一些處理,則應該在 readObject() 方法中對其例項變數進行相應的反處理,以便正確恢復該物件。
            
    readObjectNoData(), 當序列化流不完整時, readObjectNoData() 方法可以用來正確地初始化反序列化物件。例如,接收方使用的反序列化類的版本不同於傳送方,或者接收方版本擴充套件的類不是傳送方版本擴充套件的類,
            或者序列化流被篡改,系統都會呼叫 readObjectNoData() 方法來初始化反序列化的物件
            
            
例子:
public class Person
    implements java.io.Serializable
{
    private String name;
    private int age;
    // 注意此處沒有提供無引數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有引數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    private void writeObject(java.io.ObjectOutputStream out)
        throws IOException
    {
        // 將name例項變數的值反轉後寫入二進位制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        // 將讀取的字串反轉後賦給name例項變數
        this.name = ((StringBuffer)in.readObject()).reverse()
            .toString();
        this.age = in.readInt();
    }
}

writeObject() 方法儲存例項變數的順序應該和 readObject() 方法中恢復例項變數的順序一致,否則不能正常恢復該 Java 物件

還有一種更測底的自定義機制,它甚至可以在序列化物件時將該物件替換成其他物件,如果需要實現序列化某個物件時替換該物件,則應為序列化類提供如下特殊方法
    
        ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
        
    此 writeReplace() 方法將由序列化機制呼叫,只要該方法存在。
    因為該方法可以擁有 private, protected, package-private 等訪問許可權,所以其子類有可能獲得該方法。
    
例子:
public class Person
    implements java.io.Serializable
{
    private String name;
    private int age;
    // 注意此處沒有提供無引數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有引數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    //    重寫writeReplace方法,程式在序列化該物件之前,先呼叫該方法
    private Object writeReplace()throws ObjectStreamException
    {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

public class ReplaceTest
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("replace.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("replace.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 系統將per物件轉換位元組序列並輸出
            oos.writeObject(per);
            // 反序列化讀取得到的是ArrayList
            ArrayList list = (ArrayList)ois.readObject();
            System.out.println(list);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

    Java 的序列化機制保證在序列化某個物件之前,先呼叫該物件的 writeReplace() 方法,如果該方法返回一個 Java 物件,則系統轉為序列化這個返回的物件。
    系統在序列化某個物件之前,會呼叫該物件的 writeReplace() 和 writeObject() 兩個方法,系統總是先呼叫 writeReplace() 方法,如果該方法返回另一個物件,
    系統將再次呼叫返回物件的 writeReplace()方法... 直到該方法不再返回另一個物件為之。程式最後將呼叫該物件的 writeObject() 方法來儲存物件的狀態。

    與 writeReplace() 方法相對的是, 序列化機制裡還有一個特殊的方法,它可以實現保護性複製整個物件,這個方法就是:
    
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

    這個方法會緊接著 readObject() 之後被呼叫,該方法的返回值將會代替原來的反序列化物件,而原來 readObject() 反序列化的物件將會被立即丟棄。

例子:
public class Orientation
    implements java.io.Serializable
{
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
    private int value;
    private Orientation(int value)
    {
        this.value = value;
    }
    // 為列舉類增加readResolve()方法
    private Object readResolve()throws ObjectStreamException