新聞中心
Java IO 是一個龐大的知識體系,很多人學著學著就會學懵了,包括我在內也是如此,所以本文將會從 Java 的 BIO 開始,一步一步深入學習,引出 JDK1.4 之后出現(xiàn)的 NIO 技術,對比 NIO 與 BIO 的區(qū)別,然后對 NIO 中重要的三個組成部分進行講解(緩沖區(qū)、通道、選擇器),最后實現(xiàn)一個簡易的客戶端與服務器通信功能。

從網站建設到定制行業(yè)解決方案,為提供成都網站制作、成都網站設計服務體系,各種行業(yè)企業(yè)客戶提供網站建設解決方案,助力業(yè)務快速發(fā)展。創(chuàng)新互聯(lián)公司將不斷加快創(chuàng)新步伐,提供優(yōu)質的建站服務。
傳統(tǒng)的 BIO
Java IO流是一個龐大的生態(tài)環(huán)境,其內部提供了很多不同的輸入流和輸出流,細分下去還有字節(jié)流和字符流,甚至還有緩沖流提高 IO 性能,轉換流將字節(jié)流轉換為字符流······看到這些就已經對 IO 產生恐懼了,在日常開發(fā)中少不了對文件的 IO 操作,雖然 apache 已經提供了 Commons IO 這種封裝好的組件,但面對特殊場景時,我們仍需要自己去封裝一個高性能的文件 IO 工具類,本文將會解析 Java IO 中涉及到的各個類,以及講解如何正確、高效地使用它們。
BIO NIO 和 AIO 的區(qū)別
我們會以一個經典的燒開水的例子通俗地講解它們之間的區(qū)別
| 類型 | 燒開水 |
|---|---|
| BIO | 一直監(jiān)測著某個水壺,該水壺燒開水后再監(jiān)測下一個水壺 |
| NIO | 每隔一段時間就看看所有水壺的狀態(tài),哪個水壺燒開水就去處理哪個水壺 |
| AIO | 不用監(jiān)測水壺,每個水壺燒開水后都會主動通知線程說:“我的水燒開了,來處理我吧” |
BIO (同步阻塞 I/O)
這里假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 小菠蘿一直看著著這個水壺,直到這個水壺燒開,才去處理下一個水壺。線程在等待水壺燒開的時間段什么都沒有做。
NIO(同步非阻塞 I/O)
還拿燒開水來說,NIO的做法是小菠蘿一邊玩著手機,每隔一段時間就看一看每個水壺的狀態(tài),看看是否有水壺的狀態(tài)發(fā)生了改變,如果某個水壺燒開了,可以先處理那個水壺,然后繼續(xù)玩手機,繼續(xù)隔一段時間又看看每個水壺的狀態(tài)。
AIO (異步非阻塞 I/O)
小菠蘿覺得每隔一段時間就去看一看水壺太費勁了,于是購買了一批燒開水時可以嗶嗶響的水壺,于是開始燒水后,小菠蘿就直接去客廳玩手機了,水燒開時,就發(fā)出“嗶嗶”的響聲,通知小菠蘿來關掉水壺。
什么是流
知識科普:我們知道任何一個文件都是以二進制形式存在于設備中,計算機就只有 0 和1,你能看見的東西全部都是由這兩個數(shù)字組成,你看這篇文章時,這篇文章也是由01組成,只不過這些二進制串經過各種轉換演變成一個個文字、一張張圖片躍然屏幕上。
而流就是將這些二進制串在各種設備之間進行傳輸,如果你覺得有些抽象,我舉個例子就會好理解一些:
“下圖是一張圖片,它由01串組成,我們可以通過程序把一張圖片拷貝到一個文件夾中,
把圖片轉化成二進制數(shù)據集,把數(shù)據一點一點地傳遞到文件夾中 , 類似于水的流動 , 這樣整體的數(shù)據就是一個數(shù)據流”
IO 流讀寫數(shù)據的特點:
- 順序讀寫。讀寫數(shù)據時,大部分情況下都是按照順序讀寫,讀取時從文件開頭的第一個字節(jié)到最后一個字節(jié),寫出時也是也如此(RandomAccessFile 可以實現(xiàn)隨機讀寫)
- 字節(jié)數(shù)組。讀寫數(shù)據時本質上都是對字節(jié)數(shù)組做讀取和寫出操作,即使是字符流,也是在字節(jié)流基礎上轉化為一個個字符,所以字節(jié)數(shù)組是 IO 流讀寫數(shù)據的本質。
流的分類
根據數(shù)據流向不同分類:輸入流 和 輸出流
- 輸入流:從磁盤或者其它設備中將數(shù)據輸入到進程中
- 輸出流:將進程中的數(shù)據輸出到磁盤或其它設備上保存
圖示中的硬盤只是其中一種設備,還有非常多的設備都可以應用在IO流中,例如:打印機、硬盤、顯示器、手機······
根據處理數(shù)據的基本單位不同分類:字節(jié)流 和 字符流
- 字節(jié)流:以字節(jié)(8 bit)為單位做數(shù)據的傳輸
- 字符流:以字符為單位(1字符 = 2字節(jié))做數(shù)據的傳輸
“字符流的本質也是通過字節(jié)流讀取,Java 中的字符采用 Unicode 標準,在讀取和輸出的過程中,通過以字符為單位,查找對應的碼表將字節(jié)轉換為對應的字符。”
面對字節(jié)流和字符流,很多讀者都有疑惑:什么時候需要用字節(jié)流,什么時候又要用字符流?
我這里做一個簡單的概括,你可以按照這個標準去使用:
字符流只針對字符數(shù)據進行傳輸,所以如果是文本數(shù)據,優(yōu)先采用字符流傳輸;除此之外,其它類型的數(shù)據(圖片、音頻等),最好還是以字節(jié)流傳輸。
根據這兩種不同的分類,我們就可以做出下面這個表格,里面包含了 IO 中最核心的 4 個頂層抽象類:
| 數(shù)據流向 / 數(shù)據類型 | 字節(jié)流 | 字符流 |
|---|---|---|
| 輸入流 | InputStream | Reader |
| 輸出流 | OutputStream | Writer |
現(xiàn)在看 IO 是不是有一些思路了,不會覺得很混亂了,我們來看這四個類下的所有成員。
[來自于 cxuan 的 《Java基礎核心總結》]
看到這么多的類是不是又開始覺得混亂了,不要慌,字節(jié)流和字符流下的輸入流和輸出流大部分都是一一對應的,有了上面的表格支撐,我們不需要再擔心看見某個類會懵逼的情況了。
看到 Stream 就知道是字節(jié)流,看到 Reader / Writer 就知道是字符流。
這里還要額外補充一點:Java IO 提供了字節(jié)流轉換為字符流的轉換類,稱為轉換流。
| 轉換流 / 數(shù)據類型 | 字節(jié)流與字符流之間的轉換 |
|---|---|
| (輸入)字節(jié)流 => 字符流 | InputStreamReader |
| (輸出)字符流 => 字節(jié)流 | OutputStreamWriter |
注意字節(jié)流與字符流之間的轉換是有嚴格定義的:
- 輸入流:可以將字節(jié)流 => 字符流
- 輸出流:可以將字符流 => 字節(jié)流
為什么在輸入流不能字符流 => 字節(jié)流,輸出流不能字節(jié)流 => 字符流?
“在存儲設備上,所有數(shù)據都是以字節(jié)為單位存儲的,所以輸入到內存時必定是以字節(jié)為單位輸入,輸出到存儲設備時必須是以字節(jié)為單位輸出,字節(jié)流才是計算機最根本的存儲方式,而字符流是在字節(jié)流的基礎上對數(shù)據進行轉換,輸出字符,但每個字符依舊是以字節(jié)為單位存儲的?!?/p>
節(jié)點流和處理流
在這里需要額外插入一個小節(jié)講解節(jié)點流和處理流。
- 節(jié)點流:節(jié)點流是真正傳輸數(shù)據的流對象,用于向特定的一個地方(節(jié)點)讀寫數(shù)據,稱為節(jié)點流。例如 FileInputStream
- 處理流:處理流是對節(jié)點流的封裝,使用外層的處理流讀寫數(shù)據,本質上是利用節(jié)點流的功能,外層的處理流可以提供額外的功能。處理流的基類都是以 Filter 開頭。
上圖將 ByteArrayInputStream封裝成 DataInputStream,可以將輸入的字節(jié)數(shù)組轉換為對應數(shù)據類型的數(shù)據。例如希望讀入int類型數(shù)據,就會以2個字節(jié)為單位轉換為一個數(shù)字。
Java IO 的核心類 File
Java 提供了 File類,它指向計算機操作系統(tǒng)中的文件和目錄,通過該類只能訪問文件和目錄,無法訪問內容。它內部主要提供了 3 種操作:
- 訪問文件的屬性:絕對路徑、相對路徑、文件名······
- 文件檢測:是否文件、是否目錄、文件是否存在、文件的讀/寫/執(zhí)行權限······
- 操作文件:創(chuàng)建目錄、創(chuàng)建文件、刪除文件······
上面舉例的操作都是在開發(fā)中非常常用的,F(xiàn)ile 類遠不止這些操作,更多的操作可以直接去 API 文檔中根據需求查找。
訪問文件的屬性:
| API | 功能 |
|---|---|
| String getAbsolutePath() | 返回該文件處于系統(tǒng)中的絕對路徑名 |
| String getPath() | 返回該文件的相對路徑,通常與 new File() 傳入的路徑相同 |
| String getName() | 返回該文件的文件名 |
文件檢測:
| API | 功能 |
|---|---|
| boolean isFIle() | 校驗該路徑指向是否一個文件 |
| boolean isDirectory() | 校驗該路徑指向是否一個目錄 |
| boolean isExist() | 校驗該路徑指向的文件/目錄是否存在 |
| boolean canWrite() | 校驗該文件是否可寫 |
| boolean canRead() | 校驗該文件是否可讀 |
| boolean canExecute() | 校驗該文件/目錄是否可以被執(zhí)行 |
操作文件:
| API | 功能 |
|---|---|
| mkdirs() | 遞歸創(chuàng)建多個文件夾,路徑中間有可能某些文件夾不存在 |
| createNewFile() | 創(chuàng)建新文件,它是一個原子操作,有兩步:檢查文件是否存在、創(chuàng)建新文件 |
| delete() | 刪除文件或目錄,刪除目錄時必須保證該目錄為空 |
多了解一些
文件的讀/寫/執(zhí)行權限,在 Windows 中通常表現(xiàn)不出來,而在 Linux 中可以很好地體現(xiàn)這一點,原因是 Linux 有嚴格的用戶權限分組,不同分組下的用戶對文件有不同的操作權限,所以這些方法在 Linux 下會比在 Windows 下更好理解。下圖是 redis 文件夾中的一些文件的詳細信息,被紅框標注的是不同用戶的執(zhí)行權限:
- r(Read):代表該文件可以被當前用戶讀,操作權限的序號是 4
- w(Write):代表該文件可以被當前用戶寫,操作權限的序號是 2
- x(Execute):該文件可以被當前用戶執(zhí)行,操作權限的序號是 1
root root 分別代表:當前文件的所有者,當前文件所屬的用戶分組。Linux 下文件的操作權限分為三種用戶:
- 文件所有者:擁有的權限是紅框中的前三個字母,-代表沒有某個權限
- 文件所在組的所有用戶:擁有的權限是紅框中的中間三個字母
- 其它組的所有用戶:擁有的權限是紅框中的最后三個字母
Java IO 流對象
回顧流的分類有2種:
- 根據數(shù)據流向分為輸入流和輸出流
- 根據數(shù)據類型分為字節(jié)流和字符流
所以,本小節(jié)將以字節(jié)流和字符流作為主要分割點,在其內部再細分為輸入流和輸出流進行講解。
字節(jié)流對象
字節(jié)流對象大部分輸入流和輸出流都是成雙成對地出現(xiàn),所以學習的時候可以將輸入流和輸出流一一對應的流對象關聯(lián)起來,輸入流和輸出流只是數(shù)據流向不同,而處理數(shù)據的方式可以是相同的。
注意不要認為用什么流讀入數(shù)據,就需要用對應的流寫出數(shù)據,在 Java 中沒有這么規(guī)定,下圖只是各個對象之間的一個對應關系,不是兩個類使用時必須強制關聯(lián)使用。
“下面有非常多的類,我會介紹基類的方法,了解這些方法是非常有必要的,子類的功能基于父類去擴展,只有真正了解父類在做什么,學習子類的成本就會下降。”
InputStream
InputStream 是字節(jié)輸入流的抽象基類,提供了通用的讀方法,讓子類使用或重寫它們。下面是 InputStream 常用的重要的方法。
| 重要方法 | 功能 |
|---|---|
| public abstract int read() | 從輸入流中讀取下一個字節(jié),讀到尾部時返回 -1 |
| public int read(byte b[]) | 從輸入流中讀取長度為 b.length 個字節(jié)放入字節(jié)數(shù)組 b 中 |
| public int read(byte b[], int off, int len) | 從輸入流中讀取指定范圍的字節(jié)數(shù)據放入字節(jié)數(shù)組 b 中 |
| public void close() | 關閉此輸入流并釋放與該輸入流相關的所有資源 |
還有其它一些不太常用的方法,我也列出來了。
| 其它方法 | 功能 |
|---|---|
| public long skip(long n) | 跳過接下來的 n 個字節(jié),返回實際上跳過的字節(jié)數(shù) |
| public long available() | 返回下一次可讀?。ㄌ^)且不會被方法阻塞的字節(jié)數(shù)的估計值 |
| public synchronized void mark(int readlimit) | 標記此輸入流的當前位置,對 reset() 方法的后續(xù)調用將會重新定位在 mark() 標記的位置,可以重新讀取相同的字節(jié) |
| public boolean markSupported() | 判斷該輸入流是否支持 mark() 和 reset() 方法,即能否重復讀取字節(jié) |
| public synchronized void reset() | 將流的位置重新定位在最后一次調用 mark() 方法時的位置 |
(1)ByteArrayInputStream
ByteArrayInputStream 內部包含一個 buf 字節(jié)數(shù)組緩沖區(qū),該緩沖區(qū)可以從流中讀取的字節(jié)數(shù),使用 pos 指針指向讀取下一個字節(jié)的下標位置,內部還維護了一個count 屬性,代表能夠讀取 count 個字節(jié)。
bytearrayinputstream
“必須保證 pos 嚴格小于 count,而 count 嚴格小于 buf.length 時,才能夠從緩沖區(qū)中讀取數(shù)據”
(2)FileInputStream
文件輸入流,從文件中讀入字節(jié),通常對文件的拷貝、移動等操作,可以使用該輸入流把文件的字節(jié)讀入內存中,然后再利用輸出流輸出到指定的位置上。
(3)PipedInputStream
管道輸入流,它與 PipedOutputStream 成對出現(xiàn),可以實現(xiàn)多線程中的管道通信。PipedOutputStream 中指定與特定的 PipedInputStream 連接,PipedInputStream 也需要指定特定的 PipedOutputStream 連接,之后輸出流不斷地往輸入流的 buffer 緩沖區(qū)寫數(shù)據,而輸入流可以從緩沖區(qū)中讀取數(shù)據。
(4)ObjectInputStream
對象輸入流,用于對象的反序列化,將讀入的字節(jié)數(shù)據反序列化為一個對象,實現(xiàn)對象的持久化存儲。
(5)PushBackInputStream
它是 FilterInputStream 的子類,是一個處理流,它內部維護了一個緩沖數(shù)組buf。
- 在讀入字節(jié)的過程中可以將讀取到的字節(jié)數(shù)據回退給緩沖區(qū)中保存,下次可以再次從緩沖區(qū)中讀出該字節(jié)數(shù)據。所以PushBackInputStream 允許多次讀取輸入流的字節(jié)數(shù)據,只要將讀到的字節(jié)放回緩沖區(qū)即可。
需要注意的是如果回推字節(jié)時,如果緩沖區(qū)已滿,會拋出 IOException異常。
它的應用場景:對數(shù)據進行分類規(guī)整。
假如一個文件中存儲了數(shù)字和字母兩種類型的數(shù)據,我們需要將它們交給兩種線程各自去收集自己負責的數(shù)據,如果采用傳統(tǒng)的做法,把所有的數(shù)據全部讀入內存中,再將數(shù)據進行分離,面對大文件的情況下,例如1G、2G,傳統(tǒng)的輸入流在讀入數(shù)組后,由于沒有緩沖區(qū),只能對數(shù)據進行拋棄,這樣每個線程都要讀一遍文件。
使用 PushBackInputStream 可以讓一個專門的線程讀取文件,喚醒不同的線程讀取字符:
- 第一次讀取緩沖區(qū)的數(shù)據,判斷該數(shù)據由哪些線程讀取
- 回退數(shù)據,喚醒對應的線程讀取數(shù)據
- 重復前兩步
- 關閉輸入流
到這里,你是否會想到 AQS 的 Condition 等待隊列,多個線程可以在不同的條件上等待被喚醒。
(6)BufferedInputStream
緩沖流,它是一種處理流,對節(jié)點流進行封裝并增強,其內部擁有一個 buffer 緩沖區(qū),用于緩存所有讀入的字節(jié),當緩沖區(qū)滿時,才會將所有字節(jié)發(fā)送給客戶端讀取,而不是每次都只發(fā)送一部分數(shù)據,提高了效率。
(7)DataInputStream
數(shù)據輸入流,它同樣是一種處理流,對節(jié)點流進行封裝后,能夠在內部對讀入的字節(jié)轉換為對應的 Java 基本數(shù)據類型。
(8)SequenceInputStream
將兩個或多個輸入流看作是一個輸入流依次讀取,該類的存在與否并不影響整個 IO 生態(tài),在程序中也能夠做到這種效果
(9)StringBufferInputStream
將字符串中每個字符的低 8 位轉換為字節(jié)讀入到字節(jié)數(shù)組中,目前已過期
InputStream 總結:
- InputStream 是所有輸入字節(jié)流的抽象基類
- ByteArrayInputStream 和 FileInputStream 是兩種基本的節(jié)點流,他們分別從字節(jié)數(shù)組 和 本地文件中讀取數(shù)據
- DataInputStream、BufferedInputStream 和 PushBackInputStream 都是處理流,對基本的節(jié)點流進行封裝并增強
- PipiedInputStream 用于多線程通信,可以與其它線程公用一個管道,讀取管道中的數(shù)據。
- ObjectInputStream 用于對象的反序列化,將對象的字節(jié)數(shù)據讀入內存中,通過該流對象可以將字節(jié)數(shù)據轉換成對應的對象
OutputStream
OutputStream 是字節(jié)輸出流的抽象基類,提供了通用的寫方法,讓繼承的子類重寫和復用。
| 方法 | 功能 |
|---|---|
| public abstract void write(int b) | 將指定的字節(jié)寫出到輸出流,寫入的字節(jié)是參數(shù) b 的低 8 位 |
| public void write(byte b[]) | 將指定字節(jié)數(shù)組中的所有字節(jié)寫入到輸出流當中 |
| public void write(byte b[], int off, int len) | 指定寫入的起始位置 offer,字節(jié)數(shù)為 len 的字節(jié)數(shù)組寫入到輸出流當中 |
| public void flush() | 刷新此輸出流,并強制寫出所有緩沖的輸出字節(jié)到指定位置,每次寫完都要調用 |
| public void close() | 關閉此輸出流并釋放與此流關聯(lián)的所有系統(tǒng)資源 |
OutputStream 中大多數(shù)的類和 InputStream 是對應的,只不過數(shù)據的流向不同而已。從上面的圖可以看出:
- OutputStream 是所有輸出字節(jié)流的抽象基類
- ByteArrayOutputStream 和 FileOutputStream 是兩種基本的節(jié)點流,它們分別向字節(jié)數(shù)組和本地文件寫出數(shù)據
- DataOutputStream、BufferedOutputStream 是處理流,前者可以將字節(jié)數(shù)據轉換成基本數(shù)據類型寫出到文件中;后者是緩沖字節(jié)數(shù)組,只有在緩沖區(qū)滿時,才會將所有的字節(jié)寫出到目的地,減少了 IO 次數(shù)。
- PipedOutputStream 用于多線程通信,可以和其它線程共用一個管道,向管道中寫入數(shù)據
- ObjectOutputStream 用于對象的序列化,將對象轉換成字節(jié)數(shù)組后,將所有的字節(jié)都寫入到指定位置中
- PrintStream 在 OutputStream 基礎之上提供了增強的功能,即可以方便地輸出各種類型的數(shù)據(而不僅限于byte型)的格式化表示形式,且 PrintStream 的方法從不拋出 IOEception,其原理是寫出時將各個數(shù)據類型的數(shù)據統(tǒng)一轉換為 String 類型,我會在講解完
字符流對象
字符流對象也會有對應關系,大多數(shù)的類可以認為是操作的數(shù)據從字節(jié)數(shù)組變?yōu)樽址?,類的功能和字?jié)流對象是相似的。
“字符輸入流和字節(jié)輸入流的組成非常相似,字符輸入流是對字節(jié)輸入流的一層轉換,所有文件的存儲都是字節(jié)的存儲,在磁盤上保留的不是文件的字符,而是先把字符編碼成字節(jié),再保存到文件中。在讀取文件時,讀入的也是一個一個字節(jié)組成的字節(jié)序列,而 Java 虛擬機通過將字節(jié)序列,按照2個字節(jié)為單位轉換為 Unicode 字符,實現(xiàn)字節(jié)到字符的映射。”
Reader
Reader 是字符輸入流的抽象基類,它內部的重要方法如下所示。
| 重要方法 | 方法功能 |
|---|---|
| public int read(java.nio.CharBuffer target) | 將讀入的字符存入指定的字符緩沖區(qū)中 |
| public int read() | 讀取一個字符 |
| public int read(char cbuf[]) | 讀入字符放入整個字符數(shù)組中 |
| abstract public int read(char cbuf[], int off, int len) | 將字符讀入字符數(shù)組中的指定范圍中 |
還有其它一些額外的方法,與字節(jié)輸入流基類提供的方法是相同的,只是作用的對象不再是字節(jié),而是字符。
- Reader 是所有字符輸入流的抽象基類
- CharArrayReader 和 StringReader 是兩種基本的節(jié)點流,它們分別從讀取 字符數(shù)組和 字符串 數(shù)據,StringReader 內部是一個 String 變量值,通過遍歷該變量的字符,實現(xiàn)讀取字符串,本質上也是在讀取字符數(shù)組
- PipedReader 用于多線程中的通信,從共用地管道中讀取字符數(shù)據
- BufferedReader 是字符輸入緩沖流,將讀入的數(shù)據放入字符緩沖區(qū)中,實現(xiàn)高效地讀取字符
- InputStreamReader 是一種轉換流,可以實現(xiàn)從字節(jié)流轉換為字符流,將字節(jié)數(shù)據轉換為字符
Writer
Reader 是字符輸出流的抽象基類,它內部的重要方法如下所示。
| 重要方法 | 方法功能 |
|---|---|
| public void write(char cbuf[]) | 將 cbuf 字符數(shù)組寫出到輸出流 |
| abstract public void write(char cbuf[], int off, int len) | 將指定范圍的 cbuf 字符數(shù)組寫出到輸出流 |
| public void write(String str) | 將字符串 str 寫出到輸出流,str 內部也是字符數(shù)組 |
| public void write(String str, int off, int len) | 將字符串 str 的某一部分寫出到輸出流 |
| abstract public void flush() | 刷新,如果數(shù)據保存在緩沖區(qū),調用該方法才會真正寫出到指定位置 |
| abstract public void close() | 關閉流對象,每次 IO 執(zhí)行完畢后都需要關閉流對象,釋放系統(tǒng)資源 |
- Writer 是所有的輸出字符流的抽象基類
- CharArrayWriter、StringWriter 是兩種基本的節(jié)點流,它們分別向Char 數(shù)組、字符串中寫入數(shù)據。StringWriter 內部保存了 StringBuffer 對象,可以實現(xiàn)字符串的動態(tài)增長
- PipedWriter 可以向共用的管道中寫入字符數(shù)據,給其它線程讀取。
- BufferedWriter 是緩沖輸出流,可以將寫出的數(shù)據緩存起來,緩沖區(qū)滿時再調用 flush() 寫出數(shù)據,減少 IO 次數(shù)。
- PrintWriter 和 PrintStream 類似,功能和使用也非常相似,只是寫出的數(shù)據是字符而不是字節(jié)。
- OutputStreamWriter 將字符流轉換為字節(jié)流,將字符寫出到指定位置
字節(jié)流與字符流的轉換
從任何地方把數(shù)據讀入到內存都是先以字節(jié)流形式讀取,即使是使用字符流去讀取數(shù)據,依然成立,因為數(shù)據永遠是以字節(jié)的形式存在于互聯(lián)網和硬件設備中,字符流是通過字符集的映射,才能夠將字節(jié)轉換為字符。
所以 Java 提供了兩種轉換流:
- InputStreamReader:從字節(jié)流轉換為字符流,將字節(jié)數(shù)據轉換為字符數(shù)據讀入到內存
- OutputStreamWriter:從字符流轉換為字節(jié)流,將字符數(shù)據轉換為字節(jié)數(shù)據寫出到指定位置
“
了解了 Java 傳統(tǒng)的 BIO 中字符流和字節(jié)流的主要成員之后,至少要掌握以下兩個關鍵點:
(1)傳統(tǒng)的 BIO 是以流為基本單位處理數(shù)據的,想象成水流,一點點地傳輸字節(jié)數(shù)據,IO 流傳輸?shù)倪^程永遠是以字節(jié)形式傳輸。
(2)字節(jié)流和字符流的區(qū)別在于操作的數(shù)據單位不相同,字符流是通過將字節(jié)數(shù)據通過字符集映射成對應的字符,字符流本質上也是字節(jié)流。
”
接下來我們再繼續(xù)學習 NIO 知識,NIO 是當下非?;馃岬囊环N IO 工作方式,它能夠解決傳統(tǒng) BIO 的痛點:阻塞。
- BIO 如果遇到 IO 阻塞時,線程將會被掛起,直到 IO 完成后才喚醒線程,線程切換帶來了額外的開銷。
- BIO 中每個 IO 都需要有對應的一個線程去專門處理該次 IO 請求,會讓服務器的壓力迅速提高。
我們希望做到的是當線程等待 IO 完成時能夠去完成其它事情,當 IO 完成時線程可以回來繼續(xù)處理 IO 相關操作,不必干干的坐等 IO 完成。在 IO 處理的過程中,能夠有一個專門的線程負責監(jiān)聽這些 IO 操作,通知服務器該如何操作。所以,我們聊到 IO,不得不去接觸 NIO 這一塊硬骨頭。
新潮的 NIO
我們來看看 BIO 和 NIO 的區(qū)別,BIO 是面向流的 IO,它建立的通道都是單向的,所以輸入和輸出流的通道不相同,必須建立2個通道,通道內的都是傳輸==0101001···==的字節(jié)數(shù)據。
而在 NIO 中,不再是面向流的 IO 了,而是面向緩沖區(qū),它會建立一個通道(Channel),該通道我們可以理解為鐵路,該鐵路上可以運輸各種貨物,而通道上會有一個緩沖區(qū)(Buffer)用于存儲真正的數(shù)據,緩沖區(qū)我們可以理解為一輛火車。
通道(鐵路)只是作為運輸數(shù)據的一個連接資源,而真正存儲數(shù)據的是緩沖區(qū)(火車)。即通道負責傳輸,緩沖區(qū)負責存儲。
理解了上面的圖之后,BIO 和 NIO 的主要區(qū)別就可以用下面這個表格簡單概括。
| BIO | NIO |
|---|---|
| 面向流(Stream) | 面向緩沖區(qū)(Buffer) |
| 單向通道 | 雙向通道 |
| 阻塞 IO | 非阻塞 IO |
| 選擇器(Selectors) |
緩沖區(qū)(Buffer)
緩沖區(qū)是存儲數(shù)據的區(qū)域,在 Java 中,緩沖區(qū)就是數(shù)組,為了可以操作不同數(shù)據類型的數(shù)據,Java 提供了許多不同類型的緩沖區(qū),除了布爾類型以外,其它基本數(shù)據類型都有對應的緩沖區(qū)數(shù)組對象。
“為什么沒有布爾類型的緩沖區(qū)呢?
在 Java 中,boolean 類型數(shù)據只占用 1 bit,而在 IO 傳輸過程中,都是以字節(jié)為單位進行傳輸?shù)模?boolean 的 1 bit 完全可以使用 byte 類型的某一位,或者 int 類型的某一位來表示,沒有必要為了這 1 bit 而專門提供多一個緩沖區(qū)。”
| 緩沖區(qū) | 解釋 |
|---|---|
| ByteBuffer | 存儲字節(jié)數(shù)據的緩沖區(qū) |
| CharBuffer | 存儲字符數(shù)據的緩沖區(qū) |
| ShortBuffer | 存儲短整型數(shù)據的緩沖區(qū) |
| IntBuffer | 存儲整型數(shù)據的緩沖區(qū) |
| LongBuffer | 存儲長整型數(shù)據的緩沖區(qū) |
| FloatBuffer | 存儲單精度浮點型數(shù)據的緩沖區(qū) |
| DoubleBuffer | 存儲雙精度浮點型數(shù)據的緩沖區(qū) |
分配一個緩沖區(qū)的方式都高度一致:使用allocate(int capacity)方法。
例如需要分配一個 1024 大小的字節(jié)數(shù)組,代碼就是下面這樣子。
- ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
緩沖區(qū)讀寫數(shù)據的兩個核心方法:
- put():將數(shù)據寫入到緩沖區(qū)中
- get():從緩沖區(qū)中讀取數(shù)據
緩沖區(qū)的重要屬性:
- capacity:緩沖區(qū)中最大存儲數(shù)據的容量,一旦聲明則無法改變
- limit:表示緩沖區(qū)中可以操作數(shù)據的大小,limit 之后的數(shù)據無法進行讀寫。必須滿足 limit <= capacity
- position:當前緩沖區(qū)中正在操作數(shù)據的下標位置,必須滿足 position <= limit
- mark:標記位置,調用 reset() 將 position 位置調整到 mark 屬性指向的下標位置,實現(xiàn)多次讀取數(shù)據
緩沖區(qū)為高效讀寫數(shù)據而提供的其它輔助方法:
- flip():可以實現(xiàn)讀寫模式的切換,我們可以看看里面的源碼
- public final Buffer flip() {
- limit = position;
- position = 0;
- mark = -1;
- return this;
- }
調用 flip() 會將可操作的大小 limit 設置為當前寫的位置,操作數(shù)據的起始位置 position 設置為 0,即從頭開始讀取數(shù)據。
- rewind():可以將 position 位置設置為 0,再次讀取緩沖區(qū)中的數(shù)據
- clear():清空整個緩沖區(qū),它會將 position 設置為 0,limit 設置為 capacity,可以寫整個緩沖區(qū)
“更多的方法可以去查閱 API 文檔,本文礙于篇幅原因就不貼出其它方法了,主要是要理解緩沖區(qū)的作用”
我們來看一個簡單的例子
- public Class Main {
- public static void main(String[] args) {
- // 分配內存大小為11的整型緩存區(qū)
- IntBuffer buffer = IntBuffer.allocate(11);
- // 往buffer里寫入2個整型數(shù)據
- for (int i = 0; i < 2; ++i) {
- int randomNum = new SecureRandom().nextInt();
- buffer.put(randomNum);
- }
- // 將Buffer從寫模式切換到讀模式
- buffer.flip();
- System.out.println("position >> " + buffer.position()
- + "limit >> " + buffer.limit()
- + "capacity >> " + buffer.capacity());
- // 讀取buffer里的數(shù)據
- while (buffer.hasRemaining()) {
- System.out.println(buffer.get());
- }
- System.out.println("position >> " + buffer.position()
- + "limit >> " + buffer.limit()
- + "capacity >> " + buffer.capacity());
- }
- }
執(zhí)行結果如下圖所示,首先我們往緩沖區(qū)中寫入 2 個數(shù)據,position 在寫模式下指向下標 2,然后調用 flip() 方法切換為讀模式,limit 指向下標 2,position 從 0 開始讀數(shù)據,讀到下標為 2 時發(fā)現(xiàn)到達 limit 位置,不可繼續(xù)讀。
整個過程可以用下圖來理解,調用 flip() 方法以后,讀出數(shù)據的同時 position 指針不斷往后挪動,到達 limit 指針的位置時,該次讀取操作結束。
“介紹完緩沖區(qū)后,我們知道它是存儲數(shù)據的空間,進程可以將緩沖區(qū)中的數(shù)據讀取出來,也可以寫入新的數(shù)據到緩沖區(qū),那緩沖
當前文章:為什么一個還沒畢業(yè)的大學生能夠把 IO 講的這么好?
文章出自:http://m.fisionsoft.com.cn/article/djeophg.html


咨詢
建站咨詢
