瞧瞧,瞧瞧,Java IO 也太上頭了吧!

“老王,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 工程師吧,一起衝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章