面試官:位元組流可以處理一切檔案為什麼還需要字元流呢?

JavaBuild發表於2024-06-15

一、寫在開頭

在計算機領域中百分之九十以上的程式擁有著和外部裝置互動的功能,這就是我們常說的IO(Input/Output:輸入/輸出),所謂輸入就是外部資料匯入計算機記憶體中的過程,輸出則是將記憶體或者說程式中的資料匯入到外部儲存中,如資料庫、檔案以及其他本地磁碟等。

二、什麼是IO流

這種輸入輸出往往遵循著先入先出,順序存取的特點,像水流一般,因此我們稱這樣的操作為流(Stream),如下我們根據不同的標準,將IO流分為幾個門類:
image

根據資料流向:

  1. 輸入流:資料流向程式
  2. 輸出流:資料從程式流出。

根據處理單位:

  1. 位元組流:一次讀入或讀出是8位二進位制;
  2. 字元流:一次讀入或讀出是16位二進位制
  3. JDK 中字尾是 Stream 是位元組流;字尾是 Reader,Writer 是字元流。

根據功能點:

  1. 節點流:直接與資料來源相連,讀入或寫出;
  2. 處理流:與節點流一塊使用,在節點流的基礎上,再套接一層。

三、輸入與輸出

在java.io包中多達40多個類,它們的基類來源於InputStream、OutputStream、Reader、Writer這四個,我們一一看過。

3.1 InputStream(位元組輸入流)

InputStream作為所有位元組輸入流的父類,主要作用是將外部資料讀取到記憶體中,主要方法如下(JDK8):

  1. read():返回輸入流中下一個位元組的資料。返回的值介於 0 到 255 之間。如果未讀取任何位元組,則程式碼返回 -1 ,表示檔案結束。
  2. read(byte b[ ]) : 從輸入流中讀取一些位元組儲存到陣列 b 中。如果陣列 b 的長度為零,則不讀取。如果沒有可用位元組讀取,返回 -1。如果有可用位元組讀取,則最多讀取的位元組數最多等於 b.length , 返回讀取的位元組數。這個方法等價於 read(b, 0, b.length)。
  3. read(byte b[], int off, int len):在read(byte b[ ]) 方法的基礎上增加了 off 引數(偏移量)和 len 引數(要讀取的最大位元組數)。
  4. skip(long n):忽略輸入流中的 n 個位元組 ,返回實際忽略的位元組數。
  5. available():返回輸入流中可以讀取的位元組數。
  6. close():關閉輸入流釋放相關的系統資源。
  7. markSupported() :該輸入流是否支援mark()和reset()方法。
  8. mark(int readlimit) :標誌輸入流的當前位置,隨後呼叫reset()方法將該流重新定位到最近標記的位置;引數readlimit表示:在標記位置失效前可以讀取位元組的最大限制。
  9. reset() :將此流重新定位到最後一次對此輸入流呼叫 mark 方法時的位置。

image

我們使用FileInputStream(檔案位元組輸入流)進行如上方法的使用測試:

public class Test {
    public static void main(String[] args) throws IOException {
        try (InputStream fis = new FileInputStream("E:\\input.txt")) {
            System.out.println("可讀取位元組數:"
                    + fis.available());
            int content;
            long skip = fis.skip(3);
            System.out.println("忽略位元組數:" + skip);
            System.out.print("剩餘全量位元組:");
            while ((content = fis.read()) != -1) {
                System.out.print((char) content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

image

輸出:

可讀取位元組數:20
忽略位元組數:3
剩餘全量位元組:name is JavaBuild

3.2 OutputStream(位元組輸出流)

outputstream作為所有位元組輸出流的父類,主要則是將記憶體或者說程式中的資料以位元組流的方式匯入到外部儲存中,如資料庫、檔案以及其他本地磁碟等。它的使用方法相比較位元組輸入流要少:

  1. write(int b):將特定位元組寫入輸出流。
  2. write(byte b[ ]) : 將陣列b 寫入到輸出流,等價於 write(b, 0, b.length) 。
  3. write(byte[] b, int off, int len) : 在write(byte b[ ]) 方法的基礎上增加了 off 引數(偏移量)和 len 引數(要讀取的最大位元組數)。
  4. flush():重新整理此輸出流並強制寫出所有緩衝的輸出位元組。
  5. close():關閉輸出流釋放相關的系統資源。

image

我們同樣以FileOutputStream為例進行上述方法的測試:

public class Test {
    public static void main(String[] args) throws IOException {
        try (FileOutputStream output = new FileOutputStream("E://output.txt")) {
            byte[] array = "JavaBuild".getBytes();
            //將一個位元組陣列寫入本地E盤的外部檔案output.txt中
            output.write(array);

            //換行方式1:Windows下的換行符為"\r\n"
            output.write("\r\n".getBytes());
            //換行方式2:推薦使用,具有良好的跨平臺性
            String newLine = System.getProperty("line.separator");
            output.write(newLine.getBytes());

            //輸出位元組,這裡的數字會被轉為asicc碼中對應的字元
            output.write(64);
            output.write(56);
            output.write(56);
            output.write(56);
            //關閉輸出流
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

效果:
image

這裡可以直接輸出單位元組資料,也可以輸出指定的位元組陣列。輸出位元組時以int型別輸出,最終根據ASCII錶轉為字元。如十進位制64的轉為@符號。

3.3 Reader(字元輸入流)

在講解字元流之前,我們來解釋一個面試問題:“為什麼有了位元組流了還需要使用更耗時的字元流”

確實,位元組作為資訊儲存的最小單元,我們可以透過位元組流實現所有資訊的輸入與輸出,但有時候會存在一些問題,比如中文輸入時的編碼問題,將上述3.1中的測試程式碼稍微改一下,執行結果如下,中文在控制檯輸出時亂碼了。當然我們可以透過設定編碼來規避這個問題,但有時候不曉得編碼時,亂碼真的會帶來潛在風險!
image

字元流與位元組流的區別:

  • 位元組流一般用來處理影像、影片、音訊、PPT、Word等型別的檔案。字元流一般用於處理純文字型別的檔案,如TXT檔案等,但不能處理影像影片等非文字檔案。
  • 位元組流本身沒有緩衝區,緩衝位元組流相對於位元組流,效率提升非常高。而字元流本身就帶有緩衝區,緩衝字元流相對於效率提升不明顯。

說了這麼多,我們現在來看一下Reader這個字元輸入流提供的主要方法吧,其實和InputStream差不多,只不過一個是以位元組為單位的讀取,一個是以字元為單位。

  1. read() : 從輸入流讀取一個字元。
  2. read(char[] cbuf) : 從輸入流中讀取一些字元,並將它們儲存到字元陣列 cbuf中,等價於 read(cbuf, 0, cbuf.length) 。
  3. read(char[] cbuf, int off, int len):在read(char[] cbuf) 方法的基礎上增加了 off 引數(偏移量)和 len 引數(要讀取的最大字元數)。
  4. skip(long n):忽略輸入流中的 n 個字元 ,返回實際忽略的字元數。
  5. close() : 關閉輸入流並釋放相關的系統資源。

image

我們將上述3.1中的測試程式碼稍作加工,採用FileReader流進行輸入,列印結果:

image

可以看到即便有中文,輸出在控制檯也沒有亂碼,因為字元流預設採用的是 Unicode 編碼。

那麼字元流是如何實現txt檔案讀取的呢?透過FileReader類的繼承關係我們可以看到它繼承了InputStreamReader,這是一個位元組轉字元輸入流,所以說從根本上,字元流底層依賴的還是位元組流!

// 位元組流轉換為字元流的橋樑
public class InputStreamReader extends Reader {
}
// 用於讀取字元檔案
public class FileReader extends InputStreamReader {
}

3.4 Writer(字元輸出流)

writer是將記憶體或者說程式中的資料以字元流的方式匯入到外部儲存中,如資料庫、檔案以及其他本地磁碟等。
常用方法也和OutputStream相似:

  1. write(int c) : 寫入單個字元。
  2. write(char[] cbuf):寫入字元陣列 cbuf,等價於write(cbuf, 0, cbuf.length)。
  3. write(char[] cbuf, int off, int len):在write(char[] cbuf) 方法的基礎上增加了 off 引數(偏移量)和 len 引數(要讀取的最大字元數)。
  4. write(String str):寫入字串,等價於 write(str, 0, str.length()) 。
  5. write(String str, int off, int len):在write(String str) 方法的基礎上增加了 off 引數(偏移量)和 len 引數(要讀取的最大字元數)。
  6. append(CharSequence csq):將指定的字元序列附加到指定的 Writer 物件並返回該 Writer 物件。
  7. append(char c):將指定的字元附加到指定的 Writer 物件並返回該 Writer 物件。
  8. flush():重新整理此輸出流並強制寫出所有緩衝的輸出字元。
  9. close():關閉輸出流釋放相關的系統資源。

我們同樣以FileWriter為例,去測試一下:

public class Test {
    public static void main(String[] args) throws IOException {
        try (FileWriter fw = new FileWriter("E:\\outwriter.txt")) {
           fw.write("大家好!!!");
           fw.append("我是JavaBuild");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

image

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
image

相關文章