译者序:这是一篇比较使用的I/O新特性的介绍文章。文中使用了大量的代码实例来演示和解说如何使用J2SE1.4的新I/O特性并提供你应用程序的性能,而且提供了两个完整的例子,其中包括一个循环WEB服务器的雏形,非常值得我们参考。
回溯到2000年的1月,当人们正在争论着公元2000年究竟是一个世纪的开始还是一个实际的结束的时候,一份新的JAVA规范——JSR(Java Specification Request)51也被审核通过了。这份JSR的名字是《New I/O APIs for the Java Platform》(JAVA平台的新I/O API)。许多人认为这份新的规范只会给大家带来非阻塞I/O操作的能力,但是在JSDK1.4Beta(JavaTM 2 Platform, Standard Edition)中引入的新的特性,却还包含其它的一些新而有趣的特征。新的API在提供了可升级的套接口(socket)和文件I/O操作的同时(这是理所当然的),你也可以找到一个正则表达的包来支持模式匹配,以及对字符集转换的编码器和解码器,和优化过的文件系统支持如文件锁定、内存映射等功能。我们在这篇文章中的讨论会全面覆盖上面所说的四个新特性。注意:JAVA本地接口(JNI)为新的I/O操作所做的修改我们将不会涉及,如果你需要了解有关的内容,请参考本文结尾“资源”部分的有关内容。
Buffers
按照从最简单到最复杂的习惯,我们将从java.nio包中的一系列Buffer类开始说起。Buffer提供了一种在内存容器中保存一系列原始数据的机制。基本上,你可以设想一下,把DataInputStream/DataOutputStream组合在一起封装成一个固定字节大小的数组而只允许读写一种数据类型,例如char,int,或者double。在这个包里,总共有7种这样的Buffer可用:
· ByteBuffer · CharBuffer · DoubleBuffer · FloatBuffer · IntBuffer · LongBuffer · ShortBuffer
实际上,ByteBuffer也能够对其它六种类型进行读写,但是这些特别的Buffer更有针对性,更专门化一些。为了示范如何使用一个Buffer,接下来这一小片代码将完成一个从String型变量到一个CharBuffer的转换,并从这个Buffer中逐一的读出单个字符。你可以用warp方法来完成转换,用get方法来取一个字符。
CharBuffer buff = CharBuffer.wrap(args[0]);
for (int i=0, n=buff.length(); i
{System.out.println(buff.get());}
在使用Buffer的时候,一定要注意它目前的大小(sizing)和位置(positioning)的值是有区别的,千万不要混淆了。方法length是不规范的,尤其是对于CharBuffer而言。当然这并非是出了什么错,而是它返回的是Buffer中的剩余长度的值,所以如果position并非在Buffer的开始处的话,返回值将不是Buffer的长度,而是在Buffer中剩余的字符的长度。换句话说,上面程序中的循环也可以修改成这样:
CharBuffer buff = CharBuffer.wrap(args[0]);
for (int i=0; buff.length() 0; i++)
{System.out.println(buff.get());}
我们回到正题,继续讨论大小(sizing)和位置(positioning)的关系,在这里,有四个概念必须明确,它们是mark(标记),position(位置),limit(限制),和capacity(容量)。· mark——用mark方法设置的可设位置,mark方法可以使用reset来重置position,<=position,=0;· position——在Buffer中目前读写的位置,<=limit;· limit——第一个不应该被读取的元素的位置的index(索引号),<=cpacity;· capcity——Buffer的大小,=size。Position(位置)属性值是我们在对一个Buffer读取或者写入的时候需要时刻牢记的信息。例如,如果你想读取你刚刚写入的字符,你不许把position移动到你想读取的位置,否则,你将越过limit的限制,而读到一个不知道是什么的字符。这时候你需要立刻使用flip方法,把limit移动到当前的位置,并把position移动到0位置。你也可以回绕一个buffer来保持当前的limit位置,而把position返回到0位置。举个例子,如果从下面这一小段代码中的flip调用去掉,将返回一个空白,因为在buffer中还什么都没有。
buff.put('a');
buff.flip();
buff.get();
上面的封装机制是一个非直接缓冲(non-direct buffer)的例子。非直接缓冲也可以通过allocate方法来创建和限定大小,本质上来说,只是把数据封装到一个数组里了。如果愿意消耗稍微多一点的创建资源,你也可以通过allocateDirect方法开辟一块连续的内存来保存数据,这也可以称作直接缓冲。直接缓冲是依赖于系统的本地接口的I/O操作来优化存取操作的。
文件映射
MappedByteBuffer是一个专门用于直接缓冲的ByteBuffer,这个类用字节缓冲来映射一个文件。想要映射一个文件到MappedByteBuffer,你必须先取得这个文件的通道(channel)。通道是某种已建立的连接,例如管道(pipe),套接口(socket),或者文件(file)等能够完成I/O操作的对象。如果是文件通道,你可以通过FileInputStream(文件输入流),FileOutputStream(文件输出流)或者RandomAccessFile(随机存取文件)的getChannel方法来获得一个通道。一旦你取得这个通道,你就可以通过它的map方法,指明映射模式来把你想映射的那一部分文件映射到缓冲中去。文件通道可以使用FileChannel.MapMode的任一个常数打开:只读(READ_ONLY),私有/写时拷贝(PRIVATE),或者读写(READ_WRITE)。下面是一个从文件中创建只读的MappedByteBuffer的基本例程:
String filename = ...;
FileInputStream input = new FileInputStream(filename);
FileChannel channel = input.getChannel();
int fileLength = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength);
你可以在java.nio.channels包里找到与通道有关的类。一旦MappedByteBuffer被建立了,你就可以象存取其它任何ByteBuffer一样来操作它。当然在这个例子里它是只读的,所以加入你试图写入一些东西的时候,它会抛出一个NonWritableChannelException的异常。假如你想把它当作字符来处理的话,你必须制定一个字符集把ByteBuffer转化成CharBuffer。这个字符集是在Charset类中定义的。然后你用CharsetDecoder类对文件的内容进行解码。它相反的操作是由CharsetEncoder类来完成的。
// ISO-8859-1 是ISO拉丁字符表#1Charset
charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
这个类可以在java.nio.charset包中找到。
正则表达式
一旦你完成了从文件到CharBuffer的可输入映射,你就可以对文件内容进行模式匹配。就像我们分别使用grep命令和wc命令来进行正则表达的匹配和单词计数一样。其中使用到了java.util.regex中的Pattern和Matcher两个类。Pattern类为匹配正则表达提供了所有的构造类型。一般来说,你的模式表达是一个字符串,可以查阅类文档得到模式的完整细节,这里只提供一些简单常用的例子:· 行模式,任意个字符然后以回车换行借宿并且/或者行结束:.* ?或.*$ · 连续的数字:[0-9]* 或者 d*· 一个控制符:{cntrl}· 一个大写或者小写US-ASCII字符,接着一个空格,接着标点:[p{Lower}p{Upper}]sp{Punct}注:不幸的是,J2SE 1.4 beta3中打断了这一切,因为它对正则表达式所必须的字符缓冲的次序支持的非常不好。从SUN的Bug Parade可以看到这个问题的详细资料(希望你有JDC的帐号,呵呵,没有就快去申请啊,还愣着干什么?)。很遗憾,这意味着你不能用模式匹配同时去读取一个词或者一行。如果想获得更多的有关正则表达式库的信息,可以参考本文最后所列“资源”中的《Regular Expressions and the Java Programming Language》(正则表达和java编程语言)
套接口通道
下面我们要从文件通道转移到读写一个套接口连接的通道中来。这个通道可以用做阻塞模式,也可以用作非阻塞模式。如果是阻塞模式,取决于你的程序是服务器端还是客户端,只需把你的调用换成connect或者accept。而在非阻塞模式,它们的处理方式是不一样的。这些新类处理基本套接口的读写操作。在java.net包中的InetSocketAddress类指定连接地址,java.nio.channels包中的SocketChannel类来完成实际的读写操作。使用InetSocketAddress来进行连接非常类似于普通的Socket类的操作。你所要做的一切仅仅是提供主机和端口号:
String host = ...;
InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
一旦你获得了InetSocketAddress,一切都改变了(怎么听着象童话^&^)。你可以打开一个SocketChannel来连接到InetSocketAddress,用它来取代我们以前从套接口的输入流来读取、向套接口的输出流写入的所有操作:
SocketChannel channel = SocketChannel.open();
channel.connect(socketAddress);
在连接完成之后,你立刻可以使用By