Java之RandomAccessFile类存取数据理解

Java之RandomAccessFile类存取数据理解

文章链接:http://blog.csdn.net/qq_16628781/article/details/70767078

知识点

  1. RandomAccessFile类的方法理解;
  2. 利用RandomAccessFile写入和读取数据;
  3. 创建特定大小文件,并且分段插入数据;
  4. 新名词记录:{FileChannel#force(boolean), FileChannel:进行读写,映射和操作文件的通道;}

RandomAccessFile类说明

RandomAccessFile是用来支持读写随机存取文件的类。随机存取文件的行为就像存取在文件系统里一个很大的字节数组一样。为了方便存取文件,此字节数组提供了“文件指针”概念:类似于游标和下标,使用getFilePointer()方法获得,利用seek()方法设置下标。如果没有到达文件的末尾,读取的路径都是可达的,返回true,否则抛出EOFException异常。如果其它原因不能进行读取,那么会抛出IOException异常。如果输入输出流被关闭了,也会抛出IOException异常。

方法解释

  • 构造方法

    “`
    public RandomAccessFile(String name, String mode){};

构造方法:根据name创建一个随机存取流,对文件进行读写操作。与此同时,还会创建一个**FileDescriptor**对象来代表对文件的连接。如果系统文件有安全管理,则会根据name去检查对此文件是否有读取权限。同理,如果需要写权限,也需要进行检查。抛出的异常有:不合法参数,文件未找到和安全异常。
 >参数1:独立于系统的文件名

 >参数2:文件操作的模式。此参数有固定的输入字串,必须为"r"/"rw"/"rws"/"rwd"其中一个("rws"/"rwd"共同点:和FileChannel.force(boolean)方法功能类似,都是为了更加高效;"rws":出了读写,还要求每次更新文件内容和元数据都需要同步到设备的存储空间去;"rwd":仅仅是将文件内容写入到存储空间去),否则报不合法参数异常。

 最后这个方法的实现中,Android根据需要加多了一个mode:O_CREAT,表示如果该文件不存在就创建该文件。


- 获取流描述
 ```
 public final FileDescriptor getFD(){};
 ```
 方法解释:返回RandomAccessFile对象不透明的文件流的描述对象。会抛出IOException异常。

- 获取文件通道
 ```
 public final FileChannel getChannel(){}
 ```
 方法解释:返回RandomAccessFile对象的唯一的文件通道。关于FileChannel类,请看这篇文章:[FileChannel类的说明]()

 此方法和FileChannel.position()方法返回的对象是一样的。position()返回的是对象的具有偏移量的文件指针(getFilePointer()获取)的文件通道。

 方法实现里面我们看到用到了同步,但是同步的却是整个类,势必造成效率低下。

- 读取文件内容
 ```
 public int read(){};
 ```
 方法解释:读取文件数据的一个字节。一个字节以0--255的整数返回,返回-1表示到达文件的末尾。如果没有可用的输入流,此方法会阻塞。

 这个方法的理解,就和InputStream类的read()方法的理解一样就OK了。
 ```
 public int read(byte b[], int off, int len)
 ```
 方法解释:从文件中读取一定长度的字节,放入到参数1的数组中。返回-1表示已经到达文件末尾。抛出io异常,如果b为null抛出空指针异常和越界异常。

 这个方法和类的方法read(byte[], int, int)的理解是一样的。

 ```
 public int read(byte b[])
 ```
 方法解释:从文件中读取b.length长度的字节,放入到参数b中。

 此方法和InputStream.read(byte[])是类似的,这里就不多阐述了。

 ```
 public final void readFully(byte b[]){};
 public final void readFully(byte b[], int off, int len){};
 ```
 方法解释:从文件中读取特定长度字节的数组。此方法会重复的读取,直至读取到的字节长度大于等于要求读取的长度。如果还未读完需要的长度就到达了文件末尾,则抛出EOFException异常,或者io异常。

 上面提到的方法,最终都是会调用到以下的方法:
 ```
 private int readBytes(byte b[], int off, int len){};
 ```
 方法解释:返回字节序列数组。然后在调用到io桥的read方法:**IoBridge.read(fd, b, off, len)**。但是这个方法是一个私有方法,如果真的需要调用,需要用到反射知识。

 ```
 //从文件的“文件指针”当前位置,读取一个字节,如果返回0,则为false,其它值为true。
 public final boolean readBoolean(){}

 //从文件的“文件指针”当前位置,读取一个8位的字节。字节是从0-255。
 public final byte readByte(){}

 //读取一个**无符号**的8位字节。
 public final int readUnsignedByte(){}

 //方法解释:从文件中读取一个16位的有符号/无符号的数据。
 public final short readShort(){};
 public final int readUnsignedShort(){}

 //读取文件中一个有符号的32位整数
 public final int readInt(){}

 //读取文件中一个有符号的64位整数
 public final long readLong(){}

 //读取文件中的一个float数据
 public final float readFloat(){}

 //读取文件中的一个double数据
 public final double readDouble(){}

 //
 public final char readChar(){}

 //读取文件中的下一行内容
 public final String readLine(){}

 //以utf-8编码读取数据
 public final String readUTF(){}
 ```
 以上的方法有对应的writeXXX()方法。这里就不一一列出来解释了,和上面的方法的功能说明是类似的。

- 跳过特定字节
 ```
 public int skipBytes(int n){};
 public void seek(long offset){};
 ```
 这里将两个方法放到一起来对比讲解。

 skipBytes()方法跳过n个字节数据,返回的是实际上跳过的字节数,如果为负数,则没有跳过任何字节数九。如果到达了文件末尾,那么就不会再跳过,也不会返回EOFException异常。

 seek()方法是设置文件指针的偏移量。如果偏移量超出了文件长度,不会改变文件长度。

 例如文件的长度为300,当我们需要跳过50个字节数(length)的时候,会先去获取“文件指针”所在的位置在150处(offSet),所以“文件指针”跳到的位置应该是200(offSet+length)。当然,如果我需要跳过300个字节的话,那么就到了450的位置去了,这是不可能的,还需要做一个判断,如果到达了文件的最大值,就跳到文件的最大长度去。

- 写入数据
 ```
 public void write(int b){};
 ```
 方法解释:写入特定长度的字节到文件中去,也可以这么理解:追加内容到“文件指针”当前指定的地方。

 ```
 public void write(byte b[], int off, int len) {};
 ```
 方法解释:此方法除了指定写入数据的长度,还制定了应该从off位置开始写入。

 在后面还有一系列的writeXXX()方法,和readXXX()方法是相类似的,可以查看前面的各个readXXX()方法

- 获取文件长度
 ```
  public long length(){};
 ```
 方法解释:获取文件的长度。

- 设置文件长度
 ```
 public void setLength(long newLength){};
 ```
 方法解释:设置文件的长度。如果设置的文件长度比原来的文件长度要小,那么多出来的文件内容会被切掉。设置的文件长度比原来的文件长度要打,那么就会将此文件进行扩展,扩展后的文件内容是为定义的。此方法之后,获取到的文件长度就是设置的文件的长度了。

- 关闭文件流
 ```
 public void close(){};
 ```
 方法解释:关闭random access文件流,并且释放与此流相关的系统资源。

- 读取方法
 与writeXXX()一系列的方法相类似。这里就不一一贴出来讲解了。

###具体的使用
上面讲了这么多,我们来看一下这是如何来使用。因为我们需要在Android设备上面进行使用,我这里将创建一个rafile.dat的文件,放到内部存储空间的根目录,代码如下:

File file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
String path = file.getAbsolutePath() + “rafile.dat”;

>注意:这里和上面讲的可能不太一样,这里获取的路径为:/storage/emulated/0/DCIMrafile.dat,很明显,我传入的不仅仅是一个文件名了,而是文件的绝对路径了,貌似这样也是OK的。可能是因为系统会自动的进行根据反斜杠进行判断。

public static void raFile(String fileName) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(fileName, “rw”);
randomAccessFile.writeInt(1);
randomAccessFile.writeChars(“A”);

    FileDescriptor fd = randomAccessFile.getFD();
    CommonLog.logInfo(TAG, fd.toString());
    //FileUtils: FileDescriptor[28]

    long filePointer = randomAccessFile.getFilePointer();
    CommonLog.logInfo(TAG, filePointer);
    //FileUtils:  6

    //最后关闭流
    randomAccessFile.close();

    RandomAccessFile randomAccessFile2 = new RandomAccessFile(fileName, "r");
    int age = randomAccessFile2.readInt();
    CommonLog.logInfo(TAG, age);
    //FileUtils:  1

    char c = randomAccessFile2.readChar();
    CommonLog.logInfo(TAG, c);
    //FileUtils:  A

    //最后关闭流
    randomAccessFile2.close();
}
在new RandomAccessFile对象的时候,我们传入了读写的权限。首先写入了一个整数1和一个大写字母A,然后关闭流操作。接着又打开文件,依次读出整数和大写字母A。在中间,我们打印出流说明和文件指针的位置。

下面是运行的结果。
后台输出的文件:
![image description](http://example.com/example.png)

rafile.dat文件的内容截图:
![image description](http://example.com/example.png)

对于读者可能有这样的疑惑,明明整数1,为什么存储的变成了0000 0001了呢?原因是存储在文件中的是使用16进制来存储的。

我们首先快速的来进行一下复习,这里关系到的是字节和位,还有“与”操作。
>字节:byte;位:bit。1 byte = 8 bit。

>int占4个字节,long占8个字节,short占2个字节,char占2个字节,float占4个字节,double占8个字节。

>举例说明:比如我int = 1,那么二进制应该表示为:00000000 00000000 00000000 00000001,如果使用16进制来表示呢?那应该是00 00 00 01,这不就是上面的结果了么。我们可以将上面的输出0000 0001转成2进制来看下,因为每2位16进制数代表一个字节(例如0xff=11111111),最后一个字节:01,转成10进制为:1,就是对应我们的10进制的整数1了。同理,如果我们将储存的是255的数,那么在文件中输出的就是000000ff了,如果是256,那么就是00000100了。


>注意:因为我们写入的字节是有一定的顺序的,同时读出的时候不按着写入顺序来,那么就会变成了乱码。

###创建特定大小的文件
/**
 * 写入内容到文件
 *
 * @param fileName 文件名
 * @param length   文件大小,比如创建2m的文件空间,就是2 * 1024 * 1024 = 1048576 = 2M
 * @throws IOException io错误
 */
public static void fcFile(String fileName, int length) throws IOException {
    //
    FileChannel fc = new RandomAccessFile(fileName, "rw").getChannel();
    //注意,文件通道的可读可写要建立在文件流本身可读写的基础之上
    MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, length);
    for (int i = 0; i < length; i++) {
        out.put((byte) 0x44); //0x44表示B的16进制
    }
    //读取文件中间6个字节内容
    for (int half = length / 2, i = half; i < half + 6; i++) {
        CommonLog.logInfo(TAG, (char) out.get(i));
    }
    fc.close();
}
上面的是创建一个特定大小的文件(2M),然后写入数据,这里写入的是大写的字母D,并且是2m那么多的D。然后我们将中间的数据读取出来。

运行的结果如下:
写入2m的D,这里就不贴图了,都是DDDDDDD的数据。

读出来的也都是全部D。如下:

![image description](http://example.com/example.png)


###数据分段插入文件
设想有这样一种情况,在后台获取到的数据,并不是全部的数据,而是分成了好几块传输到前端,那么就需要在前端这里进行重新的“拼接”,组合成一个完整的数据。

下面代码是插入文件的方法。
/**
 * 将数据插入到文件中
 *
 * @param skip     跳过多少过字节进行插入数据
 * @param content  要插入的字符串
 * @param fileName 文件路径
 */
public static void saveFile(long skip, String content, String fileName) {
    try {
        RandomAccessFile randomAccessFile = new RandomAccessFile(fileName, "rw");
        if (skip < 0 || skip > randomAccessFile.length()) {
            CommonLog.logInfo(TAG, "skip不在文件中,请重试!");
            return;
        }
        byte[] bytes = content.getBytes();
        randomAccessFile.setLength(randomAccessFile.length() + bytes.length);
        for (long i = randomAccessFile.length() - 1; i > bytes.length + skip - 1; i--) {
            randomAccessFile.seek(i - bytes.length);
            byte tempByte = randomAccessFile.readByte();
            randomAccessFile.seek(i);
            randomAccessFile.writeByte(tempByte);
        }
        randomAccessFile.seek(skip);
        randomAccessFile.write(bytes);
        randomAccessFile.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
用法:例如一个完整的文件是300大小,分成了3块获取回来,每一块的代销为100,那么分别需要在不同位置进行插入分块数据。

//数据回来了
private void onDataBack() {
try {
RandomAccessFile raf = null;
raf = new RandomAccessFile(“文件名”, “rw”);
//为了怕不够用,事先分配1024大小空间
raf.setLength(1024);

        //模拟获取回来的3块数据
        String s1 = "内容1";
        String s2 = "内容2";
        String s3 = "内容3";

        skipInsert(0, s1.getBytes());
        skipInsert(100, s2.getBytes());
        skipInsert(200, s3.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

//分段插入数据
private void skipInsert(int skip, byte[] content) {
try {
randomAccessFile = new RandomAccessFile(“D://abc.txt”, “rw”);
randomAccessFile.seek(skip);
randomAccessFile.write(content);
} catch (IOException e) {
e.printStackTrace();
}
}
“`
我们这里看到,给s1,s2和s3是分块的数据,大小是100,那么每一块就需要中0-100,101-200,201-300的数据块进行存放。那么就可以做到返回的数据的连贯性了。


总结

上面的内容有很多,主要是RandomAccessFile类的方法的说明,方便自己去查阅加深理解这个类,然后就是讲解了此类的具体用法,以及如何进行文件的分块插入等等。

以上就是所有内容,如有任何问题,请及时与我联系,谢谢。

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值