NIO入门
传统的输入流、输出流(面向流的输入/输出系统)一次只能处理一个字节(即使我们不直接去处理字节流,但底层的实现还是依赖于字节处理),因此面向流的输入/输出系统通常效率不高。新IO使用了不同的方式来处理输入/输出,新IO采用内存映射的方式的来处理输入/输出(即将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,模拟了操作系统上的虚拟内存的概念),这种方式效率要高。
Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;
Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过访方法可直接将“一块数据”映射到内存中。如果说传统输入/输出系统是面向流的处理,则新IO则是面向块的处理
1.Buffer
Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。
Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作,其它基本类型也都有对应的Buffer类(boolean除外):CharBuffer、IntBuffer.....这些Buffer类都没有提供构造器,通过static XxxBuffer allocate(int capacity)创建一个容量为capacity的XxxBuffer对象。
使用较多的是ByteBuffer和CharBuffer,ByteBuffer还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。
Buffer中有3个重要的概念:capacity、limit、position(初始时position=0,limit=capacity). 0<=mark<=position<=limit<=capacity Buffer的主要作用就是装入数据,然后输出数据,每放入一些数据position就相应地向后移动一些位置,装入数据结束后,调用它的flip()方法(会使limit跳到了position的位置,但position又跳到了0的位置),为输出数据作好准备。输出数据结束后,调用clear()方法会使position置0, limit置为capacity,又为装入数据作好准备。
Buffer的所有子类还提供了两个重要的方法:put()和get()方法,用于向Buffer中放入数据和从Buffer中取出数据(既支持单个数据也支持批量数据(以数组为参数))。
使用put()和get()来访问Buffer中数据分为相对(影响position的位置)和绝对(并不影响position的位置)两种。
public class BufferTest
{
public static void main(String[] args)
{
// 创建Buffer
CharBuffer buff = CharBuffer.allocate(8); // ①
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c'); // ②
System.out.println("加入三个元素后,position = "
+ buff.position());
// 调用flip()方法
buff.flip(); // ③
System.out.println("执行flip()后,limit = " + buff.limit());
System.out.println("position = " + buff.position());
// 取出第一个元素
System.out.println("第一个元素(position=0):" + buff.get()); // ④
System.out.println("取出一个元素后,position = "
+ buff.position());
// 调用clear方法
buff.clear(); // ⑤
System.out.println("执行clear()后,limit = " + buff.limit());
System.out.println("执行clear()后,position = "
+ buff.position());
System.out.println("执行clear()后,缓冲区内容并没有被清除:"
+ "第三个元素为:" + buff.get(2)); // ⑥
System.out.println("执行绝对读取后,position = "
+ buff.position());
}
}
通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer,直接Buffer的创建成本比普通Buffer的创建成本高,但直接Buffer的读取效率更高。
2. Channel
Channel类似于传统的流对象,但与传统的流对象有两个区别:
Channel可以直接将指定文件的部分或全部直接映射成Buffer;
程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。
java为Channel接口提供了DatagramChannel(UDP)、FileChannel、Pipe.SinkChannel/Pipe.SourceChannel(线程之间通信的管道)、SelectableChannel、ServerSocketChannel/SocketChannel(TCP)等实现类。
所有的Channel都不应该通过构造来直接创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。
Channel中最常用的3类方法是map()、read()和write().map()用于将Channel对应的部分或全部映射成ByteBuffer.剩下两个(有一系列的重载形式)用于从Buffer读取数据或向Buffer写入数据。map()方法签名:MappedByteBuffer map(FileChannle.MapMode mode, long position, long size)
示例如下,下面程序实现了两个功能:
public class FileChannelTest {
public static void main(String[] args) {
File f = new File(".\\src\\FileChannelTest.java");
try {
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length()); // ①
// 使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
// 直接将buffer里的数据全部输出
outChannel.write(buffer); // ②
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
FileInputStream获取的FileChannel只参读,FileOutputStream获取的FileChannel只能写。
RandomAccessFile中也包含了一个getChannel()方法,它返回的只读的还是读写的取决于raf打开文件的模式。
下面程序实现的功能是:
public class RandomFileChannelTest {
public static void main(String[] args) throws IOException {
File f = new File("a.txt");
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel();
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
// 把Channel的记录指针移动到最后
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
如果习惯了传统IO的用“竹筒多次重复取水”的过程,或者担心Channel对应的文件过大,也可以使用Channel和Buffer传统的“竹筒多次重复取水”的方式:
public class ReadFile {
public static void main(String[] args) throws IOException {
// 创建文件输入流
FileInputStream fis = new FileInputStream(".\\src\\ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel();
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(256);
// 将FileChannel中数据放入ByteBuffer中
while (fcin.read(bbuff) != -1) {
// 锁定Buffer的空白区
bbuff.flip();
// 创建Charset对象
Charset charset = Charset.forName("GBK");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
上面代码虽然使用FileChannel和Buffer来读取文件,但处理方式和使用InputStream、byte[]来读取文件的方式几乎一样,都是采用“用竹筒多次重复取水”的方式。