详解 RandomAccessFile 的使用以及使用场景分析

1 概述

  • java.io.RandomAccessFile

RandomAccessFile

  • RandomAccessFile 用于在文件的任意位置读写数据,并且不会消耗太多的内存。
  • RandomAccessFile 虽然属于 java.io 下的类,但它不是 InputStream 或者 OutputStream 的子类;它也不同于 FileInputStream 和 FileOutputStream。 FileInputStream 只能对文件进行读操作,而 FileOutputStream 只能对文件进行写操作。
  • RandomAccessFile 与输入流和输出流不同之处就是 RandomAccessFile 可以访问文件的任意地方同时支持文件的读和写,并且它通过 seek 方法实现在文件的任意位置读写访问。
  • RandomAccessFile 包含 InputStream 的三个 read 方法,也包含 OutputStream 的三个 write 方法。同时 RandomAccessFile 还包含一系列的 readXxx 和 writeXxx 方法完成输入输出。

2 关键点

  1. 通过 seek 方法设置开始随机读写文件的位置,以字节为单位。
  2. 通过 length 方法返回目标文件的长度,以字节为单位。

3 构造函数

  1. RandomAccessFile(File file, String mode):创建随机访问文件流,以从 File 参数指定的文件中读取,并可选择写入文件。
  2. RandomAccessFile(String name, String mode): 创建随机访问文件流,以从中指定的完整文件路径和名称读取,并可选择写入文件。

其中构造函数中 mode 参数传值介绍:

  1. r 代表以只读方式打开指定文件。
  2. rw 以读写方式打开指定文件。
  3. rws 读写方式打开,并对内容或元数据都同步写入底层存储设备。
  4. rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备。

4 具体使用

4.1 每次固定从文件中读取指定数量的字节,并通过 Consumer 接口对象进行处理

  • 封装功能如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 每次固定从文件中读取指定数量的字节,并通过 Consumer 接口对象进行处理
*
* @param file 指定文件
* @param bytesCount 每次读取的固定字节数,如果为 -1 表示读取全部的
* @param consumer Consumer 接口对象
*/
public static void fixRateReadBytes(File file, int bytesCount, Consumer<byte[]> consumer) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
long dataSize = randomAccessFile.length();
logger.debug(String.format("file length=%s, 求模=%s, getFilePointer=%s", dataSize, dataSize % 2, randomAccessFile.getFilePointer()));

if (bytesCount == -1) {
byte[] allData = new byte[(int)dataSize];
randomAccessFile.read(allData);
consumer.accept(allData);
}
AtomicInteger count = new AtomicInteger(0);
IntStream.range(0, (int) dataSize).forEach(i -> {
int index = count.incrementAndGet();
if (index == bytesCount) {
byte[] rowData = new byte[bytesCount];
try {
randomAccessFile.read(rowData);
consumer.accept(rowData);
} catch (Exception e) {
logger.error("fixRateReadBytes has error", e);
}
count.set(0);
}
});

} catch (Exception e) {
logger.error("fixRateReadBytes has error", e);
}
}
  • 测试如下
1
2
3
4
5
6
7
8
@Test
public void test_fixRateReadBytes() {
System.out.println(System.getProperty("file.encoding"));

FileUtils.fixRateReadBytes(new File("E:\\test.txt"), 2, data -> {
System.out.println(String.format("data:%s", new String(data)));
});
}
  • 执行结果如下
1
2
3
4
5
UTF-8
data:ab
data:ee
data:zz
data:ee
  • 其中 E:\test.txt 的文件内容如下
1
abeezzee

4.2 跳过指定的字节数后再读取指定字节的数据

  • 封装功能如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 跳过指定的字节数后再读取指定字节的数据
*
* @param file 指定文件
* @param skipBytesCount 跳过指定的字节数
* @param readBytes 读取的字节数
* @param consumer Consumer 接口对象
*/
public static void skipReadBytes(File file, int skipBytesCount, int readBytes, Consumer<byte[]> consumer) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
long dataSize = randomAccessFile.length();
logger.debug(String.format("file length=%s, 求模=%s, getFilePointer=%s", dataSize, dataSize % 2, randomAccessFile.getFilePointer()));
randomAccessFile.seek(skipBytesCount);

byte[] rowData = new byte[readBytes];
randomAccessFile.read(rowData);

consumer.accept(rowData);
} catch (Exception e) {
logger.error("skipReadBytes has error", e);
}
}
  • 测试如下
1
2
3
4
5
6
7
8
@Test
public void test_skipReadBytes() {
System.out.println(System.getProperty("file.encoding"));

FileUtils.skipReadBytes(new File("E:\\test.txt"), 2, 2, data -> {
System.out.println(String.format("data:%s", new String(data)));
});
}
  • 执行结果如下
1
2
UTF-8
data:ee
  • 其中 E:\test.txt 的文件内容如下
1
abeezzee

4.3 跳过指定的字节数后再 写入 指定字节的数据,最后返回被替换的字节数

  • 封装功能如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
*
* 跳过指定的字节数后再 写入 指定字节的数据,最后返回被替换的字节数
*
* @param file 指定的文件
* @param skipBytesCount 跳过指定的字节数
* @param writeBytes 写入的字节
* @return byte[] 被替换的字节
*/
public static byte[] skipWriteBytes(File file, int skipBytesCount, byte[] writeBytes) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
long dataSize = randomAccessFile.length();
logger.debug(String.format("file length=%s, 求模=%s, getFilePointer=%s", dataSize, dataSize % 2, randomAccessFile.getFilePointer()));
// 跳过的字节数
randomAccessFile.seek(skipBytesCount);

byte[] replaceBytes = new byte[writeBytes.length];
randomAccessFile.read(replaceBytes);
// 退回到读取字节前的位置
randomAccessFile.seek(skipBytesCount);

// 写入新的数据
randomAccessFile.write(writeBytes);

return replaceBytes;
} catch (Exception e) {
logger.error("skipWriteBytes has error", e);
return null;
}
}
  • 测试如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test_skipWriteBytes() {
System.out.println(System.getProperty("file.encoding"));

String dataString = "ee";
byte[] dataBytes = dataString.getBytes();

byte[] replaceBytes = FileUtils.skipWriteBytes(new File("E:\\test.txt"), 2, dataBytes);
System.out.println(String.format("replace data=%s", new String(replaceBytes)));

FileUtils.skipReadBytes(new File("E:\\test.txt"), 2, dataBytes.length, data -> {
System.out.println(String.format("data:%s", new String(data)));
});
}
  • 执行结果如下
1
2
3
UTF-8
replace data=eez
data:yin
  • 其中 E:\test.txt 的文件内容前后变化如下
1
abeezzee
1
abyinzee

4.4 向文件追加指定字节数据

  • 封装功能如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 向文件追加指定字节数据
*
* @param file 指定的文件
* @param writeBytes 追加的字节数据
*/
public static void appendBytes(File file, byte[] writeBytes) {
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
long dataSize = randomAccessFile.length();
logger.debug(String.format("file length=%s, 求模=%s, getFilePointer=%s", dataSize, dataSize % 2, randomAccessFile.getFilePointer()));
// 跳过的字节数
randomAccessFile.seek(dataSize);
// 写入新的数据
randomAccessFile.write(writeBytes);
} catch (Exception e) {
logger.error("appendBytes has error", e);
}
}
  • 测试如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test_appendBytes_1() throws Exception {
String dataString = "abc";
byte[] dataBytes = dataString.getBytes();

File file = new File("E:\\test.txt");
// 追加数据前
FileUtils.fixRateReadBytes(file, -1, data -> {
System.out.println(String.format("追加数据前 data=%s", new String(data)));
});

// 追加数据
FileUtils.appendBytes(file, dataBytes);

// 追加数据前
FileUtils.fixRateReadBytes(file, -1, data -> {
System.out.println(String.format("追加数据后 data=%s", new String(data)));
});
}
  • 执行结果如下
1
2
追加数据前 data=abyinzeeabc
追加数据后 data=abyinzeeabcabc

5 方法

常用的方法如下:

  • void close(): 关闭此随机访问文件流并释放与该流关联的所有系统资源。
  • FileChannel getChannel(): 返回与此文件关联的唯一 FileChannel 对象。
  • FileDescriptor getFD(): 返回与此流关联的不透明文件描述符对象。
  • long getFilePointer(): 返回此文件中的当前偏移量。
  • long length(): 返回此文件的长度。
  • int read(): 从此文件中读取一个数据字节。
  • int read(byte[] b): 将最多 b.length 个数据字节从此文件读入 byte 数组。
  • int read(byte[] b, int off, int len): 将最多 len 个数据字节从此文件读入 byte 数组。
  • boolean readBoolean(): 从此文件读取一个 boolean。
  • byte readByte(): 从此文件读取一个有符号的八位值。
  • char readChar(): 从此文件读取一个字符。
  • double readDouble(): 从此文件读取一个 double。
  • float readFloat(): 从此文件读取一个 float。
  • void readFully(byte[] b): 将 b.length 个字节从此文件读入 byte 数组,并从当前文件指针开始。
  • void readFully(byte[] b, int off, int len): 将正好 len 个字节从此文件读入 byte 数组,并从当前文件指针开始。
  • int readInt(): 从此文件读取一个有符号的 32 位整数。
  • String readLine(): 从此文件读取文本的下一行。
  • long readLong(): 从此文件读取一个有符号的 64 位整数。
  • short readShort(): 从此文件读取一个有符号的 16 位数。
  • int readUnsignedByte(): 从此文件读取一个无符号的八位数。
  • int readUnsignedShort(): 从此文件读取一个无符号的 16 位数。
  • String readUTF(): 从此文件读取一个字符串。
  • void seek(long pos): 设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。
  • void setLength(long newLength): 设置此文件的长度。
  • int skipBytes(int n): 尝试跳过输入的 n 个字节以丢弃跳过的字节。
  • void write(byte[] b): 将 b.length 个字节从指定 byte 数组写入到此文件,并从当前文件指针开始。
  • void write(byte[] b, int off, int len): 将 len 个字节从指定 byte 数组写入到此文件,并从偏移量 off 处开始。
  • void write(int b): 向此文件写入指定的字节。
  • 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,然后按八字节数量将该 long 值写入该文件,先定高字节。
  • void writeFloat(float v): 使用 Float 类中的 floatToIntBits 方法将浮点参数转换为一个 int,然后按四字节数量将该 int 值写入该文件,先写高字节。
  • void writeInt(int v): 按四个字节将 int 写入该文件,先写高字节。
  • void writeLong(long v): 按八个字节将 long 写入该文件,先写高字节。
  • void writeShort(int v): 按两个字节将 short 写入该文件,先写高字节。
  • void writeUTF(String str): 使用 modified UTF-8 编码以与机器无关的方式将一个字符串写入该文件。

6 使用场景

  1. 断点续传,记录已经下载的字节数,继续下载的时候,跳过已经下载的字节,继续进行下载。
  2. 向大文件(比如 100G 的文件)任意位置读取或者插入,修改指定内容。

6.1 断点续传

  1. 手机端向服务器端下载文件,初始请求关键字段: 手机标识,文件名称,字节偏移(seek 方法的参数 = 0)
  2. 突然网络中断,手机端统计共下载了 25%,500 个字节,还剩下 1500 个字节
  3. 手机端再次向服务器发送下载请求的时候,需要请求的关键字段:手机标识,文件名称,字节偏移(seek 方法的参数 = 500)

6.2 大文件读写

  1. 将数十亿商品详细信息存储在一个文件中,共计 10 G,然后在一个 hashMap 中存储了每个商品的 id 以及对应商品详细信息在文件中的偏移(offset)。
  2. 这样根据 id 找到 偏移(offset),就可以通过 seek(offset) 直接获取商品的详细信息。
Buy me a cup of coffee