Java——IO系統概覽

前言

對程序語言的設計者來講,建立一個好的輸入/輸出(IO)系統是一項艱難的任務。這艱難主要來自於要涵蓋I/O的全部可能性。不只存在各類I/O源端和想要與之通訊的接收端(源端/接收端:文件、控制檯和網絡鏈接等),並且它們之間可能還須要以不一樣的方式進行通訊(順序、隨機存取、緩衝、二進制、按字符、按行和按字等)java

Java類庫的設計者經過建立大量的類來解決這個難題。在Java 1.0版本以後,Java的I/O類庫發生顯著改變,在原來面向字節的類中添加了面向字符和基於Unicode的類。在Java 1.4版本中,添加了nio類進來爲了改進性能以及功能。所以,在熟練使用Java I/O類庫以前,咱們須要先學習至關數量的I/O類。算法

下面將概述Java的I/O類庫中的類的含義以及使用方法。編程

I/O類庫繼承框架

這裏用相同的色號的標註表示這個類的功能類似(同時標註了輸入和輸出)。數組

輸入和輸出

什麼是I/O流?服務器

咱們能夠發現不少的類名都跟着一個Stream的後綴,即流。編程語言的I/O類庫中也常用這個抽象概念,它表明任何有能力產出數據的數據源對象或者有能力接收數據的接收端對象。「流」屏蔽了實際I/O設備中處理數據的細節。 例如,咱們使用Java經過Http協議遠程訪問網絡資源,獲取網絡數據(這叫作輸入)。咱們的主機與服務器就如同下圖的管道兩端的點,服務器響應給咱們的數據,就經過這根管道流向咱們。由於輸入/輸出的方式相似於流水在水管中流動,咱們就稱輸入/輸出爲輸入流/輸出流。網絡

輸入和輸出是站在程序(計算機內存)的角度來說的。程序讀取外部的數據叫作輸入,程序將數據送出外部叫作輸出。 app

查看JDK文檔咱們能夠知道,任何繼承自InputStream和Reader的類都含有read方法,用於讀取單個的字節或者字節數組;任何繼承自OutputStream和Writer的類都含有Write方法,用於寫單個的字節和字節數組。可是,咱們一般都不用這些方法,這些方法之因此存在是由於能夠供其餘類使用。在Java中咱們不多建立單一的流對象,而是經過疊合多個對象來提供所指望的功能裝飾器模式)。框架

InputStream 和 OutputStream

在Java 1.0中,類庫的設計者限定與輸入有關的類都應該從InputStram繼承,與輸出有關的類都應該從OutputStream繼承。dom

InputStream類型 編程語言

InputStream是用來表示從不一樣的數據源產生輸入的類。這些數據源包括:字節數組、String對象、文件、管道、一個由其餘種類的流組成的序列方便咱們能夠將它們收集合併到一個流內和其餘數據源,如Internet鏈接等。

每一種數據源都有相應的InputStream子類。FilterInputStream也屬於一種InputStream,是裝飾器類的基類,裝飾器類能夠將屬性和有用的接口與輸入流鏈接起來,爲輸入流提供更加豐富的功能。

OutputStream類型

OutputStream是用來表示程序輸出要去往的地方:字節數組、文件或者管道。FilterOutputStream是屬於OutputStream的,也是裝飾器類的基類,「裝飾器」類將屬性和有用的接口與輸出流鏈接了起來,爲輸出流提供更加多樣的功能。

Reader和Writer

Java 1.1對基本的I/O類庫進行了重大的修改,添加了Reader和Writer類以及繼承自它們的子類。一眼看到Reader和Writer可能會認爲是用於替代InputStream和OutputStream的類。可是,事實並不是如此,儘管一些原始的流類庫再也不被使用。可是InputStream和OutputStream在以面向字節形式的I/O中仍然能夠提供極有價值的功能,Reader和Writer則提供兼容Unicode與面向字符的I/O功能

裝飾器類FilterInputStream和FilterOutputStream

FilterInputStream和FilterOutputStream用來提供裝飾器類的接口以控制特定的輸入流(InputStream)和輸出流(OutputStream)兩個類。

經過FilterInputStream從InputStream中讀取數據

FileterInputStream類能夠完成兩種不一樣的事情。

其中,DataInputStream能夠直接讀取DataOutputStream寫入文件的基本數據類型和String對象(使用以read開頭的方法),兩者搭配,咱們就能夠經過數據「流」,將基本類型的數據從一個地方遷移到另一個地方

DataInputStream和DataOutputStream的構造函數要求傳入一個InputStream或者OutpurStream對象,因而咱們就傳入文件對象以作示範。

其餘的FilterInputStream子類則在內部修改InputStream類的行爲:是否緩衝、是否保留它所讀過的行(容許咱們查詢行數或者設置行數),以及是否把一個單一字符推回輸入流等等。實現最後兩個功能的類添加像是爲了建立一個編譯器(使用Java構建的編譯器),通常狀況下咱們不會用到它們。

FilterInputStream類型


功能 構造器/如何使用
DataInputStream 與DataOutputStream搭配使用,能夠按照可移植的方式從流中讀取基本類型數據 DataInputStream(InputStream in);包含用於讀取基本數據類型的全部接口
BufferedInputStream 使用它能夠防止每次讀取時都得進行實際的寫操做。表明「使用緩衝區」。 BufferedInputStream(InputStream in)BufferedInputStream(InputStream in, int size);本質上不提供接口,只是向進程中添加緩衝區所必需的。與接口對象搭配
LineNumberInputStream 跟蹤輸入流中的行號;能夠調用getLineNumber()和setLineNumber(int) LineNumberInputStream(InputStream in);僅增長了行號
PushbackInputStream 具備「能彈出一個字節的緩衝區」,所以能夠將讀到的最後一個字符回退 PushbackInputStream(InputStream in)PushbackInputStream(InputStream in, int size);一般做爲編譯器的掃描器

經過FilterOutputStream向OutputStream中寫入

FilterOutputStream子類中地DataOutputStream能夠將各類基本數據類型以及String對象格式化輸出到「流」中;這樣任何機器上使用DataInputStream就能夠讀取它們。

PrintStream最初的目的是爲了便於以可視化格式打印全部基本數據類型以及String對象。它和DataOutputStream不一樣,後者的目的是將數據元素置於「流」中,使DataInputStream可以可移植地重構它們。DataOutputStream用於處理數據存儲,PrintStream用於處理顯示

BufferedOutputStream是一個修改過的OutputStream,它對數據流使用緩衝技術;所以當每次向流中寫入時,沒必要每次都進行實際的物理寫動做。因此在進行輸出時,咱們可能更常用它。

FilterOutputStream類型


功能 構造器/如何使用
DataOutpurStream 與DataInputStream搭配使用,所以能夠安裝可移植的方式向流中寫入基類類型數據(int,char,long等) DataOutputStream(OutputStream out) ; 包含用於寫入基本類型數據的所有接口
PrintStream 用於產生格式化輸出。 構造參數爲OutputStream或者是指定的文件名或文件
BufferedOutputStream 使用它避免每次發送數據時都要進行實際的寫操做。表明「使用緩衝區」。能夠調用flush()函數清空緩衝區 BufferedOutputStream(OutputStream out)BufferedOutputStream(OutputStream out, int size);本質上不提供接口,只是向進程中添加緩衝區所必需的。與接口對象搭配

隨即訪問文件RandomAccessFile

RandomAccessFile適用於由大小已知的記錄組成的文件,咱們就可使用seek()將文件指針位置從一處轉到另外一處,而後讀取或者修改記錄。咱們事先要知道每條記錄的大小以及它們在文件中的位置,那麼咱們就能夠實現隨機訪問。

RandomAccessFile不是InputStream或者OutputStream繼承層次結構中的一部分。RandomAccessFile的工做方式相似於將DataInputStream和DataOutputStream組合起來使用。在Java 1.4中,RandomAccessFile的大多數功能將由nio存儲映射文件所代替。

I/O流的典型使用方式

緩衝輸入文件

若要打開一個文件進行字符輸入,咱們使用String或者File對象做爲構造參數的FileReader爲了提升文件的讀取速度,咱們可使用帶緩衝(Buffer)的BufferedReader讀取必定數量的文件中字符先存放在BufferedReader中的Buffer中,即BufferedReader中的Buffer爲一個字符數組。Buffer能夠緩和一個字符一個字符進行讀取的頻繁操做的延遲,由於一個一個讀取將大量時間都花費在了開始讀取和結束讀取操做上)。咱們將FileReader的引用傳遞給BufferedReader的構造器,就構造出咱們須要的對象,此對象還擁有讀取一行字符串的readLine()方法。(這種方式也叫作裝飾器模式,BufferedReader讓咱們的本來的對象擁有緩衝以及按行讀取字符串的方法)。下面舉例簡單應用BufferedReader。

public class TestBufferedReader {
    public static String read(String fileName) {
        BufferedReader br = null;
        FileReader fReader = null;
        StringBuilder sBuilder = new StringBuilder();
        try {
            fReader = new FileReader(fileName);
            br = new BufferedReader(fReader);
            String str = null;
            //按行獲取文件內容
            while((str = br.readLine()) != null) {
                sBuilder.append(str);
                sBuilder.append("\n"); //readLine()刪除了換行符
            }
        }catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (br != null) {
                try {
                    br.close();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fReader != null) {
                try {
                    fReader.close();
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sBuilder.toString();
    }
    public static void main(String[] args) {
        System.out.println(read("mmdou.txt"));
    }
}
/*
output:
This is  mmdou.txt
To be or not to be that is a question.
*/

從內存輸入

使用TestBufferedReader.read()返回的字符串來構造一個StringReader對象。而後調用StringReader的read()方法每次讀取一個字符,並將其發送到控制檯。

public class MemoryInput {
    public static void main(String[] args) throws IOException {
        StringReader in = new StringReader(TestBufferedReader.read("mmdou.txt"));
        int ch;
        while((ch = in.read()) != -1) {
            System.out.print((char)ch); //read是以int形式返回下一字節,因此須要強制轉換
        }
    }
}
/*
output:
This is  mmdou.txt
To be or not to be that is a question.
*/

格式化的內存輸入

如果要讀取格式化數據,可使用DataInputStream,它是面向字節的I/O類。建立DataInputStream須要提供InputStream類型參數。這裏咱們使用將ByteArrayInputStream做爲傳入給DataInputStream的InputStream。使用TestBufferedReader.read("dis.txt").getBytes()做爲ByteArrayInputStream的構造參數。

public class FormattedMemoryInput {
    public static void main(String[] args) throws IOException {
        try {
            DataInputStream dis = new DataInputStream(new ByteArrayInputStream
                    (TestBufferedReader.read("dis.txt").getBytes()));
            while(true) {
                System.out.print((char)dis.readByte());
            }
        }catch (EOFException e) {
            System.err.println("End of file.");
        }
    }
}
/*
output:
Road
End of file.
*/

上面使用捕獲異常來結束來檢測輸入流是否結束是不正確的用法!咱們要判斷輸入是否結束可使用avaliable()方法返回能夠今後輸入流中讀取(或跳過)的字節數的估計值(在沒有阻塞的狀況下)。下面將使用avaliable()演示如何一個字節一個字節地讀取文件:

public class TestEOF {
    public static void main(String[] args) throws IOException{
        DataInputStream in = new DataInputStream(new ByteArrayInputStream
                (TestBufferedReader.read("dis.txt").getBytes()));
        while(in.available() != 0) {
            System.out.print((char)in.readByte());
        }
    }
}

基本的文件輸出

FileWriter對象能夠向文件寫入數據。咱們會建立一個與指定文件鏈接的FileWriter。一般,咱們會使用BufferedWriter將其包裝起來用以緩衝輸出(緩衝每每能夠顯著增長I/O操做性能,就像前面一小節緩衝輸入文件所解釋同樣)。在本例中,爲了提供格式化機制,它被包裝成了PrintWriter。安照這種方式建立的數據文件,能夠被做爲普通文本讀取。

public class BasicFileOutput {
    static String file = "BasicFileOutput.out";
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new 
                StringReader(TestBufferedReader.read("BasicFileOutput.java")));
        //PrintWriter out = new PrintWriter(file) 等價於下面一句。此方式隱含幫咱們執行全部裝飾工做
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
        
        int lineCount = 1;
        String s;
        while((s = in.readLine()) != null) {
            //添加行號
            out.println(lineCount++ + ":" + s);
        }
        //刷新緩衝區
        out.flush();//or out.close();
        System.out.println(TestBufferedReader.read(file));
    }
}

存儲和恢復數據

如果咱們想要實現輸出可供另一個「流」恢復的數據,那麼就須要使用DataOutpurStream寫入數據,並用DataInputStream恢復數據。在介紹裝飾器類FilterInputStream和FilterOutputStream時,咱們介紹過這兩個類,在此就不在使用例子說明。

咱們使用DataOutputStream寫入數據,Java保證咱們可使用DataInputStream準確地讀取數據——不管讀和寫數據的平臺多麼地不一樣。

管道流

PipedInputStream、PipedOutputStream、PipedReader和PipedWriter用於任務之間的通訊,將在後面介紹。

標準I/O

標準I/O這個術語參考的是Unix中"程序所使用的單一信息流"這個概念。程序的全部輸入均可以來自於標準輸入,它的全部輸出也均可以發送到標準輸出,以及全部錯誤信息均可以發送到標準錯誤。標準I/O的意義在於:咱們能夠很容易地把程序串聯起來,一個程序的標準輸出能夠做爲另外一個程序的標準輸入。

從標準輸入中讀取

按照標準I/O模型,Java提供了System.inSystem.out、和System.err。查看System類的源碼,咱們能夠發現,System.out和System.err是PrintStream對象,System.in倒是沒有未經包裝的InputStream對象。因此,咱們在讀取System.in以前須要對其進行包裝。

一般咱們會使用readLine()一次一行地讀取,爲此,咱們將System.in包裝成BufferedReader。在建立BufferedReader時,咱們須要使用InputStreamReader將System.in轉換成Reader。InputStreamReader是一個適配器,接收InputStream對象並將其轉換成Reader對象。

下面的例子將回顯輸入的每一行:

public class Echo {
    public static void main(String[] args) throws IOException{
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = stdin.readLine()) != null && s.length() != 0) {
            System.out.println(s);
        }
    }
}

將System.out轉換成PrintWriter

System.out是一個PrintStream,而PrintStream是一個OutputStream。PrintWriter剛好有一個能夠接受OutputStream做爲參數的構造器。

public class ChangeSystemOut {
    public static void main(String[] args) {
        PrintWriter out = new PrintWriter(System.out, true);//如果不設置爲true則看不到輸出
        out.println("Hello World!");
    }
}
/*
output:
Hello World!
*/

標準I/O重定向

Java的System類提供了一些簡單的靜態方法調用,以容許咱們對標準輸入、輸出和錯誤I/O流進行重定向:

若是咱們忽然在顯示器上建立大量輸出,並且這些輸出滾動得太快以致於沒法閱讀,此時重定向輸出就顯得很重要(咱們能夠將輸出定向至其餘地方通常爲輸出到一個文件中)。或者,咱們想重複測試某個特定輸入樣例,此時重定向輸入就頗有必要(如將標準輸入重定向至一個文件)。下面簡單演示這些方法的使用。

public class Redirecting {
    public static void main(String[] args) throws IOException {
        PrintStream console = System.out;
        //帶緩衝的輸入流和輸出流對象
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("Redirecting.java"));
        PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out")));
        
        System.setIn(in);   //重定向標準輸入爲Redirecting.java文件
        System.setOut(out); //重定向標準輸出爲test.out文件
        System.setErr(out); //重定向標準錯誤未test.out
        
        //讀取重定向後的標準輸入即Redirecting.java文件
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = br.readLine()) != null) {
            //將讀取的數據重定向輸出至test.out中
            System.out.println(s);  
        }
        out.close();
        System.setOut(console);
    }
}

這個程序將標準輸入附接到文件上,並將標準輸出和標準錯誤重定向到另一個文件中。注意,在程序的開頭處存儲了對最初的System.out對象的引用,而且在結尾處將系統輸出恢復到了該對象上。

I/O重定向操做的是字節流而不是字符流,因此使用InputStream和OutputStream。

新I/O(nio)

通道和緩衝器

JDK 1.4中的java.nio.*包中引入了新的Java I/O類庫,目的在於提升速度。實際上,舊的I/O包已經使用nio從新實現過,以便充分利用這種速度提升。

速度的提升來自於所使用的結構更接近於操做系統執行I/O的方式:通道緩衝器。咱們能夠將通道想象成包含煤層(數據)的礦藏,而緩衝器則是派送到礦藏的卡車。卡車滿載而歸,咱們再從卡車上得到煤礦。即,咱們沒有直接與通道交互;咱們只是和緩衝器交互,緩衝器與通道交互。因此,通道是向緩衝器發送數據和從緩衝器得到數據。

惟一直接與通道交互的緩衝器是ByteBuffer。查看JDK文檔能夠知道,它是一個基礎的類也是一個抽象類:經過告知分配多少存儲空間來建立一個ByteBuffer對象,而且還有一個方法集,用以原始字節形式或基本數據類型輸出和讀取數據。包含的這些方法也是抽象方法,沒有辦法輸出和讀取對象。

舊I/O類庫中有三個類被修改了,用以產生FileChannel。這三個類爲FIleInputStreamFileOutputStream以及用於隨機讀寫的RandomAccessFile。這些都是字節操縱流,與底層的nio性質一致。Reader和Writer這種字符模式類不能用於產生通道,可是java.nio.channels.Channels類提供了實用方法,用以在通道中產生Reader和Writer。

下面的示例簡單演示了三種類型的流用以產生可寫的、可讀可寫以及可讀的通道。

public class GetChannel {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws IOException{
        //經過通道和緩衝器寫文件
        FileChannel fChannel = new FileOutputStream("data.txt").getChannel();
        fChannel.write(ByteBuffer.wrap("Some text ".getBytes()));
        fChannel.close();
        
        //RandomAccessFile對文件的權限爲可讀可寫  向文件data.txt末尾添加
        fChannel = new RandomAccessFile("data.txt","rw").getChannel();
        fChannel.position(fChannel.size());
        fChannel.write(ByteBuffer.wrap("Some more".getBytes()));
        fChannel.close();
        
        //經過通道和緩衝器讀文件
        fChannel = new RandomAccessFile("data.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        fChannel.read(buffer);
        buffer.flip();//讓別人作好讀取字節的準備
        
        while(buffer.hasRemaining()) {
            System.out.print((char)buffer.get());
        }
    }
}

FileOutputStream、RandomAccessFile和RandomAccessFile都有一個getChannel()方法產生一個FileChannel與實際文件關聯。在以上程序中,咱們使用ByteBuffer向FileChannel通道中傳入數據和獲取數據,就像前面提過的,通道從緩衝器獲取數據或者向緩衝器發送數據。

將字節存放於ByteBuffer中的方法之一是:使用一種「put」方法直接對它們進行填充,填入一個或多個字節,或者基本數據類型的值。不過,正如程序中所見可使用warp()方法將已存在的字節數組「包裝」到ByteBuffer中。這樣,就不會再複製底層的數組,而是把它做爲所產生的ByteBuffer的存儲器,咱們稱之爲數組支持的ByteBuffer。

從通道中獲取數據,咱們使用ByteBuffer.allocate()分配了緩衝器的大小。如果咱們想要達到更快的速度,也可使用allocateDirect(),這個將產生一個與操做系統有更高耦合性的「直接」緩衝器。可是,這種分配的開始也會很大,並且也會隨着操做系統的不一樣而不一樣,所以須要依照實際來選擇。

看ByteBuffer做爲橋樑在兩個通道之間傳遞數據(文件複製)的例子:

public class ChannelCopy {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws IOException{
        FileChannel in = new FileInputStream("in.txt").getChannel();
        FileChannel out = new FileOutputStream("out.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        
        while((in.read(buffer)) != -1) {
            buffer.flip();  //通知別人準備讀取
            out.write(buffer);  //通道從buffer中獲取數據
            buffer.clear(); //清除buffer準備下一次數據的讀取
        }
    }
}

打開兩個FileChannel,一個用於讀取一個用於寫入。每次read()以後,數據就會被寫入到緩衝器中,flip()則準備緩衝器以便它的信息能夠由write()提取。write()操做以後,信息仍然存在緩衝器中,接着clear()操做則對全部的內部指針從新安排,以便緩衝器在另一個read()操做前可以作好接受數據的準備。

然而,使用一個緩衝器當作橋樑完成這種操做不是最恰當的方法。特殊方法transferTo()transferFrom()則容許咱們將一個通道和另外一個通道直接相連:

public class ChannelCopy2 {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws IOException{
        FileChannel in = new FileInputStream("in.txt").getChannel();
        FileChannel out = new FileOutputStream("out.txt").getChannel();
        in.transferTo(0,in.size(),out);
    }
}

將字節數據轉換爲字符串

public class BufferToText {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throws IOException{
        
        FileChannel fc = new FileOutputStream("data2.txt").getChannel();
        fc.write(ByteBuffer.wrap("Some words ".getBytes()));
        fc.close();
        
        fc = new FileInputStream("data2.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        fc.read(buffer);
        buffer.flip();
        //直接輸出asCharBuffer()
        System.out.println(buffer.asCharBuffer());
        //-----------------------
        //回到緩衝器數據開始部分
        buffer.rewind();
        //發現默認字符集
        String enconding = System.getProperty("file.encoding");
        //取出緩衝器中的數據進行解碼
        System.out.println("Decoding using " + enconding + ": " +Charset.forName(enconding).decode(buffer));
        
        fc = new FileOutputStream("data2.txt").getChannel();
        //在輸出數據時進行編碼
        fc.write(ByteBuffer.wrap("some beautiful flowers".getBytes("UTF-16BE")));
        fc.close();
        
        //獲取編碼後的數據
        fc = new FileInputStream("data2.txt").getChannel();
        buffer.clear();
        fc.read(buffer);
        buffer.flip();
        System.out.println(buffer.asCharBuffer());
        //------------------------
        fc = new FileOutputStream("data2.txt").getChannel();
        buffer = ByteBuffer.allocate(12);   //分配了24字節
        //經過CharBuffer向ByteBuffer寫入
        buffer.asCharBuffer().put("Some");
        fc.write(buffer);
        fc.close();
        
        fc = new FileInputStream("data2.txt").getChannel();
        buffer.clear();
        fc.read(buffer);
        buffer.flip();
        System.out.println(buffer.asCharBuffer());
    }
}

在GetChannel.java程序中,爲了輸出文件中的信息,咱們每次只讀取一個字節的數據,而後將每一個byte類型強制轉換成char類型。這種方法看起來有點原始,若是咱們查看java.nio.CharBuffer這個類,將會發現它有一個toString()方法的定義爲:Returns a string containing the characters in this buffer(返回一個包含緩衝區全部字符的字符串)。ByteBuffer是具備asCharBuffer()方法返回一個CharBuffer。那麼咱們就可使用此方式輸出字符串,可是,從輸出的第一行能夠看出,這種方法不太恰當。

緩衝器容納的是普通的字符,爲了把它們轉換成字符,咱們要麼在輸入它們時對其進行編碼,要麼在將其從緩衝器輸出時對它們進行解碼。如程序中所寫,使用java.nio.charset.Charset即可以實現這些功能。

咱們看最後一個部分,咱們經過CharBuffer向ByteBuffer中寫入。爲ByteBuffer分配了12字節。一個字符須要兩個字符,ByteBuffer能夠容納6個字符,咱們的"Some"佔4個字符,但是咱們看輸出結果,發現剩下的兩個沒有內容的字符也會被輸出。

獲取基本數據類型

儘管ByteBuffer只能保存字節類型的數據,可是它具備能夠從其所容納的字節中產生出各類不一樣基本數據類型的方法。下面將展現如何使用這些方法來插入和讀取各類數值。

public class GetData {
    private static final int BSIZE = 1024;
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        int i = 0;
        //檢測緩衝器的初始內容是否爲0
        while(i ++ < bb.limit()) {
            if(bb.get() != 0)
                System.out.println("nozero");
        }
        System.out.println("i = " + i);
        
        bb.rewind();
        bb.asCharBuffer().put("Happpy!");
        char c;
        while((c = bb.getChar()) != 0) {
            System.out.print(c+"\t");
        }
        System.out.println();
        
        bb.rewind();
        bb.asShortBuffer().put((short)471142);//超過short類型最大值32767須要強制類型轉換 會截斷
        System.out.println(bb.getShort());
        
        bb.rewind();
        bb.asIntBuffer().put(99471142);
        System.out.println(bb.getInt());
        
        bb.rewind();
        bb.asLongBuffer().put(99471142);
        System.out.println(bb.getLong());
        
        bb.rewind();
        bb.asFloatBuffer().put(99471142);
        System.out.println(bb.getFloat());
        
        bb.rewind();
        bb.asDoubleBuffer().put(99471142);
        System.out.println(bb.getDouble());
    }
}
/*
output:
i = 1025
H   a   p   p   p   y   !   
12390
99471142
99471142
9.9471144E7
9.9471142E7
*/

向ByteBuffer插入基本類型數據的最簡單的方法是:利用asCharBuffer()、asShortBuffer()等得到該緩衝器上的視圖,而後使用該視圖的put()方法。

視圖緩衝器

視圖緩衝器(view buffer)可讓咱們經過某個特定的基本數據類型的視窗查看其底層的ByteBuffer。ByteBuffer依舊是實際存儲數據的地方,「支持」着視圖,所以,對視圖的任何修改都會映射爲對ByteBuffer中數據的修改。如上面程序中所示,使用視圖能夠很方便地向ByteBuffer中插入數據與讀取數據。

在同一個ByteBuffer上創建不一樣的視圖緩衝器,將同一字節序列翻譯成char、short、int、float、long和double類型的數據。

public class ViewBuffers {
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[] {0, 0, 0, 0, 0, 0, 0, 'a'});
        
        bb.rewind();
        System.out.print("Byte Buffer: " );
        while(bb.hasRemaining()) {
            System.out.print(bb.position() + ":" + bb.get() + ", ");
        }
        System.out.println();
        
        //讀取成字符
        bb.rewind();
        CharBuffer cb = bb.asCharBuffer();
        System.out.print("Char Buffer: ");
        while(cb.hasRemaining()) {
            System.out.print(cb.position() + ":" + cb.get() + ", ");
        }
        System.out.println();
        
        //讀取短整型
        bb.rewind();
        ShortBuffer sb = bb.asShortBuffer();
        System.out.print("Short Buffer: ");
        while(sb.hasRemaining()) {
            System.out.print(sb.position() + ":" + sb.get() + ", ");
        }
        System.out.println();       
        
        //讀取成單精度浮點型
        bb.rewind();
        FloatBuffer fb = bb.asFloatBuffer();
        System.out.print("Float Buffer: ");
        while(fb.hasRemaining()) {
            System.out.print(fb.position() + ":" + fb.get() + ", ");
        }
        System.out.println();
                
        //讀取整型
        bb.rewind();
        IntBuffer ib = bb.asIntBuffer();
        System.out.print("Int Buffer: ");
        while(ib.hasRemaining()) {
            System.out.print(ib.position() + ":" + ib.get() + ", ");
        }
        System.out.println();
                
        //讀取長整型
        bb.rewind();
        LongBuffer lb = bb.asLongBuffer();
        System.out.print("Long Buffer: ");
        while(lb.hasRemaining()) {
            System.out.print(lb.position() + ":" + lb.get() + ", ");
        }
        System.out.println();
        
        //讀取雙精度浮點型
        bb.rewind();
        DoubleBuffer db = bb.asDoubleBuffer();
        System.out.print("Double Buffer: ");
        while(db.hasRemaining()) {
            System.out.print(db.position() + ":" + db.get() + ", ");
        }
    }
}

下面的這張圖能夠形象說明以上輸出的緣由。

字節的存放順序

不一樣的機器會以不一樣的字節排序方法來存儲數據。有「big endian」(大端法)和「little endian」(小端法)兩種。大端法將重要的字節存放在存儲器的低地址位;小端法將重要字節放在存儲器的高地址位。當存儲量大於一個字節,好比int、float等,就須要考慮字節存儲的順序問題。

ByteBuffer是以大端法存數數據的,而且數據在網上傳輸也是大端法順序。咱們是可使用ByteOrder.BIG_ENDIAN或者ByteOrder.LITTLE_ENDIAN的order()方法改變ByteBuffer的字節排序方式

用緩衝器操做數據

nio類之間的關係

上面的這張圖闡明瞭nio之間的關係,便於咱們理解怎樣去移動和轉換數據。例如,想要把一個字節數組寫到文件中去,那麼咱們就應該作如下事情:

  • 使用ByteBuffer.wrap()方法把字節數組包裝起來
  • 而後,使用getChannel()方法在FileOutputStream上打開一個通道
  • 最後,將ByteBuffer中的數據寫到FileChannel中去

咱們須要注意:ByteBuffer是將數據移進移出通道的惟一方式。咱們不能將基本類型緩衝器轉換成ByteBuffer,可是,咱們能夠經由基本類型緩衝器(視圖緩衝器)來操縱ByteBuffer中的數據。

緩衝器的更多方法使用

Buffer由數據和能夠高效地訪問及操縱這些數據的四個索引組成,這四個索引是:mark(標記)、position(位置)、limit(界限)和capacity(容量)。下面是設置索引和復位索引以及查詢它們的值的方法。

方法 含義
capacity() 返回緩衝區容量
clear() 清空緩衝區,將position設置爲0,limit設置爲容量。
flip() 將limit設置爲position,position設置爲0。此方法用於準備從緩衝區讀取已經寫入的方法
limit() 返回limit值
limit(int lim) 設置limit值
mark() 將mark設置爲position
reset() 將此緩衝區的位置重置爲之前標記的位置
position() 返回position值
position(int pos) 設置position值
remaining() 返回(limit-position),即緩衝區還剩餘多少空間
hasRemaining() 如有介於position和limit之間的元素,則返回true

在緩衝器中插入和提取數據將會更新這些索引,用於反應所發生的變化。下面將經過一個簡單的交換相鄰字符來描繪這種變化過程。

public class UsingBuffers {
    private static void symmetricScramble(CharBuffer buffer) {
        while(buffer.hasRemaining()) {
            buffer.mark();
            char c1 = buffer.get();
            char c2 = buffer.get();
            buffer.reset();
            buffer.put(c2).put(c1);
        }
    }
    
    public static void main(String[] args) {
        char[] data = "UsingBuffers".toCharArray();
        ByteBuffer bb = ByteBuffer.allocate(data.length * 2);
        CharBuffer cb = bb.asCharBuffer();
        cb.put(data);
        System.out.println(cb.rewind());
        symmetricScramble(cb);
        System.out.println(cb.rewind());
        symmetricScramble(cb);
        System.out.println(cb.rewind());
        
    }
}
/*
output:
UsingBuffers
sUniBgfuefsr
UsingBuffers
*/

咱們在這裏採用的是分配一個底層的ByteBuffer,在其之上產生一個CharBuffer視圖緩衝器來進行操做。

下面的這組圖形將描繪交換相鄰字符時,緩衝區內的變化狀況:

內存映射文件

大多數操做系統均可以利用虛擬內存實現來將一個文件或者文件的一部分「映射」到內存中。而後,這個文件就能夠當作內存數組同樣地訪問,這比傳統的文件操做要快的多。java.nio包中使得內存映射使用變得十分簡單,咱們如果要使用則先得到一個文件上的通道,而後調用map()產生mappedByteBuffer,這是一種特殊類型的直接緩衝器,還須要指定映射文件的初始位置和映射區域的長度,這也就說明咱們能夠只映射某個大文件的一小部分。

public class LargeMappedFiles {
    static int lenght = 0x8FFFFFF;  //128MB
    public static void main(String[] args) throws Exception{
        MappedByteBuffer out = new RandomAccessFile("test.txt", "rw").
                getChannel().map(FileChannel.MapMode.READ_WRITE, 0, lenght);
        for(int i=0; i<lenght; i++) {
            out.put((byte)'x');
        }
        System.out.println("Finished writing");
        for(int i=lenght/2; i<lenght/2+6; i++) {
            System.out.println((char)out.get(i));
        }
    }
}

對象序列化

咱們有時候會想將程序運行過程當中的對象保存下來,等下一次運行程序時就能夠被重建而且擁有擁有和它上一次相同的信息。Java的對象序列化就能夠幫咱們實現這些。Java的對象序列化將那些實現了Serializable接口的對象轉換成一個字節序列,而且可以在之後將這個字節序列徹底恢復爲原來的對象。這一過程甚至能夠經過網絡進行,這也意味着序列化機制可以自動彌補不一樣操做系統之間的差別。

對象序列化是一項很是有趣的技術,它能夠實現輕量級持久性(lightweight persistence)。"持久性"意味着一個對象的生命週期並不取決於程序是否正在執行;它能夠生存在程序的調用之間。經過將一個序列化對象寫入磁盤,而後在從新調用程序時恢復該對象,就能夠實現持久性的效果。對象在程序中必須顯示地序列化和反序列化還原。若是須要一個更嚴格的持久性機制,能夠考慮Hibernate之類的工具。

對象序列化出現的緣由主要是爲了支持兩種特性:

  • Java的遠程方法調用(Remote Method Invocation, RMI),它使存活於其餘計算機上的對象使用起來就像是存活在本機上同樣。
  • 對於Java Beans,對象的序列化也是須要的。使用一個Beans時,通常狀況下是在設計階段對它的狀態信息進行配置。這種狀態信息必須保存下來,並在程序啓動時進行後期恢復;這種具體的工做就是由對象序列化完成的。

如何序列化和反序列化一個對象

序列化:首先該對象要實現了Serializeble接口(標記接口,不包括任何方法)。建立某些OutputStream對象,而後將其封裝到一個ObjectOutputStream對象內。這時,調用writeObject()方法即可以將對向序列化,並將其發送給OutputStream(對象序列化是基於字節的)。反序列化:須要將一個InputStream封裝到ObjectInputStream內,而後調用readObject(),得到的是一個指向Object的引用,須要向下轉型設置成咱們須要的對象。

對象序列化不只保存了對象的「全景圖」,並且還能追蹤到對象內所包含的全部引用,並保存那些對象,接着又能對對象內包含的每一個這樣的引用進行追蹤;依次類推。

public class Student implements Serializable {
    private static final long serialVersionUID = 1L;//自動添加的一個序列號
    private String name;
    private Integer age;
    public Student() {}
    public Student(String n, Integer a){
        name = n;
        age = a;
    }   
    @Override
    public String toString(){
        return "Student info [name=" + name + " , age=" + age + "]";
    }
    
    public static void main(String[] args){
        //--------------序列化
        ObjectOutputStream oops = null;
        try{
            //將對象寫入文件
            Student stu = new Student("sakura", 20);
            oops = new ObjectOutputStream(new FileOutputStream("E://test.txt"));
            oops.writeObject(stu);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{
            if(oops != null){
                try{
                    oops.close();
                }
                catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
        //-----------反序列化
        ObjectInputStream oips = null;
        try{
            oips = new ObjectInputStream(new FileInputStream("E://test.txt"));
            //將Student對象的信息讀取出來組裝成一個Object對象,而後向下轉型爲Student對象
            Student stu_back = (Student)oips.readObject();//向下轉型
            System.out.println(stu_back);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally{
            if(oips != null){
                try{
                    oips.close();
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
/*
output:
Student info [name=sakura , age=20]
*/

注意在對一個Serializable對象進行還原的過程當中,沒有調用任何構造器,包括默認的構造器。整個對象都是經過從InputStream中取得數據恢復而來的。

對象序列化的文件內容格式再此就不在詳解。

transient(瞬時)關鍵字

當咱們進行對序列化進行控制時,可能某個特定的子對象不想讓Java的序列化機制自動保存與恢復。好比子對象保存的是密碼等敏感信息。那麼咱們就可使用transient(瞬時)關鍵字逐個字段地關閉序列化。

好比在上例中不想保留age域:

private transient Integer age;

將對象序列化而後反序列化後的結果爲:

Student info [name=sakura , age=null]

沒有被序列化的屬性的值將爲null

序列化的算法

每一個對象都是用序列號(serila number)(序列號代替了對象的內存地址)保存的,這也是這種機制稱爲序列化的緣由。序列化的算法大體以下:

  • 對你遇到的每個對象引用都關聯一個序列號
  • 對於每一個對象,當第一次遇到時,保存其對象數據到輸出流中
  • 若是某個對象以前已經被保存過了,那麼只寫出「與以前保存過的序列號爲x的對象相同」

在讀回對象時,整個過程是反過來的

  • 對於對象輸入流中的對象,在第一次遇到其序列號時,構建它,並使用流中數據來初始化它,而後記錄這個序列號和新對象以前的關聯
  • 當碰見「與以前保存過的序列號爲x的對象相同」標記時,獲取與這個順序號相關聯的對象的引用

File類

最後,咱們簡單介紹下File類。這個名字具備必定的誤導性,咱們可能會認爲它指代的是一個文件,可是事實卻並不是如此。它既能夠表明一個特定的文件的名稱,又能表明目錄下的一組文件的名稱。使用FilePath可能更準確來命名這個類。這個類表示的是文件和目錄名的抽象表示。具體的使用再也不過多介紹,查看JDK文檔即可以瞭解。

小結

本篇博客大致介紹了Java I/O流的一個發展(字符流到字節流的),使用裝飾器類讓流對象具備更多的功能,I/O流的典型使用方式,Java中的標準I/O,NIO中的速度的提高靠的是通道和緩衝區(緩衝區內數據變化時緩衝區的狀態變化)和對象序列化。總結一下,Java來建立一個合適的流對象要先建立不少的類確實是有點麻煩的,可是理解每一個類對象實現的功能是什麼以及組裝好一個新對象能夠擁有什麼功能的話,這樣的組裝也就顯得不是那麼的麻煩。

Java語言不少時候都是使用基本的類、屬性和方法來對操做系統層面的操做進行描述。它只有描述能力,並無直接操做能力。博客一開始就介紹了什麼是流,流也還能夠這樣理解:對操做系統層面操做的一個抽象描述以及封裝。

對Java中I/O類庫的介紹暫記以上。

參考:

《Java編程思想》第4版

《Java核心技術2》第10版

相關文章
相關標籤/搜索