“老王首妖,Java IO 也太上頭了吧?”新兵蛋子小二向頭頂很涼快的老王抱怨道爷恳,“你瞧有缆,我就按照傳輸方式對 IO 進行了一個簡單的分類,就能搞出來這么多的玩意!”
好久沒搞過 IO 了棚壁,老王看到這幅思維導(dǎo)圖也是吃了一驚杯矩。想想也是,他當初學(xué)習(xí) Java IO 的時候頭也大袖外,烏央烏央的一片史隆,全是類,估計是所有 Java 包里面類最多的曼验,一會是 Input 一會是 Output泌射,一會是 Reader 一會是 Writer,真不知道 Java 的設(shè)計者是怎么想的蚣驼。
看著肺都快要氣炸的小二魄幕,老王深深地吸了一口氣,耐心地對小二說:“主要是 Java 的設(shè)計者考慮得比較多吧颖杏,所以 IO 給人一種很亂的感覺纯陨,我來給你梳理一下×舸ⅲ”
01翼抠、傳輸方式劃分
就按照你的那副思維導(dǎo)圖來說吧。
傳輸方式有兩種获讳,字節(jié)和字符阴颖,那首先得搞明白字節(jié)和字符有什么區(qū)別,對吧丐膝?
字節(jié)(byte)是計算機中用來表示存儲容量的一個計量單位量愧,通常情況下,一個字節(jié)有 8 位(bit)帅矗。
字符(char)可以是計算機中使用的字母偎肃、數(shù)字、和符號浑此,比如說 A 1 $ 這些累颂。
通常來說,一個字母或者一個字符占用一個字節(jié)凛俱,一個漢字占用兩個字節(jié)紊馏。
具體還要看字符編碼,比如說在 UTF-8 編碼下蒲犬,一個英文字母(不分大小寫)為一個字節(jié)朱监,一個中文漢字為三個字節(jié);在 Unicode 編碼中原叮,一個英文字母為一個字節(jié)赌朋,一個中文漢字為兩個字節(jié)凰狞。
PS:關(guān)于字符編碼,可以看前面的章節(jié):錕斤拷
明白了字節(jié)與字符的區(qū)別沛慢,再來看字節(jié)流和字符流就會輕松多了赡若。
字節(jié)流用來處理二進制文件,比如說圖片啊团甲、MP3 啊逾冬、視頻啊。
字符流用來處理文本文件躺苦,文本文件可以看作是一種特殊的二進制文件身腻,只不過經(jīng)過了編碼,便于人們閱讀匹厘。
換句話說就是嘀趟,字節(jié)流可以處理一切文件,而字符流只能處理文本愈诚。
雖然 IO 類很多她按,但核心的就是 4 個抽象類:InputStream、OutputStream炕柔、Reader酌泰、Writer。
(抽象大法真好)
雖然 IO 類的方法也很多匕累,但核心的也就 2 個:read 和 write陵刹。
InputStream 類
-
int read()
:讀取數(shù)據(jù) -
int read(byte b[], int off, int len)
:從第 off 位置開始讀,讀取 len 長度的字節(jié)欢嘿,然后放入數(shù)組 b 中 -
long skip(long n)
:跳過指定個數(shù)的字節(jié) -
int available()
:返回可讀的字節(jié)數(shù) -
void close()
:關(guān)閉流衰琐,釋放資源
OutputStream 類
-
void write(int b)
: 寫入一個字節(jié),雖然參數(shù)是一個 int 類型炼蹦,但只有低 8 位才會寫入碘耳,高 24 位會舍棄(這塊后面再講) -
void write(byte b[], int off, int len)
: 將數(shù)組 b 中的從 off 位置開始,長度為 len 的字節(jié)寫入 -
void flush()
: 強制刷新框弛,將緩沖區(qū)的數(shù)據(jù)寫入 -
void close()
:關(guān)閉流
Reader 類
-
int read()
:讀取單個字符 -
int read(char cbuf[], int off, int len)
:從第 off 位置開始讀,讀取 len 長度的字符捕捂,然后放入數(shù)組 b 中 -
long skip(long n)
:跳過指定個數(shù)的字符 -
int ready()
:是否可以讀了 -
void close()
:關(guān)閉流瑟枫,釋放資源
Writer 類
-
void write(int c)
: 寫入一個字符 -
void write( char cbuf[], int off, int len)
: 將數(shù)組 cbuf 中的從 off 位置開始,長度為 len 的字符寫入 -
void flush()
: 強制刷新指攒,將緩沖區(qū)的數(shù)據(jù)寫入 -
void close()
:關(guān)閉流
理解了上面這些方法慷妙,基本上 IO 的靈魂也就全部掌握了。
二允悦、操作對象劃分
小二膝擂,你細想一下,IO IO,不就是輸入輸出(Input/Output)嘛:
- Input:將外部的數(shù)據(jù)讀入內(nèi)存架馋,比如說把文件從硬盤讀取到內(nèi)存狞山,從網(wǎng)絡(luò)讀取數(shù)據(jù)到內(nèi)存等等
- Output:將內(nèi)存中的數(shù)據(jù)寫入到外部,比如說把數(shù)據(jù)從內(nèi)存寫入到文件叉寂,把數(shù)據(jù)從內(nèi)存輸出到網(wǎng)絡(luò)等等萍启。
所有的程序,在執(zhí)行的時候屏鳍,都是在內(nèi)存上進行的勘纯,一旦關(guān)機,內(nèi)存中的數(shù)據(jù)就沒了钓瞭,那如果想要持久化驳遵,就需要把內(nèi)存中的數(shù)據(jù)輸出到外部,比如說文件山涡。
文件操作算是 IO 中最典型的操作了堤结,也是最頻繁的操作。那其實你可以換個角度來思考佳鳖,比如說按照 IO 的操作對象來思考霍殴,IO 就可以分類為:文件、數(shù)組系吩、管道来庭、基本數(shù)據(jù)類型、緩沖穿挨、打印月弛、對象序列化/反序列化,以及轉(zhuǎn)換等科盛。
1)文件
文件流也就是直接操作文件的流帽衙,可以細分為字節(jié)流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。
FileInputStream 的例子:
int b;
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循環(huán)讀取
while ((b = fis1.read())!=-1) {
System.out.println((char)b);
}
// 關(guān)閉資源
fis1.close();
FileOutputStream 的例子:
FileOutputStream fos = new FileOutputStream("fos.txt");
fos.write("沉默王二".getBytes());
fos.close();
FileReader 的例子:
int b = 0;
FileReader fileReader = new FileReader("read.txt");
// 循環(huán)讀取
while ((b = fileReader.read())!=-1) {
// 自動提升類型提升為 int 類型贞绵,所以用 char 強轉(zhuǎn)
System.out.println((char)b);
}
// 關(guān)閉流
fileReader.close();
FileWriter 的例子:
FileWriter fileWriter = new FileWriter("fw.txt");
char[] chars = "沉默王二".toCharArray();
fileWriter.write(chars, 0, chars.length);
fileWriter.close();
當掌握了文件的輸入輸出厉萝,其他的自然也就掌握了,都大差不差榨崩。
2)數(shù)組
通常來說谴垫,針對文件的讀寫操作,使用文件流配合緩沖流就夠用了母蛛,但為了提升效率翩剪,頻繁地讀寫文件并不是太好,那么就出現(xiàn)了數(shù)組流彩郊,有時候也稱為內(nèi)存流前弯。
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);
//獲取數(shù)據(jù)
byte[] dest =bos.toByteArray();
//釋放資源
bos.close();
3)管道
Java 中的管道和 Unix/Linux 中的管道不同蚪缀,在 Unix/Linux 中,不同的進程之間可以通過管道來通信恕出,但 Java 中询枚,通信的雙方必須在同一個進程中,也就是在同一個 JVM 中剃根,管道為線程之間的通信提供了通信能力哩盲。
一個線程通過 PipedOutputStream 寫入的數(shù)據(jù)可以被另外一個線程通過相關(guān)聯(lián)的 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)基本數(shù)據(jù)類型
基本數(shù)據(jù)類型輸入輸出流是一個字節(jié)流狈醉,該流不僅可以讀寫字節(jié)和字符廉油,還可以讀寫基本數(shù)據(jù)類型。
DataInputStream 提供了一系列可以讀基本數(shù)據(jù)類型的方法:
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 提供了一系列可以寫基本數(shù)據(jù)類型的方法:
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 很快苗傅,它比內(nèi)存快 100 倍抒线,比磁盤快百萬倍。那也就意味著渣慕,程序和內(nèi)存交互會很快嘶炭,和硬盤交互相對就很慢,這樣就會導(dǎo)致性能問題逊桦。
為了減少程序和硬盤的交互眨猎,提升程序的效率,就引入了緩沖流强经,也就是類名前綴帶有 Buffer 的那些睡陪,比如說 BufferedInputStream、BufferedOutputStream匿情、BufferedReader兰迫、BufferedWriter。
緩沖流在內(nèi)存中設(shè)置了一個緩沖區(qū)炬称,只有緩沖區(qū)存儲了足夠多的帶操作的數(shù)據(jù)后汁果,才會和內(nèi)存或者硬盤進行交互。簡單來說玲躯,就是一次多讀/寫點据德,少讀/寫幾次,這樣程序的性能就會提高跷车。
6)打印
恐怕 Java 程序員一生當中最常用的就是打印流了:System.out
其實返回的就是一個 PrintStream 對象棘利,可以用來打印各式各樣的對象。
System.out.println("沉默王二是真的二姓赤!");
PrintStream 最終輸出的是字節(jié)數(shù)據(jù),而 PrintWriter 則是擴展了 Writer 接口仲吏,所以它的 print()/println()
方法最終輸出的是字符數(shù)據(jù)不铆。使用上幾乎和 PrintStream 一模一樣蝌焚。
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("沉默王二");
}
System.out.println(buffer.toString());
7)對象序列化/反序列化
序列化本質(zhì)上是將一個 Java 對象轉(zhuǎn)成字節(jié)數(shù)組,然后可以將其保存到文件中誓斥,或者通過網(wǎng)絡(luò)傳輸?shù)竭h程只洒。
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeUTF("沉默王二");
}
System.out.println(Arrays.toString(buffer.toByteArray()));
與其對應(yīng)的,有序列化劳坑,就有反序列化毕谴,也就是再將字節(jié)數(shù)組轉(zhuǎn)成 Java 對象的過程。
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File("Person.txt")))) {
String s = input.readUTF();
}
8)轉(zhuǎn)換
InputStreamReader 是從字節(jié)流到字符流的橋連接距芬,它使用指定的字符集讀取字節(jié)并將它們解碼為字符涝开。
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 將一個字符流的輸出對象變?yōu)樽止?jié)流的輸出對象,是字符流通向字節(jié)流的橋梁框仔。
File f = new File("test.txt") ;
Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字節(jié)流變?yōu)樽址?
out.write("hello world!!") ; // 使用字符流輸出
out.close() ;
“小二啊舀武,你看,經(jīng)過我的梳理离斩,是不是感覺 IO 也沒多少東西银舱!針對不同的場景、不同的業(yè)務(wù)跛梗,選擇對應(yīng)的 IO 流就可以了寻馏,用法上就是讀和寫『顺ィ”老王一口氣講完這些诚欠,長長的舒了一口氣。
此時此刻的小二宪祥,還沉浸在老王的滔滔不絕中聂薪。不僅感覺老王的肺活量是真的大,還感慨老王不愧是工作了十多年的“老油條”蝗羊,一下子就把自己感覺頭大的 IO 給梳理得很清晰了藏澳。
這是《Java 程序員進階之路》專欄的第 68 篇。Java 程序員進階之路耀找,該專欄風(fēng)趣幽默翔悠、通俗易懂,對 Java 初學(xué)者極度友好和舒適??野芒,內(nèi)容包括但不限于 Java 語法蓄愁、Java 集合框架、Java IO狞悲、Java 并發(fā)編程撮抓、Java 虛擬機等核心知識點。
GitHub 地址:https://github.com/itwanger/toBeBetterJavaer
碼云地址:https://gitee.com/itwanger/toBeBetterJavaer
CodeChina 直達地址:https://codechina.csdn.net/qing_gee/toBeBetterJavaer
亮白版和暗黑版的 PDF 也準備好了呢摇锋,讓我們一起成為更好的 Java 工程師吧丹拯,一起沖站超!