Java IO/NIO

在Java程序中,對於數據的輸入/輸出以”流”(Stream)方式進行
java.io 包定義了多個流類型:
  1) 按數據流方向分爲 – 輸入流和輸出流
  2) 按數據單位分爲 – 字節流和字符流
  3) 按功能分爲 – 節點流和處理流

所有流類型均位於java.io包內,分別繼承以下四種抽象流類型:
      字節流      字符流
輸入流   InputStream   Reader
輸出流  OutputStream    Writer

注:輸入輸出流,都是站在【程序】的角度上來說,從文件讀數據這叫輸入流,往文件寫數據叫輸出流

一、字節 I/O 操作(InputStream和OutputStream)相關類圖

這裏寫圖片描述

1) InputStream是一個抽象類,核心方法是read()、read(byte b[])、read(byte b[], int off, int len),從不同的數據源讀取數據。
這些數據源有:
 ① 字節數組。 
 ② String對象。 
 ③ 文件。
 ④ “管道”,一端輸入,另一端輸出。
 ⑤ 一個由其他種類的流組成的序列。 
 ⑥ 其他數據源,如Internet等。
每一種數據源都對應相應的InputStream子類:
 ByteArrayInputStream:緩衝區,處理字節數組,允許將內存的緩衝區當作InuputStream。
 StringBufferInputStream:將String轉換成 InputStream,底層使用StringBuffer實現。
 FileInputStream:從文件中讀取信息。
 PipedInputStream:從管道讀數據,最爲多線程中的數據源。
 SequenceInputStream:將多個流對象轉換成一個InputStream。
 FilterInputStream:“裝飾器”類的基類(這裏是裝飾器模式),爲其它InputStream類提供功能:
   DataInputStream:用於讀取基本類型數據。
   BufferedInputStream:使用緩衝區,防止每次讀取時都得進行實際寫操作。
   LineNumberInputStream:跟蹤輸入流中的行號,getLineNumber()、setLineNumber(int)。
   PushbackInputStream:具有能彈出一個字節的緩衝區,可以將讀到的最後一個字符回退。
 
2) OutputStream是一個抽象類,核心方法是write(int b)、write(byte b[])、write(byte b[], int off, int len),決定輸出的目標:字節數組、文件或管道。
 ByteArrayOutputStream:將數據寫入一個byte數組緩衝區。
 FileOutputStream:將數據寫入文件。
 PipedOutputStream:指定多線程數據的目的地,向與其它線程共用的管道中寫入數據,用於多線程之間任務的通信
 FilterOutputStream:“裝飾器”類,提供特定的輸出流:
   DataOutputStream:與DataInputStream搭配使用,用於寫入基本類型數據。
   BufferedOutputStream:使用緩衝區,避免每次發送數據時都進行實際的寫操作,可使用 flush() 清空緩衝區。
   PrintStream:產生格式化輸出。

二、字符 I/O 操作(Reader和Writer)相關類圖

這裏寫圖片描述

Reader和Writer操作的是字符,通過 InputStreamReaderOutputStreamWriter 進行字節和字符的轉換

三、基於磁盤的I/O操作(File類)
  File類既能代表一個特定文件的名稱,又能代表一個目錄下的一組文件的名稱。
【1】文件、目錄的創建和刪除

public class FileOperation {
    public static void main(String[] args) {
        // 使用 File.separator 系統默認名稱分隔符,Windows和Linux文件路徑分割符不同
        String fileName = "C:" + File.separator + "hello.txt"; 
        File file = new File(fileName);

        // 創建一個新文件
        try {
            // 指定的文件不存在且成功創建返回 true;文件已存在返回 false
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 刪除文件
        if(file.exists()){
            // 成功刪除文件或目錄時返回 true
            file.delete();
        }

        // 創建一個文件夾
        String dirName = "C:" + File.separator + "hello";
        File dirFile = new File(dirName);
        dirFile.mkdir();
        dirFile.isDirectory(); // 判斷是否爲目錄
    }
}

【2】查看目錄列表

public class FileOperation {
    public static void main(String[] args) {
        File path = new File("C:");
        String[] list;

        // 查看目錄所有內容
        list = path.list();

        // 查看目錄指定內容(以 .txt 結尾的)
        final String regStr = ".+.txt";

        // 使用一個匿名的目錄過濾器內部類,通過正則表達式驗證過濾,list()可以回調accept()方法
        list = path.list(new FilenameFilter() {
            private Pattern pattern = Pattern.compile(regStr); 
            @Override
            public boolean accept(File dir, String name) {
                return pattern.matcher(name).matches();
            }
        });
        for(String dirItem : list){
            System.out.println(dirItem);
        }
    }
}

四、I/O流典型使用方式
  儘管可以通過不同的方式組合 I/O 流類,但我們可能也就只用到其中的幾種組合。
【1】緩衝輸入文件(讀取)

public class BufferedInputFile {
    public static String read(String fileName) throws Exception{
        BufferedReader br = new BufferedReader(new FileReader(fileName));
        String str;
        StringBuffer sb = new StringBuffer();
        while((str = br.readLine()) != null){
            sb.append(str + "\n");
        }
        br.close();
        return sb.toString();
    }
    public static void main(String[] args) throws Exception {
        System.out.println(read("src/com/test/BufferedInputFile.java"));
    }
}

【2】從內存輸入(讀取)

public class MemoryInput {
    public static void main(String[] args) throws Exception {
        StringReader in = new StringReader(BufferedInputFile.read("src/com/test/MemoryInput.java"));
        int c;
        while((c = in.read()) != -1){
            System.out.println((char)c);
        }
    }
}

read()一個字符一個字符的讀取,以int的形式返回下一個字符,需強制轉換爲char才能正常打印

【3】基本文件輸出

/**
 * FileWriter向文件寫入數據,使用BufferWriter將其封裝緩衝輸出,爲了格式化裝飾成 PrintWriter
 *
 */
public class BasicFileOutput {
    static String file = "BasicFileOutput.out";
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new StringReader(BufferedInputFile.read("src/com/test/BasicFileOutput.java")));
        // PrintWriter有個構造函數,內部實現了BufferedWriter緩衝
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
        //可以簡寫爲 PrintWriter out = new PrintWriter(file);
        int lineCount = 1;
        String s;
        while((s = br.readLine()) != null){
            out.println(lineCount++ + ": " + s);
        }
        out.close();
        System.out.println(BufferedInputFile.read(file));
    }
}

【4】存儲和恢復數據

/**
 * 存儲和恢復數據,使用 DataOutputStream寫入數據,並用 DataInputStream 恢復數據
 * 這些流可以是任何形式,以文件爲例,對讀寫都進行緩衝處理
 */
public class StoringAndRecoveringData {
    public static void main(String[] args) throws Exception {
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
                new FileOutputStream("C:" + File.separator + "Data.txt")));
        out.writeDouble(3.14159265);
        out.writeUTF("That was PI");
        out.writeDouble(1.41413);
        out.writeUTF("Square root of 2");
        out.close();

        DataInputStream in = new DataInputStream(new BufferedInputStream(
                new FileInputStream("C:" + File.separator + "Data.txt")));
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
        in.close();
    }
}

【5】讀寫隨機訪問文件

/**
 * 讀寫隨機訪問文件 RandomAccessFile,類似於組合使用 DataInputStream和DataOutputStream
 * 另外可以使用 seek() 可以在文件中到處移動,並修改文件中的某個值
 * double 總是佔 8 個字節,故第五個就是  5*8
 */
public class UsingRandomAccessFile {
    static String file = "rtest.dat";
    static void display() throws IOException{
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        for(int i=0; i<7; i++){
            System.out.println("Value " + i + ": " + raf.readDouble());
        }
        System.out.println(raf.readUTF());
        raf.close();
    }
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        for(int i=0; i<7; i++){
            raf.writeDouble(i * 1.414);
        }
        raf.writeUTF("The end of the file");
        raf.close();
        display();

        raf = new RandomAccessFile(file, "rw");
        raf.seek(5*8);
        raf.writeDouble(47.0001);
        raf.close();
        display();
    }
}

【6】文件讀寫使用工具
  JavaSE5 在PrintWriter 中添加了方便的構造器,可以方便的對一個文本文件進行寫入操作,但是其他常見的操作卻需要反覆執行,寫一個工具類用來簡化對文件的讀寫操作。創建一個FileTool對象,它用一個ArrayList來保存文件的若干行,當操縱文件內容時,就可以使用ArrayList的所有功能。

public class FileTool extends ArrayList<String> {
    /**
     * 讀取文件
     * @param fileName 文件名
     * @return 返回 String
     */
    public static String read(String fileName) {
        StringBuffer sb = new StringBuffer();
        try{
            BufferedReader br = new BufferedReader(new FileReader(new File(fileName).getAbsoluteFile()));
            try {
                String s;
                while((s = br.readLine()) != null){
                    sb.append(s);
                    sb.append("\n");
                }
            } finally {
                br.close();
            }
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
        return sb.toString();
    }
    /**
     * 往文件中寫數據
     * @param fileName 文件名
     * @param text 要寫入的文本
     */
    public static void write(String fileName, String text) {
        try {
            PrintWriter out = new PrintWriter(new File(fileName).getAbsoluteFile());
            try {
                out.print(text);
            } finally {
                out.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 讀取文件,並按照指定分隔符分割
     * @param fileName
     * @param splitter 分隔符
     */
    public FileTool(String fileName, String splitter){
        super(Arrays.asList(read(fileName).split(splitter)));
        if(get(0).equals("")) remove(0);
    }
    /**
     * 正常讀取文件
     * @param fileName
     */
    public FileTool(String fileName){
        this(fileName, "\n");
    }
    public void write(String fileName){
        try{
            PrintWriter out = new PrintWriter(new File(fileName).getAbsoluteFile());
            try{
                for(String item : this)
                    out.print(item);
            } finally {
                out.close();
            }
        } catch (IOException e){

        }
    }
    // 測試
    public static void main(String[] args) {
        String file = read("src/com/test/FileTool.java");
        System.out.println(file);
        write("fileToolTest.txt", file);
        FileTool fileTool = new FileTool("fileToolTest.txt");
        fileTool.write("fileToolTest.txt");
        TreeSet<String> words = new TreeSet<String>(new FileTool("src/com/test/FileTool.java", "\\W+"));
        System.out.println(words.headSet("a"));
    }
}

【7】讀取二進制文件

public class BinaryFile {
    public static byte[] read(File bFile) throws IOException{
        BufferedInputStream bi = new BufferedInputStream(new FileInputStream(bFile));
        try{
            byte[] data = new byte[bi.available()]; // available()用來產生恰當的數組尺寸
            bi.read(data); // read()方法填充數組
            return data;
        } finally {
            bi.close();
        }
    }
    public static byte[] read(String bFile) throws IOException{
        return read(new File(bFile).getAbsoluteFile());
    }
}

【7】標準 I/O
  Java提供了System.in、System.out和System.err,System.in是一個沒有被包裝過未經加工的 InputStream

public class Echo {
    public static void main(String[] args) throws IOException {
        // 將System.in包裝成BufferedReader,使用readLine()一次讀取一行
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str;
        while((str = br.readLine()) != null && str.length() != 0){
            System.out.println(str);
        }

        // 將System.out轉換成PrintWriter
        PrintWriter out = new PrintWriter(System.out, true); // 第二個參數 true 開啓自動清空功能
        out.println("Hello, world");

        // 標準 I/O 重定向   操作的是字節流
        // System類提供了一些簡單的靜態方法,允許對標準輸入、輸出和錯誤I/O流進行重定向
        // System.setIn(InputStream)  System.setOut(PrintStream)   System.setErr(PrintStream)
        PrintStream console = System.out; // System.out對象的引用
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("src/com/test/Echo.java"));
        PrintStream _out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out")));
        System.setIn(in);
        System.setOut(_out);
        System.setErr(_out);
        str = null;
        while((str = br.readLine()) != null && str.length() != 0){
            System.out.println(str); // 標準輸出將重定向到另一個文件
        }
        _out.close();
        System.setOut(console); // 恢復了System.out對象的引用
    }
}

五、Java NIO
  JDK1.4 的java.nio.* 包中引入了新的Java I/O類庫,目的在於提高速度。與傳統的 IO 相比主要有以下區別:
 ① Java IO是面向的,Java NIO是面向緩衝區(塊)的
 ② Java IO各種流是阻塞的,當一線程調用read()write()時,該線程被阻塞,直到有數據被讀取或寫入;Java NIO是非阻塞的,一線程從某通道請求讀取數據,若目前沒有數據可用,則不保持線程阻塞,直到有數據可供讀取,該線程可繼續做其他的事情,在寫入的時候也是,不等待它完全寫入,線程也可以同時去做其他的事情
  Java NIO 由以下3個核心部分組成:Channel(通道)、Buffer(緩衝區)、Selector(選擇器)。
  
(1)Channel(通道):Java NIO的通道類似流,但又有些不同:
 ① 既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。
 ② 通道可以異步地讀寫。
 ③ 通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入

在Java NIO中最重要的通道的實現:
 ① FileChannel:從文件中讀寫數據
 ② DatagramChannel:能通過UDP讀寫網絡中的數據
 ③ SocketChannel:能通過TCP讀寫網絡中的數據
 ④ ServerSocketChannel:可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel

這裏寫圖片描述

(2)Buffer(緩衝區):用於和NIO通道進行交互,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的

這裏寫圖片描述

使用Buffer讀寫數據一般遵循以下四個步驟:
 ① 寫入數據到Buffer
 ② 調用flip()方法
 ③ 從Buffer中讀取數據
 ④ 調用clear()方法或者compact()方法

  向Buffer寫入數據,當要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式,讀取之前寫入到buffer的所有數據。一旦數據讀取完畢,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用clear()或compact()方法。clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據,任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
  
(2.1)Buffer的capacity、position和limit
 ① capacity:不管Buffer處於讀模式還是寫模式,其返回緩衝區的容量
 ② position:初始值爲0,最大值爲capacity-1 。寫數據到Buffer時,表示當前位置,沒插入一條數據,向前移動一個單元;讀取數據時,從某個位置讀取數據,當Buffer切換爲讀模式時,position會被重置爲0,讀取一條數據時,會移動到下一個可讀的位置。
 ③ limit:在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity。
當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
 Buffer的分配,要想獲得一個Buffer對象首先要進行分配,如分配48字節capacity的ByteBuffer:ByteBuffer buf = ByteBuffer.allocate(48),分配一個可存儲1024個字符的CharBuffer:CharBuffer buf = CharBuffer.allocate(1024);

向Buffer寫數據,兩種方式:
 ① 從Channel寫到Buffer
  int bytesRead = inChannel.read(buf); //read into buffer
 ② 通過Buffer的put()方法寫到Buffer裏
  buf.put(127);
 
從Buffer中讀數據,兩種方式:
 ① 從Buffer讀取數據到Channel
  int bytesWritten = inChannel.write(buf);
 ② 使用get()方法從Buffer中讀取數據
  byte aByte = buf.get();

rewind()方法
Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
  
mark()與reset()方法
通過調用Buffer.mark()方法,可以標記Buffer中的一個特定position。之後可以通過調用Buffer.reset()方法恢復到這個position。

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing. 
buffer.reset();  //set position back to mark.

【1】FileChannel
  用於讀取、寫入、映射和操作文件的通道,在現版本中,可從現有的 FileInputStream、FileOutputStream 或 RandomAccessFile 對象獲得文件通道,方法是調用該對象的 getChannel 方法,這會返回一個連接到相同底層文件的文件通道。

public class GetFileChannel {
    public static void main(String[] args) throws Exception {
        FileChannel fileChannel = new FileOutputStream("data.txt").getChannel();

        // 將 byte 數組包裝到緩衝區中,並將數據寫入通道(文件)
        fileChannel.write(ByteBuffer.wrap("FileOutputStream--FileChannel Test".getBytes())); 
        fileChannel.close(); // 關閉此通道

        // 向文件末尾增加數據  rw讀寫  r只讀
        fileChannel = new RandomAccessFile("data.txt", "rw").getChannel();

        // 調用position()方法獲取FileChannel的當前位置,調用position(long pos)方法設置FileChannel的當前位置
        // FileChannel實例的size()方法將返回該實例所關聯文件的大小
        fileChannel.position(fileChannel.size()); //  設置此通道的文件位置,末尾
        fileChannel.write(ByteBuffer.wrap("RandomAccessFile--FileChannel Test".getBytes()));
        fileChannel.close();

        // FileChannel.force(boolean metaData)方法將通道里尚未寫入磁盤的數據強制寫到磁盤上
        // true 表示是否同時將文件元數據(權限信息等)寫到磁盤上
        // 讀取文件
        fileChannel = new FileInputStream("data.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配一個緩衝對象
        fileChannel.read(buffer); // 將字節序列從此通道讀入給定的緩衝區,返回讀取的字節數
        buffer.flip(); // 切換到讀模式,讓別人做好讀取字節的準備
        while(buffer.hasRemaining()){
            System.out.print((char)buffer.get());
        }   
    }
}

  上述代碼中,在讀取通道數據並輸出時,使用(char)buffer.get()每次讀取一個字節,然後強制轉換爲char類型(這種方法顯然比較原始),如果不強制轉換,將會是亂碼,這是因爲ByteBuffer 存儲的是普通的字節,爲了把它們轉換成字符,有兩種方式可以解決:① 在輸入它們的時候對其進行編碼;② 從緩衝器輸出時對它們進行解碼
  從api文檔中可以看到CharBuffer有一個toString()方法可以返回緩衝器包含的所有的字符,而ByteBuffer可以調用asCharBuffer()方法作爲 char 類型緩衝區

public static void main(String[] args) throws Exception {
    final int BSIZE = 1024;
    FileChannel fileChannel = new RandomAccessFile("data.txt", "rw").getChannel();
    fileChannel.write(ByteBuffer.wrap("some text".getBytes()));
    fileChannel.close();
    ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
    fileChannel = new FileInputStream("data.txt").getChannel();
    fileChannel.read(buffer);
    buffer.flip();
    // 在這裏直接調用asCharBuffer()顯示不行輸出亂碼
    System.out.println(buffer.asCharBuffer());
    buffer.rewind(); // 將position設回0,可以重讀Buffer中的所有數據

    /** 1) 在輸出之前進行解碼 Decoding**/
    // 獲取系統編碼,使用java.nio.charset.Charset進行解碼
    // decode(ByteBuffer bb) 返回一個CharBuffer對象       
    String encoding = System.getProperty("file.encoding");
    System.out.println("使用 " + encoding +" 解碼輸出:" + Charset.forName(encoding).decode(buffer));

    /** 2) 在寫入通道之前進行編碼,這樣輸出纔有意義 **/
    fileChannel = new FileOutputStream("data1.txt").getChannel();
    // 使用 UTF-8 寫入
    fileChannel.write(ByteBuffer.wrap("some text2".getBytes("UTF-16BE")));
    fileChannel.close();
    // 讀取
    fileChannel = new FileInputStream("data1.txt").getChannel();
    buffer.clear();
    fileChannel.read(buffer);
    buffer.flip();
    System.out.println(buffer.asCharBuffer());// 正常輸出

    /** 3) 在寫入通道使用buffer.asCharBuffer() **/
    fileChannel = new FileOutputStream("data3.txt").getChannel();
    buffer = ByteBuffer.allocate(24);
    buffer.asCharBuffer().put("some text3");
    fileChannel.write(buffer);
    fileChannel.close();
    // 讀取
    fileChannel = new FileInputStream("data3.txt").getChannel();
    buffer.clear();
    fileChannel.read(buffer);
    buffer.flip();
    System.out.println(buffer.asCharBuffer());// 正常輸出
}

待續… …

Java IO學習總結:
http://www.cnblogs.com/rollenholt/archive/2011/09/11/2173787.html
http://blog.csdn.net/zhangerqing/article/details/8466532
http://ifeve.com/overview/
http://www.importnew.com/17735.html
http://blog.csdn.net/maritimesun/article/details/7973603

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章