java I/O系統(4)-RandomAccessFile類

引言

RandomAccessFile類是在java.io中的一個工具類,它獨立於字符流與字節流之外,自成一派。它的核心功能就是random功能。他可以隨機訪問到文件的任意位置的資源,對於超大文件的局部修改有很大的幫助。在本篇博文中詳細介紹RandomAccessFile類的組成結構,闡述它所解決的問題,並給出demo進行測試。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

RandomAccessFile介紹

1、與其他IO類的關係
沒有關係,它是獨立的,所有的方法都是爲這個類獨立開發的,而且它繼承Object類。之所以沒有關係,很大原因就是它獨特的文件處理方式:它可以實現我們在文件中前後移動,通過一個叫“文件指針”的東西指定當前處理位置,下面是它類定義源碼:

public class RandomAccessFile implements DataOutput, DataInput, Closeable {
}

在類的定義中,實現了Closeable接口,說明RandomAccessFile是一個資源類,需要對資源進行close操作。同時還實現了DataOutput與DataInput接口,但是注意這2個接口與其他IO類並沒有直接關係。準確來說與InputStream與OutputStream沒有什麼關係,只是他們的子類DataOutputSteram與DataInputStream也實現了這2個接口。看上去眼熟,但並不是你所喊的那個。

2、構造方法
下面是源碼中的兩種構造方法:

//構造1
 public RandomAccessFile(String name, String mode)
        throws FileNotFoundException
    {
        this(name != null ? new File(name) : null, mode);
    }

//構造2
  public RandomAccessFile(File file, String mode)
        throws FileNotFoundException
    {
    }

從上面可以看出,RandomAccessFile具有兩個構造方法,一個基於文件路徑,一個基於文件。其實是一樣的,中間進行轉化了一步。
這裏闡述構造函數,其實是想說明RandomAccessFile的mode參數。這個參數用於控制對這個文件的操作方式,在jdk_api中分爲4種:

“r” :以只讀方式打開。調用結果對象的任何 write 方法都將導致拋出 IOException。
“rw” :打開以便讀取和寫入。如果該文件尚不存在,則嘗試創建該文件。
“rws” :打開以便讀取和寫入,對於 “rw”,還要求對文件的內容或元數據的每個更新都同步寫入到底層存儲設備。
“rwd” :打開以便讀取和寫入,對於 “rw”,還要求對文件內容的每個更新都同步寫入到底層存儲設備。

對於上面的縮寫 r = read; w = write ; s = synchronously; d = device?

我們一般都用rw來設置它的運行模式,但是對於一些及時操作的時候,我們就需要用rws和rwd。爲什麼會這樣呢?

在上一篇博文中,我們介紹字符流的時候,我們說對於文件的操作是存在緩存功能的,測試就是不關閉流的情況下查看結果。其實RandomAccessFile也是一樣,它也是具有緩存的功能,所以用rws與rwd的話,那麼每次我們修改文件內容的時候,都可以及時的同步到文件中去。
獲取有小夥伴問:rws與rwd有什麼區別呢?從上面看出來是rws多了一個元數據嗎?其實rws能同步的不僅僅是文件內容,還可以同步文件屬性,比如說創建時間,修改時間等等。這些屬性方面的東西,稱爲元數據。

但是,雖然這麼說,經過博主的測試發現rw與rws都是及時讀寫的。下面是測試代碼:

package com.brickworkers.io;

import java.io.IOException;
import java.io.RandomAccessFile;
/**
 * 
 * @author Brickworker
 * Date:2017年5月16日下午3:29:19 
 * 關於類RandomAccessFileTest.java的描述:隨機訪問文件測試
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class RandomAccessFileTest {

    public static void main(String[] args) throws IOException, InterruptedException{
        //指定文件,設置模式爲讀寫
        RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw");
        RandomAccessFile rws = new RandomAccessFile("F:/java/io/rws.txt", "rws");
        try {
            byte[] bytes = "hello".getBytes();
            for (byte b : bytes) {
                rw.write(b);
                rws.write(b);
                //第一次遍歷的時候,主線程休眠
                Thread.sleep(5000);
            }
        } finally{
            rw.close();
            rws.close();
        }



    }
}


發現每寫入一個字節,對應的文檔就會增加一個字節,也沒有查到相關資料,不知道大家對這個mode參數是否有更深的理解呢?

3、核心方法
基於RandomAccessFile的隨意訪問特性,它的核心方法就是對指針的操作,指針操作包括以下方法:
long getFilePointer() : 獲取當前指針位置
void seek(long pos) : 設置指針當前的位置
除卻這兩個方法之外就是對文件的讀取和寫入的一些操作,其中還涉及到nio的操作,nio在後面在闡述。

4、解決問題
基於RandomAccessFile的特性,它可以解決載入不了進內存的大文件的修改。我們可以通過它對要修的文件的局部進行操作。不過在RandomAccessFile的核心方法中,沒有辦法對文件中間的部分進行操作,因爲對於文件中間部分操作必然需要把後面的數據往後移動,這個是沒有天然辦法的,需要我們自己把後面部分讀取暫存起來,插入結束之後,再把暫存的數據補回到文件中。但是對於開頭和結尾的操作會顯得非常方便。
同時,因爲RandomAccessFile可以標記當前處理位置,那麼它可以對一個文件進行間斷操作:只需要在一次操作完成之後,記錄好操作位置,恢復的時候就可以先獲取原先操作位置,繼續執行。
最後,我們還可以利用多線程,把一個超大的文件分割成一個個小文件,然後進行載入,那麼可以極大的提升效率。分割的想法很好理解,比如說線程1操作1~10000字節。線程二操作100001到20000字節就可以了。核心其實還是對指針的位置進行處理。

文件指針

要深入理解RandomAccessFile,核心問題就是要理解文件指針的操作,我們觀察指針指向的位置和移動情況。下面這張圖是一個簡單的txt文件,預先寫入的內容:
這裏寫圖片描述

下面是一個測試demo:

package com.brickworkers.io;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
 * 
 * @author Brickworker
 * Date:2017年5月16日下午3:29:19 
 * 關於類RandomAccessFileTest.java的描述:隨機訪問文件測試
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class RandomAccessFileTest {

    public static void main(String[] args) throws IOException{
        //指定文件,設置模式爲讀寫
        RandomAccessFile accessFile = new RandomAccessFile("F:/java/io/bw.txt", "rw");
        try {
            //查看初始文件指針位置
            System.out.println(accessFile.getFilePointer());
            //讀取5個字節的數據
            byte[] five = new byte[5];
            accessFile.read(five);
            System.out.println(new String(five));
            //查看獲取5個字節的數據之後指針的位置
            System.out.println(accessFile.getFilePointer());
            //跳過2個字節
            accessFile.skipBytes(2);
            //查看跳過2個字節之後指針的位置
            System.out.println(accessFile.getFilePointer());
            //把指針設置爲起始位置
            accessFile.seek(0);
            //獲取指針位置
            System.out.println(accessFile.getFilePointer());
            //獲取文件所有內容
            byte[] full = new byte[(int) accessFile.length()];
            accessFile.readFully(full);
            System.out.println(new String(full));
            //讀入所有文件之後指針位置
            System.out.println(accessFile.getFilePointer());
        } finally{
            accessFile.close();
        }



    }
}




//輸出結果:
//0
//my na
//5
//7
//0
//my name is brickworker
//22
//

我們可以總結出幾個規律:
①指針位置,用字節作爲單位移動
②初始情況下,指針位置爲0
③讀取完所有文件之後,指針的位置是file.length
④skip方法操作的時候,會把指針往後移動skip的字節
⑤seek方法操作的時候,會直接把指針指向目標位置
任何read操作,都會把指針移動read操作的字節長度
⑦空格是佔一個字節。(中文要看是什麼編碼才能確定字節長度)

多線程實現大文件輸入/輸出

在下面的代碼中實現了單線程讀取和多線程讀取:

package com.brickworkers.io;

import java.io.IOException;
import java.io.RandomAccessFile;
/**
 * 
 * @author Brickworker
 * Date:2017年5月16日下午3:29:19 
 * 關於類RandomAccessFileTest.java的描述:隨機訪問文件測試
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class RandomAccessFileTest {

    //單線程載入文件
    private void  sigleLoad() throws IOException{

        try(RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw")){
            byte[] bytes = new byte[(int) rw.length()];
            rw.read(bytes);
            System.out.println(new String(bytes));
        }

    }



    //多線程載入文件
    private void MultiLoad() throws IOException, InterruptedException{
        //把文件進行拆分成2分
        RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw");
        byte[] bytes1 = new byte[(int) (rw.length()/2)+1];//+1是爲了避免奇偶問題
        byte[] bytes2 = new byte[(int) (rw.length()/2)+1];

        Thread t1 = new Thread(() -> {
            try {
                //把指針指向起始位置
                rw.seek(0);
                rw.read(bytes1, 0, bytes1.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                //把指針指向中間位置
                rw.seek(bytes1.length);
                rw.read(bytes2);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();//必須要等待線程結束,不然關閉資源之後,會造成讀取不了的問題
        rw.close();
        System.out.println(new String(bytes1).trim()+new String(bytes2).trim());

    }


    public static void main(String[] args) throws IOException, InterruptedException{

        RandomAccessFileTest accessFileTest = new RandomAccessFileTest();
        accessFileTest.sigleLoad();
        accessFileTest.MultiLoad();

    }
}


之所以能用多線程來解決RandomAccessFile,是因爲文件指針可以標記操作位置。不論是輸入還是輸出,都可以實現:
輸入:在上面就已經實現了輸入方式。首先我們對文件大小是可知的。那麼我們可以用多線程來執行任務,一個線程那多少字節的文件,從文件指針哪裏開始,長度多少。這樣每個線程的任務就非常清晰。
輸出:同理,如果要實現多線程輸出,那麼我們只需要創立一個空文件,同時我們要預估計這個文件佔多大存儲空間。然後用多線程執行任務,一個線程寫入多少字節,從文件指針哪裏開始,長度多少。這樣也就可以完成輸出操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章