前面一节说了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数组),这样就会导致复制的最后一部分出现重复。
关于访问文件流的处理方式,会在下一节详细总结。