RandomAccessFile支持对文件的读取和写入随机访问(其他的输入流或者输出流只能进行一种操作,要么输入,要么输出)。RandomAccessFile把随机访问的文件对象看作存储在文件系统中的一个大型 byte 数组,然后通过指向该 byte 数组的光标或索引(即:文件指针 FilePointer)在该数组任意位置读取或写入任意数据。输入操作从文件指针开始读取字节(以字节为单位进行读取),并随着对字节的读取而前移此文件指针。如果RandomAccessFile访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。
在磁盘中和内存中,所有的存储都是以右边为开始点(低),左边为结束点(高),字节的读取或者写入也都是从右到左的顺序。
1、RandomAccessFile(File file, String mode)
2、RandomAccessFile(String name, String mode)
两个构造方法的第一个参数不做介绍,
mode :第二个参数是指以什么模式创建读写流,此参数有固定的输入值,必须为"r"/"rw"/"rws"/"rwd"其中一个。
r:以只读方式打开指定文件。如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException
rw:以读取、写入方式打开指定文件。如果该文件不存在,则尝试创建文件
rws:以读取、写入方式打开指定文件。相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情形下(rw模式下),是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候儿才真正的写到文件
rwd:与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据
RandomAccessFile包含三个方法来操作文件记录指针
long getFilePointer()
:返回文件记录指针的当前位置
void seek(long pos)
:将文件记录指针定位到pos位置
skipBytes(int n)
该方法用于尝试跳过输入的n个字节以丢弃跳过的字节(跳过的字节不读取):int skipBytes(int n)
该方法可能跳过一些较少数量的字节(可能包括0),这可能由任意数量的条件引起,在跳过n个字节之前已经到大文件的末尾只是其中的一种可能
该方法不抛出EOFException,返回跳过的实际字节数,如果n为负数,则不跳过任何字节
FileDescriptor getFD() : 可以返回这个文件的文件描述符
native long length() : 可以返回文件的长度
- setLength 为什么还能设置文件长度?
- 你可以理解为这是一个"动态数组"!!假设你想要设置为newLength 长度
- 如果这个长度小于 实际长度(length方法返回的值), 文件被截断,并且如果getFilePointer 大于newLength ,那么它将变成newLength
- 如果 newLength大于 实际长度(length方法返回的值),则该文件将被扩展, 在此情况下,未定义文件扩展部分的内容。
- seek方法设置的偏移量,下一次的读写将从这个位置开始,偏移量的设置可能会超出文件末尾,这并不会改变什么,但是一旦你在这个超出文件末尾的偏移量位置写入数据,长度将会改变
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 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,然后按四
public static void main(String[] args){
RandomAccessFile randomAccessFile = null;
try {
// 可读写
randomAccessFile = new RandomAccessFile(new File("d:\\new.txt"), "rw");
// 写
for (int i = 1; i <= 10; i++) {
randomAccessFile.write((i+" 设备名/devicename,设备数量/devicenum\n\r").getBytes());
System.out.println("当前指针位置:"+randomAccessFile.getFilePointer());
}
// randomAccessFile.writeBoolean(true);
// randomAccessFile.writeByte(11);
// randomAccessFile.writeDouble(12);
// randomAccessFile.writeUTF("操作");
// 读
randomAccessFile.seek(0);// 读时指针重新置为开始位置,事实上可以从文件内容的任何位置开始
byte[] bs = new byte[1024];
int len=0;
while ((len=randomAccessFile.read(bs))!=-1) {
System.out.println(new String(bs, 0, len));
}
// System.out.println("readLine:"+ randomAccessFile.readLine());
// System.out.println("readDouble:"+randomAccessFile.readDouble());
// System.out.println("readByte:"+randomAccessFile.readByte());
// System.out.println("readUTF:"+randomAccessFile.readUTF());
// RandomAccessFile的记录指针放在文件尾部,用于追加内容
randomAccessFile.seek(randomAccessFile.length());// 指针移到文件末尾
randomAccessFile.write((21+" 设备名/devicename,设备数量/devicenum\n\r").getBytes());
// 任意位置插入写
int position = 102;
String insetstr = "----------------------";
randomAccessFile.seek(position);// 指定插入的位置
// 先把该位置后的所有内容先缓存起来,防止被覆盖
List blists = new ArrayList<>();
byte[] bs1 = new byte[1024];
while (randomAccessFile.read(bs1)!=-1) {
blists.add(bs1);
}
randomAccessFile.seek(position);// 再次返回指定位置
// 插入要插入的内容
randomAccessFile.write(insetstr.getBytes());
// 再把缓存的内容写入
for (int i = 0; i < blists.size(); i++) {
byte[] cachestr = blists.get(i);
randomAccessFile.write(cachestr);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally{
if (randomAccessFile!=null) {
try {
randomAccessFile.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
public class DownLoadThread extends Thread {
private long start;
private File src;
private long total;
private File desc;
/**
*
* @param start 开始下载的位置
* @param src 要下载的文件
* @param desc 要下载的目的地
* @param total 要下载的总量
*/
public DownLoadThread(long start, File src, File desc, long total) {
this.start = start;
this.src = src;
this.desc = desc;
this.total = total;
}
@Override
public void run() {
try {
// 创建输入流关联源,因为要指定位置读和写,所以我们需要用随机访问流
RandomAccessFile src = new RandomAccessFile(this.src, "rw");
RandomAccessFile desc = new RandomAccessFile(this.desc, "rw");
// 源和目的都要从start开始
src.seek(start);
desc.seek(start);
// 开始读写
byte[] arr = new byte[1024];
int len;
long count = 0;
while ((len = src.read(arr)) != -1) {
//分三种情况
if (len + count > total) {
//1.当读取的时候操作自己该线程的下载总量的时候,需要改变len
len = (int) (total - count);
desc.write(arr, 0, len);
//证明该线程下载任务已经完毕,结束读写操作
break;
} else if (len + count < total) {
//2.证明还没有到下载总量,直接将内容写入
desc.write(arr, 0, len);
//并且使计数器任务累加
count += arr.length;
} else {
//3.证明改好到下载总量
desc.write(arr, 0, len);
//结束读写
break;
}
}
src.close();
desc.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class TestRandomAccess {
public static void main(String[] args) {
//关联源
File src = new File("a.txt");
//关联目的
File desc = new File("b.txt");
//获取源的总大小
long length = src.length();
// 开两条线程,并分配下载任务
new DownLoadThread(0, src, desc, length / 2).start();
new DownLoadThread(length / 2 , src, desc, length - (length / 2)).start();
}
}
/**
* 面向对象思想封装 分割文件并合并
* 1.根据数据源 src,输出文件夹 destDir,分割后文件存储路径 destPaths,块大小 blockSize,size 块数,初始化分割文件对象
* 2. 构造方法(数据源,输出源) 构造方法(数据源,输出源,分割块大小)
* 3. 初始化文件分割对象时,调用init() 计算切分块数,切分后所有文件名 ,如果保存切分后文件的目录不存在,则创建
* 4. 调用split()方法根据文件总长度以及分割块大小blockSize ,调用splitDetails()将分割后每一块的文件输出到 destDir 下面
* 5. 调用 merge(String destPath) 将 destPaths 每一个分割文件,声明一个缓冲输入流,然后保存到 Vector中,
* 然后使用SequenceInputStream将Vector合并到一个输入流中,
* 最后使用destPath,创建一个缓冲输出流,将合并输入流读取字节,全部写入输出流中
*/
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* @ClassName 面向对象思想封装 分割文件并合并
* @Author JianPeng OuYang
* @Description TODO
* @Date 2019/12/22 19:56
* @Version v1.0
*/
public class SplitFileUtils {
private File src;//输入源
private String destDir;//分割子文件输出的目录
private List destPaths;//保存每个分割子文件的路径
private int blockSize;//切割大小
private int size;//切割总块数
public SplitFileUtils(String srcDir, String destDir) {
this(srcDir, destDir, 1024);
}
public SplitFileUtils(String srcDir, String destDir, int blockSize) {
this.src = new File(srcDir);//初始化输入源
this.destDir = destDir;//初始化分割子文件输出的目录
this.destPaths = new ArrayList<>();//初始化保存分割子文件的路径容器
this.blockSize = blockSize;//初始化切割大小
//初始化对象参数
init();
System.out.println("初始化SplitFileUtils完成");
}
/**
* 初始化SplitFileUtils 参数
*/
private void init() {
long len = this.src.length();//文件总长度
this.size = (int) Math.ceil(len * 1.0 / this.blockSize);// 四舍五入计算分割总总次数
//根据size循环保存 切分子文件路径
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 = src.length();
//起始位置和实际大小
int beginPos = 0;
int actualSize = (int) (blockSize > len ? len : blockSize);
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[] flush = new byte[actualSize];
int len = -1; //接收长度
while ((len = readRaf.read(flush)) != -1) {
if (actualSize > len) { //获取本次读取的所有内容
writeRaf.write(flush, 0, len);
actualSize -= len;
} else {
writeRaf.write(flush, 0, actualSize);
break;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 5. 调用 merge(String destPath) 将 destPaths 每一个分割文件,声明一个缓冲输入流,然后保存到 Vector中,
* 然后使用SequenceInputStream将Vector合并到一个输入流中,
* 最后使用destPath,创建一个缓冲输出流,将合并输入流读取字节,全部写入输出流中
*
* @param destPath
*/
public void merge(String destPath) {
//声明向量集合保存 InputStream
Vector vis = new Vector<>();
SequenceInputStream sis = null;
BufferedOutputStream bos = null;
try {
// 将每一个切分后的子文件使用输入流 保存到 向量集合中
for (int i = 0; i < this.destPaths.size(); i++) {
vis.add(new BufferedInputStream(new FileInputStream(this.destPaths.get(i))));
}
// 将向量集合中的输入流合并成一个 合并流 SequenceInputStream
sis = new SequenceInputStream(vis.elements());
// 声明缓冲输出流
bos = new BufferedOutputStream(new FileOutputStream(destPath,true));
//拷贝
//3、操作 (分段读取)
byte[] flush = new byte[1024]; //缓冲容器
int len = -1; //接收长度
while ((len = sis.read(flush)) != -1) {
bos.write(flush, 0, len); //分段写出
}
bos.flush();;
System.out.println("子文件"+this.destDir+"合并完成");
delFileByPath(new File(this.destDir));
System.out.println("删除子文件夹"+this.destDir+"完成");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) {
bos.close();
}
if (sis != null) {
sis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 5. 调用 merge(String destPath) 将 destPaths 每一个分割文件,声明一个缓冲输入流,然后保存到 Vector中,
* 然后使用SequenceInputStream将Vector合并到一个输入流中,
* 最后使用destPath,创建一个缓冲输出流,将合并输入流读取字节,全部写入输出流中
*
* @param destPath
*/
/* public void merge(String destPath) {
//输出流
OutputStream os = new BufferedOutputStream(new FileOutputStream(destPath, true));
Vector vi = new Vector<>();
SequenceInputStream sis = null;
//输入流
for (int i = 0; i < destPaths.size(); i++) {
vi.add(new BufferedInputStream(new FileInputStream(destPaths.get(i))));
}
sis = new SequenceInputStream(vi.elements());
//拷贝
//3、操作 (分段读取)
byte[] flush = new byte[1024]; //缓冲容器
int len = -1; //接收长度
while ((len = sis.read(flush)) != -1) {
os.write(flush, 0, len); //分段写出
}
os.flush();
sis.close();
os.close();
}*/
/**
* @param src 递归删除文件
*/
public void delFileByPath(File src) {
if (null == src || !src.exists()) {
return;
}
if (src.isFile()) {
src.delete();
}
if (src.isDirectory()) { //文件夹
for (File sub : src.listFiles()) {
delFileByPath(sub);
}
src.delete();
}
}
@Test
public void testSplitFile() {
// 源文件
String srcDir = "random.txt";
// 保存子文件目录
String destDir = "random";
// 输出文件
String destPath = "devRandom.txt";
//初始化 SplitFileUtils
SplitFileUtils splitFileUtils = new SplitFileUtils(srcDir,destDir);
// 读取 srcDir ,切分子文件到 destDir目录 下面
splitFileUtils.split();
// 合并 destDir目录下的子文件并输出到 destPath 中
splitFileUtils.merge(destPath);
}
}
如何高效的使用RandomAccessFile