前文傳送門:小白學Java:I/O流
小白學Java:RandomAccessFile
之前我們所學習的所有的流在對數據操作的時候,都是只讀或者只寫的,使用這些流打開的文件很難進行更新。Java提供了RandomAccessFile
類,允許在文件的任意位置上進行讀寫。
概述
官方文檔的解釋是這樣的:
Instances of this class support both reading and writing to a random access file.
- 支持對文件進行讀寫,可以認為這是一個雙向流。
A random access file behaves like a large array of bytes stored in the file system.
- 在操作文件的時候,將文件看作一個大型的字節數組。
There is a kind of cursor, or index into the implied array, called the file pointer; input operations read bytes starting at the file pointer and advance the file pointer past the bytes read
-
有個叫做文件指針(file pointer)的玩意兒作為數組索引。在讀取的時候,從文件指針開始讀取字節,讀取完后,將文件指針移動到讀取的字節之后。
-
其實很好理解,就像我們打字的時候光標,光標在哪,就從哪開始打字,這就是輸出的過程。讀取的過程差不多意思,相當於選中要讀取的內容,這使光標就移動到選中的最后一個字節的后面。
繼承與實現
RandomAccessFile
直接繼承自Object
類,看上去就不像是我們之前學習的那么多的輸入輸出流,都繼承於抽象基類。但是,RandomAccessFile
通過接口的實現,便能夠完成對文件的輸入與輸出:
public class RandomAccessFile implements DataOutput, DataInput, Closeable
- 實現了
Closeable
的接口,Closeable
接口又繼承了AutoCloseable
接口,能夠實現流的自動關閉。 - 實現了
DataOutput
接口,提供了輸出基本數據類型和字符串的方法,如 writeInt,writeDouble,writeChar,writeBoolean,writeUTF。 - 實現了
DataInput
接口,提供了讀取基本數據類型和字符串的方法,同理對應的把write改成read即可。
我們在查看官方文檔的時候看到許多類似的話,我們以read方法舉例:
Although RandomAccessFile is not a subclass of InputStream, this method behaves in exactly the same way as the InputStream.read() method of InputStream.
大致意思就是:雖然RandomAccessFile
並不是InputStream
的子類,但該方法的行為與InputStream.read()
方法完全相同。
我們就可以推斷出,read和write等相關方法和我們之前學習過的讀寫操作是一樣的。
構造器
我們先來看看它提供的構造器:
RandomAccessFile(File file, String mode)
RandomAccessFile(String name, String mode)
只有這倆構造器,意思是創建一個支持隨機訪問文件的流,mode
是設置訪問方式的參數,前者傳入File對象,后者傳入路徑名。
模式設置
模式 | 解釋 |
---|---|
"r" | 只支持讀,任何有關寫的操作將會拋出異常 |
"rw" | 支持讀和寫,如果文件不存在,將嘗試創建 |
"rws" | 類似於"rw",需要同步更新文件的內容或者元數據到底層存儲設備上 |
"rwd" | 類似於"rws",與"rws"不同的在於沒有對元數據的要求 |
我對前兩個尚且明白它們的作用,但是對"rws"和"rwd”,咱不懂啊,我只能粗略地看看官方地解釋:
This is useful for ensuring that critical information is not lost in the event of a system crash.
大概能夠明白,當我們寫入數據量很大的時候,通常都會將數據先存入內存,然后再刻入底層存儲設備,這樣的話寫入的數據會有不能及時存儲的可能,比如突然停電啊等意外情況。"rwd"和"rws"能夠保證寫入的數據不經過內存,同步到底層存儲,確保在系統崩潰時不會丟失關鍵信息。
文件指針
上面提到,存在着文件指針這么個玩意兒,可以控制讀或寫的位置,下面是幾個與文件指針相關的方法:
//將指針位置設置為pos,即從流開始位置計算的偏移量,以字節為單位
public void seek(long pos)
//獲取指針當前位置,以字節為單位
public native long getFilePointer()
//跳過n個字節的便宜量
public int skipBytes(int n)
注:以字節為單位是指:如果讀取1個int類型的內容,讀取之后,指針將會移動4個字節。
操作數據
讀取數據
假設現在從只包含8個字節的文件中讀取內容:
//指針測試
System.out.println("首次進入文件,文件指針的位置:"+raf.getFilePointer());//0
raf.seek(4);
System.out.println("seek后,現在文件指針的位置:"+raf.getFilePointer());//4
raf.skipBytes(1);
System.out.println("skipBytes后,現在文件指針的位置:"+raf.getFilePointer());//5
以下為測試方法:
public static void testFilePointerAndRead(String fileName){
//try-with-resource
try(RandomAccessFile raf = new RandomAccessFile(fileName,"r")){
//定義緩沖字節數組
byte[] bs = new byte[1024];
int byteRead = 0;
//read(bs) 讀取bs.length個字節的數據到一個字節數組中
while((byteRead = raf.read(bs))!=-1){
System.out.println("讀取的內容:"+new String(bs,0,byteRead));
}
} catch (IOException e) {
e.printStackTrace();
}
}
RandomAccessFile
本身是實現Autocloseable
接口的,可以利用JDK1.7提出的處理異常新方式try-with-resource
,前篇已經學習過。- 在讀取字節的時候,如果指定讀取的字節超過文件本身的字節數,將會拋出
EOFException
的異常。舉個例子:假如現在我用readInt讀取四個字節的int類型的值,但是文件本身的字節數小於4,就會出錯。 - 區別於上面的例子,假如現在文件中是空的,我是用read()方法,由於達到了文件的尾部,將會返回-1,而不是拋出異常。
read(byte b[])與read()
read(byte b[])
和read()
方法的不同點(我其實是有些懵的,稍微整理一下):
public int read()
該方法的返回值是文件指針開始的下一個字節,字節作為整數返回,范圍從0到255 (0x00-0x0ff)。如果到達文件的末尾,則為-1。當我將a字符以字節寫入的時候,在文本文件中查看,卻是完完整整的a,我明白這是內部發生了轉化。
當我再調用read()方法時卻還是會返回97,因為read返回值要求是int類型,要得到字符a必須進行相應的轉換。
這些確實都沒啥問題,但是,我們上面代碼中在讀取內容的時候,並沒有在哪里進行轉換啊,當然這是我一開始的想法。我們再來看看read(byte b[])這個方法,看過之后就明白了。
public int read(byte b[])
該方法的返回值是讀入緩沖數組b的總字節數,如果沒有更多的數據,則為-1,因為已經到達文件的末尾。
以我們的代碼為例:我們上面定義了一個存儲字節的數組bs,字節就是從文件中讀取而來,我們專門定義了一個變量byteRead來表示該方法的返回值(即讀入緩沖數組的字節數),如果是-1,說明達到末尾,這個沒有異議。如果不是-1的話,就調用String的構造方法,從該字節數組的第0位開始,向后讀取byteRead長度的字節,構造一個字符串。
Constructs a new String by decoding the specified array of bytes using the platform's default charset.
String這個構造器是有些講究的,它將通過使用平台的默認字符集解碼指定的字節數組,構造一個新字符串。其實這個構造器就已經完成了從字節數組到字符的轉化。
總結
write方法中必須傳入int類型的數,我們在寫入數據的時候,假設傳入的是97,最終其實是把97的低八位二進制傳入,因為計算機只認識二進制。我們打開文件看到的完整的a其實已經時它根據對應得字符集根據二進制進行編碼轉化而來的。而在我們讀取的時候,最初接收到的也是原來的低八位二進制,read方法返回的是int類型的值,所以返回值便是97。
知道了這些,我們在文本文件中寫入97,再在程序中用read讀取,返回的是57。而字符9正好對應的就是57,意思是我們寫入的97在文本文件中其實是以'9'和'7'兩個字符存儲的。
擴展幾個ASCII常見的轉換:
二進制 | 十進制 | 十六進制 | 編寫字符 |
---|---|---|---|
00110000 | 48 | 0x30 | 0 |
01000001 | 65 | 0x41 | A |
01100001 | 97 | 0x61 | a |
追加數據
我們知道,在打開一個文件的時候,如果沒有指定文件指針的位置,默認會從頭開始。如果不設置文件指針的話,追加數據的操作將會覆蓋原文件。那么,知道這個之后,問題就十分簡單啦,追加數據嘛,考慮下面幾步即可:
- 以
"rw"
模式創建一個RandomAccessFile
對象。 - 將文件指定定位到原文件尾部。
- 調用各種各樣適合的write方法即可。
- 最后記得關流,當然可以采用新異常處理的方法。
/**
* 在指定文件尾部追加內容
* @param fileName 文件路徑名
*/
public static void addToTail(String fileName){
//try-with-resource
try(RandomAccessFile raf = new RandomAccessFile(fileName,"rw")) {
//將文件指針指向文件尾部
raf.seek(raf.length());
//以字節數組的形式寫入
raf.write("追加內容".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
還有一個有趣的點,我們知道,寫入數據的時候也是根據文件指針的位置來操作的,但是現在有一個問題,假如我文件中的字節數是4,我把文件指針設置到8的位置,再寫入數據會怎么樣呢?
既然都這么說了,那就肯定不會拋出異常,官方文檔是這樣說的:
Output operations that write past the current end of the implied array cause the array to be extended.
說實話,在沒測試的時候,是感覺有些神奇的,我用我的大白話翻譯一下:如果那把文件指針的位置設置到超過文件本身存儲的數據字節數組的長度呢,數組會被擴展,而不會拋錯。
插入數據
以下代碼參考自:RandomAccessFile類使用詳解
如果直接在指定地位置寫入數據,還是會出現覆蓋的情況。我們需要做以下操作:
- 找到插入位置,把插入位置之后的內容暫時保存起來
- 在插入位置寫入要插入的內容。
- 最后順勢寫入剛才保存到臨時文件中的內容。
/**
* 插入文件指定位置的指定內容
* @param filePath 文件路徑
* @param pos 插入文件的指定位置
* @param insertContent 插入文件中的內容
* @throws IOException
*/
public static void insert(String filePath,long pos,String insertContent)throws IOException{
RandomAccessFile raf=null;
//創建臨時文件
File tmp= File.createTempFile("tmp",null);
tmp.deleteOnExit();
try {
// 以讀寫的方式打開一個RandomAccessFile對象
raf = new RandomAccessFile(new File(filePath), "rw");
//創建一個臨時文件來保存插入點后的數據
FileOutputStream fileOutputStream = new FileOutputStream(tmp);
FileInputStream fileInputStream = new FileInputStream(tmp);
//把文件記錄指針定位到pos位置
raf.seek(pos);
//------下面代碼將插入點后的內容讀入臨時文件中保存-----
byte[] bbuf = new byte[64];
//用於保存實際讀取的字節數據
int hasRead = 0;
//使用循環讀取插入點后的數據
while ((hasRead = raf.read(bbuf)) != -1) {
//將讀取的內容寫入臨時文件
fileOutputStream.write(bbuf, 0, hasRead);
}
//-----下面代碼用於插入內容 -----
//把文件記錄指針重新定位到pos位置
raf.seek(pos);
//追加需要插入的內容
raf.write(insertContent.getBytes());
//追加臨時文件中的內容
while ((hasRead = fileInputStream.read(bbuf)) != -1) {
//將讀取的內容寫入臨時文件
raf.write(bbuf, 0, hasRead);
}
}catch (Exception e){
throw e;
}
}
參考資料:
《Java編程思想》、《Java程序語言設計》
RandomAccessFile類使用詳解