您好,登錄后才能下訂單哦!
本篇內容主要講解“如何使用Java IO”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“如何使用Java IO”吧!
前言:
對程序語言的設計者來說,創建一個好的輸入/輸出 (I/O) 系統是一項艱難的任務
Java IO:即 Java 輸入/輸出系統。大部分程序都需要處理一些輸入,并由輸入產生一些輸出,因此Java為我們提供了 java.io 包
作為一個合格的程序開發者,說到 IO 我們并不會陌生,JAVA IO 系統的知識體系如下:
看完以上的圖,才會恍然,原來 Java.io 包中為我們提供了這么多支持。而我們恍然的同時也不必感到驚慌,俗話說萬變不離其宗,我們只需要根據源頭進行擴展,相信就可以很好的掌握IO知識體系。
File類
讀寫操作少不了與文件(File)打交道,因此我們想要掌握好 IO 流,不妨先從文件入手。
文件(File)這個詞即非單數也非復數,它既能代表一個特殊的文件,又能表示一個目錄下的文件集。
列表
File 如果表示的是一個目錄下的文件集的時候,我們想要得到一個目錄可以怎么做?
File已經為我們準備好了 API,根據返回值類型,我們不難猜到每個 API 方法的用處。
已知我們 D 盤目錄下有個 TestFile 文件夾,該文件夾下有以下文件:
名稱列表
如果我們想要獲取指定目錄下的名稱列表,我們可以使用這兩個API:
list()
list(FilenameFilter filter)
不帶參數的 list() 方法默認是列出指定目錄下的所有文件名稱。如果我們想要指定名稱的目錄名稱列表我們便可以使用另一個方法:
我們期望獲取帶有test關鍵字的文件名稱,而結果也如我們所愿。
文件列表
有時候我們的很多操作不單單針對于某個文件,而是在整個文件集上做操作。要產生這個文件集,那我們就需要借助 File 的另外API方法了:
listFiles()
listFiles(FilenameFilter filter)
listFiles(FileFilter filter)
有了以上經驗,我們不難猜到 listFiles() 的作用便是列出所有的文件列表:
圖中我們已經獲取到了文件集,該方法會返回的同樣是一個數組,不過是一個 File類型的數組。
聰明的你肯定也已經知道了如果獲取帶指定關鍵字的文件集
與上述列出文件名稱如出一轍,真是個小機靈鬼~
但是listFiles(FileFilter filter) 這個方法傳遞的參數與上有何異?我們不妨一試:
同樣是一個接口,同樣需要重寫 accept() 方法,但是這個方法只有一個 File 的參數。因此這兩個參數都是用于文件過濾的,功能大同小異~
目錄工具
創建目錄
File 類的好用之處不僅能讓你對于已有目錄文件的操作,還能讓你無中生有!
文件的特性無外乎:名稱,大小,最后修改日期,可讀/寫,類型等
那么我們通過 API 也理應能夠獲得:
以上什么類型都獲取到了,唯獨少了個類型,雖然說 File 沒有提供直接獲取類型的方法,但是我們可以通過獲取文件的全名,然后通過裁剪獲取到文件的后綴,便可獲取到文件的類型:
轉手一操作,自給自足也能獲取文件類型,真是個小機靈鬼~
以上我們都是基于文件目錄存在的情況下操作的,那么如果我們想要操作的文件目錄不存在。或者由于我們的粗心將文件目錄名稱輸入錯了,那么將會發生什么情況,操作進程是否能夠正常進行?
結果便是拋出異常了,的確拋出異常才是正常的現象,針對一個不存在的文件目錄進行操作豈不是瞎胡鬧
因此在我們不確定文件目錄是否存在的情況下我們可以這樣操作:
在圖中我們可以看到兩個我們沒見過的API方法,分別是 exists() 和 mkdirs().
exists():用于驗證文件目錄是否存在
mkdirs():用于創建目錄
通過以上先驗證后操作,我們成功避免了異常。這里需要了解的是,除了 mkdirs() 可以創建目錄之外,還有一個 mkdir() 也是可以創建目錄的,這兩個方法除了少了一個 s 之外,還有其他區別呢?
mkdir(): 只能創建一層目錄
mkdirs(): 可以創建多層目錄
我們目前的場景是 Test 目錄不存在,dir01 這個目錄自然也不存在,那么這個時候就得創建兩層目錄。但是我們使用 mkdir() 這個方法是行不通的,它無法創建。因此遇到這種情況我們應當使用 mkdirs()這個方法。
File類型
File 可以是一個文件也可以是一個文件集,文件集中可包含一個文件或者是一個文件夾,如果我們想要針對一個文件做讀寫操作,卻無意對一個文件夾進行了操作,那就尷尬了,因此我們可以借助 isDirectory來判斷是否是文件夾:
輸入與輸出
上面我們談到 File 類的基本操作,接下來我們便進入了I/O模塊。
輸入和輸出我們經常使用 流 這個概念,如輸入流和輸出流。這是個抽象的概念,代表任何與能力產出數據的數據源對象或是有能力接受數據的接收端對象。流 屏蔽了實際 I/O 設備找那個處理數據的細節!
I/O 可以分為 輸入 和 輸出 兩部分。
輸入流中又分為 字節輸入流(InputStream) 和 字符輸入流(Reader),任何由 InputStream 或 Reader 派生而來的類都實現了 read() 這個方法,用來讀取單個字節或字節數組。
輸出流中又分為 字節輸出流(OutputStream) 和 字符輸出流(Writer),任何由 OutputStream 或 Writer 派生而來的類都實現了 write() 這個方法,用來寫入單個字節或字節數組。
因此我們可以看出 Java 中的規定:與輸入有關的所有類都應該從 InputStream 繼承,與輸出有關的所有類都應該從 OutputStream 繼承
InputStream
用來表示那些從不同數據源產生輸入的類
那些不同數據源具體又是哪些?常見的有:1. 字節數組 2. String 對象 3. 文件 4. “管道”(一端輸入,一端輸出)
其中每一種數據源都有對應的 InputStream 子類可以操作:
類 | 功能 |
---|---|
ByteArrayInputStream | 允許將內存的緩沖區當作 InputStream 使用 |
StringBufferInputStream | 已廢棄,將String轉換成 InputStream |
FileInputStream | 用于從文件中讀取信息 |
PipedInputStream | 產生用于寫入相關 PipedOutPutStream的數據,實現 管道化 的概念 |
SequenceInputStream | 將兩個或多個 InputStream 對象轉換成一個 InputStream |
FilterInputStream | 抽象類,作為裝飾器 的接口,為其他InputStream 提供有用的功能 |
OutPutStream
該類別的類決定了輸出所要去往的目標:1. 字節數組 2. 文件 3. 管道
常見的 OutPutStream 子類有:
類 | 功能 |
---|---|
ByteArrayOutputStream | 在內存中創建緩沖區,所有送往 “流” 的數據都要放置在此緩沖區 |
FileOutputStream | 用于將信息寫入文件 |
PipedOutputStream | 任何寫入其中的信息都會自動作為相關 PipedInputStream 的輸出,實現 管道化 的概念 |
FilterOutputStream | 抽象類,作為裝飾器 的接口,為其他 OutputStream 提供有用的功能 |
裝飾器
我們通過以上的認識,都看到了不管是輸入流還是輸出流,其中都有一個抽象類FilterInputStream 和 FilterOutputStream,這些類相當于是一個裝飾器。在Java 中I/O 操作需要多種不同的功能組合,而這個便是使用裝飾器模式的理由所在。
何為裝飾器?裝飾器必須具有和它所裝飾對象的相同接口,但它也可以擴展接口,它可以給我們提供了相當多的靈活性,但它也會增加代碼的復雜性。
FilterInputStream 和FilterOutputStream 是用來提供裝飾器類接口以控制特定輸入流(InputStream)和輸出流(OutputStream)的兩個類。
FilterInputStream
InputStream 作為字節輸入流,那么讀取的數據理應用字節數組接收,如下:
我們得借助一個 byte 數組來接收讀取到值,然后轉為字符串類型。
既然我們有了裝飾器FilterInputStream ,那是否可以借助裝飾器的子類來幫我們實現讀操作呢?我們先來看下常用的FilterInputStream子類有哪些:
類 | 功能 |
---|---|
DataInputStream | 與 DataOutputStream 搭配使用,我們可以按照可移植方式從流讀取基本數據類型(int,char,long) |
BufferedInputStream | 使用它可以防止每次讀取時都得進行實際寫操作。代表"緩沖區" |
其中DataInputStream允許我們讀取不同的基本數據類型數據以及String對象,搭配相應的DataOutputStream,我們就可以通過數據"流" 將基本類型的數據從一個地方遷移到另一個地方。
然后說到BufferedInputStream 之前我們先看一組測試代碼:
現有三個文本文件,其中test01.txt 大小約為 610M,test02/test03均為空文本文件
那我們現在分別用普通的 InputStream + OutputStream 和裝飾后的BufferedInputStream + BufferedOutputStream 寫入文本
普通組合:
緩沖區組合:
可以看出兩種方式的分別耗時,4864 ms 和 1275 ms。使用普通組合相當于是緩沖區的 4 倍之久,如果文件更大的話,這個差異可是驚人的!驚訝的同時肯定也有所詫異,這是為什么呢?
如果用read()方法讀取一個文件,每讀取一個字節就要訪問一次硬盤,這種讀取的方式效率是很低的。即便使用read(byte b[])方法一次讀取多個字節,當讀取的文件較大時,也會頻繁的對磁盤操作。
而BufferedInputStream的API文檔解釋為:在創建BufferedInputStream時,會創建一個內部緩沖區數組。在讀取流中的字節時,可根據需要從包含的輸入流再次填充該內部緩沖區,一次填充多個字節。也就是說,Buffered類初始化時會創建一個較大的byte數組,一次性從底層輸入流中讀取多個字節來填充byte數組,當程序讀取一個或多個字節時,可直接從byte數組中獲取,當內存中的byte讀取完后,會再次用底層輸入流填充緩沖區數組。因此這種從直接內存中讀取數據的方式要比每次都訪問磁盤的效率高很多。
BufferedInputStream/BufferedOutputStream不直接操作數據源,而是對其他字節流進行包裝,它們是 處理流。
程序把數據保存到 BufferedOutputStream 緩沖區中,并沒有立即保存到文件里,緩沖區中的數組在以下情況會保存到文件中:
緩沖區已滿
flush() 清空緩沖區
close() 關閉流
FilterOutputStream
OutputStream 的基本操作如下:
通過調用write() 方法便可將值寫入文件中,這里有兩點需要注意:
寫入文檔默認是覆蓋的方式
按我們理解調用兩次該方法,文本文件中的內容應該是兩行 公眾號:小菜良記,但是實際上只用一行,這是因為后面寫入的內容會覆蓋前面已經存在的內容,解決方法便是在構造函數的時候加上append = true
寫入與讀取的區別在于,讀取的時候如果文件不存在會報錯,但是寫入的時候如果文件不存在,會默認幫你創建文件
OutputStream中同樣存在裝飾器類FilterOutputStream,以下便是裝飾器類的常用子類:
類 | 功能 |
---|---|
DataOutputStream | 與DATAInputStream搭配使用,可以按照可移植方式向流中寫入基本類型數據(int,char,long等) |
BufferedOutputStream | 使用它避免每次發送數據時都要進行實際的寫操作,代表 使用緩沖區,可以調用flush 清空緩沖區 |
DataOutputStream 和 BufferedOutputStream 在上面已經講到,這里就不再贅述。
Reader 與 Writer
在 Java 1.1 的時候,對基本的I/O流類庫進行了重大的修改,增添了 Reader 和 Writer 兩個類。在我之前局限的認知中,會誤以為這兩個類的出現是為了替代 InputStream 和 OutputStream ,但事實也并非與我局限認知所似。
InputStream 和 OutputStream 是以面向字節的形式為 I/O 提供功能,而 Reader 和 Writer是提供兼容 Unicode于面向字符的形式為 I/O 提供功能
這兩者共存,并提供了適配器 - InputStreamReader 和 OutputStreamWriter
InputStreamReader 可以把 InputStream 轉換為 Reader
OutputStreamWriter 可以把 OutputStream 轉換為 Writer
這兩者雖然不能說完全相同,但也是極為相似,對照如下:
字節流 | 字符流 |
---|---|
InputStream | Reader |
OutputStream | Writer |
FileInputStream | FileReader |
FileOutputStream | FileWriter |
ByteArrayInputStream | CharArrayReader |
ByteArrayOutputStream | CharArrayWriter |
PipedInputStream | PipedReader |
PipedOutputStream | PipedWriter |
甚至裝飾者類都幾乎相似:
字節流 | 字符流 |
---|---|
FilterInputStream | FilterReader |
FilterOutputStream | FilterWriter |
BufferedInputStream | BufferedReader |
BufferedOutputStream | BufferedWriter |
PrintStream | PrintWriter |
使用Reader 和 Writer 的方式也十分簡單:
我們順便看下裝飾器的使用BufferedReader 與 BufferedWriter
RandomAccessFile
RandomAccessFile 適用于由大小已知的記錄組成的文件,所以我們可以使用 seek() 將記錄從一處轉移到另一處,然后讀取或者修改記錄。文件中記錄的大小不一定都相同,只要我們能夠確定哪些記錄有多大以及它們在文件中的位置即可。
我們從圖中可以看到 RandomAccessFile 并非繼承于 InputStream 和 OutputStream 兩個接口,而是繼承于有些陌生的DateInput 和 DataOutput。
真是個有點特立獨行的類~我們繼續來看下它的構造函數:
我們這邊只截取了構造函數的一部分,畢竟只截重點就行~
觀察構造器可以發現,這里定義了四種模式:
r | 以只讀的方式打開文本,也就意味著不能用write來操作文件 |
---|---|
rw | 讀操作和寫操作都是允許的 |
rws | 每當進行寫操作,同步的刷新到磁盤,刷新內容和元數據 |
rwd | 每當進行寫操作,同步的刷新到磁盤,刷新內容 |
這有什么用呢?說白了就是 RandomAccessFile 這個類什么都要。既能讀,又能寫
從本質上來說,RandomAccessFile 的工作方式類似于把 DataInputStream 和 DataOutputStream 組合起來使用,還添加了一些方法,其中方法getFilePointer() 用于查找當前所處的文件位置,seek()用于在文件內移至新的位置,length() 用于判斷文件的最大尺寸。第二個參數用于表明我們是 "隨機讀(r)" 還是 "既讀又寫(rw)",但它不支持單獨 寫文件。我們實際來操作一下:
獲取只讀RandomAccessFile:
獲取可讀可寫RandomAccessFile
我們首先從向文件中寫入了test 四個單詞,然后將頭指針移動3位后繼續寫入File四個單詞,結果就變成了testFile,這是因為移動指針后是以第四個位置開始寫入。
ZIP
看到zip這個詞,我們理所應當的就會想到壓縮文件,沒錯壓縮文件在 Java I/O中也是極其重要的存在。也許更應該說對文件的壓縮在我們的開發中也是極其重要的存在。
在 Java 內置類中提供了需要關于ZIP 壓縮的類,可以使用 java.util.zip 包中的ZipOutuputStream 和 ZipInputStream 來實現文件的 壓縮 和 解壓縮。我們先來看下如何對文件進行壓縮~
ZipOutputStream
ZipOutputStream 的構造方法如下:
public ZipOutputStream(OutputStream out) {/* doSomething */}
我們需要傳入一個 OutputStream 對象。因此我們也大致可以認為 壓縮文件 相當于是向一個 壓縮文件中寫入數據,聽起來可能會有點繞。我們先看下ZipOutputStream中有哪些API:
方法 | 返回值 | 說明 |
---|---|---|
putNextEntry(ZipEntry e) | void | 開始寫一個新的 ZipEntry,并將流內的位置移至此 entry 所值數據的開頭 |
write(byte[] b, int off, int len) | void | 將字節數組寫入當前 ZIP 條目數據 |
setComment(String command) | void | 設置此 ZIP 文件的注釋文字 |
finish() | void | 完成寫入ZIP 輸出流的內容,無須關閉它所配合的 OutputStream |
我們來演示一下如何壓縮文件:
場景:我們需要將D盤目錄下的 TestFile文件夾壓縮到 D盤下的 test.zip 中
具體的操作邏輯如下:
通過以上步驟我們便可以很順利的將一個文件壓縮
ZipInputStream
說完如何將文件壓縮,那自然要會如何將文件解壓縮!
public ZipInputStream(InputStream in) {/* doSomethings */}
ZipInputStream 與壓縮流類似,構造函數同樣需要傳入一個 InputStream 對象,毋庸置疑,API肯定也是一一對應的:
方法 | 返回值 | 說明 |
---|---|---|
read(byte[] b, int off, int len) | int | 讀取目標 b 數組內 off 偏移量的位置,長度是 len 字節 |
avaiable() | int | 判斷是否已讀完目前 entry 所指定的數據,已讀完返回 0,否則返回 1 |
closeEntry() | void | 關閉當前 ZIP 條目并定位流以讀取下一個條目 |
skip(long n) | long | 跳過當前 ZIP 條目中指定的字節數 |
getNextEntry() | ZipEntry | 讀取下一個ZipEntry,并將流內的位置移至該 entry 所指數據的開頭 |
createZipEntry(String name) | ZipEntry | 以指定的name參數新建一個ZipEntry對象 |
那下面我們便動手操作一下如何解壓一個文件:
不必被代碼長度嚇到,認真閱讀便會發現解壓文件也很簡單:
我們通過 getNextEntry() 方法來獲取到一個ZipEntry,這里取到文件方式類似于深度遍歷,每次返回的目錄大致如下:
每次都會遍歷完一個目錄下的所有文件,例如 dir01 文件夾下的所有文件,才會繼續遍歷 dir02 文件夾,所以我們不必使用遞歸的方式去獲取所有文件。取到每一個文件后,通過 ZipFile獲取輸出流,然后寫入到解壓后的文件中。大致流程如下:
新 I/O
JDK1.4的java.nio.* 包中引入了新的 JavaI/O 類庫,其目的也簡單,就是提高速度。實際上,舊的I/O包已經使用 nio 重新實現過,以便充分利用這種速度提高。
只要使用的結構更接近于操作系統執行I/O的方式,那么速度自然也會提高,因此就產生了兩個概念:通道 和 緩沖器。
我們該怎么理解 通道 和 緩沖器 兩個概念呢。我們可以認為緩沖器相當于是一輛煤礦中的小火車,通道相當于火車的軌道,小火車載著滿滿的煤礦從礦源運往它處。因此我們并沒有直接和通道交互,而是和緩沖器交互,并把緩沖器派送到通道。通道要么從緩沖器獲得數據,要么向緩沖器發送數據。
ByteBuffer是唯一直接與通道直接交互的緩沖器,可以存儲未加工字節的緩沖器。
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer 的創建方式通常可以通過allocate()方法來指定大小創建。同時ByteBuffer中支持 4中創建 ByteBuffer
為了更好支持 新I/O ,舊 I/O 類庫中有三個類被修改了,用以產生FileChannel。這個被修改的類分別的:FileInputStream,FileOutputStream以及用于讀寫兼備的 RandomAccessFile。這里值得注意的是這些都是字節操作流,因為字符流不能用于產生通道,但是 Channels 中提供了實用的方法,用于在通道中產生 Reader 和 Writer
獲取通道
我們在上面已經了解到了有三個類支持產生通道,具體產生通道的方法如下:
以上便是創建通道的三種方式,并且進行了讀寫操作的測試。我們看一下圖中的測試代碼,然后總結一下:
getChannel()方法將會產生一個 FileChannel。我們可以向它傳送可用于讀寫的ByteBuffer。我們將字節存放于 ByteBuffer 的方法之一是:使用 put()方法直接對它們進行填充,填入一個或多個字節,或基本數據類型的值。不過,也可以使用 wray()方法將已存在的字節數組 "包裝" 到 ByteBuffer 中。這樣子就可以不用在復制底層的數組,而是把它作為所產生的 ByteBuffer 的存儲器,可以稱之為 數組支持的ByteBuffer
我們還可以看到 FileChannel 使用到的 position() 方法,這個方法可以在文件內隨處移動FileChannel,在這里,我們把它移動到最后,然后進行其他的讀寫操作。
對于只讀訪問,我們必須顯式地使用靜態的allocate() 方法來分配 ByteBuffer。如果我們想要獲取更好的速度我們也可以使用 allocateDirect() ,以產生一個與操作系統有更高耦合性的 "直接" 緩沖器。但是這種分配的開支會更大,并且具體實現也隨操作系統的不同而不同。
如果我們想要的調用 read() 來向ByteBuffer 存儲字節,就必須調用緩沖器上的flip() 方法,這是用來告知 FileChannel ,讓它準備好讓別人讀取字節的準備,當然,這也是為了獲取最大速度。這里我們用 ByteBuffer 來接收字節后就沒有繼續使用緩沖器來進一步操作,如果需要繼續read() 的話,我們就必須得調用 clear() 方法來為每個 read() 方法做準備。
通道相連
程序員往往都是比較懶惰的,上面那種讀取后再通知 FileChannel 的方式似乎有些麻煩。那么有沒有更加簡單的方法?肯定是有的,不然我也不會問是吧~ 那就是讓一個通道與另外一個通道直接相連接,這就得借助特殊的方法transferTo() 和 transferFrom() 。具體使用如下:
借助方法1 或 方法2 都可以成功將文件寫入到 test03.txt 文件中
到此,相信大家對“如何使用Java IO”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。