今天学习RandomAccessFile。
RandomAccessFile,用来随机访问文件的类。它支持对随机访问文件的读取和写入。这里的随机并不意味着不可控制,而是意味着可以访问文件内容的任意位置。
随机访问文件是如何实现的呢?
随机访问文件是通过“文件指针”来实现的。文件指针可看做指向文件中任意位置的光标或索引。该文件指针可以通过getFilePointer方法读取,通过seek方法设置。输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入文件末尾之后的输出操作导致该数组扩展。
源码太长,放在最后,有兴趣的可以向后看。
demo
import java.io.File;
import java.io.RandomAccessFile;
import org.junit.Test;
import java.io.IOException;
/**
* RandomAccessFile demo
*/
public class RandomAccessFileTest {
private static final File file = new File("randomAccessFileTest.txt");
private void prepare() {
// 若文件存在,则删除该文件。目的是避免干扰。
if (file.exists())
file.delete();
}
// 测试seek、getFilePointer、length方法。打印结果为
/**
* 偏移量的设置超出文件末尾不会改变文件的长度。
* getFilePointer():54
* length():54
* getFilePointer():111111
* length():54
* 只有在偏移量的设置超出文件末尾的情况下对文件进行写入才会更改其长度。
*/
@Test
public void testSeek() {
prepare();
try {
RandomAccessFile raf = new RandomAccessFile(file, "rw");
raf.writeChars("abcdefghijklmnopqrstuvwxyz\n");
System.out.println("偏移量的设置超出文件末尾不会改变文件的长度。");
System.out.println("getFilePointer():" + raf.getFilePointer());
System.out.println("length():" + raf.length());
raf.seek(111111);
System.out.println("getFilePointer():" + raf.getFilePointer());
System.out.println("length():" + raf.length());
System.out.println("只有在偏移量的设置超出文件末尾的情况下对文件进行写入才会更改其长度。");
} catch (IOException e) {
e.printStackTrace();
}
}
//测试testSetLength方法。打印结果为
/**
* SetLength会改变文件大小,并没有改变下一个要写入的内容的位置。
* getFilePointer():54
* length():54
* getFilePointer():54
* length():111111
*/
@Test
public void testSetLength() {
prepare();
try {
RandomAccessFile raf = new RandomAccessFile(file, "rw");
raf.writeChars("abcdefghijklmnopqrstuvwxyz\n");
System.out.println("SetLength会改变文件大小,并没有改变下一个要写入的内容的位置。");
System.out.println("getFilePointer():" + raf.getFilePointer());
System.out.println("length():" + raf.length());
raf.setLength(111111l);
System.out.println("getFilePointer():" + raf.getFilePointer());
System.out.println("length():" + raf.length());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向文件中写入内容
*/
@Test
public void testWrite() {
prepare();
try {
RandomAccessFile raf = new RandomAccessFile(file, "rw");
raf.writeChars("abcdefg\n");
raf.writeBoolean(true);
raf.writeByte(0x51);
raf.writeChar('h');
raf.writeShort(0x3c3c);
raf.writeInt(0x66);
raf.writeLong(0x123456789L);
raf.writeFloat(1.1f);
raf.writeDouble(1.111);
raf.writeUTF("潘威威的博客");
raf.writeChar('\n');
//追加内容
raf.seek(raf.length());
raf.write("追加的内容".getBytes());
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//读取文件并打印。打印结果为
/**
* readChar():a
* read(byte[],int,len):
*/
@Test
public void testRead() {
try {
RandomAccessFile raf = new RandomAccessFile(file, "r");
System.out.println("readChar():" + raf.readChar());
raf.skipBytes(2);//跳过两个字节,即跳过了'b'字符
byte[] buf = new byte[10];
raf.read(buf, 0, buf.length);
System.out.println("read(byte[],int,len):" + (new String(buf)));
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
源码
下面是观看源码的总结。
RandomAccessFile并没有继承InputStream或者OutputStream,而是实现了DataOutput和DataInput。这说明它可以操作Java基础数据类型,而且既可读,也可写。
打开文件的访问模式有四种:
- “r”——以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。
- “rw”——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
- “rws”——打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
- “rwd”——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
一般使用前两个模式。
RandomAccessFile中读写Java基本数据类型的方法实现和DataInputStream与DataOutputStream极其相似,可以参考Java8 I/O源码-DataInputStream与DataOutputStream一文了解这些方法的实现。
/**
* RandomAccessFile并没有继承InputStream或者OutputStream,而是实现了DataOutput和DataInput。
* 这说明它可以操作Java基础数据类型,而且既可读,也可写。
*
*/
public class RandomAccessFile implements DataOutput, DataInput, Closeable {
//文件描述符
private FileDescriptor fd;
//文件通道
private FileChannel channel = null;
//标识此文件是否既可以读又可以写
private boolean rw;
/**
* 引用文件的路径
*/
private final String path;
//关闭锁
private Object closeLock = new Object();
//标识是否关闭
private volatile boolean closed = false;
//不同读写模式对应的值
private static final int O_RDONLY = 1;//只读模式
private static final int O_RDWR = 2;//读写模式
private static final int O_SYNC = 4;
private static final int O_DSYNC = 8;
/**
* 创建从中读取和向其中写入(可选)的随机访问文件流。
* 将创建一个新的FileDescriptor对象来表示到文件的连接。
* mode参数指定用以打开文件的访问模式。允许的值及其含意如RandomAccessFile(File,String)构造方法所指定的那样。
*
* 如果存在安全管理器,则使用name作为其参数调用其checkRead方法,以查看是否允许对该文件进行读取访问。
* 如果该模式允许写入,那么还使用name作为安全管理器的参数来调用其checkWrite方法,以查看是否允许对该文件进行写入访问。
*
* @param name 文件名
* @param mode 访问模式,参考RandomAccessFile(File,String)构造方法
*/
public RandomAccessFile(String name, String mode)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, mode);
}
/**
* 创建从中读取和向其中写入(可选)的随机访问文件流,该文件由File参数指定。
*
* mode参数指定用以打开文件的访问模式。允许的值及其含意为:
* "r"——以只读方式打开。调用结果对象的任何write方法都将导致抛出IOException。
* "rw"——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
* "rws"——打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
* "rwd"——打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。
*
* 如果存在安全管理器,则使用name作为其参数调用其checkRead方法,以查看是否允许对该文件进行读取访问。
* 如果该模式允许写入,那么还使用name作为安全管理器的参数来调用其checkWrite方法,以查看是否允许对该文件进行写入访问。
*/
public RandomAccessFile(File file, String mode)
throws FileNotFoundException
{
//获取文件名
String name = (file != null ? file.getPath() : null);
int imode = -1;
//如果模式为只读模式
if (mode.equals("r"))
imode = O_RDONLY;
else if (mode.startsWith("rw")) {
imode = O_RDWR;
rw = true;
//还要根据具体的情况判断是rws还是rwd模式
if (mode.length() > 2) {
if (mode.equals("rws"))
imode |= O_SYNC;
else if (mode.equals("rwd"))
imode |= O_DSYNC;
else
imode = -1;
}
}
//如果模式不合法,抛出异常
if (imode < 0)
throw new IllegalArgumentException("Illegal mode \"" + mode
+ "\" must be one of "
+ "\"r\", \"rw\", \"rws\","
+ " or \"rwd\"");
//如果存在安全管理器,则使用name作为其参数调用其checkRead方法,以查看是否允许对该文件进行读取访问。
//如果该模式允许写入,那么还使用name作为安全管理器的参数来调用其checkWrite方法,以查看是否允许对该文件进行写入访问。
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
if (rw) {
security.checkWrite(name);
}
}
//如果文件名为null,抛出异常
if (name == null) {
throw new NullPointerException();
}
//如果文件路径不可用
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
//创建一个新的FileDescriptor对象来表示到文件的连接。
fd = new FileDescriptor();
fd.attach(this);
path = name;
//以指定的模式打开指定文件
open(name, imode);
}
/**
* 返回与此流关联的不透明文件描述符对象。
*/
public final FileDescriptor getFD() throws IOException {
if (fd != null) {
return fd;
}
throw new IOException();
}
/**
* 返回与此文件关联的唯一FileChannel对象。
*/
public final FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, rw, this);
}
return channel;
}
}
/**
* 以指定模式打开指定文件
*
* native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。
* Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
* JNI是Java本机接口(Java Native Interface),是一个本机编程接口,
* 它是Java软件开发工具箱(Java Software Development Kit,SDK)的一部分。
* JNI允许Java代码使用以其他语言编写的代码和代码库。
* Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,
* 从而允许程序员从本机代码内部调用Java代码。
* 所以想要了解open0方法的具体实现只能去查看JVM源码了。
*
*/
private native void open0(String name, int mode)
throws FileNotFoundException;
// wrap native call to allow instrumentation
/**
* 参考open0(String name, int mode)
*/
private void open(String name, int mode)
throws FileNotFoundException {
open0(name, mode);
}
// 'Read' primitives
/**
* 从此文件中读取一个数据字节。
* 以整数形式返回此字节,范围在0到255(0x00-0x0ff)。如果尚无输入可用,将阻塞此方法。
* 尽管RandomAccessFile不是InputStream的子类,但此方法的行为与 InputStream的InputStream.read()方法完全一样。
*
* @return 下一个数据字节,如果已到达文件的末尾,则返回-1。
*/
public int read() throws IOException {
return read0();
}
/**
* 从此文件中读取一个数据字节。
*
* 关于native的介绍请参考open0(String name, int mode)
* /
private native int read0() throws IOException;
/**
* Reads a sub array as a sequence of bytes.
* @param b the buffer into which the data is read.
* @param off the start offset of the data.
* @param len the number of bytes to read.
* @exception IOException If an I/O error has occurred.
*/
private native int readBytes(byte b[], int off, int len) throws IOException;
/**
* 将最多len个数据字节从此文件读入byte数组。
* 在至少一个输入字节可用前,此方法一直阻塞。
* 尽管RandomAccessFile不是InputStream的子类,
* 但此方法的行为与InputStream的InputStream.read(byte[], int, int)方法完全一样。
*/
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
/**
* 将最多b.length个数据字节从此文件读入byte数组。
*/
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
/**
* 将b.length个字节从此文件读入byte数组,并从当前文件指针开始。
* 在读取到请求数量的字节之前,此方法将从该文件重复读取。
* 在读取了请求数量的字节、检测到流的末尾或者抛出异常前,此方法一直阻塞。
*
* 参考readFully(byte b[], int off, int len)
*/
public final void readFully(byte b[]) throws IOException {
readFully(b, 0, b.length);
}
/**
* 将b.length个字节从此文件读入byte数组,并从当前文件指针开始。
* 在读取到请求数量的字节之前,此方法将从该文件重复读取。
* 在读取了请求数量的字节、检测到流的末尾或者抛出异常前,此方法一直阻塞。
*/
public final void readFully(byte b[], int off, int len) throws IOException {
int n = 0;
//在读取到请求数量的字节之前,此方法将从该文件重复读取。
do {
int count = this.read(b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
} while (n < len);
}
/**
* 尝试跳过输入的n个字节以丢弃跳过的字节。
* 此方法可能跳过一些较少数量的字节(可能包括零)。
* 这可能由任意数量的条件引起;在跳过n个字节之前已到达文件的末尾只是其中的一种可能。
* 此方法从不抛出EOFException。返回跳过的实际字节数。
* 如果n为负数,则不跳过任何字节。
*
* @param n 要跳过的字节数。
* @return 跳过的实际字节数。
*/
public int skipBytes(int n) throws IOException {
long pos;
long len;
long newpos;
if (n <= 0) {
return 0;
}
//获取文件指针当前位置
pos = getFilePointer();
len = length();
newpos = pos + n;
if (newpos > len) {
newpos = len;
}
//将指针设置到新位置
seek(newpos);
/* 返回跳过的实际字节数 */
return (int) (newpos - pos);
}
// 'Write' primitives
/**
* 向此文件写入指定的字节。
* 从当前文件指针开始写入。
*/
public void write(int b) throws IOException {
write0(b);
}
/**
* 向此文件写入指定的字节。
* 从当前文件指针开始写入。
*
* 关于native的介绍请参考open0(String name, int mode)
*/
private native void write0(int b) throws IOException;
/**
* 将b.length个字节从指定byte数组写入到此文件,并从当前文件指针开始。
*
* 关于native的介绍请参考open0(String name, int mode)
*/
private native void writeBytes(byte b[], int off, int len) throws IOException;
/**
* 将b.length个字节从指定byte数组写入到此文件,并从当前文件指针开始。
*/
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length);
}
/**
* 将 len 个字节从指定 byte 数组写入到此文件,并从偏移量 off 处开始。
*/
public void write(byte b[], int off, int len) throws IOException {
writeBytes(b, off, len);
}
// 'Random access' stuff
/**
* 返回此文件中的当前偏移量。
*/
public native long getFilePointer() throws IOException;
/**
* 设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。
* 偏移量的设置可能会超出文件末尾。
* 偏移量的设置超出文件末尾不会改变文件的长度。只有在偏移量的设置超出文件末尾的情况下对文件进行写入才会更改其长度。
*/
public void seek(long pos) throws IOException {
if (pos < 0) {
throw new IOException("Negative seek offset");
} else {
seek0(pos);
}
}
private native void seek0(long pos) throws IOException;
/**
* 返回此文件的长度。
*/
public native long length() throws IOException;
/**
* 设置此文件的长度。
* 如果 length 方法返回的文件的现有长度大于 newLength 参数,则该文件将被截短。
* 在此情况下,如果 getFilePointer 方法返回的文件偏移量大于 newLength,那么在返回此方法后,该偏移量将等于 newLength。
* 如果 length 方法返回的文件的现有长度小于 newLength 参数,则该文件将被扩展。在此情况下,未定义文件扩展部分的内容。
*/
public native void setLength(long newLength) throws IOException;
/**
* 关闭此随机访问文件流并释放与该流关联的所有系统资源。
* 关闭的随机访问文件不能执行输入或输出操作,而且不能重新打开。
* 如果此文件具有一个关联的通道,那么该通道也会被关闭。
*/
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
//
// 下面都是读写Java基本数据类型的方法。这些方法是借鉴
// DataInputStream and DataOutputStream的.如果感兴趣可以参考 《Java8 I/O源码-DataInputStream与DataOutputStream》一文。
// 链接为http://blog.csdn.net/panweiwei1994/article/details/78266266。
//
public final boolean readBoolean() throws IOException {
int ch = this.read();
if (ch < 0)
throw new EOFException();
return (ch != 0);
}
public final byte readByte() throws IOException {
int ch = this.read();
if (ch < 0)
throw new EOFException();
return (byte)(ch);
}
public final int readUnsignedByte() throws IOException {
int ch = this.read();
if (ch < 0)
throw new EOFException();
return ch;
}
public final short readShort() throws IOException {
int ch1 = this.read();
int ch2 = this.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (short)((ch1 << 8) + (ch2 << 0));
}
public final int readUnsignedShort() throws IOException {
int ch1 = this.read();
int ch2 = this.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (ch1 << 8) + (ch2 << 0);
}
public final char readChar() throws IOException {
int ch1 = this.read();
int ch2 = this.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (char)((ch1 << 8) + (ch2 << 0));
}
public final int readInt() throws IOException {
int ch1 = this.read();
int ch2 = this.read();
int ch3 = this.read();
int ch4 = this.read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}
public final long readLong() throws IOException {
return ((long)(readInt()) << 32) + (readInt() & 0xFFFFFFFFL);
}
public final float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
public final double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
public final String readLine() throws IOException {
StringBuffer input = new StringBuffer();
int c = -1;
boolean eol = false;
while (!eol) {
switch (c = read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
long cur = getFilePointer();
if ((read()) != '\n') {
seek(cur);
}
break;
default:
input.append((char)c);
break;
}
}
if ((c == -1) && (input.length() == 0)) {
return null;
}
return input.toString();
}
public final String readUTF() throws IOException {
return DataInputStream.readUTF(this);
}
public final void writeBoolean(boolean v) throws IOException {
write(v ? 1 : 0);
//written++;
}
public final void writeByte(int v) throws IOException {
write(v);
//written++;
}
public final void writeShort(int v) throws IOException {
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
//written += 2;
}
public final void writeChar(int v) throws IOException {
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
//written += 2;
}
public final void writeInt(int v) throws IOException {
write((v >>> 24) & 0xFF);
write((v >>> 16) & 0xFF);
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
//written += 4;
}
public final void writeLong(long v) throws IOException {
write((int)(v >>> 56) & 0xFF);
write((int)(v >>> 48) & 0xFF);
write((int)(v >>> 40) & 0xFF);
write((int)(v >>> 32) & 0xFF);
write((int)(v >>> 24) & 0xFF);
write((int)(v >>> 16) & 0xFF);
write((int)(v >>> 8) & 0xFF);
write((int)(v >>> 0) & 0xFF);
//written += 8;
}
public final void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
public final void writeDouble(double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
@SuppressWarnings("deprecation")
public final void writeBytes(String s) throws IOException {
int len = s.length();
byte[] b = new byte[len];
s.getBytes(0, len, b, 0);
writeBytes(b, 0, len);
}
public final void writeChars(String s) throws IOException {
int clen = s.length();
int blen = 2*clen;
byte[] b = new byte[blen];
char[] c = new char[clen];
s.getChars(0, clen, c, 0);
for (int i = 0, j = 0; i < clen; i++) {
b[j++] = (byte)(c[i] >>> 8);
b[j++] = (byte)(c[i] >>> 0);
}
writeBytes(b, 0, blen);
}
//使用 modified UTF-8 编码以与机器无关的方式将一个字符串写入该文件。
//该功能是通过DataOutputStream.writeUTF方法来完成的
public final void writeUTF(String str) throws IOException {
DataOutputStream.writeUTF(str, this);
}
private static native void initIDs();
private native void close0() throws IOException;
static {
initIDs();
}
}
关于RandomAccessFile就讲到这里,想了解更多内容请参考
END.