Java NIO学习系列一:Buffer
前面三篇文章中分别总结了标准Java IO系统中的File、RandomAccessFile、I/O流系统,对于I/O系统从其继承体系入手,力求对类数量繁多的的I/O系统有一个清晰的认识,然后结合一些I/O的常规用法来加深对标准I/O系统的掌握,感兴趣的同学可以看一下:
<<Java I/O系统学习系列一:File和RandomAccessFile>>
<<Java I/O系统学习系列三:I/O流的典型使用方式>>
从本文开始我会开始总结NIO部分,Java NIO(注意,这里的NIO其实叫New IO)是用来替换标准Java IO以及Java 网络API的,其提供了一系列不同与标准IO API的方式来处理IO,从JDK1.4开始引入,其目的在于提高速度。
之所以能够提高速度是因为其所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车满载煤炭而归,我们再从卡车上获得煤炭。也就是说,我们并没有直接和通道交互,而是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
在标准IO的API中,使用字节流和字符流。而在Java NIO中是使用Channel(通道)和Buffer(缓冲区),数据从channel中读取到buffer中,或从buffer写入到channel中。Java NIO类库中的核心组件为:
- Buffer
- Channel
- Selector
本文中我们会着重总结Buffer相关的知识点(后面的文章中会继续介绍Channel即Selector),本文主要会围绕如下几个方面展开:
1. Buffer简介
Java NIO中的Buffer一般和Channel配对使用。可以从Channel中读取数据到Buffer,或者写数据到Channel中。一个Buffer其实就是代表一个内存块,你可以往里面写数据或者从中读取数据。这个内存块被包装成一个Buffer对象,并且提供了一系列方法使得操作内存块更便捷。
通过Buffer来读写数据通常包括如下4步:
- 写数据到Buffer中;
- 调用buffer.flip();
- 从Buffer读取数据;
- 调用buffer.clear()或buffer.compact();
当往Buffer中写数据时,Buffer能够记录写了多少数据。当要从Buffer中读取数据时,就需要通过调用flip()方法将Buffer从写模式切换到读模式。一旦读完所有数据,需要清空Buffer,让它再次处于写状态。可以通过调用clear()或compact()方法来完成这一步:
- clear()方法会清空整个Buffer;
- compact()方法仅仅清空你已经从Buffer中读取的数据,未读数据会被移动到Buffer起始位置,可以紧接着未读的数据写入新的数据;
如下是一个简单的使用例子,通过FileChannel和ByteBuffer读取pom.xml文件,并逐字节输出:
public class BufferDemo { public static void main(String[] args) { try { RandomAccessFile raf = new RandomAccessFile("pom.xml","r"); FileChannel channel = raf.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(48); int byteReaded = channel.read(buffer); while(byteReaded != -1) { buffer.flip(); while(buffer.hasRemaining()) { System.out.print((char)buffer.get()); } buffer.clear(); byteReaded = channel.read(buffer); } raf.close(); }catch (Exception e) { e.printStackTrace(); } } }
2. Buffer的内部结构
上面说到Buffer封装了一块内存块,并提供了一系列的方法使得可以方便地操纵内存中的数据。至于如何操纵?Buffer提供了4个索引。要理解Buffer的工作原理,就需要从这些索引说起:
- capacity(容量);
- position(位置);
- limit(界限);
- mark(标记);
其中position和limit的含义取决于Buffer是处于什么模式(读或者写模式),capacity的含义则和模式无关,而mark则只是一个标记,可以通过mark()方法进行设置。下图描述了读写模式下三种属性分别代表的含义,详细解释见下文:
2.1 Capacity
Buffer代表一个内存块,所以其是有确定大小的,也叫“容量”。可以往buffer中写入各种数据如byte、long、chars等,当Buffer被写满了则需要将其清空(可以通过读取数据或者清空数据)之后才能继续写入数据。
2.2 Position
当往Buffer中写数据时,写入的地方就是所谓的position,其初始值为0,最大值为capacity-1。当往Buffer中写入一个byte或者long的数据时,position会前移以指向下一个即将被插入的位置。
当从Buffer中读取数据时,读取数据的地方就是所谓的position。当执行flip将Buffer从写模式切换到读模式时,position会被重置为0。随着不断从Buffer读取数据,position也会不断后移指向下一个将被读取的数据。
2.3 Limit
在写模式下,Buffer的limit是指能够往Buffer中写入多少数据,其值等于Buffer的capacity。
在读模式下,Buffer的limit是指能够从Buffer读取多少数据出来。因此当从写模式切换到读模式下时,limit就被设置为写模式下的position的值(这很好理解,写了多少才能读到多少)。
2.4 Mark
mark其实就是一个标记,可以通过mark()方法设置,设置值为当前的position。
下面是用于设置和复位索引以及查询它们值的方法:
capacity() 返回缓冲区容量
clear() 清空缓冲区,将position设置为0,limit设置为容量。我们可以调用此方法覆写缓冲区
flip() 将limit设置为position,position设置为0。此方法用于准备从缓冲区读取已经写入的数据
limit() 返回limit值
limit(int lim) 设置limit值
mark() 将mark设置为position
position() 返回position值
position(int pos) 设置position值
remaining() 返回(limit - position)
hasRemaining() 若有介于position和limit之间的元素,则返回true
3. Buffer的主要API
除了如上和索引相关的方法之外,Buffer还提供了一些其他的方法用于写入、读取等操作。
3.1 给Buffer分配空间
要获得一个Buffer对象就可以通过Buffer类的allocate()方法来实现,如下分别是分配一个48字节的ByteBuffer和1024字符的CharBuffer:
ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);
3.2 往Buffer中写数据
有两种方式往Buffer中写入数据:
- 从Channel中往Buffer写数据;
- 通过Buffer的put()方法写入数据;
int bytesRead = inChannel.read(buf); // read into buffer buf.put(127);
put()方法有多个重载版本,比如从指定位置写入数据,或写入字节数组等。
3.3 flip()
flip()方法将Buffer从写模式切换到读模式。调用flip()方法会将position设为0,limit设为position之前的值。
3.4 从Buffer读数据
也有两种方法从Buffer读取数据:
- 从Buffer中读数据到Channel中;
- 调用Buffer的get()方法读取数据;
int bytesWritten = inChannel.write(buf); // read from buffer into channel byte aByte = buf.get();
3.5 rewind()
rewind()方法将position设置为0,可以从头开始读数据。
3.6 clear()和compact()
当从Buffer读取数据结束之后要将其切换回写模式,可以调用clear()、compact()这两个方法,两者之间的区别如下:
调用clear(),会将position设为0,limit设为capacity,也就是说Buffer被清空了,但是里面的数据仍然存在,只是这时没有标记可以告诉你哪些数据是已读,哪些是未读。
如果读取到一半需要写入数据,但是未读的数据稍后还需要读取,这时可以使用compact(),其会将所有未读取的数据复制到Buffer的前面,将position设置到这些数据后面,limit设置为capacity,所以此时是从未读的数据后面开始写入新的数据。
3.7 mark()和reset()
调用mark()方法可以标志一个指定的位置(即设置mark值),之后调用reset()方法时position又会回到之前标记的位置。
4. ByteBuffer
ByteBuffer是一个比较基础的缓冲器,继承自Buffer,是可以存储未加工字节的缓冲器,并且也是唯一直接与通道交互的缓冲器。可以通过ByteBuffer的allocate()方法来分配一个固定大小的ByteBuffer,并且其还有一个方法选择集,用于以原始的字节形式或基本类型输出和读取数据。但是,没办法输出或读取对象,即使是字符串对象也不行。这种处理虽然很低级,但却正好,因为这是大多数操作系统中更有效的映射方式。
ByteBuffer也分为直接和非直接缓冲器,通过allocate()创建的就是非直接缓冲器,而通过allocateDirect()方法就可以创建出一个缓冲器直接缓冲器,这是一个与操作系统有更高耦合性的缓冲器,也就意味着它能够带来更高的速度,但是分配的开支也会更大。
尽管ByteBuffer只能保存字节类型的数据,但是它具有可以从其所容纳的字节中产生出各种不同基本类型值的方法。下面的例子展示怎样使用这些方法来插入和抽取各种数值:
public class GetData { private static final int BSIZE = 1024; public static void main(String[] args){ ByteBuffer bb = ByteBuffer.allocate(BSIZE); int i = 0; while(i++ < bb.limit()) if(bb.get() != 0) System.out.println("nonzero"); System.out.println("i = " + i); bb.rewind(); // store and read a char array: bb.asCharBuffer().put("Howdy!"); char c; while((c = bb.getChar()) != 0) System.out.print(c + " "); System.out.println(); bb.rewind(); // store and read a short: bb.asShortBuffer().put((short)471142); System.out.println(bb.getShort()); bb.rewind(); // sotre and read an int: bb.asIntBuffer().put(99471142); System.out.println(bb.getInt()); bb.rewind(); // store and read a long: bb.asLongBuffer().put(99471142); System.out.println(bb.getLong()); bb.rewind(); // store and read a float: bb.asFloatBuffer().put(99471142); System.out.println(bb.getFloat()); bb.rewind(); // store and read a double: bb.asDoubleBuffer().put(99471142); System.out.println(bb.getDouble()); bb.rewind(); } }
5. Buffer类型
Java NIO中包含了如下几种Buffer:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些Buffer类型代表着不同的数据类型,使得可以通过Buffer直接操作如char、short等类型的数据而不是字节数据。其中MappedByteBuffer略有不同,后面会专门总结。
通过ByteBuffer我们只能往Buffer直接写入或者读取字节数组,但是通过对应类型的Buffer比如CharBuffer、DoubleBuffer等我们可以直接往Buffer写入char、double等类型的数据。或者利用ByteBuffer的asCharBuffer()、asShorBuffer()等方法获取其视图,然后再使用其put()方法即可直接写入基本数据类型,就像上面的例子。
这就是视图缓冲器(view buffer)可以让我们通过某个特定的基本数据类型的视窗查看其底层的ByteBuffer。ByteBuffer依然是实际存储数据的地方,“支持”着前面的视图,因此对视图的任何修改都会映射成为对ByteBuffer中数据的修改。这使得我们可以很方便地向ByteBuffer插入数据。视图还允许我们从ByteBuffer一次一个地(与ByteBuffer所支持的方式相同)或者成批地(通过放入数组中)读取基本类型值。在下面的例子中,通过IntBuffer操纵ByteBuffer中的int型数据:
public class IntBufferDemo { private static final int BSIZE = 1024; public static void main(String[] args){ ByteBuffer bb = ByteBuffer.allocate(BSIZE); IntBuffer ib = bb.asIntBuffer(); // store an array of int: ib.put(new int[]{11,42,47,99,143,811,1016}); // absolute location read and write: System.out.println(ib.get(3)); ib.put(3,1811); // setting a new limit before rewinding the buffer. ib.flip(); while(ib.hasRemaining()){ int i = ib.get(); System.out.println(i); } } }
上例中先用重载后的put()方法存储一个整数数组。接着get()和put()方法调用直接访问底层ByteBuffer中的某个整数位置。这些通过直接与ByteBuffer对话访问绝对位置的方式也同样适用于基本类型。
6. 总结
本文简单总结了Java NIO(Java New IO),其目的在于提高速度。Java NIO类库中主要包括Buffer、Channel、Selector,本文主要总结了Buffer相关的知识点:
- Buffer叫缓冲器,她是和Channel(通道)交互的,可以从channel中读数据到buffer中,或者从buffer往channel中写数据;
- Buffer内部封装了一块内存,提供了一系列API使得可以方便地操作内存中的数据。其内部是通过capacity、position、limit、mark等变量来跟踪标记封装的数据的;
- ByteBuffer是最基本的Buffer,是唯一可以直接与通道交互的缓冲器,其可以直接操纵字节数据或字节数组;
- 除了ByteBuffer之外,Buffer还有许多别的类型如:MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer;
- 虽然只有ByteBuffer能够直接和通道交互,但是可以从ByteBuffer获取多种不同的视图缓冲器,进而同时具备了直接操作基本数据类型和与通道交互的能力;
基础知识的总结也许是比较枯燥的,但是如果你已经看到这里说明你很有耐心,如果觉得对你有帮助的话,不妨点个赞关注一下吧^_^