RandomAccessFile使用场景及总结
大家在学到Java中IO流的时候学到了各种流,对文件的各种操作。但是唯独可能对RandomAccessFile对象不会去过多的研究,那么这个到底有什么用呢?
RandomAccessFile的唯一父类是Object,与其他流父类不同。是用来访问那些保存数据记录的文件的,这样你就可以用seek( )方法来访问记录,并进行读写了。这些记录的大小不必相同;但是其大小和位置必须是可知的。
RandomAccessFile是不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至都没有用InputStream和OutputStream已经准备好的功能;它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。
基本上,RandomAccessFile的工作方式是,把DataInputStream和DataOutputStream粘起来,再加上它自己的一些方法,比如定位用的getFilePointer( ),在文件里移动用的seek( ),以及判断文件大小的length( )。此外,它的构造函数还要一个表示以只读方式("r"),还是以读写方式("rw")打开文件的参数 (和C的fopen( )一模一样)。它不支持只写文件,从这一点上看,假如RandomAccessFile继承了DataInputStream,它也许会干得更好。
只有RandomAccessFile才有seek方法,而这个方法也只适用于文件。BufferedInputStream有一个mark( )方法,你可以用它来设定标记(把结果保存在一个内部变量里),然后再调用reset( )返回这个位置,但是它的功能太弱了,而且也不怎么实用。
RandomAccessFile的绝大多数功能,如果不是全部的话,已经被JDK1.4的nio的"内存映射文件(memory-mapped files)"给取代了。
RandomAccessFile有什么用呢?
平常创建流对象关联文件,开始读文件或者写文件都是从头开始的,不能从中间开始,如果是开多线程下载一个文件我们之前学过的FileWriter或者FileReader等等都无法完成,而当前介绍的RandomAccessFile他就可以解决这个问题,因为它可以指定位置读,指定位置写的一个类,通常开发过程中,多用于多线程下载一个大文件等场景。
RandomAccessFile解析
首先我们来看他的构造方法
我们可以看到,他接受一个name和mode,
另外一个构造方法,接受一个file和一个mode,另外大家看到这里可能有就些懵了,这个mode是什么东东呢?其实mode代表了几种模式,下面我们来看:
- r:以只读方式打开指定文件。如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException
- rw:以读取、写入方式打开指定文件。如果该文件不存在,则尝试创建文件
- rws:以读取、写入方式打开指定文件。相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情形下(rw模式下),是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候儿才真正的写到文件
- rwd:与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据
看了上面的模式后大家应该也明白了。接下来我们来看他的一些API
可以看到,他的写入方法有这么多。下面我们来看个DEMO
public static void main(String[] args) {
try {
//声明一个RandomAccessFile对象
RandomAccessFile r = new RandomAccessFile("F:/tmp/xx.xx", "rw");
//写入到xx.xx
r.write("Hello World".getBytes());
r.close();
} catch (Exception e) {
e.printStackTrace();
}
}
上面的代码写一个字符串到xx.xx,我们可以看到,用RandomAccessFile写文件是非常简单的。下面我们来看读文件相关的API:
public static void main(String[] args) {
try {
// 声明一个RandomAccessFile对象
RandomAccessFile r = new RandomAccessFile("F:/tmp/xx.xx", "rw");
byte[] b = new byte[1024];
r.read(b);
System.out.println(new String(b));
r.seek(5);
byte[] b1 = new byte[1024];
r.read(b1);
System.out.println(new String(b1));
r.close();
} catch (Exception e) {
e.printStackTrace();
}
}
我们来看,第一个输出的Hello World,第二个输出的是空格World 那么前面的Hello去哪儿了呢,我们在代码中调用了r.seek(5),意思是什么呢,是把文件的指针移动到了第五个位置,然后从第五个位置开始读取,我们可以假象在打开文件的时候光标默认在第一行第一个,那么seek(5)就是把光标移动到第5个,然后从第五个开始读取后面的。我们来看看seek的源码实现:
我们可以看到他调用了seek0,seek0被声明为native,是jdk底层来实现了。有基础的同学可以去看这部分C的源码。
他里边还有个getFilePointer()的方法,也是jdk底层实现的,他的意思是返回文件的长度,比如上面的调用会返回11.。
这个时候可能有就人问,怎么追加内容呢,其实这个也很简单,我们之前学了seek,调用seek把指针移动到文件的结尾,在开始写,如下:
public static void main(String[] args) {
try {
// 声明一个RandomAccessFile对象
RandomAccessFile r = new RandomAccessFile("F:/tmp/xx.xx", "rw");
//把指针移动到结尾
r.seek(r.length());
//进行写入文件
r.write(" this".getBytes());
//关闭文件
r.close();
} catch (Exception e) {
e.printStackTrace();
}
}
这么写就实现了。下面我们来看一个综合性的案例:
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
*
* @author yuxuan
*
*/
public class RandomFileTest {
public static void main(String[] args) {
String filePath = "F:\\myWorks\\个人中心_1.png";
// 这里声明为偶数片数,下面会进行计算
int parts = 2;
File file = new File(filePath);
long len = file.length();
// 防止除法出校小数点,必须能整除才行
if (len % 2 == 0) {
parts *= 2;
} else if (len % 2 == 0) {
parts = parts * 2 - 1;
}
for (int i = 0; i < parts; i++) {
// 启动线程
new ReadCopyMoreThreadFile(i, parts, file).start();
}
System.out.println("多线程复制文件成功");
}
}
/**
* 线程类,真正干活的
*
* @author yuxuan
*
*/
class ReadCopyMoreThreadFile extends Thread {
private int start = 0;
private int parts = 0;
private File file;
public ReadCopyMoreThreadFile(int start, int parts, File file) {
this.start = start;
this.file = file;
this.parts = parts;
}
public void run() {
System.out.println("第" + start + "个线程正在运行!");
try {
RandomAccessFile rf = new RandomAccessFile(file, "rw");
// 获取到文件的总长度
long len = rf.length();
/*
*
* 5174 * 0 / 2 = 0 5174 * 1 / 2 = 2587
*
* 跳到第start部分开始读
*/
rf.seek(len * start / parts);
byte[] buf = new byte[(int) (len / parts)];
// 读取
rf.read(buf);
// 关闭
rf.close();
int index = file.getName().lastIndexOf(".");
String newFileName = file.getName().substring(0, index) + "-bak_" + file.getName().substring(index);
// 创建目标文件
rf = new RandomAccessFile(newFileName, "rw");
// 指针移动到需要写的位置
rf.seek(len * start / parts);
// 写入
rf.write(buf);
// 关闭
rf.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的案例可以实现多个线程复制文件,当然下载文件也是可以的,大家可以自行研究。
有问题可以在下面评论,技术问题可以在私聊我。