Java 的 IO 很复杂?用思路带领你去battle他!
所有編程語言都涉及IO,java也不例外。
初學者入門Java,難理解是正常的,包括我。
簡單的說:IO就是和Java以外的文件打交道。
既然要處理文件,那么就需要Java提供的IO接口進行處理:
- Input指從外部讀入數據到內存
- Output指把數據從內存輸出到外部
要掌握Java的IO,核心就是:選擇合適的IO流讀寫文件。
以下是Java硬核的IO技術文介紹,希望可以幫到學習IO的小伙伴:
1、什么是IO
I/O 是指Input/Output,即輸入和輸出。
- Input指從外部讀入數據到內存,例如,把文件從磁盤讀取到內存,從網絡讀取數據到內存等等。
- Output指把數據從內存輸出到外部,例如,把數據從內存寫入到文件,把數據從內存輸出到網絡等等。
Java程序在執行的時候,是在內存進行的,外部的數據需要讀寫到內存才能處理;而在內存中的數據是隨著程序結束就消失的,有時候我們也需要把數據輸出到外部文件。
Java中,是通過流 處理IO的,這種處理模式稱為 IO流,IO流是一種順序讀寫數據的模式。
你可以想象它是一根水管,數據就像水一樣, 起點—終點 可互相流動。
1.1、流的特點:
1.2、IO流的分類
1.1.1、按方向分
按數據流的方向分為 輸入流、輸出流,是相對內存來說的。
- 輸入流:從外部(數據源)把數據輸入到程序(內存)。
- 輸出流:把程序的數據(內存)輸出到外部(數據源)。
1.1.2、按處理數據類型分
按處理的數據類型可分為 字節流、字符流
1字符 = 2字節 、 1字節(byte) = 8位(bit)- 字節流:每次讀 (寫)一個字節,當傳輸的資源文件有中文時,就會出現亂碼,讀寫的單位是byte,在InputStream/OutputStream中單向流動
- 字符流:每次讀取(寫出)兩個字節,有中文時,使用該流就可以正確傳輸顯示中文,讀寫的單位是char,在Reader/Writer中單向流動
字節流和字符流的原理是相同的,只不過處理的單位不同而已。后綴是Stream是字節流,而后綴是Reader,Writer是字符流。
為什么要有字符流?
Java中字符是采用Unicode標準,Unicode 編碼中,一個英文為一個字節,一個中文為兩個字節。但是編碼不同,中文字符占的字節數不一樣,而在UTF-8編碼中,一個中文字符是3個字節。
如果統一使用字節流處理中文,因為讀寫是一個字節一個字節,這樣就會對中文字符有影響,就會出現亂碼。
為了更方便地處理中文這些字符,Java就推出了字符流。
字節流和字符流的其他區別:
1.1.3、按功能分
按功能不同分為 節點流、處理流
- 節點流:以從或向一個特定的地方(節點)讀寫數據。如FileInputStream
- 處理流:是對一個已存在的流的連接和封裝,通過所封裝的流的功能調用實現數據讀寫。如BufferedReader。處理流的構造方法總是要帶一個其他的流對象做參數。一個流對象經過其他流的多次包裝.
1.1.4、按有無緩沖分
還有一種流是緩沖流,區別于沒有緩沖的流。
因為程序和內存交互很快,而程序和磁盤交互是很慢的,這樣會導致程序出現性能問題。
為了減少程序與磁盤的交互,是提升程序效率,引入了緩沖流。
普通流每次讀寫一個字節,而緩沖流在內存中設置一個緩存區,緩沖區先存儲足夠的待操作數據后,再與內存或磁盤進行交互。這樣,在總數據量不變的情況下,通過提高每次交互的數據量,減少了交互次數。
有緩沖的流,類名前綴是帶有Buffer的,比如BufferedInputStream、BufferedReader
2、Java IO 流對象詳解
以上說了這么多流,看起來很復雜,但其實只需要記住以下四種流即可:
這四個都是抽象類,都位于 java.io 包目錄。
我們平時使用流去處理數據,都是通過這四個流的子類展開的。
挑一些常用的放在下面一一講解。
2.1、InputStream ——字節流輸入流
InputStream 這個抽象類是表示以上輸入字節流的所有類的超類(父類)。
InputStream 中的三個基本的讀方法:
- abstract int read() :讀取一個字節數據,并返回讀到的數據,如果返回 -1,表示讀到了輸入流的末尾。
- int read(byte[] b) :將數據讀入一個字節數組,同時返回實際讀取的字節數。如果返回-1,表示讀到了輸入流的末尾。
- int read(byte[] b, int off, int len) :將數據讀入一個字節數組,同時返回實際讀取的字節數。如果返回 -1,表示讀到了輸入流的末尾。off 指定在數組 b 中存放數據的起始偏移位置;len 指定讀取的最大字節數。
InputStream 的子類有:
- ByteArrayInputStream
- FileInputStream
- FilterInputStream
- PushbackInputStream
- DataInputStream
- BufferedInputStream
- LineNumberInputStream
- ObjectInputStream
- PipedInputStream
- SequenceInputStream
- StringBufferInputStream
這么多子類不需要每一個都記住,只需要記住兩個:
FileInputStream
FileInputStream是文件字節輸入流,就是對文件數據以字節的方式來處理,如音樂、視頻、圖片等。
BufferedInputStream
使用方式基本和FileInputStream一致。
BufferedInputStream有一個內部緩沖區數組,一次性讀取較多的字節緩存起來,默認讀取defaultBufferSize = 8192,作用于讀文件時可以提高性能。
2.2、OutputStream——字節輸出流
OutputStream 是相對 InputStream 的,既然有輸入就有輸出。OutputStream 這個抽象類是表示以上輸出字節流的所有類的超類(父類)。
OutputStream 中的三個基本的寫方法:
- abstract void write(int b):往輸出流中寫入一個字節。
- void write(byte[] b) :往輸出流中寫入數組b中的所有字節。
- void write(byte[] b, int off, int len) :往輸出流中寫入數組 b 中從偏移量 off 開始的 len 個字節的數據。
其它重要方法:
- void flush() :刷新輸出流,強制緩沖區中的輸出字節被寫出。
- void close() :關閉輸出流,釋放和這個流相關的系統資源。
OutputStream 的子類有:
- ByteArrayOutputStream
- FileOutputStream
- FilterOutputStream
- BufferedOutputStream
- DataOutputStream
- PrintStream
- ObjectOutputStream
- PipedOutputStream
FileOutputStream、BufferedOutputStream 和 FileInputStream、BufferedInputStream 是相對的。
2.3、Reader——字符輸入流
Reader 是所有的輸入字符流的父類,它是一個抽象類。
常見的子類有:
- BufferedReader
- LineNumberReader
- CharArrayReader
- FilterReader
- PushbackReader
- InputStreamReader
- FileReader
- PipedReader
- StringReader
總結:
Reader 基本的三個讀方法(和字節流對應):
(1) public int read() throws IOException; 讀取一個字符,返回值為讀取的字符。
(2) public int read(char cbuf[]) throws IOException; 讀取一系列字符到數組 cbuf[]中,返回值為實際讀取的字符的數量。
(3) public abstract int read(char cbuf[],int off,int len) throws IOException; 讀取 len 個字符,從數組 cbuf[] 的下標 off 處開始存放,返回值為實際讀取的字符數量,該方法必須由子類實現。
2.4、Writer——字符輸出流
Writer 是所有的輸出字符流的父類,它是一個抽象類。
常見的子類有:
- BufferedWriter
- CharArrayWriter
- FilterWriter
- OutputStreamWriter
- FileWriter
- PipedWriter
- PrintWriter
- StringWriter
總結:
writer 的主要寫方法:
3、使用方法
3.1、FileOutputStream寫文件、FileInputStream讀文件
分別為 單個字節寫、字節數字寫、單個字節讀取、字節數組讀取、一次性讀取:
public class OutputStreamTest {public static void main(String[] args) throws IOException {writeFile(); //單個字節寫、字節數字寫readFile1();//單個字節讀取readFile2();//字節數組讀取readFile3();//一次性讀取} ?static void writeFile() throws IOException {//1、第一種方法寫,單個字節寫//會自動創建文件,目錄不存在會報錯, true 表示 追加寫,默認是falseFileOutputStream fileOutputStream = new FileOutputStream("F:\\hello.txt", false);//往文件里面一個字節一個字節的寫入數據fileOutputStream.write((int) 'H');fileOutputStream.write((int) 'a');fileOutputStream.write((int) 'C'); ?//2、第二種方法寫 字節數組寫String s = " HelloCoder";//入文件里面一個字節數組的寫入文件,文件為UTF_8格式fileOutputStream.write(s.getBytes(StandardCharsets.UTF_8));//刷新流fileOutputStream.flush();//關閉流fileOutputStream.close();} ?static void readFile1() throws IOException {//1、第一種讀的方法,但字節讀System.out.println("------一個字節讀------");//傳文件夾的名字來創建對象FileInputStream fileInputStream = new FileInputStream("F:\\hello.txt");int by = 0;//一個字節一個字節的讀出數據while ((by = fileInputStream.read()) != -1) {System.out.print((char) by);}//關閉流fileInputStream.close();} ?static void readFile2() throws IOException {//2、第二種讀的方法,字節數組讀System.out.println();System.out.println("------字節數組讀------");FileInputStream fileInputStream = new FileInputStream("F:\\hello.txt");//通過File對象來創建對象fileInputStream = new FileInputStream(new File("F:\\hello.txt"));int by = 0;byte[] bytes = new byte[10];//一個字節數組的讀出數據,高效while ((by = fileInputStream.read(bytes)) != -1) {for (int i = 0; i < by; i++) {System.out.print((char) bytes[i]);}}//關閉流fileInputStream.close();} ?static void readFile3() throws IOException {//3、第三種讀方法,一次性讀System.out.println();System.out.println("------一次性讀文件------");FileInputStream fileInputStream = new FileInputStream("F:\\hello.txt");fileInputStream = new FileInputStream(new File("F:\\hello.txt"));//一次性讀文件int iAvail = fileInputStream.available();int by = 0;byte[] bytesAll = new byte[iAvail];while ((by = fileInputStream.read(bytesAll)) != -1) {for (int i = 0; i < by; i++) {System.out.print((char) bytesAll[i]);}}fileInputStream.close();} }輸出:
------一個字節讀------ HaC HelloCoder ------字節數組讀------ HaC HelloCoder ------一次性讀文件------ HaC HelloCoder這里介紹了三種方法讀一個文件,詳細的介紹都寫在了注釋里。
?? 字符串如果包含中文,就會出現亂碼,這是因為FileOutputStream是字節流,將文本按字節寫入。3.2、FileWriter寫文件、FileReader讀文件
分別為 字符串寫、單字符讀、字符數組讀:
public class ReaderTest {public static void main(String[] args) throws IOException {write(); //字符串寫read1();//read2();//} ?static void write() throws IOException {FileWriter fileWriter = new FileWriter("F:\\Hello1.txt");//為防止亂碼,可以這樣寫,字符流和字節流互轉 // Writer fileWriter = new BufferedWriter(new OutputStreamWriter( // new FileOutputStream("F:\\Hello1.txt"), StandardCharsets.UTF_8));fileWriter.write("今天打工你不狠,明天地位就不穩\n" +"今天打工不勤快,明天社會就淘汰");// 如果沒有刷新,也沒有關閉流的話 數據是不會寫入文件的fileWriter.flush();fileWriter.close();} ?static void read1() throws IOException {System.out.println("------一個一個char讀-------");FileReader fileReader = new FileReader("F:\\Hello1.txt");int ch = 0;String str = "";//一個一個char讀while ((ch = fileReader.read()) != -1) {str += (char) ch;}System.out.println(str);} ?static void read2() throws IOException {System.out.println("------char數組[]讀-------");FileReader fileReader = new FileReader(new File("F:\\Hello1.txt"));int len = 0;char[] chars = new char[10];while ((len = fileReader.read(chars)) != -1) {//這種讀有誤 // System.out.print(new String(chars));System.out.print((new String(chars, 0, len)));}fileReader.close();} }輸出:
------一個一個char讀------- 今天打工你不狠,明天地位就不穩 今天打工不勤快,明天社會就淘汰 ------char數組[]讀------- 今天打工你不狠,明天地位就不穩 今天打工不勤快,明天社會就淘汰FileWriter、FileReader 可以用來讀寫一個含中文字符的文件。
注意點:
1、流轉換
// Writer fileWriter = new BufferedWriter(new OutputStreamWriter( ? // new FileOutputStream("F:\\Hello1.txt"), StandardCharsets.UTF_8));這里其實是把字節流轉換為字符流,用來解決亂碼。
2、讀的位置
這里的寫法需要注意,因為這里讀寫是一次性讀10個char類型的字符,如果換成以下
int len = 0; char[] chars = new char[10]; while ((len = fileReader.read(chars)) != -1) {//不能這樣寫System.out.print(new String(chars));//System.out.print((new String(chars, 0, len))); }則輸出:
------char數組[]讀------- 今天打工你不狠,明天地位就不穩 今天打工不勤快,明天社會就淘汰勤快,明天社會就淘可以看到輸出不正確,因為一次性讀10個char,
第一次讀的是 今天打工你不狠,明天
第二次讀的是 地位就不穩\n今天打工
第三次讀的是 不勤快,明天社會就淘
第四次輸出是 汰勤快,明天社會就淘 ,其實這一次它只讀了汰 一個字符,其中 勤快,明天社會就淘 是上一個數組的內容,因為它是已存在在數組的舊數據。
所以需要new String(chars, 0, len) ,len 是這次讀到的字符長度,只需要截取這次的字符即可。
以上這兩個例子中,還需要注意的幾個地方:
1、只有在寫文件的時候才需要flush()方法,而讀是不需要的。
2、讀、寫 完畢都需要調用close() 方法關閉流。
3、單個字節、字符讀寫效率較慢,建議使用字節、字符數組讀取。
3.3、BufferedInputStream、BufferedOutputStream 緩沖字節流
BufferedInputStream 是帶緩沖區的,在復制、移動文件操作會快一點。
建議使用緩沖字節流這不是普通字節流,但構造方法入參還是InputStream和OutputStream。Java使用IO 讀取文件時,會進入核心態,在調用驅動進行IO,本身就會緩存在系統級別的,當你第二次讀取時,會由用戶態進入核心態,讀取系統緩存。BufferedInputStream就一次性讀取較多,緩存起來。
這樣下次就從緩存中讀,而不用在用戶態和核心態之間切換,從而提升效率。
eg:
public class InputStrem與BufferenInputStream復制文件 {public static void main(String[] args) throws IOException {useInputStreamCopyFile(); //緩沖流復制文件useBufferenInputStream(); //普通流復制文件} ?static void useInputStreamCopyFile() throws IOException {File file = new File("F:\\楊超越.png");InputStream is = new FileInputStream(file); ?File file2 = new File("F:\\楊超越_copy.png");OutputStream os = new FileOutputStream(file2);int len = 0;byte[] bytes = new byte[1024];while ((len = is.read(bytes)) != -1) {os.write(bytes);}is.close();os.close();} ?static void useBufferenInputStream() throws IOException {BufferedInputStream bis = new BufferedInputStream(new FileInputStream("F:\\楊超越.png"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\楊超越_copy2.png"));int len = 0;byte[] bytes = new byte[1024];while ((len = bis.read(bytes)) != -1) {bos.write(bytes, 0, len);}bos.close();bis.close();} }結果輸出:
3.4、BufferedReader、BufferedWriter 字符緩沖流
BufferedReader 有一個好處,就是它提供了readline()、newLine()方法,可以按行讀取文件。
eg:
public class BufferedReaderTest {public static void main(String[] args) throws IOException {useInputStreamCopyFile(); //這種方法適用于任何文件//下面兩種方法copy的文件變大了,因為是使用字符流處理的useBufferedReaderCopyFile(); //這種方法只適用于字符文件useFileReaderCopyFile(); //這種方法一步到位,只適用于字符文件}static void useInputStreamCopyFile() throws IOException {File file = new File("F:\\Hello1.txt");InputStream is = new FileInputStream(file);File file2 = new File("F:\\Hello1_copy1.txt");OutputStream os = new FileOutputStream(file2);int len = 0;byte[] bytes = new byte[1024];while ((len = is.read(bytes)) != -1) {os.write(bytes, 0, len);}is.close();os.close();}static void useBufferedReaderCopyFile() throws IOException {File file = new File("F:\\Hello1.txt");InputStream is = new FileInputStream(file);Reader reader = new InputStreamReader(is);//創建字符流緩沖區,BufferedReader 的構造入參是一個 ReaderBufferedReader bufferedReader = new BufferedReader(reader);File file2 = new File("F:\\Hello1_copy2.txt");OutputStream os = new FileOutputStream(file2);Writer writer = new OutputStreamWriter(os);//創建字符流緩沖區,BufferedWriter 的構造入參是一個 WriterBufferedWriter bufferedWriter = new BufferedWriter(writer);String line = null;//readLine()方法 是根據\n 換行符讀取的while ((line = bufferedReader.readLine()) != null) {bufferedWriter.write(line);//這里要加換行bufferedWriter.newLine();}bufferedReader.close();bufferedWriter.close();}static void useFileReaderCopyFile() throws IOException {//使用FileReader、FileWriter 一步到位Reader reader = new FileReader("F:\\Hello1.txt");BufferedReader bufferedReader = new BufferedReader(reader);Writer writer = new FileWriter("F:\\Hello1_copy3.txt");BufferedWriter bufferedWriter = new BufferedWriter(writer);String line = null;while ((line = bufferedReader.readLine()) != null) {bufferedWriter.write(line);bufferedWriter.newLine();}bufferedReader.close();bufferedWriter.close();} }4、close() 與flush()
先上個例子:
public class FlushTest {public static void main(String[] args) throws IOException {FileReader fileReader = new FileReader("F:\\Hello1.txt"); //大文件FileWriter fileWriter = new FileWriter("F:\\Hello2.txt");int readerCount = 0;//一次讀取1024個字符char[] chars = new char[1024];while (-1 != (readerCount = fileReader.read(chars))) {fileWriter.write(chars, 0, readerCount);}} }這里并沒有調用close()方法。
close()方法包含flush()方法 ,即close會自動flush結果:
可以看到,復制的文件變小了。
明顯,數據有丟失,丟失的就是緩沖區“殘余”的數據。
在計算機層面,Java對磁盤進行操作,IO是有緩存的,并不是真正意義上的一邊讀一邊寫,底層的落盤(數據真正寫到磁盤)另有方法。
所以,最后會有一部分數據在內存中,如果不調用flush()方法,數據會隨著查詢結束而消失,這就是為什么數據丟失使得文件變小了。
BufferedOutputStream、BufferedFileWriter 同理再舉個例子:
class FlushTest2{public static void main(String[] args) throws IOException {FileWriter fileWriter = new FileWriter("F:\\Hello3.txt");fileWriter.write("今天打工你不狠,明天地位就不穩\n" +"今天打工不勤快,明天社會就淘汰");} }不調用flush()方法你會發現,文件是空白的,沒有把數據寫進來,也是因為數據在內存中而不是落盤到磁盤了。
所以為了實時性和安全性,IO在寫操作的時候,需要調用flush()或者close()
close() 和flush()的區別:
- 關close()是閉流對象,但是會先刷新一次緩沖區,關閉之后,流對象不可以繼續再使用了,否則報空指針異常。
- flush()僅僅是刷新緩沖區,準確的說是"強制寫出緩沖區的數據",流對象還可以繼續使用。
總結一下:
Java的IO有一個 緩沖區 的概念,不是Buffer概念的緩沖區。
如果是文件讀寫完的同時緩沖區剛好裝滿 , 那么緩沖區會把里面的數據朝目標文件自動進行讀或寫(這就是為什么總剩下有一點沒寫完) , 這種時候你不調用close()方法也0不會出現問題 ;
如果文件在讀寫完成時 , 緩沖區沒有裝滿,也沒有flush(), 這個時候裝在緩沖區的數據就不會自動的朝目標文件進行讀或寫 , 從而造成緩沖區中的這部分數據丟失 , 所以這個是時候就需要在close()之前先調用flush()方法 , 手動使緩沖區數據讀寫到目標文件。
舉個很形象的例子加深理解:
我從黃桶(讀)通過水泵(管道)把水抽到綠桶(寫),水管就相當于緩沖區,當我看到黃桶水沒有了,我立馬關了水泵,但發現水管里還有水沒有流到綠桶,這些殘留的水就相當于內存中丟失的數據。
如果此時我再把水泵打開,此時水管里面丟失的水(丟失的數據)又流到了綠桶,這就相當于調用了flush()方法。
5、總結
寫了這么多,IO確實是挺復雜的,一般的業務需求是讀寫文件,其實更多的是生成文件、復制文件、移動文件。所以如何選擇IO流,是需要我們掌握的。
1、字節流是原生的操作,字符流是經過處理后的操作。
輸入:Reader, InputStream類型的子類
輸出:Writer, OutputStream類型的子類
2、字節流一般用來處理圖像、視頻、音頻、PPT、Word等類型的文件。字符流一般用于處理純文本類型的文件,如TXT文件等,但不能處理圖像視頻等非文本文件。
用一句話說就是:字節流可以處理一切文件,而字符流只能處理純文本文件。含有漢子的文件就使用字符流處理。
3、需要轉換?是,使用轉換流;是否需要高效,使用緩沖流。
4、使用流之后一定要close()
總結
以上是生活随笔為你收集整理的Java 的 IO 很复杂?用思路带领你去battle他!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 真香!原来 CLI 开发可以这么简单
- 下一篇: ECMAScript 2021(ES12