在Java中,文件的I/O大致分为了三类:1)普通IO,存在于java.io包中,分为面向字符和字节两种;2)文件通道FileChannel,存在于 java.nio 包中,属于 NIO 的一种,但是是阻塞的;2)MMap内存映射,由FileChannel调用map方法产生的特殊的读写方式
在Java类库中,对于流的处理方向分为了输入和输出两种类型,每种方向又分为了面向字符和字节两种类型:
由于提供的接口过于底层,在使用时通常会叠合多个对象来达到期望的功能,使用的是Java中的装饰器模式;然而,这就导致了创建单一的流却需要创建多个对象
InputStream的作用是用来表示从不同数据源产生的输入类:
每一种数据源都有对应的InputStream子类作为基础的对象,同时还有一些装饰类来丰富基础类的功能,装饰类之间还可以相互嵌套,将功能组合起来得到我们想要的对象,具体的关系如下图所示:
【示例一】
通过FileOutputStream
获取对文件的输出流,再通过BufferedOutputStream
装饰器包装成带缓冲的输出流,最后再用DataOutputStream
装饰器包装,获得了格式化的输出输出数据
public class App {
public static void main(String[] args) {
try {
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("test.txt")));
out.writeDouble(3.343123);
out.writeUTF("dsdasdas");
out.close();
DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
double x = in.readDouble();
String m = in.readUTF();
System.out.println(x);
System.out.println(m);
in.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
【示例二】
使用ByteArrayInputStream
将字符串转换为内存中的数据流,并通过DataInputStream
一个字节一个字节的读取数据,需要注意的是,任何一个读取的字节都是合法的结果,因此无法判断是否读到末尾;当读到了文件的末尾会抛出EOFException异常
public class App {
public static void main(String[] args) throws IOException {
String data = "aaabbbcccddd";
DataInputStream in = new DataInputStream(new ByteArrayInputStream(data.getBytes()));
try {
while (true) {
System.out.print((char) in.readByte());
}
}catch (EOFException e) {
System.out.println();
System.out.println("read to end");
}
}
}
除了抓住异常来判断是否读到文件的结尾,也可以使用in.available()
方法来检测输入是否结束
相较于InputStream和OutputStream,提供了兼容Unicode与面向字节流的I/O功能,并且:
InputStreamReader
可以将InputStream
转换为Reader
;同理,OutputStreamWriter
可以将OutputStream
转换成Writer
和InputStream
和OutputStream
类似,它们也有对应的基础的类和丰富功能接口的装饰类,这里就不再赘述,重点讲解几个不同的点和示例
PrintWriter
它提供了一个既能接受Writer
对象又能接受任何OutputStream
对象的构造器,还提供了自动执行清空的选项,在每个Println()
之后自动清空内容
readLine()
在使用readLine()
时,应该使用BufferedReader
对象而不应该使用DataInputStream
【示例一】 缓冲输入文件
FileReader
打开文件获取输入流,BufferedReader
包装成带缓冲功能的输入流,按行读取数据public class App {
public static String read(String filename) throws IOException {
BufferedReader in = new BufferedReader(new FileReader(filename));
String str;
StringBuilder sb = new StringBuilder();
while((str = in.readLine()) != null) {
sb.append(str);
}
return sb.toString();
}
public static void main(String[] args) throws IOException {
String str = read("test.txt");
System.out.println(str);
}
}
【示例二】 基本文件输出
BufferedReader
包装StringReader
输入流PrintWriter
包装BufferedWriter
包装过的FileWriter
public class App {
static String file = "test.txt";
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new StringReader("sdad\nsdasd\ndsdsd"));
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
int lineCount = 1;
String s;
while((s = in.readLine()) != null) {
pw.write(lineCount++ + ": " + s);
}
pw.close();
in.close();
}
}
【示例三】 文件输出的快捷方式
PrintWriter
中添加了一个辅助构造器,使得每次创建文档并写入数据时,不再需要执行所有的装饰的操作,包含了缓存的操作而不需要自己去实现
PrintWriter out = new PrintWriter(filename);
out.println(...);
out.close();
使用RandomAccessFile
来读写随机访问文件,类似于组合使用的DataInputStream
和DataOutputStream
的效果,并且提供了seek()
函数在文件中随机移动。
在构造函数中,还有一个参数用于控制是"r(只读)"或者"rw(读写)"的方式来打开文件。
称为新I/O,利用通道和缓冲器来加快I/O的速度:我们只能和缓冲器交互,把缓冲器派送到通道处,由它和通道进行交互,完成数据的写入和读取操作。
在这里,缓冲器为ByteBuffer
对象,是一个非常基础的类,有如下功能:
ByteBuffer
对象在旧的I/O库中有三个类被修改可以产生Channel,分别是FileInputStream,FileOutputStream和随机访问文件的RandomAccessFile。
以下代码分别展示了三个功能:
FileOutputStream
获取Channel,并且用ByteBuffer.wrap()
来包装byte数组的数据RandomAccessFile
获取Channel,使用position()
调整指针位置进行随机的读写FileInputStream
获取Channel,通过ByteBuffer.allocate(SIZE)
创建指定空间的缓冲器,调用fc.read(buff);
将数据读入到缓冲区内,buff.flip()
调整指针信息,最后调用buff.get()
完成读操作注意
在ByteBuffer
内部有Capacity,Position和Limit三个概念
Capacity是用户指定的缓冲大小
Position类似于读写指针,表示当前读(写)到什么位置
Limit在写模式下表示最多能写入多少数据,此时和Capacity相同,在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同
在写模式下调用flip方法,那么limit就设置为了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读,即调用flip()
后,指针移动到缓存头部,limit为当前数据的尾部,可以读取整个缓存空间中有数据的部分。
public class App {
private static final int BSIZE = 1024;
public static void main(String args[]) throws IOException {
// FileOutputStream
FileChannel fc = new FileOutputStream("test.txt").getChannel();
fc.write(ByteBuffer.wrap("Some text".getBytes()));
fc.close();
// RandomAccessFile
fc = new RandomAccessFile("test.txt","rw").getChannel();
// 移动到文件末尾
fc.position(fc.size());
fc.write(ByteBuffer.wrap("Some more".getBytes()));
fc.close();
// Read file
fc = new FileInputStream("test.txt").getChannel();
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
// 将文件中的数据读入到buff中
fc.read(buff);
buff.flip();
while (buff.hasRemaining()) {
System.out.print((char)buff.get());
}
}
}
拷贝的功能通过channel读取文件放入到buffer中,再将buffer中的输入刷入到另一个文件中完成,一个文件读完的标志是in.read(buff) == -1
,则可说明读到文件末尾
ByteBuffer
中flip()
调整指针位置ByteBuffer
public class App {
private static final int BSIZE = 1024;
public static void main(String args[]) throws IOException {
FileChannel in = new FileInputStream("test.txt").getChannel();
FileChannel out = new FileOutputStream("text.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while (in.read(buffer) != -1) {
buffer.flip(); // ready to write
out.write(buffer);
buffer.clear(); // ready to read
}
}
}
视图缓冲器可以让我们通过某个特定的基本数据类型的视窗查看底层的ByteBuffer
,而ByteBuffer
才是实际存放数据的地方,对视图的任何修改都会实际影响底层存储的数据。
视图还允许非常容易的存取数据,不管是单个数据还是成批数据,具体的操作如下所示:
public class App {
private static final int BSIZE = 1024;
public static void main(String args[]) throws IOException {
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
IntBuffer ib = bb.asIntBuffer();
ib.put(new int[]{1,2,3,4,5,6});
System.out.println(ib.get(3));
ib.put(3,2222);
System.out.println(ib.get(3));
}
}
若往缓冲器中放入8个字节的数据[]byte{0,0,0,0,0,0,'a'}
,对应不同的视图可以看到不同的数据,结果如下图所示:
mmap 把文件映射到用户空间里的虚拟内存,省去了从内核缓冲区复制到用户空间的过程,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统 VMS 才根据缺页加载的机制从磁盘加载对应的数据块到物理内存进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高
RandomAccessFile.getChannel()
获得文件对应的channel信息public class App {
private static final int length = 0x8ffffff; // 128MB
public static void main(String args[]) throws IOException {
MappedByteBuffer out = new RandomAccessFile("test.dat","rw")
.getChannel()
.map(FileChannel.MapMode.READ_WRITE,0,length);
for(int i = 0; i < length; i++) {
out.put((byte)'x');
}
for (int i = 0; i < 100; i++) {
System.out.print((char) out.get(i));
}
}
}
[1] Java编程思想
[2] 文件 IO 操作的一些最佳实践