Java面向对象系列[v1.0.0][Buffer和Channel]

Java新IO的概念和作用

新IO和传统的IO目的是相同的,都是用于处理输入和输出,但新IO使用了不同的方式来处理,就是内存映射文件,它将文件或文件的一段区域映射到内存中,像访问内存一样来访问文件,大幅提高了IO的性能

新IO相关的包如下:

  • java.nio:主要包含各种与Buffer相关的类
  • java.nio.channels:主要包含与Channel和Selector相关的类
  • java.nio.charset:主要包含与字符集相关的类
  • java.nio.channels.spi:主要包含于Channel相关的服务提供者编程接口
  • java.nio.charset.spi:包含与字符集相关的服务提供者编程接口
  • Channel(通道)和Buffer(缓冲)是新IO的两个核心对象,Channel是对传统的输入输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;
  • Channel(通道)提供了一个map()方法,通过该map()方法可以直接将数据映射到内存中,传统的IO是面向流的处理,新IO则是面向块的处理
  • Buffer可以理解为一个容器,其本质是个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中
  • 新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类
  • 新IO还提供了用于支持非阻塞式输入/输出的Selector类

使用Buffer和Channel完成输入和输出

Buffer类

Buffer是一个抽象类,它最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作,除了ByteBuffer以外,其他数据类型也都有对应的Buffer类:CharBuffer/ShortBuffer/IntBuffer/LongBuffer/FloatBuffer/DoubleBuffer,这些Buffer类都没有提供构造器,使用静态方法来得到其对象,如下所示

  • static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象

ByteBuffer类还有一个子类,MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。



在Buffer中有3个概念:容量capacity、界限limit、位置position

  • capacity:缓冲区的容量,表示该Buffer的最大数据容量,也就是最多可存储多少数据,该值不能为负且创建后不能改变
  • limit:第一个不应该被读出或者写入的缓冲区位置索引,也就是说位于limit后的数据既不可被读也不可被写
  • position:用于指明下一个可以被读出的或者写入的缓冲区位置索引,类似于IO流中的记录指针,当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当新建一个Buffer对象时,其position为0,如果从Channel中读取了2个数据到该Buffer中,则position为2,指向Buffer中的第三个位置(第一个位置的索引为0)
  • mark:Buffer里还支持一个可选的标记mark,类似于传统IO流中的mark,Buffer允许直接将position定位到该mark处
  • 这些值的关系是:0<=mark<=position<=limit<=capacity
    在这里插入图片描述

如图所示

  • Buffer的主要作用就是装入数据,然后输出数据,创建一个Buffer对象时,Buffer的position为0,limit为capacity,程序可以通过put()方法向Buffer中放入一些数据(或者从Channel中获取数据),每放入一些数据,Buffer的position相应的向后移动相应位置
  • Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设置为0,这样Buffer的读写指针就移动到了开始的位置
  • Buffer调用flip()方法后,Buffer为输出数据做好了准备,当Buffer输出数据结束后,Buffer调用clear()方法,它不是用来清除Buffer的数据,而是将position设置为0,将limit设置为capacity,自此Buffer可以再次被装入数据

此外Buffer还包含如下常用方法:

  • int capacity():返回Buffer的capacity大小
  • boolean hasRemaining():判断当前position和limit之间是否还有元素可供处理
  • int limit():返回Buffer的limit的位置
  • Buffer limit(int newLt):重新设置limit值,并返回一个具有新limit的Buffer对象
  • Buffer mark():设置Buffer的mark位置,只能在0和position之间mark
  • int position():返回Buffer的position
  • Buffer position(int newPs):设置Buffer的position,并返回position被修改后的Buffer对象
  • int remaining():返回当前位置和limit之间的元素个数
  • Buffer reset():将position转到mark所在的位置
  • Buffer rewind():将position设置为0,取消设置的mark

Buffer的所有子类还提供了两个重要的方法:

  • put():用于向Buffer中放入数据
  • get():用于从Buffer中取出数据
    当使用这两个方法的时候,既可以对单个数据操作,也可以对多个数据操作,当需要对多个数据操作的时候只需要传入数组参数即可
    当时用这两个方法来访问Buffer的数据时,可以从Buffer的当前position处开始读取或写入数据,然后将position的值按处理元素的个数增加;也可以直接根据索引向Buffer中读取或写入数据,这个方式并不会影响position的值
import java.nio.*;

public class BufferTest
{
	public static void main(String[] args)
	{
		// 创建CharBuffer,capacity和limit为8,position为0
		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()方法,将limit设置为position处,把position设置为0
		buff.flip();	  
		System.out.println("执行flip()后,limit = " + buff.limit());
		System.out.println("position = " + buff.position());
		// 取出第一个元素,position向后移动一位
		System.out.println("第一个元素(position=0):" + buff.get());  
		System.out.println("取出一个元素后,position = "
			+ buff.position());
		// 调用clear方法,将position设置为0,将limit设置为与capacity相等
		buff.clear();    
		System.out.println("执行clear()后,limit = " + buff.limit());
		System.out.println("执行clear()后,position = "
			+ buff.position());
		// 根据索引来取值的方式不会影响Buffer的position
		System.out.println("执行clear()后,缓冲区内容并没有被清除:"
			+ "第三个元素为:" + buff.get(2));    
		System.out.println("执行绝对读取后,position = "
			+ buff.position());
	}
}

通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer,直接Buffer的创建成本比普通Buffer高,但读取效率也更高

Channel类

  • Channel可以直接将指定文件的部门或者全部映射成Buffer
  • 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互

Java为Channel接口提供了DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel、SocketChannel等实现类,所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。例如FileInputStream、FileOutputStream的getChannel()返回的是FileChannel,而PipedInputStream和PipedOutputStream的getChannel()返回的是Pipe.SinkChannel、Pipe.SourceChannel

Channel中最常用的三类方法是map()、read()和write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer,而read()和write()方法都有一系列重载形式,这些方法用于对Buffer读取或写入数据

map()方法的方法签名为:MappedByteBuffer map(FileChannel.MapMode mode, long position, long size),第一个参数是执行映射时的模式,分别有只读、读写等模式,第二个、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class FileChannelTest
{
	public static void main(String[] args)
	{
		var f = new File("FileChannelTest.java");
		try (
			// 创建FileInputStream,以该文件输入流创建FileChannel
			var inChannel = new FileInputStream(f).getChannel();
			// 以文件输出流创建FileBuffer,用以控制输出
			var 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();
		}
	}
}

RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel()是只读的还是读写的,则取决于RandomAccessFile打开文件的模式

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class RandomFileChannelTest
{
	public static void main(String[] args)
		throws IOException
	{
		var f = new File("a.txt");
		try (
			// 创建一个RandomAccessFile对象
			var 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);
		}
	}
}

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class ReadFile
{
	public static void main(String[] args)
		throws IOException
	{
		try (
			// 创建文件输入流
			var fis = new FileInputStream("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();
			}
		}
	}
}

每次读取数据后调用flip()方法将没有数据的区域封住,避免程序从Buffer中取出null值,数据取出后立即调用clear()方法将Buffer的position设为0,为下一次读取数据做准备

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章