Java 的 I/O 操做類在包 java.io 下,有將近 80 個類。css
按數據格式分類:前端
按做用位置分類:java
(1)面向字節:操做以8位爲單位對二進制數據進行操做,不對數據進行轉換。這些類都是InputStream 和 OutputStream的子類。以InputStream/OutputStream爲後綴的類都是字節流,能夠處理全部類型的數據。數據庫
(2)面向字符:操做以字符爲單位,讀時將二進制數據轉換爲字符,寫時將字符轉換爲二進制數據Writer 和 Reader的子類,以Writer/Reader爲後綴的都是字符流。後端
硬盤上全部的文件都是以字節形式保存,字符只在內存中才會造成。即只在處理純文本文件時,優先考慮使用字符流,除此以外都用字節流。瀏覽器
其中:緩存
字符流:服務器
字節流:網絡
讀寫操做實例:數據結構
/** * 使用FileReader進行讀取文件,而後FileWriter寫入另外一個文件 */ @Test public void testFileReaderAndFileWriter() throws IOException { FileReader fileReader = new FileReader("h:\\haha.txt"); char[] buff = new char[512]; StringBuffer stringBuffer = new StringBuffer(); while (fileReader.read(buff) > 0) { stringBuffer.append(buff); } System.out.println(stringBuffer.toString()); FileWriter fileWriter = new FileWriter("h:\\haha2.txt"); fileWriter.write(stringBuffer.toString().trim()); fileWriter.close(); System.out.println("寫入文件成功"); } /** * 使用InputStreamReader進行讀取文件,而後用OutputStreamWriter寫入文件 */ @Test public void testInputStreamReader() throws IOException { //操做數據的方式是能夠組合的,此處FileInputStream讀出的字節流用InputStreamReader轉化爲了字符流對象 InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("h:\\haha.txt"), "utf-8"); char[] buff = new char[512]; StringBuffer stringBuffer = new StringBuffer(); while (inputStreamReader.read(buff) > 0) { stringBuffer.append(buff); } System.out.println(stringBuffer.toString());
//寫文件時,要指定寫入的地方(網絡或本地)、路徑 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("h:\\haha2.txt"), "utf-8"); outputStreamWriter.write(stringBuffer.toString().trim()); outputStreamWriter.close(); } @Test public void testIntputStream2() throws IOException { InputStreamReader inputStreamReader = new InputStreamReader(new StringBufferInputStream("hello world")); char[] buff = new char[512]; int n = inputStreamReader.read(buff); System.out.println(n); System.out.println(buff); }
FileReader類繼承了InputStreamReader,FileReader讀取文件流,經過StreamDecoder解碼成char,其解碼字符集使用的是默認字符集。在Java中,咱們應該使用File對象來判斷某個文件是否存在,若是咱們用FileOutputStream或者FileWriter打開,那麼它確定會被覆蓋。
(1) 磁盤IO工做機制
前面介紹了基本的 Java I/O 的操做接口,這些接口主要定義瞭如何操做數據,以及介紹了操做兩種數據結構:字節和字符的方式。還有一個關鍵問題就是數據寫到何處,其中一個主要方式就是將數據持久化到物理磁盤,下面將介紹如何將數據持久化到物理磁盤的過程。
數據在磁盤的惟一最小描述是文件,也就是說上層應用程序只能經過文件來操做磁盤上的數據,文件也是操做系統和磁盤驅動器交互的一個最小單元。值得注意的是 Java 中一般的 File 並不表明一個真實存在的文件對象,當你經過指定一個路徑描述符時,它就會返回一個表明這個路徑相關聯的一個虛擬對象,這個多是一個真實存在的文件或者是一個包含多個文件的目錄。
什麼時候真正會要檢查一個文件存不存?就是在真正要讀取這個文件時,例如 FileInputStream 類都是操做一個文件的接口,注意到在建立一個 FileInputStream 對象時,會建立一個 FileDescriptor 對象,其實這個對象就是真正表明一個存在的文件對象的描述,當咱們在操做一個文件對象時能夠經過 getFD() 方法獲取真正操做的與底層操做系統關聯的文件描述。例如能夠調用 FileDescriptor.sync() 方法將操做系統緩存中的數據強制刷新到物理磁盤中。
從磁盤讀取文件過程:
當傳入一個文件路徑,將會根據這個路徑建立一個 File 對象來標識這個文件,而後將會根據這個 File 對象建立真正讀取文件的操做對象,這時將會真正建立一個關聯真實存在的磁盤文件的文件描述符 FileDescriptor,經過這個對象能夠直接控制這個磁盤文件。因爲咱們須要讀取的是字符格式,因此須要 StreamDecoder 類將 byte 解碼爲 char 格式,至於如何從磁盤驅動器上讀取一段數據,由操做系統幫咱們完成。
(2)網絡IO工做機制(Socket)
Socket 描述計算機之間完成相互通訊一種抽象功能。Socket 也同樣有多種,大部分狀況下咱們使用的都是基於 TCP/IP 的流套接字,它是一種穩定的通訊協議。
下典型的基於 Socket 的通訊的場景:
主機 A 的應用程序要能和主機 B 的應用程序通訊,必須經過 Socket 創建鏈接,而創建 Socket 鏈接必須須要底層 TCP/IP 協議來創建 TCP 鏈接。創建 TCP 鏈接須要底層 IP 協議來尋址網絡中的主機。咱們知道網絡層使用的 IP 協議能夠幫助咱們根據 IP 地址來找到目標主機,可是一臺主機上可能運行着多個應用程序,如何才能與指定的應用程序通訊就要經過 TCP 或 UPD 的地址也就是端口號來指定。這樣就能夠經過一個 Socket 實例惟一表明一個主機上的一個應用程序的通訊鏈路了。
(TCP/UDP:找端口號,從而與應用程序通訊。IP:找主機)
當客戶端要與服務端通訊,客戶端首先要建立一個 Socket 實例,操做系統將爲這個 Socket 實例分配一個沒有被使用的本地端口號,並建立一個包含本地和遠程地址和端口號的套接字數據結構,這個數據結構將一直保存在系統中直到這個鏈接關閉。在建立 Socket 實例的構造函數正確返回以前,將要進行 TCP 的三次握手協議,TCP 握手協議完成後,Socket 實例對象將建立完成,不然將拋出 IOException 錯誤。
與之對應的服務端將建立一個 ServerSocket 實例,ServerSocket 建立比較簡單隻要指定的端口號沒有被佔用,通常實例建立都會成功,同時操做系統也會爲 ServerSocket 實例建立一個底層數據結構,這個數據結構中包含指定監聽的端口號和包含監聽地址的通配符,一般狀況下都是「*」即監聽全部地址。以後當調用 accept() 方法時,將進入阻塞狀態,等待客戶端的請求。當一個新的請求到來時,將爲這個鏈接建立一個新的套接字數據結構,該套接字數據的信息包含的地址和端口信息正是請求源地址和端口。這個新建立的數據結構將會關聯到 ServerSocket 實例的一個未完成的鏈接數據結構列表中,注意這時服務端與之對應的 Socket 實例並無完成建立,而要等到與客戶端的三次握手完成後,這個服務端的 Socket 實例纔會返回,並將這個 Socket 實例對應的數據結構從未完成列表中移到已完成列表中。因此 ServerSocket 所關聯的列表中每一個數據結構,都表明與一個客戶端的創建的 TCP 鏈接。
傳輸數據是咱們創建鏈接的主要目的,如何經過 Socket 傳輸數據:
當鏈接已經創建成功,服務端和客戶端都會擁有一個 Socket 實例,每一個 Socket 實例都有一個 InputStream 和 OutputStream,正是經過這兩個對象來交換數據。同時咱們也知道網絡 I/O 都是以字節流傳輸的。當 Socket 對象建立時,操做系統將會爲 InputStream 和 OutputStream 分別分配必定大小的緩衝區,數據的寫入和讀取都是經過這個緩存區完成的。寫入端將數據寫到 OutputStream 對應的 SendQ 隊列中,當隊列填滿時,數據將被髮送到另外一端 InputStream 的 RecvQ 隊列中,若是這時 RecvQ 已經滿了,那麼 OutputStream 的 write 方法將會阻塞直到 RecvQ 隊列有足夠的空間容納 SendQ 發送的數據。值得特別注意的是,這個緩存區的大小以及寫入端的速度和讀取端的速度很是影響這個鏈接的數據傳輸效率,因爲可能會發生阻塞,因此網絡 I/O 與磁盤 I/O 在數據的寫入和讀取還要有一個協調的過程,若是兩邊同時傳送數據時可能會產生死鎖,在後面 NIO 部分將介紹避免這種狀況。
提高磁盤 I/O 性能一般的方法:
網絡 I/O 優化一般有一些基本處理原則:
參考連接:https://www.ibm.com/developerworks/cn/java/j-lo-javaio/