🍧 Peach

蜜桃学代码

IO - RandomAccessFile 任意访问文件


一、使用场景

📌 RandomAccessFile 既可以读取文件内容,也可以向文件输出数据,是一个更接近于操作系统API的封装类。

  • 与普通的输入/输出流不同的是, 它支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。(与InputStream、Reader需要依次向后读取相区分)
  • 它将文件内容存储在一个大型byte数组中。它存在着指向该隐含byte数组的光标或索引,称为文件指针,该指针位置可以通过seek方法设置

使用场景:

  %%{
    init: {
      'themeVariables': {
         'fontSize': '13px'
       }
    }
  }%%
  graph LR
  T(["RandomAccessFile 的使用场景"]):::p
  T --> A("仅访问文件部分内容"):::lp
  T --> B("向已存在的文件后追加内容"):::lp

  classDef p fill:#ddaebd
  classDef b fill:#aab7d2
  classDef g fill:#9ac5bb
  classDef lp fill:#f4e4e9
  classDef lb fill:#d9dfeb
  classDef lg fill:#ddebe6
  classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px
  • 只访问文件部分内容,而不是把文件从头读到尾,使用RandomAccessFile更好。
  • 向已存在的文件后追加内容:与OutputStream、Writer等输出流不同的是,RandomAccessFile允许自由定位文件记录指针,所以RandomAccessFile可以不从开始的地方开始输出,所以RandomAccessFile可以向已存在的文件后追加内容。


二、方法

java.io.RandomAccessFile

① 构造方法

方法 说明
RandomAccessFile(File file, String mode) 使用File参数来指定文件本身(打开文件)
RandomAccessFile(String name, String mode) 使用String参数来指定文件名(打开文件)
  • RandomAccessFile的4种访问模式:

    参数值 说明
    r 只读方式打开指定文件。若试图对该RamdomAceessFile执行写入方法,都将执行IOException异常
    rw 读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
    rws 以读、写方式打开指定文件。相对于”rw”模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
    rwd 以读、写方式打开指定文件。相对于”rw”模式,还要求对文件内容的每个更新都同步写入到底层存储设备。
    * rwd模式可用于减少执行的I/O操作数量。

② 操作文件指针

RandomAccessFile对象的记录指针:指向当前读写的位置,各种read/write操作都会自动更新该指针(移动单位:字节)。

方法 说明
long getFilePointer() 返回文件记录指针的当前位置。
void seek(long pos) 将文件记录指针定位到pos位置。
void skipBytes(int n) 使文件指针向前移动指定的n个字节。

* 当程序新创建一个RandomAccessFile对象时,该对象的文件记录指针位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。

③ 读写方法

RandomAccessFile实现了DataInput/DataOutput接口:

* 读取

// ------ 基本读取 --------------------------------------------
int read(); // 从该文件读取一个字节的数据。
int read(byte[] b); // 从文件中最多读取b.length个字节的数据,并将其存储在字节数组b中。
int read(byte[] b, int off, int len); // 从指定的off位置开始,从文件中最多读取len个字节的数据。

// ------ 读取指定类型的数值 -----------------------------------
boolean readBoolean(); // 读取一个boolean
byte readByte(); // 读取一个有符号的8位值
char readChar(); // 读取一个字符
double readDouble(); // 读取一个double
float readFloat(); // 读取一个float
int readInt(); // 读取一个有符号的32位整数
long readLong(); // 读取一个有符号的64位整数
short readShort(); // 读取一个有符号的16位数
int readUnsignedByte(); // 读取一个无符号的8位数
int readUnsignedShort(); // 读取一个无符号的16位数

// ------ 读取一行字符串 ---------------------------------------
String readLine()

// ------ 读取中文字符串 ---------------------------------------
String readUTF() // 以上函数均按照字节码读取的,若有中文出现,readLine()会显示乱码,可使用readUTF()读取。
// 它从当前文件指针开始读取前两个字节,类似于使用readUnsignedShort。


* 写入

// ------ 基本读取 --------------------------------------------
void write() // 在该文件的当前指针位置写入一个字节,并覆盖原有的字节。
void write(byte[] b) // 将b.length字节从指定的字节数组写入文件(从当前文件指针开始)
void write(byte[] b, int off, int len) // 从参数off指定的起始位置,将指定字节数组中的len个字节写入文件。

// ------ 写入固定类型的数值 -----------------------------------
void writeBoolean(boolean v); // 按单字节值将boolean写入该文件
void writeByte(int v); // 按单字节值将byte写入该文件
void writeBytes(String s); // 按字节序列将该字符串写入该文件
void writeChar(int v); // 按双字节值将char写入该文件,先写高字节
void writeChars(String s); // 按字符序列将一个字符串写入该文件
void writeDouble(double v); /* 使用Double类中的doubleToLongBits方法将双精度参数转换为一个long,
然后按8字节数量将该long值写入该文件,先写高字节 */
void writeFloat(float v); /* 使用Float类中的floatToIntBits方法将浮点参数转换为一个int,
然后按4字节数量将该int值写入该文件,先写高字节 */
void writeInt(int v); // 按4个字节将int写入该文件,先写高字节
void writeLong(long v); // 按8个字节将long写入该文件,先写高字节
void writeShort(int v); // 按2个字节将short写入该文件,先写高字节

// ------ 写入一个字符串(按字节写入,若中文会乱码)----------------
writeBytes()
writeChars() // 按照双字节写入,即一个字符占用2个字节。

// ------ 写入中文字符串 ---------------------------------------
String writeUTF() /* 它把2个字节从文件的当前文件指针写入到此文件,类似于使用writeShort方法
并给定要跟随的字节数。此值是实际写出的字节数,而不是该字符串的长度。 */


三、实例

① 访问指定的中间部分数据

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的文件记录指针的位置(将从300字节处开始读、写)
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();
}
}
}

// 运行上面程序,将看到程序只读取后面部分的效果。

② 向指定文件后追加内容

public class AppendContent {

public static void main(String[] args) {
try(
// 🔹 以读、写方式打开一个 RandomAccessFile 对象
RandomAccessFile raf=new RandomAccessFile("out.txt" , "rw"))
{
// 🔹 将 RandomAccessFile 对象的记录指针 移动到out.txt文件的最后
raf.seek(raf.length());
// 🔹 使用 RandomAccessFile 执行输出
raf.write("追加的内容!\r\n".getBytes());
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}

// 每运行上面程序一次,都可以看到out.txt文件中多一行“追加的内容!”字符串。

③ 向指定文件、指定位置插入内容

/** 
* 程序先将文件中插入点后的内容读入临时文件中,然后重新定位到插入点,将需要插入的内容添加到文件后面,
* 最后将临时文件的内容添加到文件后面,通过这个过程就可以向指定文件、指定位置插入内容。
*/
public class InsertContent {
public static void insert(String fileName, long pos, String insertContent)
throws IOException {

// 🔹 创建一个临时文件来保存插入点后的数据(该临时文件将在JVM退出时被删除)
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");
}
}

// 每次运行上面程序,都会看到向InsertContent.java中插入了一行字符串


四、应用

  • 多线程断点的网络下载工具(如FlashGet等)就可通过RandomAccessFile类来实现:

      %%{
      init: {
        'themeVariables': {
           'fontSize': '13px'
         }
      }
    }%%
    graph LR 
    subgraph "[ 多线程断点的网络下载工具 ]"
    A[["与被下载文件大
    小相同的空文件"]]:::lp B[["记录文件指针
    的位置文件"]]:::lb end net("网络数据"):::info -.->|"写入"| A A --> |"每写一些数据
    记下文件指针位置"|B B -.-> |"断网后再次下载
    根据记录的位置
    继续向下写数据
    "|A classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,stroke-dasharray: 3 3, stroke-width: 2px, stroke: #676666

    下载工具用多条线程启动输入流来读取网络数据,并使用RandomAccessFile将从网络上读取的数据写入前面建立的空文件中,每写一些数据后,记录文件指针的文件就分别记下每个RandomAccessFile当前的文件指针位置




- end -


🔖 笔记来自: