RandomAccessFiIe 是 Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。与普通的输人/输出流不同的是,RandornAccessFile 支持 “随机访问” 的方式,程序可以直接跳转到文件的任意地方来读写数据。
因为 RandomAccessFile 可以自由访问文件的任意位置,所以如果我们希望只访问文件部分内容,而不是把文件从头读到尾,使用 RandomAccessFile 将是更好的选择。
与 OutputStream、Writer 等输出流不同的是,RandomAccessFile 允许自由定位文件记录指针,所以 RandomAccessFile 可以不从开始的地方开始输出,所以 RaridomAccessFile 可以向已存在的文件后追加内容。因此,如果程序需要向已存在的文件后追加附容,则应该使用 RandomAccessFile。
RandomAccessFile 对象也包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头(也就是 0 处),当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandomAccessFile 可以自由移动该圮录指针,既可以向前移动,也可以向后移动。RandomAccessFile 包含了如下两个方法来操作文件记录指针。
long getFilePointer():返回文件记录指针的当前位置。
void seek(long pos):将文件记录指针定位到 pos 位置。
RandomAccessFile 既可以读文件,也可以写,所以它既包含了完全类似于 InputStream 的 3 个 read() 方法,其用法和 InputStream 的 3 个 read() 方法完全一样。也包含了完全类似于 OutputStream 的 3 个write() 方法,’其用法和 OutputStream 的 3 个 write() 方法完全一样。除此之外,RandomAccessFile 还包含了一系列的 readXxx() 和 writeXxx() 方法来完成输入、输出。
计算机里的 “随机访问” 是一个很奇怪的词,对于汉语而言,随机访问是具有不确定性的——具有一会儿访问这里,一会儿访问那里的意思,如果按这种方式采理解 “随机访问”,那么就会对所谓的 “随机访问” 方式感到十分迷惑,这也是十多年前笔者刚接触 RAM(Random Access Memory,即内存)感到万分迷惑的地方。实际上,“随机访问” 是由 Random Access 两个单词翻译而来,而 Random 在英语里不仅有随机的意思,还有任意的意思———如果能这样理解 Random,就可以更好地理解 Random Access了——应该是任意访问,而不是随机访问,也就是说,RAM 是可以自由访问任意存储点的存储器(与磁盘、磁带等需要寻道、倒带才可访问指定存储点等存储器相区分);而 RandomAccessFile 的含义是可以自由访问文件的任意地方(与 InputStreamReader 需要依次向后读取相区分),所以 RandomAccessFil e的含义决不是 “随机访问”,而应该是 “任意访问”。在后来的日子里,笔者无数次发现一些计算机专业翻译得如此让人深恶痛绝,于是造成了很多人觉得 IT 行业较难的后果;再后来,笔者决定从此不看被翻译后的 IT 技术文章,要么看原版 IT 技术文章,要么就直接看国内的 IT 技术文章。
RandomAccessFile 类有两个构造器,其实这两个构造器基本相同,只是指定文件的形式不同而已——一个使用 String 参数来指定文件名,一个使用 File 参数来指定文件本身。除此之外,创建 RandomAccessFile 对象时还需要指定一个 mode 参数,该参数指定 RandomAcccssFile 的访问模式,该参数有如下4个值。
"r":以只读方式打开指定文件。如果试图对该 RandomAccessFile 执行写入方法,都将抛出 IOException 异常。
"rw":以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
"rws":以读、写方式打开指定文件。相对于 "rw" 模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
"rwd":以读、写方式打开指定文件。相对于 "rw" 模式,还要求对文件内容的每个更新都同步写入到底层存储设备。
下面程序使用了 RandomAccessFile 来访问指定的中间部分数据。
public class RandomAccessFileTest
{
public static void main(String[] args)
{
try(
RandomAccessFile raf = new RandomAccessFile(
"RandomAccessFileTest.java" , "r"))
{
// 获取RandomAccessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:"
+ raf.getFilePointer());
// 移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
上面程序中的第 6 代码创建了一个 RandomAccessFile 对象,该对象以只读方式打开了 RandomAoocssFileTest.java 文件,这意味着该 RandomAccessFile 对象只能读取文件内容,不能执行写入。
程序中第 13 行代码将文件记录指针定位到 300 处,也就是说,程序将从 300 字节处开始读写,程序接下来的部分与使用 InputSteam 读取并没有太大的区别.运行上面程序,将看到程序只读取后面部分的效果。
下面程序示范了如何向指定文件后追加内容,为了追加内容,程序应该先将记录指针移动到文件最后,然后开始向文件中输出内容。
public class AppendContent
{
public static void main(String[] args)
{
try(
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
{
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
上面程序中的第 10 行代码先以读、写方式创建了一个 RandomAccessFile 对象,第11 行代码将 RandomAccessFile 对象的记录指针移动到最后;接下来使用 RandomAccessFile 执行输出,与使用 OutputStream 或 Writer 执行输出并没有太大区别。
每运行上面程序一次,都可以看到 out.txt 文件中多一行 “追加的内容!” 字符串,程序在该字符串后使用 “\r\n” 是为了控制换行。
RandomAccessFile 依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面。
下面程序实现了向指定文件、指定位置插入内容的功能。
public class InsertContent
{
public static void insert(String fileName , long pos
, String insertContent) throws IOException
{
File tmp = File.createTempFile("tmp" , null);
tmp.deleteOnExit();
try(
RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
// 创建一个临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp))
{
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存------
byte[] bbuf = new byte[64];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 将读取的数据写入临时文件
tmpOut.write(bbuf , 0 , hasRead);
}
// ----------下面代码插入内容----------
// 把文件记录指针重新定位到pos位置
raf.seek(pos);
// 追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0 )
{
raf.write(bbuf , 0 , hasRead);
}
}
}
public static void main(String[] args)
throws IOException
{
insert("InsertContent.java" , 45 , "插入的内容\r\n");
}
}
上面程序中使用 File 的 createTempFile(String pretix, String suffix) 方法创建了一个临时文件(该临时文件将在 JVM 退出时被删除),用以保存被插入文件的插入点后面的内容。程序先将文件中插入点后的内容读入临时文件中,然后重新定位到插入点,将需要插入的内容添加到文件后面,最后将临时文件的内容添加到文件后面,通过这个过程就可以向指定文件、指定位置插入内容。
每次运行上面程序,都会看到向 InsenContent.java 中插入了一行字符串。
多线程断点的网络下载工具(如 FlashGet 等)就可通过 RandomAccessFile 类来实现,所有的下载工具在下载开始时都会建立两个文件:一个是与被下载文件大小相同的空文件,一个是记录文件指针的位置支件,下载工具用多条线程启动输入流来读取网络数据,并使用 RandomAccessFile 将从网络上读取的数据写入前面建立的空文件中,每写一些数据后,记录文件指针的工件就分别记下每个 RandomAccessFile 当前的文件指针住置——网络断开后,再次开始下载时,每个 RandomAccessFile 都根据记录文件指针的文件中记录的位置继续向下写数据。笔者将会在介绍多线程和网络知识之后,更加详细地介绍如何开发类似于 FlashGet 的多践程断点传输工具。