全面解析IO流一:Java IO流詳解

數據流的基本概念

幾乎所有的程序都離不開信息的輸入和輸出,比如從鍵盤讀取數據,從文件中獲取或者向文件中存入數據,在顯示器上顯示數據。這些情況下都會涉及有關輸入/輸出的處理。

在Java中,把這些不同類型的輸入、輸出源抽象爲流(Stream),其中輸入或輸出的數據稱爲數據流(Data Stream),用統一的接口來表示。即數據在兩設備間的傳輸稱爲流,流的本質是數據傳輸,根據數據傳輸特性將流抽象爲各種類,方便更直觀的進行數據操作。

IO 流的分類

數據流是指一組有順序的、有起點和終點的字節集合。

①按照流的流向分,可以分爲輸入流和輸出流。

注意:這裏的輸入、輸出是針對程序來說的。

輸出:把程序(內存)中的內容輸出到磁盤、光盤等存儲設備中。

 

 輸入:讀取外部數據(磁盤、光盤等存儲設備的數據)到程序(內存)中。

②按處理數據單位不同分爲字節流和字符流。

字節流:每次讀取(寫出)一個字節,當傳輸的資源文件有中文時,就會出現亂碼。

字符流:每次讀取(寫出)兩個字節,有中文時,使用該流就可以正確傳輸顯示中文。

1字符 = 2字節; 1字節(byte) = 8位(bit); 一個漢字佔兩個字節長度,一個英文佔1個字節長度。

③按照流的角色劃分爲節點流和處理流。

節點流:從或向一個特定的地方(節點)讀寫數據。如FileInputStream。

處理流(包裝流):是對一個已存在的流的連接和封裝,通過所封裝的流的功能調用實現數據讀寫。如BufferedReader。處理流的構造方法總是要帶一個其他的流對象做參數。一個流對象經過其他流的多次包裝,稱爲流的鏈接。

 擴展:字節與字符之間的關係

編碼方式

英文字符

中文字符

GB 2312、GBK

1

2

UTF-8

1

3-4

UTF-16

2

3-4

UTF-32

4

4

字節流和字符流的區別

1.字節流讀取的時候,讀到一個字節就返回一個字節; 字符流使用了字節流讀到一個或多個字節(中文對應的字節數是兩個,在UTF-8碼錶中是3個字節)時。先去查指定的編碼表,將查到的字符返回。

2.字節流可以處理所有類型數據,如:圖片,MP3,AVI視頻文件,而字符流只能處理字符數據。只要是處理純文本數據,就要優先考慮使用字符流,除此之外都用字節流。

Java IO 流有4個抽象基類,其他流都是繼承於這四大基類的

下圖是Java IO 流的整體架構圖:

知道了 IO 流有這麼多分類,那我們在使用的時候應該怎麼選擇呢?

比如什麼時候用輸出流?什麼時候用字節流?可以根據下面三步選擇適合自己的流:

  • 首先自己要知道是選擇輸入流還是輸出流。這就要根據自己的情況決定,如果想從程序寫東西到別的地方,那麼就選擇輸入流,反之就選輸出流;
  • 然後考慮你傳輸數據時,是每次傳一個字節還是兩個字節,每次傳輸一個字節就選字節流,如果存在中文,那肯定就要選字符流了。
  • 通過前面兩步就可以選出一個合適的節點流了,比如字節輸入流 InputStream,如果要在此基礎上增強功能,那麼就在處理流中選擇一個合適的即可。

 一、字節流

1、Inputstream

InputStream有read方法,一次讀取一個字節,OutputStream的write方法一次寫一個int。這兩個類都是抽象類。意味着不能創建對象,那麼需要找到具體的子類來使用。

InputStream是所有輸入字節流的父類,是一個抽象類。ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三種基本的介質流,它們分別從Byte 數組、StringBuffer、和本地文件中讀取數據。PipedInputStream是從與其它線程共用的管道中讀取數據。

ObjectInputStream 和所有FilterInputStream 的子類都是裝飾流(裝飾器模式的主角)。

操作流的步驟都是:

  • 第一步:1:打開流(即創建流)
  • 第二步:2:通過流讀取內容
  • 第三步:3:用完後,關閉流資源

案例一:使用 read()方法,一次讀取一個字節,讀到文件末尾返回-1.

private static void showContent(String path) throws IOException {
        // 打開流
        FileInputStream fis = new FileInputStream(path);

        int len;
        while ((len = fis.read()) != -1) {
            System.out.print((char) len);
        }
        // 使用完關閉流
        fis.close();
}

案例二:使用read()方法的時候,可以將讀到的數據裝入到字節數組中,一次性的操作數組,可以提高效率。

private static void showContent2(String path) throws IOException {
        // 打開流
        FileInputStream fis = new FileInputStream(path);

        // 通過流讀取內容
        byte[] byt = new byte[1024];
        int len = fis.read(byt);
        for (int i = 0; i <len; i++) {
            System.out.print(byt[i]);
      }
    
        // 使用完關閉流
        fis.close();
    }

 2、OutputStream

OutputStream 是所有的輸出字節流的父類,它是一個抽象類。

ByteArrayOutputStream、FileOutputStream 是兩種基本的介質流,它們分別向Byte 數組、和本地文件中寫入數據。PipedOutputStream 是向與其它線程共用的管道中寫入數據,

ObjectOutputStream 和所有FilterOutputStream 的子類都是裝飾流。

OutputStram 的write方法,一次只能寫一個字節。成功的向文件中寫入了內容。但是並不高效,如何提高效率呢?可以使用緩衝,在OutputStram類中有write(byte[] b)方法,將 b.length個字節從指定的 byte 數組寫入此輸出流中。

private static void writeTxtFile(String path) throws IOException {
        // 1:打開文件輸出流,流的目的地是指定的文件
        FileOutputStream fos = new FileOutputStream(path,true);

        // 2:通過流向文件寫數據
        byte[] byt = "java".getBytes();
        fos.write(byt);

        // 3:用完流後關閉流
        fos.close();
    }

輸入輸出流綜合使用——文件拷貝實現

擴展

(1)void flush():刷新此輸出流並強制寫出所有緩衝的輸出字節。爲了加快數據傳輸速度,提高數據輸出效率,又是輸出數據流會在提交數據之前把所要輸出的數據先暫時保存在內存緩衝區中,然後成批進行輸出,每次傳輸過程都以某特定數據長度爲單位進行傳輸,在這種方式下,數據的末尾一般都會有一部分數據由於數量不夠一個批次,而存留在緩衝區裏,調用 flush() 方法可以將這部分數據強制提交。

(2)緩衝流

Java其實提供了專門的字節流緩衝來提高效率。BufferedInputStream 和 BufferedOutputStream。BufferedOutputStream和BufferedOutputStream類可以通過減少讀寫次數來提高輸入和輸出的速度。它們內部有一個緩衝區,用來提高處理效率。查看API文檔,發現可以指定緩衝區的大小。其實內部也是封裝了字節數組。沒有指定緩衝區大小,默認的字節是8192。顯然緩衝區輸入流和緩衝區輸出流要配合使用。首先緩衝區輸入流會將讀取到的數據讀入緩衝區,當緩衝區滿時,或者調用flush方法,緩衝輸出流會將數據寫出。

注意:當然使用緩衝流來進行提高效率時,對於小文件可能看不到性能的提升。但是文件稍微大一些的話,就可以看到實質的性能提升了。示例:

public class Test {
    public static void main(String[] args) throws IOException {
        String srcPath = "c:\\a.mp3";
        String destPath = "d:\\copy.mp3";
        copyFile(srcPath, destPath);
    }

    public static void copyFile(String srcPath, String destPath)
            throws IOException {
        // 打開輸入流,輸出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 使用緩衝流
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 讀取和寫入信息
        int len = 0;

        while ((len = bis.read()) != -1) {
            bos.write(len);
        }

        // 關閉流
        bis.close();
        bos.close();    
    }
}

 二、字符流

計算機並不區分二進制文件與文本文件。所有的文件都是以二進制形式來存儲的,因此,從本質上說,所有的文件都是二進制文件。所以字符流是建立在字節流之上的,它能夠提供字符層次的編碼和解碼。可以說字符流就是:字節流 + 編碼表,爲了更便於操作文字數據。字符流的抽象基類:Reader , Writer。由這些類派生出來的子類名稱都是以其父類名作爲子類名的後綴,如FileReader、FileWriter。

Java使用Unicode字符集來表示字符串和字符。爲了實現與其他程序語言及不同平臺的交互,Java提供一種新的數據流處理方案,稱作讀者(Reader)和寫者(Writer)。

1、字符輸入流Reader

Reader 是所有的輸入字符流的父類,它是一個抽象類。

  • CharReader和SringReader是兩種基本的介質流,它們分別將Char數組、String中讀取數據。
  • PipedReader 是從與其它線程共用的管道中讀取數據。
  • BufferedReader很明顯是一個裝飾器,它和其他子類負責裝飾其他Reader對象。
  • FilterReader是所有自定義具體裝飾流的父類,其子類PushBackReader對Reader對象進行裝飾,會增加一個行號。
  • InputStreamReader是其中最重要的一個,用來在字節輸入流和字符輸入流之間作爲中介,可以將字節輸入流轉換爲字符輸入流。FileReader 可以說是一個達到此功能、常用的工具類,在其源代碼中明顯使用了將FileInputStream 轉變爲Reader 的方法。

Reader 中各個類的用途和使用方法基本和InputStream 中的類使用一致。

2、字符輸出流 Writer

Writer是所有的輸出字符流的父類,它是一個抽象類。

  • CharWriter、StringWriter 是兩種基本的介質流,它們分別向Char 數組、String 中寫入數據。
  • PipedWriter 是向與其它線程共用的管道中寫入數據。
  • BufferedWriter 是一個裝飾器爲Writer 提供緩衝功能。
  • PrintWriter 和PrintStream 極其類似,功能和使用也非常相似。
  • OutputStreamWriter是其中最重要的一個,用來在字節輸出流和字符輸出流之間作爲中介,可以將字節輸出流轉換爲字符輸出流。FileWriter 可以說是一個達到此功能、常用的工具類,在其源代碼中明顯使用了將OutputStream轉變爲Writer 的方法。Writer 中各個類的用途和使用方法基本和OutputStream 中的類使用一致。

 下面展示一個字符輸入流和字符輸出流綜合使用的案例:複製文件。

擴展1:

1、轉換流

InputStreamReader 是字節流通向字符流的橋樑 
OutputStreamWriter 是字符流通向字節流的橋樑 
轉換流可以將字節轉成字符,原因在於,將獲取到的字節通過查編碼表獲取到指定對應字符。 轉換流的最強功能就是基於 字節流 + 編碼表 。沒有轉換,沒有字符流

2、打印流

PrintWriter  和 PrintStream 
注: 
A:只操作目的地,不操作數據源 
B:可以操作任意類型的數據 
C:如果啓用了自動刷新,在調用println(),printf(),format()方法的時候,能夠換行並刷新 
D:可以直接操作文件
 

擴展2:

1、序列化

把Java對象轉換爲字節序列的過程稱爲對象的序列化,也就是將對象寫入到IO流中序列化是爲了解決在對對象流進行讀寫操作時所引發的問題。序列化機制允許將實現序列化的Java對象轉換位字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以達到以後恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

要對一個對象序列化,這個對象就需要實現Serializable接口,如果這個對象中有一個變量是另一個對象的引用,則引用的對象也要實現Serializable接口,這個過程是遞歸的。Serializable接口中沒有定義任何方法,只是作爲一個標記來指示實現該接口的類可以進行序列化。

要實現序列化,只需兩步即可:

  • 步驟一:創建一個ObjectOutputStream輸出流;
  • 步驟二:調用ObjectOutputStream對象的 writeObject 方法輸出可序列化對象。

序列化只能保存對象的非靜態成員變量,而不能保存任何成員方法和靜態成員變量,並且保存的只是變量的值,變量的修飾符對序列化沒有影響。

有一些對象類不具有可持久化性,因爲其數據的特性決定了它會經常變化,其狀態只是瞬時的,這樣的對象是無法保存去狀態的,如Thread對象或流對象。對於這樣的成員變量,必須用 transient 關鍵字標明,否則編譯器將報錯。任何用 transient 關鍵字標明的成員變量,都不會被保存。

另外,序列化可能涉及將對象存放到磁盤上或在網絡上發送數據,這時會產生安全問題。對於一些需要保密的數據(如用戶密碼等),不應保存在永久介質中,爲了保證安全,應在這些變量前加上 transient 關鍵字

2、反序列化

反序列化就是從 IO 流中恢復對象。

反序列化也只需兩步即可完成:

  • 步驟一:創建 ObjectInputStream 輸入流
  • 步驟二:調用ObjectInputStream對象的readObject()得到序列化的對象。

控制檯只輸出了Person的信息,沒有輸出構造方法中的內容,說明反序列化的對象是由 JVM 自己生成的,不通過構造方法生成。

3、序列化版本號serialVersionUID

我們知道,反序列化必須擁有 class 文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級前後的兼容性呢?

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,對象也可以正確被反序列化回來。

如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常:

 

序列化版本號可自由指定,如果不指定,JVM會根據類信息自己計算一個版本號,這樣隨着class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號;
  • 如果只是修改了靜態變量,瞬態變量(transient修飾的變量),反序列化不受影響,無需修改版本號;如果修改了非瞬態變量,則可能導致反序列化失敗。
  • 如果新類中實例變量的類型與序列化時類的類型不一致,則會反序列化失敗,這時候需要更改serialVersionUID。如果只是新增了實例變量,則反序列化回來新增的是默認值;如果減少了實例變量,反序列化時會忽略掉減少的實例變量。

序列化使用場景

  • 所有需要網絡傳輸的對象都需要實現序列化接口,通過建議所有的javaBean都實現Serializable接口。
  • 對象的類名、實例變量(包括基本類型,數組,對其他對象的引用)都會被序列化;方法、類變量、transient實例變量都不會被序列化。
  • 如果想讓某個變量不被序列化,使用transient修飾。序列化對象的引用類型成員變量,也必須是可序列化的,否則,會報錯。
  • 反序列化時必須有序列化對象的class文件。當通過文件、網絡來讀取序列化後的對象時,必須按照實際寫入的順序讀取。
  • 單例類序列化,需要重寫readResolve()方法;否則會破壞單例原則。
  • 同一對象序列化多次,只有第一次序列化爲二進制流,以後都只是保存序列化編號,不會重複序列化。建議所有可序列化的類加上serialVersionUID 版本號,方便項目升級。

File類

File類是對文件系統中文件以及文件夾進行封裝的對象,可以通過對象的思想來操作文件和文件夾。

File類保存文件或目錄的各種元數據信息,包括文件名、文件長度、最後修改時間、是否可讀、獲取當前文件的路徑名,判斷指定文件是否存在、獲得當前目錄中的文件列表,創建、刪除文件和目錄等方法。

RandomAccessFile簡介

我們在對文件的操作過程中,除了使用字節流和字符流的方式之外,我們還可以使用RandomAcessFile這個工具類來實現。

RandomAccessFile可以實現對文件的讀 和 寫,但是他並不是繼承於以上4中基本虛擬類。而且在對文件的操作中RandomAccessFile有一個巨大的優勢他可以支持文件的隨機訪問,程序快可以直接跳轉到文件的任意地方來讀寫數據。所以如果需要訪問文件的部分內容,而不是把文件從頭讀到尾,使用RandomAccessFile將是更好的選擇。

RandomAccessFile的方法雖然多,但它有一個最大的侷限,就是隻能讀寫文件,不能讀寫其他IO節點。RandomAccessFile的一個重要使用場景就是網絡請求中的多線程下載及斷點續傳。

mode中,有4中啓動的方式:

  • "r" 以只讀方式打開。調用結果對象的任何 write 方法都將導致拋出 IOException。
  • "rw" 打開以便讀取和寫入。如果該文件尚不存在,則嘗試創建該文件。
  • "rws" 打開以便讀取和寫入,對於 "rw",還要求對文件的內容或元數據的每個更新都同步寫入到底層存儲設備。
  • "rwd" 打開以便讀取和寫入,對於 "rw",還要求對文件內容的每個更新都同步寫入到底層存儲設備

RandomAccessFile使用:

讀取文件內容

 RandomAccessFile raf = new RandomAccessFile(file,"r");
            String s = null;
            while ((s = raf.readLine())!=null){
                System.out.println(s);
            }
            raf.close();

寫入文件內容

String text = "寫入的內容 \n";
            RandomAccessFile raf = new RandomAccessFile(file,"rw");
            raf.seek(12);   //改變寫入偏移的位置,從地12個字節的位置開始寫入
            raf.write(text.getBytes());
            raf.close();

注意:RandomAccessFile雖然可以設置了偏移的方法,但他不能實現中間插入的效果,如果你需要實現文本中間插入的話,要先將後面的文件內容拷貝,然後寫入,最後在寫入的寫一行,將拷貝的東西複製回來。

總結:

  1. FileInputStream/FileOutputStream 需要逐個字節處理原始二進制流的時候使用,效率低下。
  2. FileReader/FileWriter 需要組個字符處理的時候使用。
  3. StringReader/StringWriter 需要處理字符串的時候,可以將字符串保存爲字符數組。
  4. PrintStream/PrintWriter 用來包裝FileOutputStream 對象,方便直接將String字符串寫入文件 。
  5. Scanner 用來包裝System.in流,很方便地將輸入的String字符串轉換成需要的數據類型。
  6. InputStreamReader/OutputStreamReader , 字節和字符的轉換橋樑,在網絡通信或者處理鍵盤輸入的時候用。
  7. BufferedReader/BufferedWriter, BufferedInputStream/BufferedOutputStream, 緩衝流用來包裝字節流後者字符流,提升IO性能,BufferedReader還可以方便地讀取一行,簡化編程。
  8. SequenceInputStream(InputStream s1, InputStream s2)序列流,合併流對象時使用.
  9. ObjectInputStream、ObjectOutputStream,方法用於序列化對象並將它們寫入一個流,另一個方法用於讀取流並反序列化對象。
  10. ByteArrayInputStream、ByteArrayOutputStream,操作數組
  11. DataInputStream、DataOutputStream操作基本數據類型和字符串。

 

[文章參考]

https://blog.csdn.net/zzy1832/article/details/48154825

https://baijiahao.baidu.com/s?id=1659851047751244423&wfr=spider&for=pc

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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