為什么隨機讀寫比按順序讀寫文件就是快?
一、背景介紹
RandomAccessFile 類,也被稱為隨機訪問文件類。
RandomAccessFile 可以說是 Java 體系中功能最為豐富的文件操作類,相比之前介紹的通過字節(jié)流或者字符流接口方式讀寫文件,RandomAccessFile 類可以跳轉(zhuǎn)到文件的任意位置處進行讀寫數(shù)據(jù),而無需把文件從頭讀到尾,因此讀寫性能上要快很多,但是該類僅限于操作文件,不能訪問其他的 IO 設(shè)備,如網(wǎng)絡(luò)、內(nèi)存映像等。
所以如果需要訪問文件的部分內(nèi)容,而不是把文件從頭讀到尾,使用 RandomAccessFile 將是更好的選擇。
實際上,雖然RandomAccessFile類具備隨機讀寫數(shù)據(jù)的功能,但是它既不是InputStream的子類,也不是OutputStream的子類,絕大部分的方法都是從零開始寫的,這可能是因為 RandomAccessFile 需要在文件里面前后移動,它的行為與其它的 I/O 類有著根本性的不同,所以相對比較獨立。
RandomAccessFile對象類中內(nèi)置了一個位置指示器,可以指向當(dāng)前讀寫處的位置,當(dāng)讀寫 n 個字節(jié)后,文件指示器將指向這 n 個字節(jié)后的下一個字節(jié)處。剛打開文件時,文件指示器指向文件的開頭處,當(dāng)移動文件指示器到新的位置時,隨后的讀寫將從新的位置開始,這是它與其他的文件讀寫方式最大的不同。
基本上,RandomAccessFile的工作方式是,結(jié)合DataInputStream和DataOutputStream類完成數(shù)據(jù)的讀寫,再加上自己的一些方法,比如定位用的 getFilePointer方法,在文件里移動用的seek方法,以及判斷文件大小length方法、跳過多少字節(jié)數(shù)的skipBytes方法等,來完成文件的隨機訪問和讀寫操作。
具體怎么使用呢,我們一起來看看!
二、RandomAccessFile 類基本介紹
下面先來看看一個簡單的例子。
// 獲取隨機訪問文件對象
RandomAccessFile raf = new RandomAccessFile(new File("randomFileDemo.txt"), "rw");
for (int i = 0; i < 10; i++) {
// 寫入數(shù)據(jù),1個int占4個字節(jié)
raf.writeInt(i);
}
raf.close();
System.out.println("================修改前的內(nèi)容===============" );
// 重新獲取隨機訪問文件對象
raf = new RandomAccessFile(new File("randomFileDemo.txt"), "rw");
for (int i = 0; i < 10; i++) {
System.out.println("Value:" + i + ": " + raf.readInt());
}
raf.close();
// 重新獲取隨機訪問文件對象
raf = new RandomAccessFile(new File("randomFileDemo.txt"), "rw");
// 設(shè)置文件指針偏移量,從0開始,直接將文件指針移到第6個int數(shù)據(jù)后面(1個int占4個字節(jié))
raf.seek(5 * 4);
//覆蓋第6個int數(shù)據(jù)
raf.writeInt(16);
raf.close();
System.out.println("================修改后的內(nèi)容===============" );
// 重新獲取隨機訪問文件對象
raf = new RandomAccessFile(new File("randomFileDemo.txt"), "rw");
for (int i = 0; i < 10; i++) {
System.out.println("Value:" + i + ": " + raf.readInt());
}
raf.close();
輸出結(jié)果:
================修改前的內(nèi)容===============
Value:0: 0
Value:1: 1
Value:2: 2
Value:3: 3
Value:4: 4
Value:5: 5
Value:6: 6
Value:7: 7
Value:8: 8
Value:9: 9
================修改后的內(nèi)容===============
Value:0: 0
Value:1: 1
Value:2: 2
Value:3: 3
Value:4: 4
Value:5: 16
Value:6: 6
Value:7: 7
Value:8: 8
Value:9: 9
RandomAccessFile類為用戶提供了兩種構(gòu)造方法,具體操作方式如下:
// 第一種構(gòu)造方法:指定 file 對象和讀寫模式
RandomAccessFile raf = new RandomAccessFile(File file, String mode);
// 第二種構(gòu)造方法:指定 filename 路徑和讀寫模式
RandomAccessFile raf = new RandomAccessFile(String filename, String mode);
其實第二種構(gòu)造方法也是new File()出來,再調(diào)用第一種構(gòu)造方法,兩者都可以獲取隨機訪問文件對象。
至于mode,表示以何種方式打開文件,Java給開發(fā)者提供了四種mode值,具體解釋如下!
圖片
值得注意的地方是,"rw"模式下,Java 并不強求指定的路徑下一定存在某個文件,假如文件不存在,會自動創(chuàng)建。
RandomAccessFile類為用戶提供的方法比較多,我們可以關(guān)注下幾個重要的方法即可,詳細(xì)方法如下圖!
圖片
方法的使用,可以參考如下樣例:
RandomAccessFile file = new RandomAccessFile("file.txt", "rw");
// 以下向file文件中寫數(shù)據(jù)
file.writeInt(20);// 占4個字節(jié)
file.writeDouble(8.236598);// 占8個字節(jié)
file.writeUTF("這是一個UTF字符串");// 這個長度寫在當(dāng)前文件指針的前兩個字節(jié)處,可用readShort()讀取
file.writeBoolean(true);// 占1個字節(jié)
file.writeShort(395);// 占2個字節(jié)
file.writeLong(2325451L);// 占8個字節(jié)
file.writeUTF("又是一個UTF字符串");
file.writeFloat(35.5f);// 占4個字節(jié)
file.writeChar('a');// 占2個字節(jié)
file.seek(0);// 把文件指針位置設(shè)置到文件起始處
// 以下從file文件中讀數(shù)據(jù),要注意文件指針的位置
System.out.println("——————從file文件指定位置讀數(shù)據(jù)——————");
System.out.println(file.readInt());
System.out.println(file.readDouble());
System.out.println(file.readUTF());
file.skipBytes(3);// 將文件指針跳過3個字節(jié),本例中即跳過了一個boolean值和short值。
System.out.println(file.readLong());
file.skipBytes(file.readShort()); // 跳過文件中“又是一個UTF字符串”所占字節(jié),注意readShort()方法會移動文件指針,所以不用加2。
System.out.println(file.readFloat());
//以下演示文件復(fù)制操作
System.out.println("——————文件復(fù)制(從file到fileCopy)——————");
file.seek(0);
RandomAccessFile fileCopy=new RandomAccessFile("fileCopy.txt","rw");
int len=(int)file.length();//取得文件長度(字節(jié)數(shù))
byte[] b=newbyte[len];
file.readFully(b);//讀取全部內(nèi)容
fileCopy.write(b);//全部寫入目標(biāo)文件
System.out.println("復(fù)制完成!");
根據(jù)以上的方法介紹,我們可以利用RandomAccessFile實現(xiàn)一個在任意位置插入數(shù)據(jù)的操作,具體實例如下:
public class RandomAccessFileTest1 {
/**
* 插入數(shù)據(jù)
* @param skip 跳過多少過字節(jié)進行插入數(shù)據(jù)
* @param str 要插入的字符串
* @param fileName 文件路徑
*/
public static void insert(long skip, String str, String fileName){
try {
RandomAccessFile raf = new RandomAccessFile(fileName,"rw");
if(skip < 0 || skip > raf.length()){
System.out.println("跳過字節(jié)數(shù)無效");
return;
}
byte[] b = str.getBytes();
raf.setLength(raf.length() + b.length);
// 將尾部數(shù)據(jù)進行遷移
for(long i = raf.length() - 1; i > b.length + skip - 1; i--){
raf.seek(i - b.length);
byte temp = raf.readByte();
raf.seek(i);
raf.writeByte(temp);
}
// 從指定的位置,開始覆寫數(shù)據(jù)
raf.seek(skip);
raf.write(b);
raf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 方法測試
* @param args
* @throws Exception
*/
public static void main(String[] args) {
insert(0, "一起學(xué)習(xí)Java", "test.txt");
insert(0, "Hello,", "test.txt");
}
}
文件內(nèi)容結(jié)果如下:
Hello,一起學(xué)習(xí)Java
三、RandomAccessFile 類的應(yīng)用
在實際的開發(fā)過程中,RandomAccessFile 的一個重要應(yīng)用場景就是網(wǎng)絡(luò)請求中的文件多線程下載及斷點續(xù)傳。
首先將文件分成幾塊,然后每塊用不同的線程進行下載,下面是一個利用多線程在寫文件時的例子:
public class RandomAccessFileTest2 {
public static void main(String[] args) throws Exception {
// 預(yù)分配文件所占的磁盤空間,磁盤中會創(chuàng)建一個指定大小的文件
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
raf.setLength(1024*1024); // 預(yù)分配 1M 的文件空間
raf.close();
// 所要寫入的文件內(nèi)容
String s1 = "第一個字符串";
String s2 = "第二個字符串";
String s3 = "第三個字符串";
String s4 = "第四個字符串";
String s5 = "第五個字符串";
// 利用多線程同時寫入一個文件
new FileWriteThread(1024*1,s1.getBytes()).start(); // 從文件的1024字節(jié)之后開始寫入數(shù)據(jù)
new FileWriteThread(1024*2,s2.getBytes()).start(); // 從文件的2048字節(jié)之后開始寫入數(shù)據(jù)
new FileWriteThread(1024*3,s3.getBytes()).start(); // 從文件的3072字節(jié)之后開始寫入數(shù)據(jù)
new FileWriteThread(1024*4,s4.getBytes()).start(); // 從文件的4096字節(jié)之后開始寫入數(shù)據(jù)
new FileWriteThread(1024*5,s5.getBytes()).start(); // 從文件的5120字節(jié)之后開始寫入數(shù)據(jù)
}
}
class FileWriteThread extends Thread{
privateint skip;
privatebyte[] content;
public FileWriteThread(int skip,byte[] content){
this.skip = skip;
this.content = content;
}
@Override
public void run(){
RandomAccessFile raf = null;
try {
// 利用線程在文件的指定位置寫入指定數(shù)據(jù)
raf = new RandomAccessFile("test.txt", "rw");
raf.seek(skip);
raf.write(content);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
raf.close();
} catch (Exception e) {
}
}
}
}
四、小結(jié)
本文主要圍繞 RandomAccessFile 類的基本概念和常用方法,做了一次簡單的知識總結(jié),該類是 IO 流體系中功能最豐富的文件內(nèi)容訪問類,既可以讀取文件中任意位置的內(nèi)容,也可以向文件任意位置寫入數(shù)據(jù)。
當(dāng)然 RandomAccessFile 當(dāng)讀寫大文件的時候,會出現(xiàn)內(nèi)存溢出問題,此時可以采用內(nèi)存映射文件方式進行讀寫數(shù)據(jù),關(guān)于技術(shù)會在后期的文章中進行介紹。
五、參考
1、https://www.cnblogs.com/xrq730/p/4888288.html
2、https://blog.csdn.net/akon_vm/article/details/7429245