對InputStream,OutputStream,Reader,Writer的詳解

1、IO流介紹

基本介紹:常使用的File類是用來描述文件或者文件夾的。File對象可以幫我們獲取文件或者文件夾的一些屬性數據,但是無法讀取文件裏面的數據,如果想讀取文件的數據,那麼需要IO流技術。
IO流技術:解決設備與設備之間數據的傳輸。如:硬盤---->內存、內存------->硬盤

1.1、IO流技術分類

IO流技術按照不同的功能或者需要可以分成不同的類別。
一、按照數據的流向分:輸入流與輸出流;
二、按照數據單位劃分:字節流與字符流;
字節流的簡介:字節流讀取的數據都是文件中存儲的二進制數據,並且讀取的二進制數據不經過任何的加工處理;
字符流的簡介:字符流讀取的也是文件中的二進制數據,只不過讀取的二進制數據按照使用的編碼表解碼成我們能識別的字符數據。所以可以說:字符流 = 字節流+解碼

1.2、輸入輸出的判斷

輸入輸出的判斷,我們可以使用程序本身來當參照物
輸入流:從文件中讀取數據到程序中。以程序爲參照物,數據流入程序內;
輸出流:將程序中的數據寫到文件中。以程序爲參照物,數據流出程序;
下面上圖來總結一下:
在這裏插入圖片描述

2、字節流

2.1、字節流分類

字節流按照數據的流向分爲:輸入字節流,輸出字節流;

2.1.1、輸入字節流(InputStream)

InputStream:是一個抽象類(不能實例化),是所有輸入輸入字節流的超類。它的子類有:ByteArrayInputStream, FileInputStream,ObjectInputStream, SequenceInputStream…這些不同的子類讀取的對象不一樣的。選着什麼樣的子類來操作目標對象完全是顧名思義的。舉個例子:ByteArrayInputStream(字節數組輸入流)是操作字節數組的、ObjectInputStream(對象的輸入字節流)用來操作對象的。
接下來,主要以FileInputStream這個類的學習爲例,其他的輸入字節流的使用都是類似的。

2.1.1.1、文件輸入字節流(FileInputStream)

首先看代碼,FileInputStream是繼承InputStream的。

public class FileInputStream extends InputStream

FileInputStream從文件系統中的某個文件中獲取輸入字節,至於具體是什麼文件,取決於主機環境。用於讀取圖像之類的數據的原始字節流。
FileInputStream中使用一個read()方法來讀取文件中的字節數據,返回一個0~255範圍內的int數據,讀取的時候是一個字節一個字節的讀取。如果讀取已經達到文件末尾,那個read方法返回-1
FileInputStream的使用步驟:
1、找到目標文件;
2、建立傳輸管道;
3、讀取目標數據;
4、關閉傳輸管道;

2.1.1.2、讀取文件方式一

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData01();
	}
	
   /**
	 * 缺點:只能讀取一個字節的數據,不能讀取完文件數據
	 * 	read():從此輸入流中讀取下一個數據字節。返回一個 0 到 255 範圍內的 int 字節值。
	 * 	                  如果因爲已經到達流末尾而沒有字節可用,則返回 -1
	 * @throws IOException
	 */
	public static void getFileData01() throws IOException {
	   //1、找到目標文件;
		File file = new File("F:\\Web_project\\io_demo.txt");
		//2、建立傳輸管道;
		FileInputStream fileInputStream = new FileInputStream(file);
		// 3、讀取目標數據;
		int content = fileInputStream.read();
		System.out.println("得到的內容是----》"+(char)content);
		// 關閉釋放資源
		fileInputStream.close();
	}
}

這是是非常基礎的,只返回一個字節的數據。
read()會拋出一個IO異常,是因爲怕讀取數據過程中傳輸中斷;

int content = fileInputStream.read();

這是句代碼是將讀取到的字節數據返回到content當中,再經過強轉content就可以了;
這個方法有一個致命的缺點:只能讀取一個字節,不能讀全文件,所以我們來看下一個方法;

2.1.1.3、讀取文件方式二

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData02();
	}
	/**
	 * 改良後的方法一,可以讀全文件數據,但是在循環中,如果文件過大,效率較低
	 * @throws IOException
	 */
	public static void getFileData02() throws IOException {
		File file = new File("F:\\Web_project\\io_demo.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		int content = 0;
		while ((content = fileInputStream.read()) != -1) {
			System.out.print((char)content);
		}
		// 關閉釋放資源
		fileInputStream.close();
	}
}

方式二跟方式一樣的地方:

        int content = 0;
		while ((content = fileInputStream.read()) != -1) {
			System.out.print((char)content);
		}

這一片代碼,是將讀取語句放入循環中讀取,並將讀取的值賦給content,進而判斷content是否爲-1來判斷是否文件讀取完;
read()方法的讀取方式,和迭代器有些相識,也是獲取一個相應的指針移到下一個元素,讀取到末尾時,返回一個標識符;
接下來我們來看看帶有緩衝數組的的讀取,方式三。

2.1.1.4、讀取文件方式三

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData03();
	}
	public static void getFileData03() throws IOException {
		// 1、找到目標文件;
		File file = new File("F:\\Web_project\\io_demo.txt");
		// 2、建立傳輸管道;
		FileInputStream fileInputStream = new FileInputStream(file);
		// 定義一個字節數組
		byte[] buff = new byte[10];
		// 3、讀取目標數據;
		int length = fileInputStream.read(buff);
		System.out.println("讀取到的文件內容---->"+new String(buff));
		System.out.println("讀取到的長度----->"+length);
		// 4、關閉傳輸管道;
		fileInputStream.close();
	}
}

然後我們來聊一下這種方式的讀取數據:
首先建立了一個緩衝字節數組byte[] buff = new byte[10];長度爲10。再將這個數組作爲實參放入read()方法當中。這樣read()讀取的數據就存放在buff數組裏。

int length = fileInputStream.read(buff);

這裏的返回值length爲讀入緩衝區的字節總數,如果因爲已經到達文件末尾而沒有更多的數據,則返回 -1。 也就是lengthread往數組裏存放多少個二進制數據,當然長度不會超過數組本身的長度。
讀取到的buf二進制數據經過new String(buf),由String本身的解碼功能,解碼成我們看得懂的字符串;
這種方式同樣有致命的缺陷:不能讀全文件,因爲這個方法能讀取文件的多少,是有緩衝數組的長度來決定的,總不能聲明一個非常非常大的數組在那裏佔用空間吧。那麼我們來看看,方式四。

2.1.1.5、讀取文件方式四

public class InStream {
	public static void main(String[] args) throws IOException {
		getFileData04();
	}
    public static void getFileData04() throws IOException {
		File file = new File("F:\\Web_project\\io_demo.txt");
		FileInputStream fileInputStream = new FileInputStream(file);
		byte[] buff = new byte[1024];
		int length = 0;
		while ((length = fileInputStream.read(buff))!= -1) {
			System.out.println("解析出的文件內容--->"+new String(buff,0,length));
		}
		fileInputStream.close();
	}

方式四不用說也是方式三的升級版:

        int length = 0;
        byte[] buff = new byte[1024];
		while ((length = fileInputStream.read(buff))!= -1) {
			System.out.println("解析出的文件內容--->"+new String(buff,0,length));
		}

也是將讀取數據的代碼放入到了循環,如果read(buf)到達文件末尾並且沒有文件可讀,返回-1
這裏要注意的點:

new String(buff,0,length)

一般都是這樣子寫的。至於原因嘛
是因爲read方法將讀取的二進制數據放入緩衝數組,當緩衝數組存儲滿的時候,read方法返回讀入緩衝區的字節總數length。進行第二次讀取的時候要注意了,read()方法並不是清空之前buf緩衝數組裏的數據,而是覆蓋緩衝數組裏的數據,也就意味着,如果下一次讀取時,剩餘的文件數據小於緩衝數組長度,那麼在緩衝數組的倒數的幾個位置會存在上一次讀取的文件數據。

2.1.1.6、總結

我們來總結一下,只有方式二和方式四能讀全文件,
要點一:方式四的效率是要遠大於方式二的
舉個例子:好像去井裏打水,一個是拿到一瓢水就跑回家,將這一瓢水倒在水缸裏後再返回往井裏打水。而另一個是將一瓢一瓢的水倒在水桶裏,等水桶裝滿了,才拎着水桶倒家裏再返回往井裏打水。這當然是用水桶的效率高。
要點二:read()方法使用緩衝數組時,也是一個字節一個字節的讀取的,只不過讀取的數據是先存放在緩衝數組裏而已。
要點三:read方法再次向緩衝數組裏讀入數據時,並不是先清空緩衝數組裏的數據,而是覆蓋。這也正是爲什麼read要返回:讀入緩衝區的字節總數

2.1.2、輸出字符流(OutputStream)

OutputStream:抽象類是表示輸出字節流的所有類的超類,它也有很多的子類ByteArrayOutputStream, FileOutputStream, ObjectOutputStream,也同樣瞭解FileOutputStream的使用,其他子類的使用方式也是差不多的。

2.1.2.1、文件輸出字符流(FileOutputStream)

看代碼,是繼承了OutputStream,是OutputStream的親兒子。

public class FileOutputStream extends OutputStream

FileOutputStream:文件輸出流是用於將數據寫入 File 或 FileDescriptor 的輸出流, 用於寫入諸如圖像數據之類的原始字節的流。
FileOutputStream的使用步驟與FileInputStream一樣:
1、找到目標文件;
2、建立傳輸管道;
3、讀取目標數據;
4、關閉傳輸管道;

2.1.2.2、寫數據方式一

public class OutStream {
	public static void main(String[] args) throws IOException {
		writeInfo01();
	}
	public static void writeInfo01() throws IOException {
		File file = new File("F:\\Web_project\\out_demo.txt");
		FileOutputStream fileOutputStream = new FileOutputStream(file);
		fileOutputStream.write(97);
		// 釋放資源
		fileOutputStream.close();
	}

這方法只寫一個int類型的數據97(也就是a)
好了我們看看加了緩衝數組的方式二

2.1.2.3、寫數據方式二

public class OutStream {
	public static void main(String[] args) throws IOException {
		writeInfo03();
	}
	public static void writeInfo03() throws IOException {
		File file = new File("F:\\Web_project\\out_demo.txt");
		FileOutputStream fileOutputStream = new FileOutputStream(file,true);
		String msg = "hello word ";
		byte[] buf = msg.getBytes();
		fileOutputStream.write(buf);
		fileOutputStream.close();
	}

看看一片代碼:

        String msg = "hello word ";
		byte[] buf = msg.getBytes();
		fileOutputStream.write(buf);

將String的msg轉成字節數組,再將字節數組作爲實參傳給write方法。當然write也是一個字節一個字節的寫。

3、字節流的異常處理

上面的所有例子都是直接拋的異常,但這並不太是正確的姿勢,下面我上一份拷貝文件的代碼:

public class CopyPictureExection {
	public static void main(String[] args) {
		copyPic();
	}
	
	public static void copyPic() {
		FileInputStream fileInputStream = null;
		FileOutputStream fileOutputStream = null;
		try {
			File inFile = new File("F:\\Web_project\\info.txt");
			File outFile = new File("F:\\Web_project\\Bagua\\記事本.txt");
			fileInputStream = new FileInputStream(inFile);
			fileOutputStream = new FileOutputStream(outFile);
			byte[] buf = new byte[1024];
			int length = 0;
			while ((length = fileInputStream.read(buf)) != -1) {
				fileOutputStream.write(buf, 0, length);
			}
		}catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				// 先聲明的後關閉,後聲明的先關閉
				if (fileOutputStream != null) {
					fileOutputStream.close();
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}finally {
				try {
					if (fileInputStream != null) {
						fileInputStream.close();
					}
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		}
	}
}

代碼上完了,接下來分別講一些要點:
要點一:關閉流時遵循先聲明的後關閉,後聲明的先關閉

			fileInputStream = new FileInputStream(inFile);
			fileOutputStream = new FileOutputStream(outFile);

分別聲明瞭一個輸入流,一個輸出流。由於先聲明的是fileInputStream輸入流,所以關閉流的時候先關閉輸,再關閉輸出流;
要點二:流使用完是必須得關閉的
如果不關閉的話,將一直佔用某個資源,是其他代碼無法操作那個資源。

4、編碼表的內容

使用字節流來讀取中文的時候會出現亂碼,這是因爲一箇中文是兩個字節,而字節流讀取數據這是一個字節一個字節進行的。也就是說只能讀取中文的一般,顯現出來的就是亂碼了。

4.1、碼錶

計算機中是隻存儲二進制的,但是我們看到的卻是各種認識的文字,是因爲將這些文字一一的使用二進制來表示,這些一一對應所形成的一張表,成爲碼錶。各個國家的語言不太一樣,所以碼錶也不一樣。
比如美國的碼錶爲:ASCII,一個英文用一個字節表示。中國的GBK,一箇中文用兩個字節表示;
一般來說英文是比較通用的,因爲在各種不同的碼錶中,英文都是使用一個字節表示的;
常見的碼錶
ASCII: 美國標準信息交換碼。用一個字節的7位可以表示。
ISO8859-1: 拉丁碼錶。歐洲碼錶,用一個字節的8位表示。又稱Latin-1(拉丁編碼)或“西歐語言”。ASCII碼是包含的僅僅是英文字母,並且沒有完全佔滿256個編碼位置,所以它以ASCII爲基礎,在空置的0xA0-0xFF的範圍內,加入192個字母及符號,
藉以供使用變音符號的拉丁字母語言使用。從而支持德文,法文等。因而它依然是一個單字節編碼,只是比ASCII更全面。
GBK: 中國的中文編碼表升級,融合了更多的中文文字符號。
UTF-8: 萬國碼表,國際性碼錶,融合了各個國家的碼錶,英文還是一個字節,最多用三個字節來表示一箇中文字符。
UTF-16: 不管英文中文都是佔兩個字節。

4.2、編碼

字符串—>字節數組
String類的getBytes() 方法進行編碼,將字符串,轉爲對映的二進制,並且這個方法可以指定編碼表。如果沒有指定碼錶,該方法會使用操作系統默認碼錶。
注意:中國大陸的Windows系統上默認的編碼一般爲GBK。在Java程序中可以使用System.getProperty(“file.encoding”)方式得到當前的默認編碼。

4.3、解碼

字節數組—>字符串
String類的構造函數完成。
String(byte[] bytes) 使用系統默認碼錶
String(byte[],charset)指定碼錶
注意:我們使用什麼字符集(碼錶)進行編碼,就應該使用什麼字符集進行解碼,否則很有可能出現亂碼(兼容字符集不會)。

5、字符流

下面來看看字符流。我們知道用字節流讀取中文會出現亂碼,字符流就正好可以用來讀取文本的數據。之所以字符流讀取不會出現亂碼,是因爲字符流讀取是一個字符一個字符的讀取的。
可以完全操作字節流的經驗來了解字符流

5.1、字符流分類

字符流可以分爲輸入字符流和輸出字符流。

5.2、輸入字符流(Reader)

Reader是輸入字符流的超類,其子類有:BufferedReader, CharArrayReader…
這裏以FileReader爲例:

public class ReaderStream {

	public static void main(String[] args) {
		readMsg();
	}
	
	public static void readMsg() {
		FileReader fileReader = null;
		try {
			File file = new File("F:\\newspace\\IoExectionDeal.java");
			fileReader = new FileReader(file);
			int length = 0;
			char[] buf = new char[9];
			while ((length = fileReader.read(buf)) != -1) {
				System.out.print(new String(buf,0,length));
			}
		}catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				if (fileReader != null) {
					fileReader.close();
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
}

之前講了字節流無論是讀寫文件數據都是一個字節一個字節的讀取,也就是這樣才導致讀取的中文有亂碼存在。而字符流是一個字符一個字符的讀取數據。除此之外,用法也都差不多的,這也是流技術容易學習的地方。

			char[] buf = new char[9];
			while ((length = fileReader.read(buf)) != -1) {
				System.out.print(new String(buf,0,length));
			}

只不過這裏的緩衝數組不再是字節數組,而是字符數組。while循環結構和字節流是一樣的。

5.3、輸出字符流(Writer)

Writer是所有輸出字符流的超,這裏也以爲例:

public class WriterStream {
	public static void main(String[] args) {
		writeMsg();
	}
	
	public static void writeMsg() {
		FileWriter fileWriter = null;
		try {
			File file = new File("F:\\Web_project\\out_demo.txt");
			fileWriter = new FileWriter(file);
			String msg = "大家好啊!";
			fileWriter.write(msg);
			fileWriter.flush();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try {
				if (fileWriter != null) {
					fileWriter.close();
					System.out.println("輸出字符流關閉成功...");
				}
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}
}

用法也和輸出字節流一樣,稍不一樣的是,這裏是可以直接寫字符串的;
來看一下之前的輸出字節流的用法:

		String msg = "hello word ";
		byte[] buf = msg.getBytes();

需要將字符串通過msg.getBytes(),getBytes方法編碼成字節數據後才能寫入數據;
再看輸出字符流:

			String msg = "大家好啊!";
			fileWriter.write(msg);

嗯,,,直接寫的字符串。
嗯,,,由於篇幅的關係,關於流的部分就愉快的先到這裏吧。
下一篇繼續接受一下流的一點點延伸的部分:緩衝字節流,以及緩衝字符流,還有序列(SequenceInputStream)

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