1、IO體系
Java IO 體系看起來類很多,感覺很複雜,但其實是 IO 涉及的因素太多了。在設計 IO 相關的類時,編寫者也不是從同一個方面考慮的,所以會給人一種很亂的感覺,並且還有設計模式的使用,更加難以使用這些 IO 類,所以特地對 Java 的 IO 做一個總結。
IO 類設計出來,肯定是為了解決 IO 相關的操作的,想一想哪裡會有 IO 操作?網路、磁碟。網路操作相關的類是在 java.net 包下,不在本文的總結範圍內。提到磁碟,你可能會想到檔案,檔案操作在 IO 中是比較典型的操作。在 Java 中引入了 “流” 的概念,它表示任何有能力產生資料來源或有能力接收資料來源的物件。資料來源可以想象成水源,海水、河水、湖水、一杯水等等。資料傳輸可以想象為水的運輸,古代有用桶運水,用竹管運水的,現在有鋼管運水,不同的運輸方式對應不同的運輸特性。
從資料來源或者說是操作物件角度看,IO 類可以分為:
- 1、檔案(file):FileInputStream、FileOutputStream、FileReader、FileWriter
- 2、陣列([]):
- 2.1、位元組陣列(byte[]):ByteArrayInputStream、ByteArrayOutputStream
- 2.2、字元陣列(char[]):CharArrayReader、CharArrayWriter
- 3、管道操作:PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
- 4、基本資料型別:DataInputStream、DataOutputStream
- 5、緩衝操作:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
- 6、列印:PrintStream、PrintWriter
- 7、物件序列化反序列化:ObjectInputStream、ObjectOutputStream
- 8、轉換:InputStreamReader、OutputStreWriter
- 9、
字串(String)Java8中已廢棄:StringBufferInputStream、StringBufferOutputStream、StringReader、StringWriter
資料來源節點也可以再進行二次處理,使資料更加容易使用,所以還可以劃分成節點流和處理流,這裡涉及到設計模式,後面會有專門的文章說。
從資料傳輸方式或者說是運輸方式角度看,可以將 IO 類分為:- 1、位元組流
- 2、字元流
位元組流是以一個位元組單位來運輸的,比如一杯一杯的取水。而字元流是以多個位元組來運輸的,比如一桶一桶的取水,一桶水又可以分為幾杯水。
位元組流和字元流的區別:
位元組流讀取單個位元組,字元流讀取單個字元(一個字元根據編碼的不同,對應的位元組也不同,如 UTF-8 編碼是 3 個位元組,中文編碼是 2 個位元組。)位元組流用來處理二進位制檔案(圖片、MP3、視訊檔案),字元流用來處理文字檔案(可以看做是特殊的二進位制檔案,使用了某種編碼,人可以閱讀)。簡而言之,位元組是個計算機看的,字元才是給人看的。
位元組流和字元流的劃分可以看下面這張圖。
不可否認,Java IO 相關的類確實很多,但我們並不是所有的類都會用到,我們常用的也就是檔案相關的幾個類,如檔案最基本的讀寫類 File 開頭的、檔案讀寫帶緩衝區的類 Buffered 開頭的類,物件序列化反序列化相關的類 Object 開頭的類。
2、IO類和相關方法
IO 類雖然很多,但最基本的是 4 個抽象類:InputStream、OutputStream、Reader、Writer。最基本的方法也就是一個讀 read() 方法、一個寫 write() 方法。方法具體的實現還是要看繼承這 4 個抽象類的子類,畢竟我們平時使用的也是子類物件。這些類中的一些方法都是(Native)本地方法、所以並沒有 Java 原始碼,這裡給出筆者覺得不錯的 Java IO 原始碼分析 傳送門,按照上面這個思路看,先看子類基本方法,然後在看看子類中還新增了那些方法,相信你也可以看懂的,我這裡就只對上後面說的常用的類進行總結。
先來看 InputStream 和 OutStream 中的方法簡介,因為都是抽象類、大都是抽象方法、所以就不貼原始碼嘍!注意這裡的讀取和寫入,其實就是獲取(輸入)資料和輸出資料。
InputStream 類
方法 | 方法介紹 |
---|---|
public abstract int read() | 讀取資料 |
public int read(byte b[]) | 將讀取到的資料放在 byte 陣列中,該方法實際上是根據下面的方法實現的,off 為 0,len 為陣列的長度 |
public int read(byte b[], int off, int len) | 從第 off 位置讀取 len 長度位元組的資料放到 byte 陣列中,流是以 -1 來判斷是否讀取結束的(注意這裡讀取的雖然是一個位元組,但是返回的卻是 int 型別 4 個位元組,這裡當然是有原因,這裡就不再細說了,推薦這篇文章,連結) |
public long skip(long n) | 跳過指定個數的位元組不讀取,想想看電影跳過片頭片尾 |
public int available() | 返回可讀的位元組數量 |
public void close() | 讀取完,關閉流,釋放資源 |
public synchronized void mark(int readlimit) | 標記讀取位置,下次還可以從這裡開始讀取,使用前要看當前流是否支援,可以使用 markSupport() 方法判斷 |
public synchronized void reset() | 重置讀取位置為上次 mark 標記的位置 |
public boolean markSupported() | 判斷當前流是否支援標記流,和上面兩個方法配套使用 |
OutputStream 類
方法 | 方法介紹 |
---|---|
public abstract void write(int b) | 寫入一個位元組,可以看到這裡的引數是一個 int 型別,對應上面的讀方法,int 型別的 32 位,只有低 8 位才寫入,高 24 位將捨棄。 |
public void write(byte b[]) | 將陣列中的所有位元組寫入,和上面對應的 read() 方法類似,實際呼叫的也是下面的方法。 |
public void write(byte b[], int off, int len) | 將 byte 陣列從 off 位置開始,len 長度的位元組寫入 |
public void flush() | 強制重新整理,將緩衝中的資料寫入 |
public void close() | 關閉輸出流,流被關閉後就不能再輸出資料了 |
再來看 Reader 和 Writer 類中的方法,你會發現和上面兩個抽象基類中的方法很像。
Reader 類
方法 | 方法介紹 |
---|---|
public int read(java.nio.CharBuffer target) | 讀取位元組到字元快取中 |
public int read() | 讀取單個字元 |
public int read(char cbuf[]) | 讀取字元到指定的 char 陣列中 |
abstract public int read(char cbuf[], int off, int len) | 從 off 位置讀取 len 長度的字元到 char 陣列中 |
public long skip(long n) | 跳過指定長度的字元數量 |
public boolean ready() | 和上面的 available() 方法類似 |
public boolean markSupported() | 判斷當前流是否支援標記流 |
public void mark(int readAheadLimit) | 標記讀取位置,下次還可以從這裡開始讀取,使用前要看當前流是否支援,可以使用 markSupport() 方法判斷 |
public void reset() | 重置讀取位置為上次 mark 標記的位置 |
abstract public void close() | 關閉流釋放相關資源 |
Writer 類
方法 | 方法介紹 |
---|---|
public void write(int c) | 寫入一個字元 |
public void write(char cbuf[]) | 寫入一個字元陣列 |
abstract public void write(char cbuf[], int off, int len) | 從字元陣列的 off 位置寫入 len 數量的字元 |
public void write(String str) | 寫入一個字串 |
public void write(String str, int off, int len) | 從字串的 off 位置寫入 len 數量的字元 |
public Writer append(CharSequence csq) | 追加吸入一個字元序列 |
public Writer append(CharSequence csq, int start, int end) | 追加寫入一個字元序列的一部分,從 start 位置開始,end 位置結束 |
public Writer append(char c) | 追加寫入一個 16 位的字元 |
abstract public void flush() | 強制重新整理,將緩衝中的資料寫入 |
abstract public void close() | 關閉輸出流,流被關閉後就不能再輸出資料了 |
下面我們就直接使用他們的子類,在使用中再介紹下面沒有的新方法。
1、讀取控制檯中的輸入
import java.io.*;
public class IOTest {
public static void main(String[] args) throws IOException {
// 三個測試方法
// test01();
// test02();
test03();
}
public static void test01() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("請輸入一個字元");
char c;
c = (char) bufferedReader.read();
System.out.println("你輸入的字元為"+c);
}
public static void test02() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("請輸入一個字元,按 q 鍵結束");
char c;
do {
c = (char) bufferedReader.read();
System.out.println("你輸入的字元為"+c);
} while (c != 'q');
}
public static void test03() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("請輸入一行字元");
String str = bufferedReader.readLine();
System.out.println("你輸入的字元為" + str);
}
}
複製程式碼
至於控制檯的輸出,我們其實一直都在使用呢,System.out.println()
,out 其實是 PrintStream 類物件的引用,PrintStream 類中當然也有 write() 方法,但是我們更常用 print() 方法和 println() 方法,因為這兩個方法可以輸出的內容種類更多,比如一個列印一個物件,實際呼叫的物件的 toString() 方法。
2、二進位制檔案的寫入和讀取
注意這裡檔案的路徑,可以根據自己情況改一下,雖然這裡的檔案字尾是txt,但該檔案卻是一個二進位制檔案,並不能直接檢視。
@Test
public void test04() throws IOException {
byte[] bytes = {12,21,34,11,21};
FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test.txt");
// 寫入二進位制檔案,直接開啟會出現亂碼
fileOutputStream.write(bytes);
fileOutputStream.close();
}
@Test
public void test05() throws IOException {
FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test.txt");
int c;
// 讀取寫入的二進位制檔案,輸出位元組陣列
while ((c = fileInputStream.read()) != -1) {
System.out.print(c);
}
}
複製程式碼
3、文字檔案的寫入和讀取
write() 方法和 append() 方法並不是像方法名那樣,一個是覆蓋內容,一個是追加內容,append() 內部也是 write() 方法實現的,也非說區別,也就是 append() 方法可以直接寫 null,而 write() 方法需要把 null 當成一個字串寫入,所以兩者並無本質的區別。需要注意的是這裡並沒有指定檔案編碼,可能會出現亂碼的問題。
@Test
public void test06() throws IOException {
FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt");
fileWriter.write("Hello,world!\n歡迎來到 java 世界\n");
fileWriter.write("不會覆蓋檔案原本的內容\n");
// fileWriter.write(null); 不能直接寫入 null
fileWriter.append("並不是追加一行內容,不要被方法名迷惑\n");
fileWriter.append(null);
fileWriter.flush();
System.out.println("檔案的預設編碼為" + fileWriter.getEncoding());
fileWriter.close();
}
@Test
public void test07() throws IOException {
FileWriter fileWriter = new FileWriter(new File("").getAbsolutePath()+"/io/test.txt", false); // 關閉追加模式,變為覆蓋模式
fileWriter.write("Hello,world!歡迎來到 java 世界\n");
fileWriter.write("我來覆蓋檔案原本的內容");
fileWriter.append("我是下一行");
fileWriter.flush();
System.out.println("檔案的預設編碼為" + fileWriter.getEncoding());
fileWriter.close();
}
@Test
public void test08() throws IOException {
FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
fileReader.close();
bufferedReader.close();
}
@Test
public void test09() throws IOException {
FileReader fileReader = new FileReader(new File("").getAbsolutePath()+"/io/test.txt");
int c;
while ((c = fileReader.read()) != -1) {
System.out.print((char) c);
}
}
複製程式碼
使用位元組流和字元流的轉換類 InputStreamReader 和 OutputStreamWriter 可以指定檔案的編碼,使用 Buffer 相關的類來讀取檔案的每一行。
@Test
public void test10() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("").getAbsolutePath()+"/io/test2.txt");
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "GBK"); // 使用 GBK 編碼檔案
outputStreamWriter.write("Hello,world!\n歡迎來到 java 世界\n");
outputStreamWriter.append("另外一行內容");
outputStreamWriter.flush();
System.out.println("檔案的編碼為" + outputStreamWriter.getEncoding());
outputStreamWriter.close();
fileOutputStream.close();
}
@Test
public void test11() throws IOException {
FileInputStream fileInputStream = new FileInputStream(new File("").getAbsolutePath()+"/io/test2.txt");
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK"); // 使用 GBK 解碼檔案
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
bufferedReader.close();
inputStreamReader.close();
}
複製程式碼
4、複製檔案
這裡筆者做了一些測試,不使用緩衝對檔案複製時間的影響,檔案的複製實質還是檔案的讀寫。緩衝流是處理流,是對節點流的裝飾。
注:這裡的時間是在我這臺華碩筆記本上測試得到的,只是為了說明使用緩衝對檔案的讀寫有好處。
@Test
public void test12() throws IOException {
// 輸入和輸出都使用緩衝流
FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
BufferedInputStream inBuffer = new BufferedInputStream(in);
FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
BufferedOutputStream outBuffer = new BufferedOutputStream(out);
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = inBuffer.read(bs)) != -1) {
outBuffer.write(bs, 0, len);
}
System.out.println("複製檔案所需的時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 200 多毫秒
inBuffer.close();
in.close();
outBuffer.close();
out.close();
}
@Test
public void test13() throws IOException {
// 只有輸入使用緩衝流
FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
BufferedInputStream inBuffer = new BufferedInputStream(in);
FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = inBuffer.read(bs)) != -1) {
out.write(bs, 0, len);
}
System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 500 多毫秒
inBuffer.close();
in.close();
out.close();
}
@Test
public void test14() throws IOException {
// 輸入和輸出都不使用緩衝流
FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
int len = 0;
byte[] bs = new byte[1024];
long begin = System.currentTimeMillis();
while ((len = in.read(bs)) != -1) {
out.write(bs, 0, len);
}
System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間 700 多毫秒
in.close();
out.close();
}
@Test
public void test15() throws IOException {
// 不使用緩衝
FileInputStream in = new FileInputStream("E:\\視訊資料\\大資料原理與應用\\1.1大資料時代.mp4");
FileOutputStream out = new FileOutputStream("1.1大資料時代.mp4");
int len = 0;
long begin = System.currentTimeMillis();
while ((len = in.read()) != -1) {
out.write(len);
}
System.out.println("複製檔案所需時間:" + (System.currentTimeMillis() - begin)); // 平均時間約 160000 毫秒,約 2 分多鐘
in.close();
out.close();
}
複製程式碼
關於序列化和反序列化的內容,這裡給出我之前寫的部落格,傳送門。 總結:Java IO 類很多,但是把握住整個體系,掌握關鍵的方法,學習起來就會輕鬆很多,看完這篇文章,你是否覺得 Java IO 並沒有你想的那麼難呢?歡迎你在下方留言,和我們討論。
歡迎關注下方的微信公眾號哦,另外還有各種學習資料免費分享!