RandomAccessFile类详解
文件存取通常是循序的,每在文件中存取一次,文件的读取位置就会相对于目前的位置前进一次。然而有时必须指定文件的某个区段进行读取或写入
的动作,也就是进行随机存取(Random Access),即要能在文件中随意地移动读取位置。这时可以使用RandomAccessFile,使用它的seek()方法来指定
文件存取的位置,指定的单位是字节。
为了移动存取位置时的方便,通常在随机存取文件中会固定每一个数据的长度。例如长度固定为每一个学生个人数据,Java中并没有直接的
方法可写入一个固定长度数据(像C/C++中的structure),所以在固定每一个长度方面必须自行设计。下面设计Employee类来说明如何自行设计每
个类的长度 ( 细看其中的size()方法 ),然后通过RandomAccessFileDemo类来说明如何从文件中随机读。
Java实例:Employee.java
public class Employee {
private String name;
private int sale;
public Employee() {
setName( "noname" );
}
public Employee( String name, int sale ) {
setName( name );
this.sale = sale;
}
public void setName( String name ) {
StringBuilder builder = null;
if ( name != null )
builder = new StringBuilder( name );
else
builder = new StringBuilder( 15 );
builder.setLength( 15 ); // 最长 15 字符
= builder.toString();
}
public void setSale( int sale ) {
this.sale = sale;
}
public String getName() {
return name;
}
public int getSale() {
return sale;
}
// 每个数据固定写入34字节
// 为什么是34字节呢?一个int类型为4个字节,类中的String固定为长度为15的字符(一个char占2字节)
public static int size() {
return 34;
}
}
//Java示例:RandomAccessFileDemo.java
import java.io.*;
import java.util.*;
public class RandomAccessFileDemo {
public static void main( String[] args ) {
Employee[] students = {
new Employee( "Chenmi", 5477 ),
new Employee( "doulmi", 2566 ),
new Employee( "karin", 4366 ),
};
try {
File file = new File( "src/test.txt" );
//建立RandomAccessFile实例并以读写模式打开文件
//RandomAccessFile randomAccessFile = new RandomAccessFile( file, "rw" );
RandomAccessFile randomAccessFile = new RandomAccessFile( "src/test.txt", "rw" );
for ( int i = 0; i < students.length; i++ ) {
// 使用对应的write方法写入数据
randomAccessFile.writeChars( students[ i ].getName() );
randomAccessFile.writeInt( students[ i ].getSale() );
}
Scanner scanner = new Scanner( System.in );
System.out.print( "读取第几个数据?" );
int num = scanner.nextInt();
// 使用seek()方法操作存取位置
randomAccessFile.seek( 0 );
randomAccessFile.skipBytes( ( num - 1 ) * Employee.size() );
Employee student = new Employee();
// 使用对应的read方法读出数据
student.setName( readName( randomAccessFile ) );
student.setSale( randomAccessFile.readInt() );
System.out.println( "姓名:" + student.getName() );
System.out.println( "薪水:" + student.getSale() );
// 设置关闭文件
randomAccessFile.close();
} catch ( ArrayIndexOutOfBoundsException e ) {
System.out.println( "请指定文件名称" );
} catch ( IOException e ) {
e.printStackTrace();
}
}
private static String readName( RandomAccessFile randomAccessfile )
throws IOException {
char[] name = new char[ 15 ];
for ( int i = 0; i < name.length; i++ )
name[ i ] = randomAccessfile.readChar();
// 将空字符取代为空格符并返回
return new String( name ).replace( '\0', ' ' );
}
}
RandomAccessFile上的相关方法实现都在批注中说明了,可以看到读写文件时几个必要的流程:
1)打开文件并指定读写方式在Java中,当实例化一个与文件相关的输入/输出类时,就会进行打开文件的动作。在实例化的同时要指定文件是要以读出(r)、写入(w)或可读可写(rw)的方式打开,可以将文件看作是一个容器,要读出或写入数据都必须打开容器的瓶盖。
2)使用对应的写入方法对文件进行写入,要使用对应的写入方法。在Java中通常是write的名称作为开头,在低级的文件写入中,要写入某种类型的数据,就要使用对应该类型的方法,如writeInt()(一次将写入4个字节)、writeChar()(一次写入2个字节)等。
3)使用对应的读出方法对文件进行读出,要使用对应的读出方法。在Java中通常是read的名称作为开头,在低级的文件读出中,要读出某种类型的数据,就要使用对应该类型的方法,如readInt()(一次将读取4个字节)、readChar()(一次将读出2个字节)等。
现在我们具体来看RandomAccessFile中最常用的两个方法(seek( long a ) 和 skipBytes( long a ) ):
seek( long a )是定位文件指针在文件中的位置。参数a确定读写位置距离文件开头的字节个数(注意哦,是字节个数)
skipBytes( long a )是指在文件中跳过a个字节。
两者的比较:seek()是绝对定位,而skipBytes()是相对定位,seek在使用过程中非常影响系统的开销,所以尽可能的少用
在使用这两个方法时一般会遇到这两个最常见的问题:
1、 使用raf.seek(0)或者是raf.seek(4),可以读出100以及200
但是raf.seek(1)、raf.seek(2)、raf.seek(3)就读取出很多的数字。。。。
比如设置raf.seek(1)后,在读取出来的值就是:25600。使用skipBytes也是一样。
这是为什么呢?
首先来解释为什么raf.seek(4)可以读出正确的数值:
我们可以这样来理解,raf.seek(4)其实应该表示成raf.seek( Integer.SIZE() / 8 ),
因为在Java中int为32位即是4个字节数,而seek读的是字节数(前面已经提到过),所以raf.seek(4)能读出200就不言而喻了
那raf.seek(1)为什么会读出那么令人惊诧的结果呢?
因为100200在16进制编辑器中为:00 00 00 64 00 00 00 C8()
00 00 00 64 = 100 ; 00 00 00 C8 = 200;(高位在前)
所以seek(1) 读到的就是 00 00 64 00 = 25600
2、 但为什么通过writeInt( int n )写入文件时文件中存入的都是乱码呢?
Java示例:
import java.io.File;
import java.io.RandomAccessFile;
public class Test2 {
private static final int MAX_NUM = 20;
public static void main( String[] args ) throws Exception {
File newFile = new File( "src/test.txt" );
RandomAccessFile raf = null;
raf = new RandomAccessFile( newFile, "rw" );
for ( int i = 0; i < MAX_NUM; i ++ ) {
raf.writeInt( i );
}
}
}
在查看test.txt时,将会惊奇的发现,写在文件中的是:
等这样一堆乱码,如果将其放在16进制阅读器中,就可发现其中的诀窍:
00 00 00 00 00 00 00 01 00 00 00 02 00 00 00 03
00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07
00 00 00 08 00 00 00 09 00 00 00 0D 0A 00 00 00
0B 00 00 00 0C 00 00 00 0D 00 00 00 0E 00 00 00
0F 00 00 00 10 00 00 00 11 00 00 00 12 00 00 00
13 0D 0A 0D 0A
我们来分析Java的API源码:
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;
}
假设我们令 v = 125,将v转为32位的2进制 0000 0000 0000 0000 0000 0000 0111 1101
1) >>>是什么符号
>>>为无符号右移,意思是移入位始终补0 ,如 125 >>> 1 = 0000 0000 0000 0000 0000 0000 0011 1110
2) v & 0xFF又是什么意思呢?
0xFF为16进制数,转化为二进制则为1111 1111,&为与运算,所以
0000 0000 0000 0000 0000 0000 0011 1110
& 0000 0000 0000 0000 0000 0000 1111 1111
结果为 0000 0000 0000 0000 0000 0000 0011 1110,即是取v的低8位(一个char类型)
3) 这和解决我们乱码的问题有什么关系呢?
也就是说其实RandomAccessFile类的writeInt( int v )方法在写入的过程中,是将一个Int类型的数分解成4个char类型之后写入,所
以我们写到文件里写出的“乱码”也就不再是所谓的乱码了
Java示例:
for ( int n = 0; n < 100; n ++ ) {
System.out.print( ( char ) n );
}
可以看到输出为 等与前面文件所存储的一样的字符,这也证明了我们的想法是正确的。