前面一节说了File类不可以访问数据(即对文件的读写操作)。那么java也设计了对与文件数据处理的方式。 在说对于数据的处理之前,需要对计算机的进制编码解码有一定的了解,如果这部分不是特别明白的,自行脑补,我就在这里不赘述了。
处理文件数据的方式一共有两种: 1.基于指针的操作来玩成对文件数据的读写 (1) 一个类用不同的方法完成对于文件的读写操作 (2) 因为由指针控制,所以可以在文件任意位置进行读写操作 2.基于流的方式完成对数据的读写 (1)使用不同的低级流或者高级流完成对于文件的读写操作,使用不同的高级流可以简化对于读写的操作(例如写对象,写字符等操作) (2)因为不是指针控制,所以只能覆盖写(读)或者追加写(读)
java.io.RandomAccessFile 该类是用于读写文件数据的。读写数据是基于指正的操作 即:总是在指正当前位置进行读写 RandomAccessFile有两种创建模式: 只读模式:仅用于读取文件数据 读写模式,对文件可以编辑(可读,可写)
构造方法:
/* * 针对raf.dat文件进行读写操作 * 构造方法: * RandomAccessFile(String path,String mode) * RandomAccessFile(File file,Sting mode) * * 模式对应的字符串: * “r”只读 * “rw”读写 */ RandomAccessFile raf= new RandomAccessFile("raf.dat","rw");RandomAccessFile的构造方法需要传入两个参数,第一个参数即可以传入文件路径,也可以直接传入File对象,第二个参数是两种模式。“r”只读模式,“rw”读写模式。 会抛出java.io.FileNotFoundException异常
write方法:
/* * void write(int d) * 将给定的int值的“低八位”2进制信息写入文件中 * vvvvvvvv(只写入这个地方) * 00000000 000000000 00000000 00000001 */ raf.write(97);//97对应的二进制0110001,对应解码为a System.out.println("写出完毕!"); //读写完毕后一定colse raf.close();需要在这里说明一下,write写入的是一个int值,大家都知道int是4个字节,也就是32位二进制,write方法写入的只是32位中的底八位,也就是说这里写入的int值范围是0-255之间的数字。
那么问题来了,如果我写入的int值大于255会是什么样? 例如
raf.write(256);这个时候写入的是什么呢? 这里其实写入的是0 二进制表示256为: 00000000 000000000 00000001 00000000 只写入底八位的字节,也就是只写入00000000, 前面的00000000 000000000 00000001并不会写入。 所以在读取时,只会读取到0。
既然每次只写入一个字节,那么为什么不直接用byte来写入呢? 假如:写入的是一个字节byte,那么这么做就会导致它的写入范围变成了-128到127之间的数了,在表示文件末尾就没办法用-1来表示了(会在read方法中看到)。
那么这就问题来了,想要写入的编码大于一个字节怎么办呢? 用多个字节拼接就可以了。
注意:write方法会抛出异常
从文件中读取字节
RandomAccessFile raf= new RandomAccessFile("raf.dat","r"); /** * int read() * 读取一个字节,并以int形式返回 * 若返回值为-1,则表示读取到了文件末尾 * 即:EOF(end of file) */ int d=raf.read(); System.out.println(d+" "+"写出完毕!"); raf.close();在读取时,每次只会读取一个字节,并且会把这一个字节填充到int的低八位去。 例如上面的例子:raf.dat文件写入的是256即000000000 读取时,读取到00000000, 并在前面填充00000000 000000000 0000000 这样读取到的就int值就为0 注意:int值-1的二进制表述为 111111111 11111111 11111111 11111111 因为在读取时,只会读取底八位的值,然后填充24个0,所以不论怎么样都不会读取到-1。 这也就是为什么write方法和read方法每次都是读写低八位的原因
此方法同样会抛出异常
RandomAccessFile提供了可以方便读写Java中不同数据类型数据的方法
RandomAccessFile raf= new RandomAccessFile("raf.dat","rw"); /* *long getFilePointer() *该方法可以获取当前RandomAccessFile的指针位置 *刚创建的RAF指针位置在文件开始处,以下标形式表示,所以第一个字节位置为0. */ long pos=raf.getFilePointer(); System.out.println("指正位置:"+pos); /* * 二进制对应的int最大值 * 01111111 111111111 11111111 11111111 */ int imax=Integer.MAX_VALUE; /* * 一次性写入4个字节,将给定的int值对应的32位2进制全部写出 */ raf.writeInt(imax); System.out.println("指正位置:"+raf.getFilePointer()); //一次性写入8个字节 double d=123.132; raf.writeDouble(d); System.out.println("指正位置:"+raf.getFilePointer()); //一次性写入8个字节 long l=12345; raf.writeLong(l); System.out.println("指正位置:"+raf.getFilePointer()); /* * void seek(long pos) * 将指针移动到pos位置 */ raf.seek(0); int i=raf.readInt(); System.out.println(raf.getFilePointer()+" "+i); raf.seek(12); long l1=raf.readLong(); System.out.println(raf.getFilePointer()+" "+l1); raf.seek(4); char c1=raf.readChar(); System.out.println(raf.getFilePointer()+" "+c1); /* * int readInt() * 连续读取4个字节,并转换为int值返回 * 若读取到文件末尾会抛出EOFException(EndOFFile) */ raf.close();关于指针在上面的代码中注释的很清楚(如果不懂什么是指针,请去查看一下C语言中的指针),现在着重来说说怎么做到一次写入4个字节的int值
在源码中可以看到
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; }从源码中我们可以清楚的看到,其实在写入int值的时候还是用了write方法,每次只写入一个字节,四个字节的写了四次。先将四个字节的头部一个字节,左移3个字节(即将头部字节移动到底八位去),写入文件,这样写入的底八位其实就是int值的头部一个字节。后面的三次写入也做了同样的移位操作。这样就保证了能完整的写入一个int值
在读取时,读取到每个字节做左移运算后加起来,这样就完整的还原了这个int值。
源码:
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)); }介绍了这么多的方法,利用这些方法来完成复制文件的操作
import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** * 复制文件 * @author Analyze * */ public class CopyDemo1 { public static void main(String[] args) throws IOException { RandomAccessFile src= new RandomAccessFile("music.flac","r"); RandomAccessFile desc=new RandomAccessFile("music_copy.mp3","rw"); int d=-1; long start=System.currentTimeMillis(); while((d=src.read())!=-1){ desc.write(d); } long end=System.currentTimeMillis(); System.out.println("复制完毕!"); System.out.println("耗时"+(end-start)+"ms"); src.close(); desc.close(); } }注意:最好不要在主函数上抛出异常,这里只是用于测试。
在上面的这段代码中,大家在测试中可能会发现一个问题,如果当复制一个文件比较大的时候(大于20MB),计算机会处理很长的时间。按照我们平时在操作系统上复制文件时,压根就不需要花费这么长的时间,这是为什么呢?
我们前一节有提到,所有文件都是在硬盘上,而你在读取时每次讲读取到到值赋值给d的时候,d这个变量都是在内存中开辟的,复制的时候又将内存中开辟的这个变量复制给了硬盘上的某个文件。这样类似于在这么干一件事,你想要将很多块砖头搬到在20层楼的家里,你每次只搬一块砖头就往二十层跑,每搬一块就跑一次,这样当然特别耗费时间精力。
那么有什么好的改良方法呢?我可以准备一个小推车,每次在小推车上把砖头装满,一次就搬运小推车的量。也就是说,我们可以准备一个数组用来承担每次运输到硬盘上的任务。
代码示例:
import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** * 若想提高读写效率,需要通过提高每次读写的数据量来减少读写的次数达到 * * 读写硬盘的次数越多,读写效率越差 * @author Analyze * */ public class CopyDemo2 { public static void main(String[] args) throws IOException { RandomAccessFile src= new RandomAccessFile("music.flac","r"); RandomAccessFile desc= new RandomAccessFile("music_copy1.mp3","rw"); /* * 一次读取一组字节的方法 * int read(byte[] date) * 一次性读取给定数组date的length个字节 * 并且将读取的字节全部存入到date数组中 * 返回值为实际读取到的字节量,若为-1,则表示本次没有读取到任何字节(文件末尾) */ //10kb(推荐使用,一般来讲10kb是最合适的读写,并不是越大越好) byte[] buf=new byte[1024*10]; int len=-1; long start=System.currentTimeMillis(); while((len=src.read(buf))!=-1){ /* * void write(byte[] date) * 将给定字节数组中的所有字节一次性写出 * * 重载的方法 * void write(byte[] d,int s,int len) * 将给定数组中从下标s处的字节开始的连续len个字节一次性写出 */ desc.write(buf, 0, len); } long end=System.currentTimeMillis(); System.out.println("复制完毕!"); System.out.println("耗时"+(end-start)+"ms"); src.close(); desc.close(); } }这里需要说明一点,为什么不write方法直接将buf数组直接写入,而用了重载的方法限制写入长度呢?
模拟一下这个过程,每次都读取到一个buf数组的容量并写入,那么如果最后一次读取时,读取到一半这个文件就到文件末尾了,那么这个buf数组就会变成前面一半是这次读到的,后面一半是上次读的后一半(因为每次都是用同一个buf数组),这样就会导致复制的最后一部分出现重复。
关于访问文件流的处理方式,会在下一节详细总结。