Java开发笔记(九十二)文件通道的基本用法

时间:2020-12-20 06:28:25

前面介绍的各色流式IO在功能方面着实强大,处理文件的时候该具备的操作应有尽有,可流式IO在性能方面不尽如人意,它的设计原理使得实际运行效率偏低,为此从Java4开始增加了NIO技术,通过全新的架构体系带来了可观的性能提升。
NIO是“Non-blocking IO”的缩写,意思是非阻塞的IO,与之相对应,传统的流式IO又被称作BIO(“Blocking IO”的缩写),意即阻塞的IO。所谓阻塞与非阻塞,说起来挺拗口,令人不知所云,这都是设计师脑袋短路惹的祸,发明了这么难懂的词汇,害得初学者一脸懵逼。其实阻塞与非阻塞的区别,犹如私家车与出租车的区别,私家车买回来以后只供车主一家开,没开的时候要么停在小区地库,要么停在公共停车场,其他人是不能随便坐上这部私家车的,如此一来私家车便处于阻塞模式,车门塞住了外人打不开。而出租车整日在街上穿行,有客人招手就停下来载客,开到目的地乘客下车,然后恢复空车状态重新揽客,这样出租车便处于非阻塞模式,车门没塞住乘客打得开。显然阻塞模式存在资源的极大浪费,一个资源分配给某人之后,即使无事可做也只能空在一边闲得发慌;而非阻塞模式充分发挥了物尽其用的原则,一个资源用完之后马上释放,随时允许下一个人接着使用。
非阻塞的NIO机制画了一个高效的大饼,谁知对于文件来说却是画饼充饥,原来非阻塞模式只适用于网络请求交互,而文件处理总是处于阻塞模式,想想看,某个文件被A用户打开之后,B用户还能往该文件写入数据吗?很明显即使A用户打开文件后啥事都不做,B用户也不能写入该文件,缘于文件已经被A用户霸占了。之所以文件没有非阻塞模式,是因为文件仅仅为磁盘上的某个存储片段,它既不智能也不主动,更无法进行任务调度,只能被动的打开和关闭。既然文件处理不支持非阻塞机制,难道NIO技术对文件来说形同虚设?当然事实并非如此,要知道NIO技术不光光包括非阻塞机制,还包括文件通道、虚拟内存等等手段,可谓博大精深、不一而足。
先看文件通道,众所周知,传统的流式IO分为输入流与输出流,输入与输出拥有各自的Stream工具,输入流工具InputStream只能用来读文件,输出流工具OutputStream只能用来写文件,二者井水不犯河水。那如果打开文件之后,想要一会儿读一会儿写,输入流和输出流可得忙坏了,读的时候招呼InputStream来个全套操作,写的时候再招呼OutputStream来个全套操作,实在是劳民伤财。文件通道就不一样,通道中的数据允许双向流动,流进来意味着读操作,流出去意味着写操作,这样文件的读写操作集中在文件通道里进行,大大节省了系统的资源开销。在操作系统层面,通道是一种专职I/O操作的简单处理器,它专门负责输入输出控制,使得CPU从繁琐的I/O处理中解放出来,从而有效地提高整个系统的资源利用率。
文件通道对应的Java类型名叫FileChannel,它的创建方式主要有两种,第一种要通过输入输出流,即调用输入输出流的getChannel方法获取通道对象。比如下面代码根据文件输入流得到了可读的文件通道:

			// 第一种方式:根据文件输入流获得可读的文件通道
FileChannel channel1 = new FileInputStream(mFileName).getChannel();

又如下面代码根据文件输出流得到了可写的文件通道:

			// 第一种方式:根据文件输出流获得可写的文件通道
FileChannel channel2 = new FileOutputStream(mFileName).getChannel();

第二种方式则要通过随机文件工具,仍旧调用随机文件工具的getChannel方法获取通道对象。此时文件通道对象的构建代码示例如下:

			// 第二种方式:根据随机访问文件获得可读的文件通道
FileChannel channel1 = new RandomAccessFile(mFileName, "r").getChannel();
// 第二种方式:根据随机访问文件获得可写的文件通道
FileChannel channel2 = new RandomAccessFile(mFileName,"rw").getChannel();

得到文件通道对象之后,接着便能调用下列方法完成相应的文件处理动作:
isOpen:判断文件通道是否打开。
size:获取文件通道的大小(即文件长度)。
truncate:截断文件大小到指定长度。
read:把文件通道中的数据读到字节缓存。
write:往文件通道写入字节缓存中的数据。
force:强制写入磁盘,相当于缓存输出流的flush方法。
close:关闭文件通道。
从以上方法列表可知,FileChannel相当于集成了FileInputStream和FileOutputStream,用起来更加方便。下面来个利用文件通道写文件的代码例子,一样的简洁明了:

	// 通过文件通道写入文件
private static void writeChannel() {
String str = "春眠不觉晓,处处闻啼鸟。\n夜来风雨声,花落知多少。";
// 根据文件输出流获得可写的文件通道。注意文件通道支持try(...)的自动关闭操作
try (FileChannel channel = new FileOutputStream(mFileName).getChannel()) {
// 生成字符串对应的字节缓存对象
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
channel.write(buffer); // 往文件通道写入字节缓存
//channel.force(true); // 强制写入磁盘,相当于输出流的flush方法
} catch (Exception e) {
e.printStackTrace();
}
}

再来利用文件通道读文件的代码例子,具体如下所示:

	// 通过文件通道读取文件
private static void readChannel() {
// 根据文件输入流获得可读的文件通道。注意文件通道支持try(...)的自动关闭操作
try (FileChannel channel = new FileInputStream(mFileName).getChannel()) {
int size = (int) channel.size(); // 获取文件通道的大小(即文件长度)
// 分配指定大小的字节缓存
ByteBuffer buffer = ByteBuffer.allocateDirect(size);
channel.read(buffer); // 把文件通道中的数据读到字节缓存
buffer.flip(); // 把缓冲区从写模式切换到读模式。从缓冲区读取数据之前,必须先调用flip方法
byte[] bytes = new byte[size]; // 创建与文件大小相同长度的字节数组
buffer.get(bytes); // 把字节缓存中的数据取到字节数组
String content = new String(bytes); // 把字节数组转换为字符串
System.out.println("content="+content);
} catch (Exception e) {
e.printStackTrace();
}
}

看来文件通道在读文件过程中也使用了缓存,整体的代码流程同缓存输入流BufferedInputStream类似,只不过与FileChannel搭配的字节缓存ByteBuffer用着不太顺手,别着急,后面的文章将细细道来ByteBuffer的详细用法。

更多Java技术文章参见《Java开发笔记(序)章节目录