Java IO 文件处理类RandomAccessFile

1. RandomAccessFile介绍

RandomAccessFile支持对文件的读取和写入随机访问(其他的输入流或者输出流只能执行一种操作,要么输入,要么输出)。RandomAccessFile把随机访问的文件对象看作存储在文件系统中的一个大型byte数组,然后通过指向该
byte数组的光标或者索引(即:文件指针FilePointer)在该数组任意位置读取或者写入数据。

随机访问:可以跳转到文件的任意位置出进行读取

输入操作从文件指针开始读取字节(以字节为单位进行读取),并随着对字节的读取而前移此文件指针。如果RandomAccessFile访问文件以读取/写入模式创建,则输出操作可以使用;输出操作从文件之后指针开始写入字节,并随着对字节的写入而前移此文件指针。

在磁盘和内存中,所有的存储都是以右边为开始点(低),左边为结束点(高),字节的读取或者写入都是从右到左的顺序。

缺点:该类仅限于操作文件,不能访问其他的I/O设备,如网络、内存映像等。

2. 构造方法

  • RandomAccessFil(File file, String mode)

  • RandomAccessFil(String name, String mode)

**mode**:表示以什么模式创建读写流,此参数右固定的输入值,必须是:rrwrwsrwd其中一个。

  • r: 以只读方式打开文件,如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException

  • rw:以读取、写入方式打开文件,如果该文件不存在,则尝试创建文件

  • rws:以读取、写入方式打开文件,相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情况下(rw模式)是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候才真正写入到文件

  • rwd:与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据

3. 文件指针FilePointer

RandomAccessFile包含三个方法来操作文件记录指针:

  • long getFilePointer: 返回文件记录指针的当前位置

  • void seek(long pos):将文件记录指针定位到pos位置

  • skipBytes(int n):尝试跳过输入的n个字节,当n大于文件的字节数读取会抛出EOFException,如果n为负数,则不跳过任何字节

其他常用方法:

  • FileDescriptor getFD():返回文件的文件描述符

  • native void setLength(long newLength): 设置文件的长度

  • native long length():返回文件的长度

  • close(): RandomAccessFile对文件访问操作全部结束后,需要调用close方法来释放与其关联的所有资源

  1. setLength:为什么需要设置文件长度?
  • 可以理解为这是一个”动态数组”,假设你想要设置newLength长度
  • 如果长度小于实际长度(length方法返回的值),文件被截断,并且如果getFilePointer大于newLength,那么它就变成newLength
  • 如果长度大于实际长度(length方法返回的值),则该文件被扩展,再次情况下,未定义文件扩展部分的内容
  • seek方法设置的片一两,下一次的读写将从这个文件开始,偏移量的设置可能会超出文件末尾,这并不会改变什么,但是一旦在这个超出文件末尾的偏移量写入数据,长度将会改变

4. 读方法介绍

  1. int read(): 从此文件中读取一个数据字节

  2. int read(byte[] b): 将最多b.length个数据字节从文件读入byte数组

  3. int read(byte[]b, int off, int len): 将最多len个数据字节从此文件中读入byte数组

  4. boolean readBoolean(): 从此文件中读取一个boolean,如果是0则是false,否则为true

  5. byte readByte(): 从此文件中读取一个有符号的八位值

  6. Char readChar(): 从此文件中读取一个字符

  7. double readDouble(): 从此文件中读取一个double

  8. float readFloat(): 从此文件中读取一个Float

  9. void readFull(byte[] b): 将b.length个字节从此文件读入byte数组,并从当前文件指针开始

  10. void read(byte[]b, int off, int len): 将正好len个字节从此文件读入byte数组,并从当前文件指针开始

  11. int readInt(): 从此文件中读取一个有符号的32位整数

  12. String readLine(): 从此文件中读取文本的下一行

  13. long readLong(): 从此文件中读取一个有符号的64位数

  14. short readShort(): 从此文件中读取一个无符号的16位数

5. 写方法介绍

  1. void write(byte[] b): 将 b.length 个字节从指定 byte 数组写入到此文件,并从当前文件指针开始。
  1. void write(byte[] b, int off, int len): 将 len 个字节从指定 byte 数组写入到此文件,并从偏移量 off 处开始
  1. void write(int b): 向此文件写入指定的字节
  1. void write(boolean b): 按单字节值将 boolean 写入该文件。
  1. void writeByte(int b): 单字节值将 byte 写入该文件。
  1. void writeBytes(String s): 字节序列将该字符串写入该文件。
  1. void writeChar(int v): 按双字节值将 char 写入该文件,先写高字节。
  1. void writeDouble(double v): 使用 Double 类中的 doubleToLongBits 方法将双精度参数转换为一个 long,然后按八字节数量将该 long 值写入该文件,先定高字节
  1. void writeFloat(float v): 用 Float 类中的 floatToIntBits 方法将浮点参数转换为一个 int,然后按四字节数量将该 float 值写入该文件,先定高字节

6. 示例代码

6.1 基本方法

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package org.ygb.file;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* @author xiaoyuge
*/
public class RandomAccessFileDemo {
public static void main(String[] args) {
RandomAccessFile randomAccessFile = null;
try {
//创建可读写流
randomAccessFile = new RandomAccessFile("/Users/xiaoyuge/Desktop/random_access_file.txt", "rw");
//写入数据
for (int i = 0; i < 10; i++) {
randomAccessFile.write(("设备名: 测试"+i+", 设备数量:"+i+"\n\r").getBytes());
}
// randomAccessFile.writeDouble(12);
//读取数据; 读时指针重新设置位开始位置,事实上可以从文件的任意位置开始
randomAccessFile.seek(0);
byte[] b = new byte[1024];
int len;
while ((len = randomAccessFile.read(b)) != -1){
System.out.println(new String(b, 0, len));
}
// System.out.println("readDouble:: "+ randomAccessFile.readDouble());

//RandomAccessFile 的记录指针放在文件末尾,用于追加内容
randomAccessFile.seek(randomAccessFile.length());
randomAccessFile.write(("设备名: 测试"+10+", 设备数量:"+10+"\n\r").getBytes());

//在任意位置写入
int pos = 120;
String insertStr = "-----------------------";
//跳到指定位置
randomAccessFile.seek(pos);
//先把该位置后的内容缓存起来,防止被覆盖
List<byte[]> bytes = new ArrayList<>();
byte[] bs1 = new byte[1024];
while (randomAccessFile.read(bs1) != -1){
bytes.add(bs1);
}
//再次回到指定位置
randomAccessFile.seek(pos);
//插入内容
randomAccessFile.write(insertStr.getBytes(StandardCharsets.UTF_8));
//最后把缓存的内容写入
for (byte[] cacheStr : bytes) {
randomAccessFile.write(cacheStr);
}
} catch (Exception ex) {
ex.printStackTrace();
}finally {
//关闭流
if (randomAccessFile != null){
try {
randomAccessFile.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
}

可以查看生成的文件内容如下:

6.2 多线程下载文件

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package org.ygb.file;

import java.io.File;
import java.io.RandomAccessFile;

/**
* @author xiaoyuge
*/
public class DownloadThread extends Thread {
//开始位置
private long start;
//源文件
private File src;
//总量
private long total;
//目标文件
private File target;

public DownloadThread(long start, File src, File target, long total) {
this.start = start;
this.src = src;
this.target = target;
this.total = total;
}

@Override
public void run() {
try {
//创建输入流关联源,因为要指定位置读和写,需要用随机访问流
RandomAccessFile src = new RandomAccessFile(this.src, "rw");
RandomAccessFile target = new RandomAccessFile(this.target, "rw");
//源和目标文件都要从start开始
src.seek(start);
target.seek(start);
//开始读和写
byte[] arr = new byte[1024];
int len;
long count = 0;
while ((len = src.read(arr)) != -1) {
//分成三种情况
if (len + count > this.total) {
//1. 读取数量超过总量时,需要改变len
len = (int) (total - count);
target.write(arr, 0, len);
//结束读写操作
break;
} else if (len + count < total) {
//2. 还没有到总量,直接写入
target.write(arr, 0, len);
//计数器累加
count += arr.length;
} else {
//3. 刚好到总量
target.write(arr, 0, len);
//结束读写
break;
}
}

src.close();
target.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
File src = new File("/Users/xiaoyuge/Desktop/src.txt");
File target = new File("/Users/xiaoyuge/Desktop/target.txt");
//获取源文件的大小
long len = src.length();
//拆分成2个任务执行
new DownloadThread(0, src, target, len / 2).start();
new DownloadThread(len / 2, src, target, len - (len / 2)).start();
}
}

6.3 大文件分割合并

面向对象思想封装:分割文件并合并

  1. 根据数据源src,输出文件夹destDir,分割后文件存储路径destPaths,块大小blockSize, size块数,初始化分割文件对象

  2. 构造方法(数据源,输出源);构造方法(数据源,输出源, 分割块大小)

  3. 初始化文件分割对象时,调用init()方法计算分块数,切分后所有文件名,如果保存切分后的文件目录不存在,则创建

  4. 调用split()方法根据文件总长度以及分块大小blockSize,调用splitDetails()将分割后的每一块文件输出到destDir下面

  5. 调用merge(String destPath)将destPaths每一个分割文件声明一个缓冲输入流,保存到Vector中,然后使用SequenceInputStream将Vector合并到一个输入流中,最后使用destPath,创建一个缓冲输出流,将合并输入流读取字节,全部写入输出流中

代码示例:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package org.ygb.file;


import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Vector;

/**
* @author xiaoyuge
*/
public class SplitFileUtils {

private File src; //输入源
private String destDir;//分割子文件输出的目录
private List<String> destPaths;//保存每一个分割子文件的路径
private int blockSize; //分割大小
private int size; // 分割总数

public SplitFileUtils(String src, String destDir) {
this(src, destDir, 1024);
}

public SplitFileUtils(String src, String destDir, int blockSize) {
this.src = new File(src); //初始化输入流
this.destDir = destDir; //初始化分割子文件输出的目录
this.destPaths = new ArrayList<>(16);//初始化保存分割子文件的路径容器
this.blockSize = blockSize; //初始化分割大小

//初始化对象参数
init();
System.out.println("初始化完成");

}

/**
* 初始化参数
*/
private void init() {
//文件总长度
long len = this.src.length();
//切分总数
this.size = (int) Math.ceil(len * 1.0 / this.blockSize);

for (int i = 0; i < size; i++) {
this.destPaths.add(this.destDir + File.separator + i + "-" + this.src.getName());
}
//如果保存切分子文件目录不存在,则创建
if (len > 0) {
File destFile = new File(this.destDir);
if (!destFile.exists()) {
destFile.mkdirs();
}
}
}

/**
* 切分
*/
public void split() {
//文件总长度
long len = this.src.length();
//起始位置和实际大小
int beginPos = 0;
int actualSize = blockSize > len ? (int) len : blockSize;

//根据分割总数size循环保存
for (int i = 0; i < size; i++) {
beginPos = i * blockSize;
if (i == size - 1) {
actualSize = (int) len;
} else {
actualSize = blockSize;
len -= actualSize; //剩余
}
splitDetail(i, beginPos, actualSize);
}
System.out.println("子文件切分后保存至" + this.destDir);
}

/**
* 根据循环次数,偏移量,实际读取量,使用随机流输入模式读取文件字节,并使用随机流读写模式写入读取字节
*
* @param i 循环次数
* @param beginPos 偏移量
* @param actualSize 实际读取量
*/
private void splitDetail(int i, int beginPos, int actualSize) {
try (
RandomAccessFile readRaf = new RandomAccessFile(this.src, "r");
RandomAccessFile writeRaf = new RandomAccessFile(this.destPaths.get(i), "rw");
) {
//设置偏移量
readRaf.seek(beginPos);
//设置缓存容器
byte[] buffer = new byte[actualSize];
int len = -1;
while ((len = readRaf.read(buffer)) != -1) {
if (actualSize > len) {
//读取量大于每次的读取长度
writeRaf.write(buffer, 0, len);
actualSize -= len;
} else {
writeRaf.write(buffer, 0, actualSize);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 调用merge方法将destPaths中每一个子文件通过流的方式保存到Vector<InputStream>,
* 然后使用SequenceInputStream将Vector<InputStream>合并到一个输入流中
* 最后使用destPath,创建一个缓冲输出流,将合并输入流读取字节,全部写入输出流中
*
* @param destPath 目标路径
*/
public void merge(String destPath) {
Vector<InputStream> vis = new Vector<>();
SequenceInputStream sis = null;
BufferedOutputStream bos = null;
try {
//将每一个切分后的子文件使用是入流保存到向量集合中
for (String path : this.destPaths) {
vis.add(new BufferedInputStream(new FileInputStream(path)));
}

//将向量结合中的输入流合并成一个SequenceInputStream
sis = new SequenceInputStream(vis.elements());

//声明缓冲输出流
bos = new BufferedOutputStream(new FileOutputStream(destPath, true));

//分段读取
byte[] buffer = new byte[1024];
int len = -1;
while ((len = sis.read(buffer)) != -1) {
bos.write(buffer, 0, len);//分段写出
}
bos.flush();

System.out.println("子文件" + this.destDir + "合并完成");
delFileByPath(new File(this.destDir));
System.out.println("子文件夹" + this.destDir + "删除成功");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
if (bos != null) {
bos.close();
}
if (sis != null) {
sis.close();
}
} catch (IOException io) {
io.printStackTrace();
}
}
}

/**
* 递归删除文件
*
* @param src 文件
*/
public void delFileByPath(File src) {
if (null == src || !src.exists()) {
return;
}
if (src.isFile()) {
src.deleteOnExit();
}
if (src.isDirectory()) {
for (File sub : Objects.requireNonNull(src.listFiles())) {
delFileByPath(sub);
}
src.deleteOnExit();
}
}

public static void main(String[] args) {
String srcDir = "/Users/xiaoyuge/Desktop/src.txt";
String destDir = "/Users/xiaoyuge/Desktop/random";
String destPath = "/Users/xiaoyuge/Desktop/target.txt";
//初始化
SplitFileUtils splitFileUtils = new SplitFileUtils(srcDir, destDir);
//读取srcDir,切分子文件到destDir中
splitFileUtils.split();
//合并destDir目录下的子文件并输出到destPath中
splitFileUtils.merge(destPath);
}
}

当在调用splitFileUtils.merge(destPath);方法时增加断点,可以看到子文件目录下的切分文件

7. 参考文章