引言
RandomAccessFile类是在java.io中的一个工具类,它独立于字符流与字节流之外,自成一派。它的核心功能就是random功能。他可以随机访问到文件的任意位置的资源,对于超大文件的局部修改有很大的帮助。在本篇博文中详细介绍RandomAccessFile类的组成结构,阐述它所解决的问题,并给出demo进行测试。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:
RandomAccessFile介绍
1、与其他IO类的关系
没有关系,它是独立的,所有的方法都是为这个类独立开发的,而且它继承Object类。之所以没有关系,很大原因就是它独特的文件处理方式:它可以实现我们在文件中前后移动,通过一个叫“文件指针”的东西指定当前处理位置,下面是它类定义源码:
public class RandomAccessFile implements DataOutput, DataInput, Closeable {
}
在类的定义中,实现了Closeable接口,说明RandomAccessFile是一个资源类,需要对资源进行close操作。同时还实现了DataOutput与DataInput接口,但是注意这2个接口与其他IO类并没有直接关系。准确来说与InputStream与OutputStream没有什么关系,只是他们的子类DataOutputSteram与DataInputStream也实现了这2个接口。看上去眼熟,但并不是你所喊的那个。
2、构造方法
下面是源码中的两种构造方法:
//构造1
public RandomAccessFile(String name, String mode)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, mode);
}
//构造2
public RandomAccessFile(File file, String mode)
throws FileNotFoundException
{
}
从上面可以看出,RandomAccessFile具有两个构造方法,一个基于文件路径,一个基于文件。其实是一样的,中间进行转化了一步。
这里阐述构造函数,其实是想说明RandomAccessFile的mode参数。这个参数用于控制对这个文件的操作方式,在jdk_api中分为4种:
“r” :以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
“rw” :打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
“rws” :打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
“rwd” :打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
对于上面的缩写 r = read; w = write ; s = synchronously; d = device?
我们一般都用rw来设置它的运行模式,但是对于一些及时操作的时候,我们就需要用rws和rwd。为什么会这样呢?
在上一篇博文中,我们介绍字符流的时候,我们说对于文件的操作是存在缓存功能的,测试就是不关闭流的情况下查看结果。其实RandomAccessFile也是一样,它也是具有缓存的功能,所以用rws与rwd的话,那么每次我们修改文件内容的时候,都可以及时的同步到文件中去。
获取有小伙伴问:rws与rwd有什么区别呢?从上面看出来是rws多了一个元数据吗?其实rws能同步的不仅仅是文件内容,还可以同步文件属性,比如说创建时间,修改时间等等。这些属性方面的东西,称为元数据。
但是,虽然这么说,经过博主的测试发现rw与rws都是及时读写的。下面是测试代码:
package com.brickworkers.io;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
*
* @author Brickworker
* Date:2017年5月16日下午3:29:19
* 关于类RandomAccessFileTest.java的描述:随机访问文件测试
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException, InterruptedException{
//指定文件,设置模式为读写
RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw");
RandomAccessFile rws = new RandomAccessFile("F:/java/io/rws.txt", "rws");
try {
byte[] bytes = "hello".getBytes();
for (byte b : bytes) {
rw.write(b);
rws.write(b);
//第一次遍历的时候,主线程休眠
Thread.sleep(5000);
}
} finally{
rw.close();
rws.close();
}
}
}
发现每写入一个字节,对应的文档就会增加一个字节,也没有查到相关资料,不知道大家对这个mode参数是否有更深的理解呢?
3、核心方法
基于RandomAccessFile的随意访问特性,它的核心方法就是对指针的操作,指针操作包括以下方法:
long getFilePointer() : 获取当前指针位置
void seek(long pos) : 设置指针当前的位置
除却这两个方法之外就是对文件的读取和写入的一些操作,其中还涉及到nio的操作,nio在后面在阐述。
4、解决问题
基于RandomAccessFile的特性,它可以解决载入不了进内存的大文件的修改。我们可以通过它对要修的文件的局部进行操作。不过在RandomAccessFile的核心方法中,没有办法对文件中间的部分进行操作,因为对于文件中间部分操作必然需要把后面的数据往后移动,这个是没有天然办法的,需要我们自己把后面部分读取暂存起来,插入结束之后,再把暂存的数据补回到文件中。但是对于开头和结尾的操作会显得非常方便。
同时,因为RandomAccessFile可以标记当前处理位置,那么它可以对一个文件进行间断操作:只需要在一次操作完成之后,记录好操作位置,恢复的时候就可以先获取原先操作位置,继续执行。
最后,我们还可以利用多线程,把一个超大的文件分割成一个个小文件,然后进行载入,那么可以极大的提升效率。分割的想法很好理解,比如说线程1操作1~10000字节。线程二操作100001到20000字节就可以了。核心其实还是对指针的位置进行处理。
文件指针
要深入理解RandomAccessFile,核心问题就是要理解文件指针的操作,我们观察指针指向的位置和移动情况。下面这张图是一个简单的txt文件,预先写入的内容:
下面是一个测试demo:
package com.brickworkers.io;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
*
* @author Brickworker
* Date:2017年5月16日下午3:29:19
* 关于类RandomAccessFileTest.java的描述:随机访问文件测试
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException{
//指定文件,设置模式为读写
RandomAccessFile accessFile = new RandomAccessFile("F:/java/io/bw.txt", "rw");
try {
//查看初始文件指针位置
System.out.println(accessFile.getFilePointer());
//读取5个字节的数据
byte[] five = new byte[5];
accessFile.read(five);
System.out.println(new String(five));
//查看获取5个字节的数据之后指针的位置
System.out.println(accessFile.getFilePointer());
//跳过2个字节
accessFile.skipBytes(2);
//查看跳过2个字节之后指针的位置
System.out.println(accessFile.getFilePointer());
//把指针设置为起始位置
accessFile.seek(0);
//获取指针位置
System.out.println(accessFile.getFilePointer());
//获取文件所有内容
byte[] full = new byte[(int) accessFile.length()];
accessFile.readFully(full);
System.out.println(new String(full));
//读入所有文件之后指针位置
System.out.println(accessFile.getFilePointer());
} finally{
accessFile.close();
}
}
}
//输出结果:
//0
//my na
//5
//7
//0
//my name is brickworker
//22
//
我们可以总结出几个规律:
①指针位置,用字节作为单位移动
②初始情况下,指针位置为0
③读取完所有文件之后,指针的位置是file.length
④skip方法操作的时候,会把指针往后移动skip的字节
⑤seek方法操作的时候,会直接把指针指向目标位置
⑥任何read操作,都会把指针移动read操作的字节长度
⑦空格是占一个字节。(中文要看是什么编码才能确定字节长度)
多线程实现大文件输入/输出
在下面的代码中实现了单线程读取和多线程读取:
package com.brickworkers.io;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
*
* @author Brickworker
* Date:2017年5月16日下午3:29:19
* 关于类RandomAccessFileTest.java的描述:随机访问文件测试
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class RandomAccessFileTest {
//单线程载入文件
private void sigleLoad() throws IOException{
try(RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw")){
byte[] bytes = new byte[(int) rw.length()];
rw.read(bytes);
System.out.println(new String(bytes));
}
}
//多线程载入文件
private void MultiLoad() throws IOException, InterruptedException{
//把文件进行拆分成2分
RandomAccessFile rw = new RandomAccessFile("F:/java/io/rw.txt", "rw");
byte[] bytes1 = new byte[(int) (rw.length()/2)+1];//+1是为了避免奇偶问题
byte[] bytes2 = new byte[(int) (rw.length()/2)+1];
Thread t1 = new Thread(() -> {
try {
//把指针指向起始位置
rw.seek(0);
rw.read(bytes1, 0, bytes1.length);
} catch (IOException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
//把指针指向中间位置
rw.seek(bytes1.length);
rw.read(bytes2);
} catch (IOException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();//必须要等待线程结束,不然关闭资源之后,会造成读取不了的问题
rw.close();
System.out.println(new String(bytes1).trim()+new String(bytes2).trim());
}
public static void main(String[] args) throws IOException, InterruptedException{
RandomAccessFileTest accessFileTest = new RandomAccessFileTest();
accessFileTest.sigleLoad();
accessFileTest.MultiLoad();
}
}
之所以能用多线程来解决RandomAccessFile,是因为文件指针可以标记操作位置。不论是输入还是输出,都可以实现:
输入:在上面就已经实现了输入方式。首先我们对文件大小是可知的。那么我们可以用多线程来执行任务,一个线程那多少字节的文件,从文件指针哪里开始,长度多少。这样每个线程的任务就非常清晰。
输出:同理,如果要实现多线程输出,那么我们只需要创立一个空文件,同时我们要预估计这个文件占多大存储空间。然后用多线程执行任务,一个线程写入多少字节,从文件指针哪里开始,长度多少。这样也就可以完成输出操作。