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[]来读取文件的方式几乎一样,都是采用“用竹筒多次重复取水”的方式。


 

 

posted @ 2017-04-05 23:04  john8169  阅读(95)  评论(0编辑  收藏  举报