1. 程式人生 > >小白學Java:RandomAccessFile

小白學Java:RandomAccessFile

目錄

  • 小白學Java:RandomAccessFile
    • 概述
    • 繼承與實現
    • 構造器
    • 模式設定
    • 檔案指標
    • 操作資料
      • 讀取資料
      • read(byte b[])與read()
      • 追加資料
      • 插入資料

前文傳送門:小白學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類使用詳解