夯實Java基礎系列16:一文讀懂Java IO流和常見面試題

a724888發表於2019-10-08

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文參考

併發程式設計網 – ifeve.com

IO概述

在這一小節,我會試著給出Java IO(java.io)包下所有類的概述。更具體地說,我會根據類的用途對類進行分組。這個分組將會使你在未來的工作中,進行類的用途判定時,或者是為某個特定用途選擇類時變得更加容易。

輸入和輸出

術語“輸入”和“輸出”有時候會有一點讓人疑惑。一個應用程式的輸入往往是另外一個應用程式的輸出
那麼OutputStream流到底是一個輸出到目的地的流呢,還是一個產生輸出的流?InputStream流到底會不會輸出它的資料給讀取資料的程式呢?就我個人而言,在第一天學習Java IO的時候我就感覺到了一絲疑惑。
為了消除這個疑惑,我試著給輸入和輸出起一些不一樣的別名,讓它們從概念上與資料的來源和資料的流向相聯絡。

Java的IO包主要關注的是從原始資料來源的讀取以及輸出原始資料到目標媒介。以下是最典型的資料來源和目標媒介:

檔案
管道
網路連線
記憶體快取
System.in, System.out, System.error(注:Java標準輸入、輸出、錯誤輸出)

下面這張圖描繪了一個程式從資料來源讀取資料,然後將資料輸出到其他媒介的原理:

[外鏈圖片轉存失敗(img-VIwahDo3-1567839588993)( http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%981.png)]

在Java IO中,流是一個核心的概念。流從概念上來說是一個連續的資料流。你既可以從流中讀取資料,也可以往流中寫資料。流與資料來源或者資料流向的媒介相關聯。在Java IO中流既可以是位元組流(以位元組為單位進行讀寫),也可以是字元流(以字元為單位進行讀寫)。

類InputStream, OutputStream, Reader 和Writer
一個程式需要InputStream或者Reader從資料來源讀取資料,需要OutputStream或者Writer將資料寫入到目標媒介中。以下的圖說明了這一點:

[外鏈圖片轉存失敗(img-dsCHgGDu-1567839588994)( http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%982.png)]

InputStream和Reader與資料來源相關聯,OutputStream和writer與目標媒介相關聯。

Java IO的用途和特徵

Java IO中包含了許多InputStream、OutputStream、Reader、Writer的子類。這樣設計的原因是讓每一個類都負責不同的功能。這也就是為什麼IO包中有這麼多不同的類的緣故。各類用途彙總如下:

檔案訪問
網路訪問
記憶體快取訪問
執行緒內部通訊(管道)
緩衝
過濾
解析
讀寫文字 (Readers / Writers)
讀寫基本型別資料 (long, int etc.)
讀寫物件

當通讀過Java IO類的原始碼之後,我們很容易就能瞭解這些用途。這些用途或多或少讓我們更加容易地理解,不同的類用於針對不同業務場景。

Java IO類概述表
已經討論了資料來源、目標媒介、輸入、輸出和各類不同用途的Java IO類,接下來是一張通過輸入、輸出、基於位元組或者字元、以及其他比如緩衝、解析之類的特定用途劃分的大部分Java IO類的表格。

[外鏈圖片轉存失敗(img-a3UYZ3ow-1567839588995)( http://ifeve.com/wp-content/uploads/2014/10/QQ%E6%88%AA%E5%9B%BE20141020174145.png)]

Java IO類圖

[外鏈圖片轉存失敗(img-n1TqKmEv-1567839588995)( https://images.cnblogs.com/cnblogs_com/davidgu/java_io_hierarchy.jpg)]

什麼是Java IO流

Java IO流是既可以從中讀取,也可以寫入到其中的資料流。正如這個系列教程之前提到過的,流通常會與資料來源、資料流向目的地相關聯,比如檔案、網路等等。

流和陣列不一樣,不能通過索引讀寫資料。在流中,你也不能像陣列那樣前後移動讀取資料,除非使用RandomAccessFile 處理檔案。流僅僅只是一個連續的資料流。

某些類似PushbackInputStream 流的實現允許你將資料重新推回到流中,以便重新讀取。然而你只能把有限的資料推回流中,並且你不能像運算元組那樣隨意讀取資料。流中的資料只能夠順序訪問。

Java IO流通常是基於位元組或者基於字元的。位元組流通常以“stream”命名,比如InputStream和OutputStream。除了DataInputStream 和DataOutputStream 還能夠讀寫int, long, float和double型別的值以外,其他流在一個操作時間內只能讀取或者寫入一個原始位元組。

字元流通常以“Reader”或者“Writer”命名。字元流能夠讀寫字元(比如Latin1或者Unicode字元)。可以瀏覽Java Readers and Writers獲取更多關於字元流輸入輸出的資訊。

InputStream

java.io.InputStream類是所有Java IO輸入流的基類。如果你正在開發一個從流中讀取資料的元件,請嘗試用InputStream替代任何它的子類(比如FileInputStream)進行開發。這麼做能夠讓你的程式碼相容任何型別而非某種確定型別的輸入流。

組合流

你可以將流整合起來以便實現更高階的輸入和輸出操作。比如,一次讀取一個位元組是很慢的,所以可以從磁碟中一次讀取一大塊資料,然後從讀到的資料塊中獲取位元組。為了實現緩衝,可以把InputStream包裝到BufferedInputStream中。

程式碼示例
InputStream input = new BufferedInputStream(new FileInputStream(“c:\data\input-file.txt”));

緩衝同樣可以應用到OutputStream中。你可以實現將大塊資料批量地寫入到磁碟(或者相應的流)中,這個功能由BufferedOutputStream實現。

緩衝只是通過流整合實現的其中一個效果。你可以把InputStream包裝到PushbackInputStream中,之後可以將讀取過的資料推回到流中重新讀取,在解析過程中有時候這樣做很方便。或者,你可以將兩個InputStream整合成一個SequenceInputStream。

將不同的流整合到一個鏈中,可以實現更多種高階操作。通過編寫包裝了標準流的類,可以實現你想要的效果和過濾器。

IO檔案

在Java應用程式中,檔案是一種常用的資料來源或者儲存資料的媒介。所以這一小節將會對Java中檔案的使用做一個簡短的概述。這篇文章不會對每一個技術細節都做出解釋,而是會針對檔案存取的方法提供給你一些必要的知識點。在之後的文章中,將會更加詳細地描述這些方法或者類,包括方法示例等等。

通過Java IO讀檔案

如果你需要在不同端之間讀取檔案,你可以根據該檔案是二進位制檔案還是文字檔案來選擇使用FileInputStream或者FileReader。
這兩個類允許你從檔案開始到檔案末尾一次讀取一個位元組或者字元,或者將讀取到的位元組寫入到位元組陣列或者字元陣列。你不必一次性讀取整個檔案,相反你可以按順序地讀取檔案中的位元組和字元。

如果你需要跳躍式地讀取檔案其中的某些部分,可以使用RandomAccessFile。

通過Java IO寫檔案

如果你需要在不同端之間進行檔案的寫入,你可以根據你要寫入的資料是二進位制型資料還是字元型資料選用FileOutputStream或者FileWriter。
你可以一次寫入一個位元組或者字元到檔案中,也可以直接寫入一個位元組陣列或者字元資料。資料按照寫入的順序儲存在檔案當中。

通過Java IO隨機存取檔案

正如我所提到的,你可以通過RandomAccessFile對檔案進行隨機存取。

隨機存取並不意味著你可以在真正隨機的位置進行讀寫操作,它只是意味著你可以跳過檔案中某些部分進行操作,並且支援同時讀寫,不要求特定的存取順序。
這使得RandomAccessFile可以覆蓋一個檔案的某些部分、或者追加內容到它的末尾、或者刪除它的某些內容,當然它也可以從檔案的任何位置開始讀取檔案。

下面是具體例子:

@Test
    //檔案流範例,開啟一個檔案的輸入流,讀取到位元組陣列,再寫入另一個檔案的輸出流
    public void test1() {
        try {
            FileInputStream fileInputStream = new FileInputStream(new File("a.txt"));
            FileOutputStream fileOutputStream = new FileOutputStream(new File("b.txt"));
            byte []buffer = new byte[128];
            while (fileInputStream.read(buffer) != -1) {
                fileOutputStream.write(buffer);
            }
            //隨機讀寫,通過mode引數來決定讀或者寫
            RandomAccessFile randomAccessFile = new RandomAccessFile(new File("c.txt"), "rw");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

字元流和位元組流

Java IO的Reader和Writer除了基於字元之外,其他方面都與InputStream和OutputStream非常類似。他們被用於讀寫文字。InputStream和OutputStream是基於位元組的,還記得嗎?

Reader
Reader類是Java IO中所有Reader的基類。子類包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。

Writer
Writer類是Java IO中所有Writer的基類。子類包括BufferedWriter和PrintWriter等等。

這是一個簡單的Java IO Reader的例子:

Reader reader = new FileReader("c:\\data\\myfile.txt");
int data = reader.read();
while(data != -1){
    char dataChar = (char) data;
    data = reader.read();
}

你通常會使用Reader的子類,而不會直接使用Reader。Reader的子類包括InputStreamReader,CharArrayReader,FileReader等等。可以檢視Java IO概述瀏覽完整的Reader表格。

整合Reader與InputStream

一個Reader可以和一個InputStream相結合。如果你有一個InputStream輸入流,並且想從其中讀取字元,可以把這個InputStream包裝到InputStreamReader中。把InputStream傳遞到InputStreamReader的建構函式中:

Reader reader = new InputStreamReader(inputStream);

在建構函式中可以指定解碼方式。

Writer

Writer類是Java IO中所有Writer的基類。子類包括BufferedWriter和PrintWriter等等。這是一個Java IO Writer的例子:

Writer writer = new FileWriter("c:\\data\\file-output.txt"); 
writer.write("Hello World Writer"); 
writer.close();

同樣,你最好使用Writer的子類,不需要直接使用Writer,因為子類的實現更加明確,更能表現你的意圖。常用子類包括OutputStreamWriter,CharArrayWriter,FileWriter等。Writer的write(int c)方法,會將傳入引數的低16位寫入到Writer中,忽略高16位的資料。

整合Writer和OutputStream

與Reader和InputStream類似,一個Writer可以和一個OutputStream相結合。把OutputStream包裝到OutputStreamWriter中,所有寫入到OutputStreamWriter的字元都將會傳遞給OutputStream。這是一個OutputStreamWriter的例子:

Writer writer = new OutputStreamWriter(outputStream);

IO管道

Java IO中的管道為執行在同一個JVM中的兩個執行緒提供了通訊的能力。所以管道也可以作為資料來源以及目標媒介。

你不能利用管道與不同的JVM中的執行緒通訊(不同的程式)。在概念上,Java的管道不同於Unix/Linux系統中的管道。在Unix/Linux中,執行在不同地址空間的兩個程式可以通過管道通訊。在Java中,通訊的雙方應該是執行在同一程式中的不同執行緒。

通過Java IO建立管道

可以通過Java IO中的PipedOutputStream和PipedInputStream建立管道。一個PipedInputStream流應該和一個PipedOutputStream流相關聯。
一個執行緒通過PipedOutputStream寫入的資料可以被另一個執行緒通過相關聯的PipedInputStream讀取出來。

Java IO管道示例
這是一個如何將PipedInputStream和PipedOutputStream關聯起來的簡單例子:

//使用管道來完成兩個執行緒間的資料點對點傳遞
    @Test
    public void test2() throws IOException {
        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    pipedOutputStream.write("hello input".getBytes());
                    pipedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    byte []arr = new byte[128];
                    while (pipedInputStream.read(arr) != -1) {
                        System.out.println(Arrays.toString(arr));
                    }
                    pipedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

管道和執行緒
請記得,當使用兩個相關聯的管道流時,務必將它們分配給不同的執行緒。read()方法和write()方法呼叫時會導致流阻塞,這意味著如果你嘗試在一個執行緒中同時進行讀和寫,可能會導致執行緒死鎖。

管道的替代
除了管道之外,一個JVM中不同執行緒之間還有許多通訊的方式。實際上,執行緒在大多數情況下會傳遞完整的物件資訊而非原始的位元組資料。但是,如果你需要線上程之間傳遞位元組資料,Java IO的管道是一個不錯的選擇。

Java IO:網路

Java中網路的內容或多或少的超出了Java IO的範疇。關於Java網路更多的是在我的Java網路教程中探討。但是既然網路是一個常見的資料來源以及資料流目的地,並且因為你使用Java IO的API通過網路連線進行通訊,所以本文將簡要的涉及網路應用。

當兩個程式之間建立了網路連線之後,他們通訊的方式如同操作檔案一樣:利用InputStream讀取資料,利用OutputStream寫入資料。換句話來說,Java網路API用來在不同程式之間建立網路連線,而Java IO則用來在建立了連線之後的程式之間交換資料。

基本上意味著如果你有一份能夠對檔案進行寫入某些資料的程式碼,那麼這些資料也可以很容易地寫入到網路連線中去。你所需要做的僅僅只是在程式碼中利用OutputStream替代FileOutputStream進行資料的寫入。因為FileOutputStream是OuputStream的子類,所以這麼做並沒有什麼問題。

//從網路中讀取位元組流也可以直接使用OutputStream
public void test3() {
    //讀取網路程式的輸出流
    OutputStream outputStream = new OutputStream() {
        @Override
        public void write(int b) throws IOException {
        }
    };
}
public void process(OutputStream ouput) throws IOException {
    //處理網路資訊
    //do something with the OutputStream
}

位元組和字元陣列

從InputStream或者Reader中讀入陣列

從OutputStream或者Writer中寫陣列

在java中常用位元組和字元陣列在應用中臨時儲存資料。而這些陣列又是通常的資料讀取來源或者寫入目的地。如果你需要在程式執行時需要大量讀取檔案裡的內容,那麼你也可以把一個檔案載入到陣列中。

前面的例子中,字元陣列或位元組陣列是用來快取資料的臨時儲存空間,不過它們同時也可以作為資料來源或者寫入目的地。
舉個例子:

//字元陣列和位元組陣列在io過程中的作用
    public void test4() {
        //arr和brr分別作為資料來源
        char []arr = {'a','c','d'};
        CharArrayReader charArrayReader = new CharArrayReader(arr);
        byte []brr = {1,2,3,4,5};
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(brr);
    }

System.in, System.out, System.err

System.in, System.out, System.err這3個流同樣是常見的資料來源和資料流目的地。使用最多的可能是在控制檯程式裡利用System.out將輸出列印到控制檯上。

JVM啟動的時候通過Java執行時初始化這3個流,所以你不需要初始化它們(儘管你可以在執行時替換掉它們)。

System.in
System.in是一個典型的連線控制檯程式和鍵盤輸入的InputStream流。通常當資料通過命令列引數或者配置檔案傳遞給命令列Java程式的時候,System.in並不是很常用。圖形介面程式通過介面傳遞引數給程式,這是一塊單獨的Java IO輸入機制。
System.out
System.out是一個PrintStream流。System.out一般會把你寫到其中的資料輸出到控制檯上。System.out通常僅用在類似命令列工具的控制檯程式上。System.out也經常用於列印程式的除錯資訊(儘管它可能並不是獲取程式除錯資訊的最佳方式)。
System.err
System.err是一個PrintStream流。System.err與System.out的執行方式類似,但它更多的是用於列印錯誤文字。一些類似Eclipse的程式,為了讓錯誤資訊更加顯眼,會將錯誤資訊以紅色文字的形式通過System.err輸出到控制檯上。

System.out和System.err的簡單例子:
這是一個System.out和System.err結合使用的簡單示例:

 //測試System.in, System.out, System.err    
    public static void main(String[] args) {
        int in = new Scanner(System.in).nextInt();
        System.out.println(in);
        System.out.println("out");
        System.err.println("err");
        //輸入10,結果是
//        err(紅色)
//        10
//        out
    }

字元流的Buffered和Filter

BufferedReader能為字元輸入流提供緩衝區,可以提高許多IO處理的速度。你可以一次讀取一大塊的資料,而不需要每次從網路或者磁碟中一次讀取一個位元組。特別是在訪問大量磁碟資料時,緩衝通常會讓IO快上許多。

BufferedReader和BufferedInputStream的主要區別在於,BufferedReader操作字元,而BufferedInputStream操作原始位元組。只需要把Reader包裝到BufferedReader中,就可以為Reader新增緩衝區(譯者注:預設緩衝區大小為8192位元組,即8KB)。程式碼如下:

Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt"));

你也可以通過傳遞建構函式的第二個引數,指定緩衝區大小,程式碼如下:

Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt"), 8 * 1024);

這個例子設定了8KB的緩衝區。最好把緩衝區大小設定成1024位元組的整數倍,這樣能更高效地利用內建緩衝區的磁碟。

除了能夠為輸入流提供緩衝區以外,其餘方面BufferedReader基本與Reader類似。BufferedReader還有一個額外readLine()方法,可以方便地一次性讀取一整行字元。

BufferedWriter

與BufferedReader類似,BufferedWriter可以為輸出流提供緩衝區。可以構造一個使用預設大小緩衝區的BufferedWriter(譯者注:預設緩衝區大小8 * 1024B),程式碼如下:

Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt"));

也可以手動設定緩衝區大小,程式碼如下:

Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt"), 8 * 1024);

為了更好地使用內建緩衝區的磁碟,同樣建議把緩衝區大小設定成1024的整數倍。除了能夠為輸出流提供緩衝區以外,其餘方面BufferedWriter基本與Writer類似。類似地,BufferedWriter也提供了writeLine()方法,能夠把一行字元寫入到底層的字元輸出流中。

值得注意是,你需要手動flush()方法確保寫入到此輸出流的資料真正寫入到磁碟或者網路中。

FilterReader

與FilterInputStream類似,FilterReader是實現自定義過濾輸入字元流的基類,基本上它僅僅只是簡單覆蓋了Reader中的所有方法。

就我自己而言,我沒發現這個類明顯的用途。除了建構函式取一個Reader變數作為引數之外,我沒看到FilterReader任何對Reader新增或者修改的地方。如果你選擇繼承FilterReader實現自定義的類,同樣也可以直接繼承自Reader從而避免額外的類層級結構。

JavaIO流面試題

什麼是IO流?

它是一種資料的流從源頭流到目的地。比如檔案拷貝,輸入流和輸出流都包括了。輸入流從檔案中讀取資料儲存到程式(process)中,輸出流從程式中讀取資料然後寫入到目標檔案。

位元組流和字元流的區別。

位元組流在JDK1.0中就被引進了,用於操作包含ASCII字元的檔案。JAVA也支援其他的字元如Unicode,為了讀取包含Unicode字元的檔案,JAVA語言設計者在JDK1.1中引入了字元流。ASCII作為Unicode的子集,對於英語字元的檔案,可以可以使用位元組流也可以使用字元流。

Java中流類的超類主要由那些?

java.io.InputStream
java.io.OutputStream
java.io.Reader
java.io.Writer

FileInputStream和FileOutputStream是什麼?

這是在拷貝檔案操作的時候,經常用到的兩個類。在處理小檔案的時候,它們效能表現還不錯,在大檔案的時候,最好使用BufferedInputStream (或 BufferedReader) 和 BufferedOutputStream (或 BufferedWriter)

System.out.println()是什麼?

println是PrintStream的一個方法。out是一個靜態PrintStream型別的成員變數,System是一個java.lang包中的類,用於和底層的作業系統進行互動。

什麼是Filter流?

Filter Stream是一種IO流主要作用是用來對存在的流增加一些額外的功能,像給目標檔案增加原始檔中不存在的行數,或者增加拷貝的效能。

有哪些可用的Filter流?

在java.io包中主要由4個可用的filter Stream。兩個位元組filter stream,兩個字元filter stream. 分別是FilterInputStream, FilterOutputStream, FilterReader and FilterWriter.這些類是抽象類,不能被例項化的。

在檔案拷貝的時候,那一種流可用提升更多的效能?

在位元組流的時候,使用BufferedInputStream和BufferedOutputStream。
在字元流的時候,使用BufferedReader 和 BufferedWriter

說說管道流(Piped Stream)

有四種管道流, PipedInputStream, PipedOutputStream, PipedReader 和 PipedWriter.在多個執行緒或程式中傳遞資料的時候管道流非常有用。

說說File類

它不屬於 IO流,也不是用於檔案操作的,它主要用於知道一個檔案的屬性,讀寫許可權,大小等資訊。

說說RandomAccessFile?

它在java.io包中是一個特殊的類,既不是輸入流也不是輸出流,它兩者都可以做到。他是Object的直接子類。通常來說,一個流只有一個功能,要麼讀,要麼寫。但是RandomAccessFile既可以讀檔案,也可以寫檔案。 DataInputStream 和 DataOutStream有的方法,在RandomAccessFile中都存在。

參考文章

https://www.imooc.com/article/24305
https://www.cnblogs.com/UncleWang001/articles/10454685.html
https://www.cnblogs.com/Jixiangwei/p/Java.html
https://blog.csdn.net/baidu_37107022/article/details/76890019

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,偶爾講點演算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2659071/,如需轉載,請註明出處,否則將追究法律責任。

相關文章