传统IO有一个特征,输出流和输入流都是阻塞式的。不仅如此,传统传统的输入,输出流都是通过字节的移动来处理的,也就是说,面向流的输入/输出系统一次只能处理一个字节,因此面向流的输入输出系统效率不高。

JDK4开始,java提供了一些列改进的输入,输出处理的新功能,这些功能被统称为新IO,简称NIO,新增了许多用于处理输出/输入的类。

新IO和传统的IO有相同的目的,都是用于进行输入和输出,但新IO使用了不同的方式来处理输入和输出,新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,通过这种方式来进行输入/输出比传统的效率要快得多。

Channel(通道)     和Buffer(缓冲)       是新IO中的两个核心对象,Channel是对传统输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输:

Channel与传统的InputStream, OutputStream最大的区别在于它提供了一个map()方法,通过该map()方法可以直接将"一块数据"映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO是面向块的处理。

Buffer  可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放在Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

除了Channel和Buffer之外,新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入/输出的Selector类。


使用Buffer
从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByetBuffer,它可以在底层字节数组上进行get/set操作。除了ByteBuffer之外,对于其他基本数据类型(boolean除外)都有相应的Buffer类。 

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

在Buffer中有三个重要的概念:容量(capacity)    界限(limit)    和位置(position)
容量(capacity):缓冲区的容量表示该Buffer的最大数据容量,即最多可以存储多少数据。缓冲区的容量不可能呢为负值,创建后不能改变。

界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据即不可被读,也不可被写。

位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当刚刚新建一个Buffer对象时,其position为0;如果从Channel中读取了两个数据到该Buffer中,则position为2,指向Buffer中第三个(第一个位置的索引为0)位置。

除此之外,Buffer里还支持一个可选的标记,(mark,类似于传统IO流中的mark), Buffer允许直接将position定位到该mark处。这些值满足如下关系。
0<=mark<=position<=limit<=capacity


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()方法,clear()方法不是清空Buffer的数据,它仅仅将position设置为0,将limit置为capacity,这样再次向Buffer中装入数据坐好准备。

Buffer中包含两个重要的方法,即flip()和clear(), flip()为从Buffer中取出数据做好准备,而clear()为再次向Buffer中装入数据做好准备。
除此之外,Buffer还包含如下一些常用方法
int capacity():返回Buffer的capacity大小
boolean hasRemaining():判断当前位置和界限之间是否还有元素可供处理
int limit():返回Buffer界限的位置
Buffer limit():重新设置界限的值,并返回一个具有新的界限的缓冲区对象
Buffer mark():设置Buffer的mark位置,它只能在0和位置之间做mark
int position():返回Buffer中的位置值

下面程序示范了Buffer的一些常规操作:

 
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):" + 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的读取效率更高。

由于直接Buffer的创建成本很高,所以直接Buffer只适用于长生存期的Buffer  而不适用于短生存期,一次用完就丢弃的Buffer。而且只有ByteBuffer才提供了allocateDirect()方法,所以只能在ByteBuffer级别上创建直接Buffer。如果希望使用其他类型,则应该将该Buffer转换成其他类型的Buffer。

 
使用Channel 
Channel类似于传统的流对象,但与传统的流对象有两个主要区别:
1:Channel可以直接将指定文件的部分或全部直接映射成Buffer 
2:程序不能直接访问Channel中的数据,包括读取,写入都不行,Channel只能于Buffer进行交互。也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。

所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStream  OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。例如,FileInputStream, FileInputStream的getChannel()返回的是FileChannel。
而PipedInputStream 和 PipedOutputStreamde 的getChannel()返回的是Pipe.SinkChannel  Pipe.SourceChannel。

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

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

下面程序示范了直接将FileChannel的全部数据映射成ByteBuffer效果:

 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
 
public class FileChannelTest 
{
    public static void main(String[] args) 
    {
        File f = new File("FileChannelTest.java");
        try(
            //创建FileInputStream,以该文件输入流创建FileChannel
            FileChannel inChannel = new FileInputStream(f).getChannel();
            //以文件输出流创建FileChannel,用以控制输出
            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 decoder = charset.newDecoder();
            //使用解码器将ByteBuffer转换成CharBuffer
            CharBuffer charBuffer = decoder.decode(buffer);
            //CharBuffer的toString方法可以获取对应的字符串
            System.out.println(charBuffer);
        }
        catch(IOException ex)
        {
            ex.printStackTrace();
        }
    }
 
}

 

posted on 2016-03-01 15:07  所谓荣耀  阅读(353)  评论(0编辑  收藏  举报