1. 程式人生 > >Java程式設計思想(五)第18章-Java IO系統

Java程式設計思想(五)第18章-Java IO系統

目錄:

1 File類

  File(檔案)類這個名字有一定的誤導性;我們可能會認為它指代的是檔案,實際上卻並非如此。它既能代表一個特定檔案的名稱,又能代表一個目錄下的一組檔案的名稱。實際上,FilePath檔案路徑)對這個類來說是更好的名字。

  如果它指的是一個檔案集,我們就可以對此集合呼叫list()方法,這個方法會返回一個字串陣列。

2 輸入和輸出(Input an output)

  程式語言的I/O類庫常使用這個抽象概念,它代表任何有能力產出資料資料來源物件或者是有能力接收資料接收物件。“流”遮蔽了實際的I/O裝置中處理資料的細節。

我們很少使用單一的類來建立流物件,而是通過疊合多個物件來提供所期望的功能(這是裝飾器設計模式)。實際上,Java中的“流”類庫讓人迷惑的主要原因就在於:建立單一的結果流,卻需要建立多個物件。

  在Java 1.0中,類庫的設計者首先限定與輸入有關的所有類都應該從InputStream繼承,而與輸出有關的所有類都應該從OutputStream繼承。

  InputStream或Reader中的read()用於讀取單個位元組或者位元組陣列,OutputStream或Writer用於寫單個位元組或者位元組陣列。

2.1 InputStream型別

  InputStream的作用是用來表示那些從不同資料來源產生輸入資料的類。每一種資料來源都有相應的InputStream子類。這些資料來源包括:

  • 位元組陣列。An array of bbytes.
  • String物件。A String object.
  • 檔案。A file.
  • “管道”(A pipe),工作方式與實際管道相似,即,從一端輸入,從另一端輸出。
  • 一個由其他種類的流組成的序列,以便我們可以將它們收集合併到一個流內。A sequence of other streams.
  • 其他資料來源,如Internet連線等。Other sources.

2.2 OutputStream型別

  OutputStream類決定了輸出所要去往的目標:

  • 位元組陣列
  • 檔案
  • 管道

3 新增屬性和有用介面

  FilterInputStreamFilterOutputStream 是用來提供裝飾器類介面以控制特定輸入流(InputStream)和輸出流

(OutputStream)的兩個類,它們的名字並不直觀。這兩個類是裝飾器的必要條件(以便能為所有正在被修飾的物件提供通用介面)。

4 Reader 和 Writer

  • InputStreamOutputStream面向位元組形式的I/O提供功能
  • ReaderWriter面向字元(相容Unicode)形式的I/O功能

設計ReaderWriter繼承層次結構主要是為了國際化。老的I/O流繼承層次結構僅支援8位位元組流,並且不能很好地處理16位的Unicode字元。由於Unicode用於字元國際化(Java本身的char也是16位的Unicode),所以新增ReaderWriter繼承層次結構就是為了在所有的I/O操作中都支援Unicode。另外新類庫的設計使得它的操作比舊類庫更快。

5 自我獨立的類:RandomAccessFile

RandomAccessFile適用於由大小已知的記錄組成的檔案,所以我們可以使用seek()將記錄從一處轉移到另一處,然後讀取或者修改記錄。

RandomAccessFile擁有和別的I/O型別本質不同的行為,因為我們可以在一個檔案 內向前和向後移動。在任何情況下,它都是自我獨立的,直接從Object派生而來。

  從本質上說,RandomAccessFile的工作方式類似於把DataInputStream和DataOutputStream組合起來使用,還添加了一些新方法:

  • getFilePointer()用於查詢當前所處的檔案位置,
  • seek()用於在檔案內移至新的位置,
  • length()用於判斷檔案的最大尺寸。

6 I/O流的典型使用方式(Typical uses of I/O streams)

儘管可以通過不同的方式組合I/O流類,但我們可能也就只用到其中的幾種組合。下面的例子可以作為典型的I/O用法的基本參考。

6.1 緩衝輸入檔案(Buffered input file)

6.2 從記憶體輸入(Input from memory)

6.3 格式化的記憶體輸入(Formatted memory input)

6.4 基本的檔案輸出(Basic file output)

6.5 儲存和恢復資料(Storing and recovering data)

6.6 讀寫隨機訪問檔案(Reading and writing random-access files)

6.7 管道流(PipedStreams)

7 標準I/O(Standard I/O)

標準I/O這個術語參考的是Unix中“程式所使用的單一資訊流”這個概念。

標準I/O的意義在於:我們可以很容易地把程式串聯起來,一個程式的標準輸出可以成為另一個程式的標準輸入。

8 新I/O

速度的提高來自於所使用的結構更接近於作業系統執行I/O的方式:通道緩衝器。我們可以把它想像成一個煤礦,通道是一個包含煤層(資料)的礦藏,而緩衝器則是派送到礦藏的卡車。卡車載滿煤炭而歸,我們再從卡車上獲得煤炭。也就是說,我們並沒法有直接和通道互動,我們只是和緩衝器互動,並把緩衝器派送到通道。通道要麼從緩衝器獲得資料,要麼向緩衝器傳送資料。

唯一直接與通道互動的緩衝器是ByteBuffer——也就是說,可以儲存未加工位元組的緩衝器。當我們查詢JDK文件中的java.nio.ByteBuffer時,會發現它是相當基礎的類:通過告知分配多少儲存空間來建立一個ByteBuffer物件,並且還有一個方法選擇集,用於以原始的位元組形式或基本資料型別輸出和讀取資料。但是,沒辦法輸出或讀取物件,即使是字串物件也不行。這種處理雖然很低階,但卻正好,因為這是大多數作業系統中更有效的對映方式。

8.1 通道FileChannel

  FileChannel操縱位元組流的。舊I/O類庫中有三個類被修改了,用以產生FileChannel:

  • FileInputStream.getChannel()
  • FileOutputSteam.getChannel()
  • RandomAccessFile.getChannel()
package net.mrliuli.io.nio;

import java.nio.*;
import java.nio.channels.*;
import java.io.*;

public class GetChannel {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws Exception {
        // Write a file:
        FileChannel fc = new FileOutputStream("data.txt").getChannel();
        fc.write(ByteBuffer.wrap("Some text ".getBytes()));
        /*
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        fc.read(buffer);                                            // NonReadableChannelException
        System.out.println((char)buffer.get());
        */
        fc.close();
        // Add to the end of the file:
        fc = new RandomAccessFile("data.txt", "rw").getChannel();   // Readable and Writable        
        fc.position(fc.size()); // Move to the end      
        fc.write(ByteBuffer.wrap("some more".getBytes()));
        fc.close();
        // Read the file:
        fc = new FileInputStream("data.txt").getChannel();
        ByteBuffer buff = ByteBuffer.allocate(BSIZE);       
        fc.read(buff);
        //fc.write(ByteBuffer.wrap("again".getBytes()));            //NonWritableChannelException
        buff.flip();
        while(buff.hasRemaining())
            System.out.print((char)buff.get()); // ByteBuffer.get() returns a byte
        System.out.println();
    }
}

8.2 緩衝器ByteBuffer

  將位元組存放於緩衝器ByteBuffer的方式:

  • 使用put()直接填充,填入一個或多個位元組,或基本資料型別的值;
  • 使用wrap()將已存在的位元組陣列包裝到ByteBuffer中。

8.3 read(), write(), flip(), write()

  設有一個輸入通道in、一個輸出通道out和一個緩衝器buffer:

  • in.read(buffer);fc中的位元組輸入buffer,此時必須再調buffer.flip();做好讓別人從buffer讀取位元組的準備。
  • out.write(buffer)buffer中的位元組輸出到outwrite()操作之後,資訊仍在緩衝器buffer中,須呼叫buffer.clear();對所有的內部指標重新安排,以便緩衝器在另一個read()操作期間能夠做好接受資料的準備。
package net.mrliuli.io.nio;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class ChannelCopy {

    private static final int BSIZE = 1024;

    public static void main(String[] args) throws Exception {

        if(args.length != 2){
            System.out.println("arguments : sourcefile destfile");
            System.exit(1);
        }

        // 開啟一個FileChaanel用於讀(輸入)
        FileChannel in = new FileInputStream(args[0]).getChannel();

        // 開啟一個FileChannel用於寫(輸出)
        FileChannel out = new FileOutputStream(args[1]).getChannel();

        // 一個緩衝器,分配了BSIZE個位元組
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);

        /*
         * return The number of bytes read, possibly zero, or <tt>-1</tt> if the channel has reached end-of-stream
         * FileChanel.read()
         * */

        // -1 一個分界符(源於Unix和C),表示到達了輸入的末尾
        while(in.read(buffer) != -1){

            buffer.flip(); // Prepare for writing
            out.write(buffer);

            // write()操作之後,資訊仍在緩衝器中,clear()操作對所有的內部指標重新安排,以便緩衝器在另一個read()操作期間能夠做好接受資料的準備。
            buffer.clear(); // Prepare for reading
        }

    }

}

8.4 轉換資料

緩衝器容納的是普通的位元組,為了把它們轉換成字元,我們要麼在輸入它們的時候對其進行編碼(這樣,它們輸出時才具有意義),要麼在將其從緩衝器輸出時對它們進行解碼。可以使用java.nio.charset.Charset類實現這些功能,該類提供子把資料編碼成多種不同型別的字符集的工具。The buffer contains plain bytes, and to turn these into characters, we must either encode them as we put them in (so that they will be meaningful when they come out) or decode them as they come out of the buffer. This can be accomplished using the java.nio.charset.Charset class, which provides tools for encoding into many different types of character set.

8.5 檢視緩衝器

  檢視緩衝器(view buffer)可以讓我們通過某個特定的基本資料型別的視窗檢視其底層的ByteBuffer。ByteBuffer依然是實際儲存資料的地方,“支援”著前面的檢視,因此對檢視的任何修改都會對映成為對ByteBuffer中資料的修改。

8.6 檔案加鎖

  檔案加鎖對其他的作業系統程序是可見的,因為Java的檔案加鎖直接對映到了本地作業系統的加鎖工具。

exclusive lock 獨佔鎖

Locking portions of a mapped file 對對映檔案的部分加鎖

cretical section 臨界區

9 壓縮(Compression)

10 物件序列化(Object serialization)

Java的物件序列化將那些實現了Serilizable介面的物件轉換成一個位元組序列,並能夠在以後將這個位元組序列完全恢復為原來的物件。這一過程甚至可通過網路朝廷這意味著序列化機制能自動彌補不同作業系統之間的差異。

就其本身來說,物件的序列化是非常有趣的,因為利用它可以實現輕量級永續性(ligthweight persistence)。永續性意味著一個物件的生存週期並不取決於程式是否正在執行,它可以生存於程式的呼叫之間。

  物件序列化的概念加入到語言中是為了支援兩種主要特性:

  • 一是Java的遠端方法呼叫(Remote Method Invocation, RMI),它使存活於其他計算機的物件使用起來就像是存活於本機上一樣。當向遠端物件傳送訊息時,需要通過物件序列化來傳輸引數和返回值。
  • 再者,對Java Beans來說,物件的序列化也是必需的。使用一個Bean時,一般情況下是在設計階段對它的狀態資訊進行配置。這種狀態資訊必須儲存下來,並在程式啟動時進行後期恢復;這種具體工作就是由物件序列化完成的。

  序列化一個物件和反序列化

  • 首先要建立一個ObjectOutputStream物件,要通過建構函式含有一個 OutputStream 物件。
  • 然後,只需呼叫 void writeObject(Object obj),即可將物件obj序列化,即轉換成位元組序列輸出到第一步所說的Outputstream
  • 反序列化,即將位元組序列還原為一個物件,則只需呼叫ObjectInputStreamObject readObject(),輸入到一個InputStream

例:

Worm.java

10.1 尋找類

  反序列,即將位元組序列還原為物件時,必須保證Java虛擬機器能夠找到要還原的物件的相關.class檔案,否則丟擲java.lang.ClassNotFoundException異常。

10.2 序列化的控制

  如果只希望一個物件的某些資訊序列化而某些資訊不序列化,即進行序列化控制,可使用Externalizable介面。

10.2.1 Externalizable介面

  Externalizable介面繼承自Serializable介面,有兩個方法如下,這兩個方法會在序列化和反序列化過程中被自動呼叫

  • void writeExternal(ObjectOutput obj),在該方法內部只對所需部分進行顯式序列化
  • void readExternal(ObjectInput in)

10.2.2 Externalizable介面與Serializable介面區別

  • Externalizable只序列化writeExternal()中的部分,而Serializable自動地全部序列化。
  • Externalizable在反序列化時(即呼叫readObject()時),會首先呼叫所有普通的預設構造器,然後呼叫readExternal()
  • Serializable在反序列化時,物件完全以它儲存的二進位制位為基礎來構造,而不用呼叫構造器

例:

Blips.java
Blip3.java

10.2.3 transient(瞬時)關鍵字

如果我們正操作的是一個Serializable物件,那麼所有序列化操作都會自動進行。為了能夠予以控制,可以用transient(瞬時)關鍵字逐個欄位地關閉序列化,它的意思是“不用麻煩你儲存或恢復資料——我會自己處理的”。

由於Externalizable物件在預設情況下不儲存任何欄位,所以transient關鍵字只能和Serializable物件一起使用。

10.3 序列化的永續性

我們可以通過一個位元組陣列來使用物件序列化,從而實現對任何可Serializable物件的“深度複製”(deep copy)——深度複製意味著我們複製的是整個物件網,而不僅僅是基本物件及其引用。

  • 一個物件被序列化在單一流中,就可以恢復出與我們寫出時一樣的物件網,並且沒有任何意外重複複製出的物件。
  • 一個物件被序列化在不同流中,再從不同流恢復時,得到的物件地址不同。

例:

MyWorld.java

物件序列化的一個重要限制是它只是Java的解決方案:只有Java程式才能反序列化這種物件。一種更具互操作性的解決方案是將資料轉換為XML格式,這可以使其被各種各樣的平臺語言使用。