引言
在java的IO系统中,在JDK1.4的时候引入了新的IO系统,也就是我们常说的nio,它的主要功能是提高速度。在本篇博文中,详细介绍关于nio的构造:通道和缓冲器,核心了解ByteBuffer,因为它是唯一与通道直接交互的缓冲器。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.youkuaiyun.com/u012403290
通道与缓冲器
通道是指资源的真实存在,而缓冲器是运输资源的媒介。比如说我们喝水和时候:水就是通道,而杯子就是缓冲器。我们不直接喝水,通常我们都是用杯子装好水,然后再用杯子喝水。类似的,我们不直接操作通道,而是操作缓冲器,通过缓冲器来操作通道。
对于nio,jdk中有4个包,在java.nio包中主要是对缓冲器和缓冲器和缓冲器扩展等的描述;在java.nio.channels包中主要是对文件通道的描述。在本篇博文中主要是对通道和缓冲器的进一步解释。
通道(FileChannel),我们一般通过在老的IO系统中获取,主要包含3个类:FileInputStream,FileoutputStream和RandomAccessFile,对于这3个类,前面两者我们可以定义出2个输入通道和输出通道。对于后者,基于RandomAccessFile的性质,我们可以在文件中任意移动位置,获取恰当的资源。而缓冲器,我们一般需要自己创建一个恰当大小的对象。
有Buffer类实现的缓冲器有如下这些:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
其中直接与通道交互的是ByteBuffer,因为它是直接面未加工的字节的。
下面的代码就是最简单的建立了2个通道与一个缓冲器:
package com.brickworkers.io.nio;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelAndBufferTest {
public static void main(String[] args) throws FileNotFoundException {
//建立一个输入通道
FileChannel in = new FileInputStream("F:/java/io/in.txt").getChannel(),
//建立一个输出通道
out = new FileInputStream("F:/java/io/out.txt").getChannel();
//建立一个缓冲器
ByteBuffer bf = ByteBuffer.allocate(1024);
}
}
ByteBuffer缓冲器详解
1、类定义
以下是ByteBuffer的类结构定义:
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
}
从上面可以看出ByteBuffer是继承Buffer实现的,同时它自身是一个抽象类,而且它还实现了比较接口,两个ByteBuffer之间可以比较大小。我们不去讨论别的,在类定义中我们核心了解的是关于缓冲器的核心结构,我们先看一下Buffer的基本组成:
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}
在Buffer中有4个重要的字段:①mark表示当前position的位置,也就是说当我们调用一次mark的时候,那么mark就会指向position所指向的位置。②position表示当前操作位置。在定义好大小的缓冲器中,我们每次读取或者写出数据的时候,这个position都会相应的移动,相当于上一篇博文中介绍的RandomAccessFile一样,文件指针的移动。③limit表示可读写的边界,当position的位置等于limit的时候就表示全部读取或者写入完成;④capacity表示定义的缓冲器容量大小。
在后面的demo中会详细展示这4个变量的具体运作情况。
2、构造函数
以下是ByteBuffer的构造函数源码:
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// Creates a new buffer with the given mark, position, limit, and capacity
//
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
看完构造函数,我们就知道,我们是不能直接通过构造函数创建ByteBuffer对象的,因为它是包等级的私密方法。那么我们应该如何获取一个缓冲器对象呢,和很多的类一样,ByteBuffer也是通过静态工厂方法创建对象的,请看下面这个方法:
//对象创建静态方法1
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
//对象创建静态方法2
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
//对象创建静态方法3
public static ByteBuffer wrap(byte[] array,
int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
//对象创建静态方法4
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
从ByteBuffer的静态工厂方法可以看出:①和②是直接建立一个空的、指定大小的新缓冲器。那么两者有什么区别呢?查阅相关资料,发现allocateDirect方法比allocate方法效率会更高。③和④比较容易理解,都是把一个byte数组包装成一个ByteBuffer缓冲器,但是③方法可以指定需要包装的byte的数组的具体位置,它有一个偏移作为入参。
3、核心方法
①从Buffer类中继承下来的方法:
1、clear() 清空缓存区
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
从源码中可以看出,并不是真正的把数据清楚,而是改变了各个参数的值,其实就是把各个指针还原到原始状态。通常这个方法在复用一个缓冲器的时候使用。
2、hasRemaining() : 判断当前缓冲器中是否有可用数据
public final boolean hasRemaining() {
return position < limit;
}
从源码中就可以看出,其实就是看当前操作的position指针有没有和limit指针重合。这个方法主要是在获取缓冲器中的数据的时候进行判断。
3、flip(): 用于重置操作指针position,规整缓冲器中的数据,保证position与limit之间的数据都是有效的
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
从源码中可以看出,相当于对缓冲器的操作复位一样,注意复位不是清空,而且,这个操作会把有效数据的结尾定义到当前操作的位置上。所以,这个方法的错误调用可能会导致缓冲器的失效,比如说你position为0的时候调用这个方法,那么有效数据就直接变为了0。
4、rewind():操作指针复位。相比于flip方法来说,这个方法不保证position与limit之间的数据都是有效的,而且不会意外的清除数据。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
这个方法一般适用于需要循环遍历某一个缓冲器的时候,比如说第一次操作从头读到尾,第二次进来需要先调用rewind方法,然后开始读,不然就读不到数据。
5、mark() 标记当前position位置
public final Buffer mark() {
mark = position;
return this;
}
这个方法主要用于标记当前操作位置,对于特定操作,比如说对一个缓冲器中所有奇数位的数据进行处理。那么我们需要记录上一次操作位置,这个时候就可以用mark来标记。
6、reset() 把当前操作指针position指向mark的地方
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
一般的,如果使用到了这个方法,那么前提是已经使用了mark方法,不然mark的默认是-1位置。
其实Buffer中的方法不仅仅到此。下面这段代码囊括了上面表述的所有方法:
package com.brickworkers.io.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelAndBufferTest {
public static void main(String[] args) throws IOException {
//建立2个通道,分别是输入和输出
FileChannel in = new FileInputStream("F:/java/io/bw.txt").getChannel(),
out = new FileOutputStream("F:/java/io/bw.txt").getChannel();
//建立一个通用缓冲器
ByteBuffer bf = ByteBuffer.allocate(64);
//get这个缓冲器中包装一个值
bf.put("brickworkers".getBytes());
//注意,在这里其实position指针的位置已经变化了,必须把操作指针移动到开始处,建议使用filp方法,因为brickwork字节数是小于64的,避免不必要的开销
bf.flip();
//把缓冲器交给输出通道,把值写入文件
out.write(bf);
out.close();
//写完之后,要把bf清空用于存储从输入通道获取的值
bf.clear();
//输入通道把值交给缓冲器
in.read(bf);
in.close();
//先把position指针回归的原始位置,这里既可以用flip也可以用rewind。
bf.flip();
//我们要把相邻的字符进行位置交换
while(bf.hasRemaining()){
//先用mark进行标记,表示当前操作位置
bf.mark();
byte ch1 = bf.get();
byte ch2 = bf.get();
//操作完这些的时候其实position指针已经向后移动了2个字节。在进行交换的时候必须把position指针从新移动到交换对象之前
bf.reset();
//进行位置交换
bf.put(ch2).put(ch1);
}
//重置position,因为在上面置换操作已经结束,所以既可以使用filp方法,也可以使用rewind方法
bf.flip();
while(bf.hasRemaining()){
System.out.print((char)bf.get());
}
}
}
//输出结果:
//rbciwkroeksr
//
但是,上面这段代码存在一个很大的问题,可能会抛出java.nio.BufferUnderflowException错误,之所以会产生这个错误是因为你写入的字符串是奇数还是偶数。如果是偶数经过两次的get(),其实position指针已经超过了limit。所以其实上面这段代码的第一个while中的hasRemaining()方法最好还是使用这个:remaining() > 2来判断会可靠的多,意思就是当最后不足2个的时候,最后一个字母不置换。大家可以自己尝试一下。
②创建一个合适的字节缓冲区视图,可以作为各种基本数据类型的缓冲区。其实就是获取一个更高级的缓冲器,比如说asCharBuffer,asIntBuffer,asDoubleBuffer等等。但是,能直接与通道交互的务必是ByteBuffer。
在上面的demo使用中,我们是用while循环输出字符,但是我们也可以用asCharBuffer直接把ByteBuffer转出CharBuffer,可以把上面的代码的最后while循环换成这样:
System.out.println(bf.asCharBuffer());
但是要注意,这里很有可能会产生乱码。在缓冲器ByteBuffer中存储的是字节,如果要转换成字符,要么对其输入的时候进行编码,要么就在输出的时候进行编码。我们一般用nio中的一个包java.nio.charset来指定编码,比如下面这段代码:
CharBuffer cb = Charset.forName("UTF-8").decode(bf);
我们就可以把ByteBuffer通过编码之后转换成CharBuffer。
②读取方法,读取缓冲器中的数据,get方法,可以读取对应的数据。可以读取完整字节数组,或者存在偏移的字节数组。甚至是getChar,getLong,getDouble等等方法。存储方法,把数据存储到position指针指向的位置。可以存储整个缓冲器的数据,或者存在偏移的字节数组。甚至是putChar,putLong,putDouble。其实这个相当于RandomAccessFile中的读写方式。具体请参考以下方法:
package com.brickworkers.io.nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
public class Test {
public static void main(String[] args) throws IOException {
//建立2个通道,分别是输入和输出
FileChannel in = new FileInputStream("F:/java/io/bw.txt").getChannel(),
out = new FileOutputStream("F:/java/io/bw.txt").getChannel();
//建立一个通用缓冲器
ByteBuffer bf = ByteBuffer.allocate(1024);
//先进行存储一个字节
bf.put((byte)'a');
//存入一个char
bf.putChar((char)'b');
//存入一个double
bf.putDouble(3.1415926D);
//存入一个long
bf.putLong(14L);
//存入一个int
bf.putInt(13);
//先把数据整理好,用flip方法。
bf.flip();
//把缓冲器中的数据运入输出通道
out.write(bf);
//先清空缓冲器
bf.clear();
in.read(bf);
bf.flip();
//接下来我们对数据进行读取
//读取一个字节
System.out.println(bf.get());
//读取一个char
System.out.println(bf.getChar());
//读取一个double
System.out.println(bf.getDouble());
//读取一个Long
System.out.println(bf.getLong());
//读取一个int
System.out.println(bf.getInt());
}
}
//输出结果:
//97
//b
//3.1415926
//14
//13
//
//
有的小伙伴很奇怪,为什么我byte输入的是a,怎么变成97了呢?因为在ASCII编码中,a字母就是97。