“老王,Java IO 也太上頭了吧?”新兵蛋子小二向頭頂很涼快的老王抱怨道,“你瞧,我就按照傳輸方式對 IO 進行了一個簡單的分類,就能搞出來這麼多的玩意!”
好久沒搞過 IO 了,老王看到這幅思維導圖也是吃了一驚。想想也是,他當初學習 Java IO 的時候頭也大,烏央烏央的一片,全是類,估計是所有 Java 包裏面類最多的,一會是 Input 一會是 Output,一會是 Reader 一會是 Writer,真不知道 Java 的設計者是怎麼想的。
看着肺都快要氣炸的小二,老王深深地吸了一口氣,耐心地對小二說:“主要是 Java 的設計者考慮得比較多吧,所以 IO 給人一種很亂的感覺,我來給你梳理一下。”
01、傳輸方式劃分
就按照你的那副思維導圖來說吧。
傳輸方式有兩種,字節和字符,那首先得搞明白字節和字符有什麼區別,對吧?
字節(byte)是計算機中用來表示存儲容量的一個計量單位,通常情況下,一個字節有 8 位(bit)。
字符(char)可以是計算機中使用的字母、數字、和符號,比如說 A 1 $ 這些。
通常來說,一個字母或者一個字符佔用一個字節,一個漢字佔用兩個字節。
具體還要看字符編碼,比如說在 UTF-8 編碼下,一個英文字母(不分大小寫)爲一個字節,一箇中文漢字爲三個字節;在 Unicode 編碼中,一個英文字母爲一個字節,一箇中文漢字爲兩個字節。
PS:關於字符編碼,可以看前面的章節:錕斤拷
明白了字節與字符的區別,再來看字節流和字符流就會輕鬆多了。
字節流用來處理二進制文件,比如說圖片啊、MP3 啊、視頻啊。
字符流用來處理文本文件,文本文件可以看作是一種特殊的二進制文件,只不過經過了編碼,便於人們閱讀。
換句話說就是,字節流可以處理一切文件,而字符流只能處理文本。
雖然 IO 類很多,但核心的就是 4 個抽象類:InputStream、OutputStream、Reader、Writer。
(抽象大法真好)
雖然 IO 類的方法也很多,但核心的也就 2 個:read 和 write。
InputStream 類
-
int read()
:讀取數據 -
int read(byte b[], int off, int len)
:從第 off 位置開始讀,讀取 len 長度的字節,然後放入數組 b 中 -
long skip(long n)
:跳過指定個數的字節 -
int available()
:返回可讀的字節數 -
void close()
:關閉流,釋放資源
OutputStream 類
-
void write(int b)
: 寫入一個字節,雖然參數是一個 int 類型,但只有低 8 位纔會寫入,高 24 位會捨棄(這塊後面再講) -
void write(byte b[], int off, int len)
: 將數組 b 中的從 off 位置開始,長度爲 len 的字節寫入 -
void flush()
: 強制刷新,將緩衝區的數據寫入 -
void close()
:關閉流
Reader 類
-
int read()
:讀取單個字符 -
int read(char cbuf[], int off, int len)
:從第 off 位置開始讀,讀取 len 長度的字符,然後放入數組 b 中 -
long skip(long n)
:跳過指定個數的字符 -
int ready()
:是否可以讀了 -
void close()
:關閉流,釋放資源
Writer 類
-
void write(int c)
: 寫入一個字符 -
void write( char cbuf[], int off, int len)
: 將數組 cbuf 中的從 off 位置開始,長度爲 len 的字符寫入 -
void flush()
: 強制刷新,將緩衝區的數據寫入 -
void close()
:關閉流
理解了上面這些方法,基本上 IO 的靈魂也就全部掌握了。
二、操作對象劃分
小二,你細想一下,IO IO,不就是輸入輸出(Input/Output)嘛:
- Input:將外部的數據讀入內存,比如說把文件從硬盤讀取到內存,從網絡讀取數據到內存等等
- Output:將內存中的數據寫入到外部,比如說把數據從內存寫入到文件,把數據從內存輸出到網絡等等。
所有的程序,在執行的時候,都是在內存上進行的,一旦關機,內存中的數據就沒了,那如果想要持久化,就需要把內存中的數據輸出到外部,比如說文件。
文件操作算是 IO 中最典型的操作了,也是最頻繁的操作。那其實你可以換個角度來思考,比如說按照 IO 的操作對象來思考,IO 就可以分類爲:文件、數組、管道、基本數據類型、緩衝、打印、對象序列化/反序列化,以及轉換等。
1)文件
文件流也就是直接操作文件的流,可以細分爲字節流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。
FileInputStream 的例子:
int b;
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循環讀取
while ((b = fis1.read())!=-1) {
System.out.println((char)b);
}
// 關閉資源
fis1.close();
FileOutputStream 的例子:
FileOutputStream fos = new FileOutputStream("fos.txt");
fos.write("沉默王二".getBytes());
fos.close();
FileReader 的例子:
int b = 0;
FileReader fileReader = new FileReader("read.txt");
// 循環讀取
while ((b = fileReader.read())!=-1) {
// 自動提升類型提升爲 int 類型,所以用 char 強轉
System.out.println((char)b);
}
// 關閉流
fileReader.close();
FileWriter 的例子:
FileWriter fileWriter = new FileWriter("fw.txt");
char[] chars = "沉默王二".toCharArray();
fileWriter.write(chars, 0, chars.length);
fileWriter.close();
當掌握了文件的輸入輸出,其他的自然也就掌握了,都大差不差。
2)數組
通常來說,針對文件的讀寫操作,使用文件流配合緩衝流就夠用了,但爲了提升效率,頻繁地讀寫文件並不是太好,那麼就出現了數組流,有時候也稱爲內存流。
ByteArrayInputStream 的例子:
InputStream is =new BufferedInputStream(
new ByteArrayInputStream(
"沉默王二".getBytes(StandardCharsets.UTF_8)));
//操作
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=is.read(flush))){
System.out.println(new String(flush,0,len));
}
//釋放資源
is.close();
ByteArrayOutputStream 的例子:
ByteArrayOutputStream bos =new ByteArrayOutputStream();
byte[] info ="沉默王二".getBytes();
bos.write(info, 0, info.length);
//獲取數據
byte[] dest =bos.toByteArray();
//釋放資源
bos.close();
3)管道
Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的進程之間可以通過管道來通信,但 Java 中,通信的雙方必須在同一個進程中,也就是在同一個 JVM 中,管道爲線程之間的通信提供了通信能力。
一個線程通過 PipedOutputStream 寫入的數據可以被另外一個線程通過相關聯的 PipedInputStream 讀取出來。
final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8));
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=pipedInputStream.read(flush))){
System.out.println(new String(flush,0,len));
}
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
4)基本數據類型
基本數據類型輸入輸出流是一個字節流,該流不僅可以讀寫字節和字符,還可以讀寫基本數據類型。
DataInputStream 提供了一系列可以讀基本數據類型的方法:
DataInputStream dis = new DataInputStream(new FileInputStream(“das.txt”)) ;
byte b = dis.readByte() ;
short s = dis.readShort() ;
int i = dis.readInt();
long l = dis.readLong() ;
float f = dis.readFloat() ;
double d = dis.readDouble() ;
boolean bb = dis.readBoolean() ;
char ch = dis.readChar() ;
DataOutputStream 提供了一系列可以寫基本數據類型的方法:
DataOutputStream das = new DataOutputStream(new FileOutputStream(“das.txt”));
das.writeByte(10);
das.writeShort(100);
das.writeInt(1000);
das.writeLong(10000L);
das.writeFloat(12.34F);
das.writeDouble(12.56);
das.writeBoolean(true);
das.writeChar('A');
5)緩衝
CPU 很快,它比內存快 100 倍,比磁盤快百萬倍。那也就意味着,程序和內存交互會很快,和硬盤交互相對就很慢,這樣就會導致性能問題。
爲了減少程序和硬盤的交互,提升程序的效率,就引入了緩衝流,也就是類名前綴帶有 Buffer 的那些,比如說 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。
緩衝流在內存中設置了一個緩衝區,只有緩衝區存儲了足夠多的帶操作的數據後,纔會和內存或者硬盤進行交互。簡單來說,就是一次多讀/寫點,少讀/寫幾次,這樣程序的性能就會提高。
6)打印
恐怕 Java 程序員一生當中最常用的就是打印流了:System.out
其實返回的就是一個 PrintStream 對象,可以用來打印各式各樣的對象。
System.out.println("沉默王二是真的二!");
PrintStream 最終輸出的是字節數據,而 PrintWriter 則是擴展了 Writer 接口,所以它的 print()/println()
方法最終輸出的是字符數據。使用上幾乎和 PrintStream 一模一樣。
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("沉默王二");
}
System.out.println(buffer.toString());
7)對象序列化/反序列化
序列化本質上是將一個 Java 對象轉成字節數組,然後可以將其保存到文件中,或者通過網絡傳輸到遠程。
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeUTF("沉默王二");
}
System.out.println(Arrays.toString(buffer.toByteArray()));
與其對應的,有序列化,就有反序列化,也就是再將字節數組轉成 Java 對象的過程。
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File("Person.txt")))) {
String s = input.readUTF();
}
8)轉換
InputStreamReader 是從字節流到字符流的橋連接,它使用指定的字符集讀取字節並將它們解碼爲字符。
InputStreamReader isr = new InputStreamReader(
new FileInputStream("demo.txt"));
char []cha = new char[1024];
int len = isr.read(cha);
System.out.println(new String(cha,0,len));
isr.close();
OutputStreamWriter 將一個字符流的輸出對象變爲字節流的輸出對象,是字符流通向字節流的橋樑。
File f = new File("test.txt") ;
Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字節流變爲字符流
out.write("hello world!!") ; // 使用字符流輸出
out.close() ;
“小二啊,你看,經過我的梳理,是不是感覺 IO 也沒多少東西!針對不同的場景、不同的業務,選擇對應的 IO 流就可以了,用法上就是讀和寫。”老王一口氣講完這些,長長的舒了一口氣。
此時此刻的小二,還沉浸在老王的滔滔不絕中。不僅感覺老王的肺活量是真的大,還感慨老王不愧是工作了十多年的“老油條”,一下子就把自己感覺頭大的 IO 給梳理得很清晰了。
這是《Java 程序員進階之路》專欄的第 68 篇。Java 程序員進階之路,該專欄風趣幽默、通俗易懂,對 Java 初學者極度友好和舒適😘,內容包括但不限於 Java 語法、Java 集合框架、Java IO、Java 併發編程、Java 虛擬機等核心知識點。
GitHub 地址:https://github.com/itwanger/toBeBetterJavaer
碼雲地址:https://gitee.com/itwanger/toBeBetterJavaer
CodeChina 直達地址:https://codechina.csdn.net/qing_gee/toBeBetterJavaer
亮白版和暗黑版的 PDF 也準備好了呢,讓我們一起成爲更好的 Java 工程師吧,一起衝!