Java中IO的內(nèi)容非常豐富泣崩,相信第一次學習的時候耻矮,所有人都會被一大堆API繞暈集侯,今天我們就來系統(tǒng)的總結(jié)一下.
Java的IO體系主要包括流式部分(各種IO流的操作)及非流式部分(文件的實體)砌函。
IO流
Java中的IO流實際上用起來都非常方便簡單,為我們封裝了大量實用性的接口端姚,同時采用了裝飾器設(shè)計模式晕粪,所以看起來很龐大,但還是比較清晰的寄锐,總覽見下圖(圖片來源于網(wǎng)絡(luò))
可見分為字符流和字節(jié)流兩大部分兵多,每部分又有輸入輸出之分,首先先了解一下字符流和字節(jié)流的區(qū)別:
字符流處理的單元為2個字節(jié)的Unicode字符橄仆,主要是操作字符剩膘、字符數(shù)組或字符串,而字節(jié)流處理單元為1個字節(jié)盆顾,操作字節(jié)和字節(jié)數(shù)組怠褐。所以字符流是由Java虛擬機將字節(jié)轉(zhuǎn)化為2個字節(jié)的Unicode字符為單位的字符而成的。一般而言您宪,音頻文件奈懒、圖片、歌曲之類的使用字節(jié)流宪巨,如果是涉及到中文(文本)的磷杏,用字符流好點
1.字節(jié)流
1.1 FileInputStream與FileOutputStream
這兩個類是專門操作文件的。這兩個類是直接繼承于裝飾器模式的頂層類:InputStream和OutputStream捏卓。是字節(jié)流體系中文件操作的核心极祸。由于是裝飾器模式,所以他們的功能也最簡單怠晴。
單從方法上看遥金,他們都只有寥寥幾個方法,主要是讀和寫(詳細APi可以參考官方文檔蒜田,標題的超鏈接就是每個類的文檔稿械,這里不復(fù)述了,每各類只記錄關(guān)鍵用法冲粤,下面也一樣)下面我們舉一個拷貝文件的例子:
try (FileInputStream inputStream = new FileInputStream(new File("file/test.png"));
FileOutputStream outputStream = new FileOutputStream(new File("file/copy.png"))){
byte[] b = new byte[1024];
int len = 0;
while ((len = inputStream.read(b))!=-1){
outputStream.write(b,0,len);
}
} catch (Exception e) {
e.printStackTrace();
}
這算是IO操作的標準寫法了美莫。上面代碼中應(yīng)用了try-with-resources語法页眯,這時Java1.7的新特性。主要是為了幫我們在IO操作或與之類似的操作中厢呵,擺脫無盡的try-catch語句餐茵,想一想,之前若沒有這樣寫法述吸,上面代碼應(yīng)該是這樣的:
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream(new File("file/test.png"));
outputStream = new FileOutputStream(new File("file/copy.png"));
byte[] b = new byte[1024];
int len = 0;
while ((len = inputStream.read(b))!=-1){
outputStream.write(b,0,len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream!=null)
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream!=null)
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try-with-resources語法只是自動幫我們調(diào)用了close方法忿族,不僅僅是用在流上,所有實現(xiàn)了AutoCloseable接口的類都可以蝌矛。而AutoCloseable只有一個close方法道批。下面的實例我都會采用這種語法。
最后還有比較重要的一點入撒,在FileOutputStream中隆豹,默認對文件操作是覆蓋形式的,也就是打開一個文件后不管寫不寫東西都會講文件內(nèi)容清空茅逮,若想追加的形式操作璃赡,可利用他的兩參數(shù)構(gòu)造,第二個布爾型參數(shù)傳入true即可献雅。后面類中對文件操作需要傳入FileOutputStream參數(shù)時碉考,也是一樣。另外字符流也有類似操作挺身,可參看API侯谁,下面就不多說了。
1.2 ObjectInputStream與ObjectOutputStream
這兩個類用的也很多章钾,用于存儲對象墙贱。曾經(jīng)有過一次開發(fā)經(jīng)驗,就是從一個xml文件中解析內(nèi)容贱傀,如果解析完之后序列化存儲到文件中惨撇,下次再反序列化時用的時間遠遠少于從xml文件中直接解析。
接下來我們就來認識一下他們府寒,以O(shè)bjectInputStream為例(ObjectOutputStream也一樣魁衙,對應(yīng)讀方法都有相應(yīng)的寫方法),他除了有基本的read方法椰棘,還有許多如readInt纺棺,readBoolean等直接讀出對應(yīng)類型的方法榄笙,另外還有一個很重要的readObject()方法邪狞,這個是通用的,可以讀一切對象茅撞,下面簡單舉一個例子:
public static void write(){
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("file/obj")))){
out.writeObject(list);
}catch (Exception e){
e.printStackTrace();
}
}
public static void read(){
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("file/obj")))){
ArrayList<String> list = (ArrayList<String>) in.readObject();
System.out.println(list.get(1));
}catch (Exception e){
e.printStackTrace();
}
}
這里有兩個問題需要額外關(guān)注一下:
1.寫的時候帆卓,所有對象都必須可序列化巨朦,如集合內(nèi)的所有對象。序列化知識參考這里
2.關(guān)于如何判斷讀完的問題剑令。首先如果不加限制的讀糊啡,一個文件中所有對象都讀完后,再讀是不會讀到null之類的吁津,而是直接拋出java.io.EOFException異常棚蓄,所以我們要加以控制。第一個辦法可以在寫的時候最后添加一個空對象碍脏,每讀一個對象判斷一次梭依,讀到null的時候不再讀了。第二個辦法是將所有要存東西添加到集合中典尾,讀的時候永遠只讀一次役拴。最后一個辦法是利用異常判斷,捕獲到異常后在finally代碼塊中處理钾埂,不過這種方法影響性能河闰,不提倡。
1.3 DataInputStream與DataOutputStream
這一對更像是上面的簡化版褥紫,他只能讀寫基本數(shù)據(jù)類型姜性,示例:
public static void write(){
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(new File("file/obj")))){
for (String str : list)
out.writeUTF(str);
}catch (Exception e){
e.printStackTrace();
}
}
public static void read(){
try (DataInputStream in = new DataInputStream(new FileInputStream(new File("file/obj")))){
System.out.println(in.readUTF());
System.out.println(in.readUTF());
System.out.println(in.readUTF());
System.out.println(in.readUTF());
}catch (Exception e){
e.printStackTrace();
}
}
由于是讀寫基本數(shù)據(jù)類型,所以不用擔心序列化問題髓考,但是還是要處理EOFException異常污抬,和上面類似,只不過不能寫集合了绳军,可以在第一個位置寫入對象數(shù)印机,后面根據(jù)數(shù)量取對象∶偶荩或者寫入特殊標志位也可以射赛。
1.4 ByteArrayInputStream與ByteArrayOutputStream
這兩個類的共同點都是有一個字節(jié)數(shù)組作為緩沖區(qū)。
先看ByteArrayInputStream奶是,該類主要是從一個字節(jié)數(shù)組中讀內(nèi)容楣责,他的兩個構(gòu)造都需要傳入字節(jié)數(shù)組,示例
try(ByteArrayInputStream bi = new ByteArrayInputStream("adsd".getBytes())){
System.out.println((char)bi.read());
System.out.println(bi.available());
}catch (Exception e){
e.printStackTrace();
}
再看ByteArrayOutputStream聂沙,它有兩個構(gòu)造方法:
public ByteArrayOutputStream()
public ByteArrayOutputStream(int size)
第一個默認創(chuàng)建長度為32的數(shù)組秆麸,第二個指定數(shù)組大小。該類所有寫方法及汉,都是把內(nèi)容寫到字節(jié)緩沖區(qū)中沮趣,最后可以調(diào)用toString或者toByteArray()取出,還可以查詢寫入的長度坷随。示例:
try(ByteArrayOutputStream bo = new ByteArrayOutputStream()){
bo.write("adsd".getBytes());
System.out.println(bo.toString());
System.out.println(bo.size());
}catch (Exception e){
e.printStackTrace();
}
這兩個類可以和FileInputStream與FileOutputStream 類比房铭,F(xiàn)ileInputStream與FileOutputStream 是讀寫文件驻龟,ByteArrayInputStream和ByteArrayOutputStream則是讀寫內(nèi)存,也就是內(nèi)存中的字節(jié)數(shù)組缸匪,可以把字節(jié)數(shù)組看做虛擬文件翁狐。
1.5 BufferedInputStream與BufferedOutputStream
我們在學習FileInputStream與FileOutputStream時就考慮到了,如果一字節(jié)一字節(jié)的從硬盤中讀寫凌蔬,由于每次都需要啟動硬盤尋找數(shù)據(jù)露懒,效率很低,所以我們設(shè)置了一個數(shù)組作為緩沖砂心。而這兩個類自帶緩沖隐锭,默認大小為8192,可以自己設(shè)定计贰。每次讀的時候他會一次讀滿緩沖钦睡,然后再從緩沖中取數(shù)據(jù),寫的時候也類似躁倒。
基本用法和FileInputStream與FileOutputStream類似荞怒,我們這里對二者做一個比較,看緩沖有沒有用:
long start = System.currentTimeMillis();
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("file/test.png"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("file/copy.png"))){
int a;
do {
a = in.read();
if (a!=-1)
out.write(a);
else
break;
}while(true);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()-start);
Runtime.getRuntime().gc();
start = System.currentTimeMillis();
try (FileInputStream in = new FileInputStream("file/test1.png");
FileOutputStream out = new FileOutputStream("file/copy1.png")){
int a;
do {
a = in.read();
if (a!=-1)
out.write(a);
else
break;
}while(true);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()-start);
Runtime.getRuntime().gc();
start = System.currentTimeMillis();
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("file/test2.png"));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("file/copy2.png"))){
byte[] b = new byte[8192];
int len = 0;
while ((len = in.read(b))!=-1){
out.write(b,0,len);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()-start);
第一次用的是帶緩沖的秧秉,但是每次只讀一個字節(jié)和寫一個字節(jié)褐桌,第二次是用不帶緩沖的,也是每次只讀一個字節(jié)和寫一個字節(jié)象迎,第三次使用的帶緩沖的荧嵌,但是每次讀寫為一整個數(shù)組,運行時間如下:
61
3840
5
可見差距還是很大的砾淌,首先說明了緩沖的確有作用啦撮,另外即使有緩沖,也不建議一字節(jié)一字節(jié)的操作汪厨,還是要借助數(shù)據(jù)成塊讀寫赃春。
1.6 SequenceInputStream
這個類的作用是合并多個輸入流,它有兩個構(gòu)造函數(shù)劫乱,若是合并兩個流织中,直接用雙參數(shù)構(gòu)造,若是多余兩個衷戈。需要以Enumeration實例的形式傳入狭吼。不多說,看代碼:
try (FileInputStream in = new FileInputStream("file/test.txt");
FileInputStream in1 = new FileInputStream("file/test1.txt");
FileInputStream in2 = new FileInputStream("file/test2.txt");
FileOutputStream out = new FileOutputStream("file/out.txt")){
Vector<InputStream> vector = new Vector<>();
vector.add(in);
vector.add(in1);
vector.add(in2);
SequenceInputStream sis = new SequenceInputStream(vector.elements());
byte[] b = new byte[1024];
int len;
while ((len = sis.read(b))!=-1){
out.write(b,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
有一點需要注意殖妇,雖然是合并刁笙,但輸出是還是有順序的,就是添加的順序。
1.7 PushbackInputStream
這個類有個緩存采盒,可以將從流中讀取到的數(shù)據(jù),回退到流中蔚润。為什么特意說有何緩存磅氨,其實回退操作是借助與緩存實現(xiàn)的,并不是流中真的有回退數(shù)據(jù)嫡纠。它有兩個構(gòu)造烦租,除了都要穿輸入流外,一個可以指定緩存的大小除盏,也就是最大回退的大小叉橱,另外一個默認為1,示例
try (PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream("asdfertghuji".getBytes()),7)){
byte[] buffer = new byte[7];
pis.read(buffer);
System.out.println(new String(buffer));
pis.unread(buffer,0,4);
pis.read(buffer);
System.out.println(new String(buffer));
}catch (Exception e){
e.printStackTrace();
}
輸出
asdfert
asdfghu
簡單解釋一下者蠕,第一次讀了7個字節(jié)窃祝,asdfert,然后回退4個字節(jié)踱侣,也就是上次讀的前四個粪小,回退的邏輯上放在流的前方,下一次在讀7字節(jié)抡句,首先讀到的是回退的asdf探膊,然后從流中在讀3字節(jié)ghu,組成asdfghu待榔。
除了回退已讀到的逞壁,還可以回退任意數(shù)據(jù)
try (PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream("asdfertghuji".getBytes()),7)){
byte[] buffer = new byte[7];
pis.read(buffer);
System.out.println(new String(buffer));
pis.unread("1234".getBytes(),0,4);
pis.read(buffer);
System.out.println(new String(buffer));
}catch (Exception e){
e.printStackTrace();
}
但是回退的長度不能超過我們在構(gòu)造中指定的長度,因為回退是基于緩存數(shù)組的锐锣,放不下就會拋異常java.io.IOException: Push back buffer is full
1.8 PipedInputStream與PipedOutputStream
這兩個類比較特殊腌闯,他們是溝通兩個線程用的,根據(jù)名字很好理解雕憔,在兩個線程中架設(shè)一個管道绑嘹。這兩個類必須成對使用,因為要在兩個線程間傳輸?shù)那疤崾荘ipedInputStream與PipedOutputStream建立連接橘茉,可以利用構(gòu)造或者connect方法工腋。其實在線程中傳輸也是利用緩存實現(xiàn)的,默認大小是1024畅卓,可以指定擅腰。簡單示例
public class Receiver extends Thread{
private PipedInputStream in = new PipedInputStream();
public PipedInputStream getIn(){
return in;
}
@Override
public void run() {
super.run();
try {
System.out.println("receiver start");
System.out.println((char)in.read());
System.out.println("receiver end");
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Sender extends Thread{
private PipedOutputStream out = new PipedOutputStream();
public PipedOutputStream getOut(){
return out;
}
@Override
public void run() {
super.run();
try {
System.out.println("sender start");
out.write('d');
System.out.println("sender end");
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
Receiver receiver = new Receiver();
Sender sender = new Sender();
receiver.getIn().connect(sender.getOut());
receiver.start();
sender.start();
}
不必擔心說先啟動Receiver 會收不到消息,首先兩個線程是并行的翁潘,其次read方法也是有阻塞的趁冈,除非PipedOutputStream 關(guān)閉了,否則會一直等待另一端寫入內(nèi)容。
關(guān)于connect方法渗勘,不管是input取綁定output沐绒,還是output綁定input都是一樣的效果。另外可以利用構(gòu)造方法綁定旺坠,詳見API介紹
2.字符流
2.1 FileReader與FileWriter
這是字符流中一對文件操作類乔遮,它是按字符從文件中讀寫,基本用法和FileInputStream/FileOutputStream類似取刃,示例
public static void main(String[] args) throws IOException {
try (FileReader reader = new FileReader("file/test.txt");
FileWriter writer = new FileWriter("file/copy.txt")){
char[] c = new char[1024];
int len;
while ((len = reader.read(c)) != -1){
writer.write(c,0,len);
}
}catch (Exception e){
e.printStackTrace();
}
}
那么這兩個類和FileInputStream/FileOutputStream有什么區(qū)別呢蹋肮?主要一點是字符上,這兩個類是以字符為單位讀的璧疗,簡單演示一下區(qū)別:
public static void main(String[] args) throws IOException {
try (FileReader reader = new FileReader("file/test.txt");
FileInputStream in = new FileInputStream("file/test.txt")){
System.out.println((char)reader.read());
System.out.println((char)in.read());
}catch (Exception e){
e.printStackTrace();
}
}
讀一個中文字符坯辩,結(jié)果如下:
陳
é
具體字符和字節(jié)的區(qū)別,可以自行學習崩侠。
2.2 BufferedReader與BufferedWriter
同字節(jié)流一樣漆魔,這里也給我們提供了自帶緩沖的類,默認大小也是8192却音,原理不多講了有送,但是這里有一個特殊方法,就是能一次讀一行readLine()僧家,以及插入換行符newLine()雀摘。
2.3 CharArrayReader與CharArrayWriter
和 ByteArrayInputStream與ByteArrayOutputStream很類似,只不過這里操作的是字符數(shù)組而已八拱,連方法都很類似阵赠。
2.4 PushbackReader
字符版的回退流,操作和字節(jié)流對應(yīng)的一樣
try (PushbackReader pis = new PushbackReader(new CharArrayReader("asdfertghuji".toCharArray()),7)){
char[] buffer = new char[7];
pis.read(buffer);
System.out.println(new String(buffer));
pis.unread("1234".toCharArray(),0,4);
pis.read(buffer);
System.out.println(new String(buffer));
}catch (Exception e){
e.printStackTrace();
}
2.5 PipedReader與PipedWriter
字符版的線程間傳輸肌稻,用法和字節(jié)版的一樣清蚀,甚至都不用改代碼,只需替換對應(yīng)的類即可爹谭,不再舉例枷邪。
2.6 StringReader與StringWriter
和CharArrayReader與CharArrayWriter類似,主不過被操作的是字符串而不是字符數(shù)組诺凡。
文件
File是文件的實體類东揣,包含了大量對文件的操作,RandomAccessFile則可以對文件內(nèi)容進行操作腹泌。File類一般都很熟悉嘶卧,它相當于一個工具類,這里不多講凉袱,主要看RandomAccessFile芥吟。
RandomAccessFile有兩個構(gòu)造:
RandomAccessFile(File file, String mode)
RandomAccessFile(String name, String mode)
都是需要指定文件和操作模式侦铜。一般有以下4種模式
r 代表以只讀方式打開指定文件 。
rw 以讀寫方式打開指定文件 钟鸵。
rws 讀寫方式打開钉稍,并對內(nèi)容或元數(shù)據(jù)都同步寫入底層存儲設(shè)備 。
rwd 讀寫方式打開棺耍,對文件內(nèi)容的更新同步更新至底層存儲設(shè)備 贡未。
與流式操作每種類只能讀或?qū)懳募煌@個以既可以讀也可以寫烈掠,他的一大特點是隨機讀寫羞秤,這也是一些斷點續(xù)傳缸托,多線程下載等技術(shù)的基本左敌。
基本的讀寫操作就不介紹了,主要介紹一些特色方法俐镐,如下例在文件末尾追加內(nèi)容:
try (RandomAccessFile file = new RandomAccessFile("file/test.txt","rw")){
file.seek(file.length()); //獲取文件長度矫限,設(shè)置指針位置
file.write('d'); //寫一個字符
System.out.println(file.getFilePointer()); //獲取當前指針位置
}catch (Exception e){
e.printStackTrace();
}
另外可以設(shè)置文件大小,只占位置佩抹,沒有內(nèi)容叼风,適合多線程下載時配置文件
file.setLength(1024*1024*8); //設(shè)置大小8M