[疯狂Java]I/O:NIO简介、Buffer

1. 新IO:

    1) 从JDK1.4开始Java提供了一系列改进的I/O新功能,称为New IO,这些API放在java.nio包下,而旧的标准IO的API放在原有的java.io包下;

    2) NIO主要是针对旧IO的如下缺点进行改造的:以下是旧IO的缺点

         i. 底层的流字节指针只能一次处理一个字节(元操作只能处理一个字节,虽然有很多批量操作(一次读写一条字符串)但在底层其实还是一个一个字节移动的),因此速度较慢;

         ii. 字节指针指向的是节点数据而非程序的内存,也就是说也就是你移动指针时是在结点数据上移动(比如访问文件,那指针直接是在磁盘上的文件上移动),这也带来的效率方面的问题,因为无论如何访问文件都需要通过物理节点的接口,而CPU的I/O处理特别费时;

         iii. I/O是阻塞式的,比如输入流在一直没有读到有效数据时线程将一直阻塞在那里等待,这样挺浪费CPU资源的;

    3) NIO的主要特性:

         i. 旧IO是通过“流”来传输数据的,而新IO是通过Channel“通道"传输数据的;

         ii. 旧IO的流中的数据是一个个字节,而新IO通道中传输的是数据块Buffer;

         iii. 数据块Buffer位于程序内存中,本质上就是一个数组,因此处理起来非常高效,并且在数组内移动指针也非常随意和方便;

         iv. 输入(Input)时,数据从节点通过通道流入程序,程序必须用Buffer来缓存通道流入的数据;

         vi. 输出(Output)时,数据一定要先在Buffer内准备好,然后用通道将Buffer块送出给节点;

         v. 也就是说通道(Channel)传输的是数据块(Buffer);

!!其实NIO将节点上的数据映射到内存Buffer中,在内存中处理数据显然效率更高,这类似于OS的内存映射;


2. Buffer简介:

    1) 即NIO通道的传输单位——块,位于内存中,从通道接受的数据以及向通道中传输的数据都在这里准备;

    2) Buffer很像数组,只能保存相同类型的数据,并且这些类型有限制,只能是Java的基本数据类型(byte、char、int、long等),但是任何类型(自定义类等)都可以转换成字节(byte)进行传输,因此不必担心支持类型不够的问题;

    3) Buffer只是一个抽象类,并不能建立对象实例,它有很多针对基本类型的实现类,类名的模式是XxxBuffer,而Xxx基本涵盖了Java的所有基本类型(Byte、Int、Long、Double等),但是Boolean除外;

    4) 构造Buffer实例:

         i. 所有的Buffer实现类都没有提供构造器(私有),统一使用allocate静态方法构建Buffer实例;

         ii. 原型:static XxxBuffer XxxBuffer.allocate(int capacity); // capacity是容量,表示块的大小

!capacity的单位是数据单位,例如,ByteBuffer(5)就表示5个字节、CharBuffer(7)就表示7个字符、DoubleBuffer(10)就表示10个double,capacity的单位并不是字节,而是数据单位;

    5) Buffer四要素:容量、界限、位置指针(简称指针)、标记

         i. 容量(capacity):Buffer最多能放多少个单位数据,其值不能为负,并且一旦创建该值就不能修改了,想修改就重新再常见一个!

         ii. 界限(limit):limit ≤ capacity,在limit之外的数据无法操作(读写,被输入输出),即limit和capacity之间的数据无法被读写,相当于可操作的界限,而capacity是容量的界限;

         iv. 指针(position):指向Buffer中下一个待操作的数据单位,就和流中的位置指针意义一样,第一个数据的位置是0;

         v. 标记(mark):其实也是一个位置,但其位于指针之前,Buffer允许直接将指针定位到mark处;

!!小结:0 ≤ mark ≤ position ≤ limit ≤ capacity


3. Buffer装卸数据是要看状态的:

    1) 在向通道输出Buffer之前以及从通道接受数据块时都需要向Buffer中装载数据,而将Buffer输出到通道时以及从Buffer中读取数据时都需要从Buffer中卸载数据(取出);

    2) 之后会讲到Buffer怎样与Channel交互,交互过程中要保证Buffer中的数据安全,因为Buffer既可以用来输出也可以用来输入,两者的冲突必然会埋下数据安全的伏笔,因此Buffer必须要有一个状态标记,该标记指示了当前Buffer是要准备装载数据还是准备卸载数据;

    3) 装载:

         i. 必须要先调用clear()方法:Buffer Buffer.clear();

         ii. 该方法会将指针移到0处,将limit移到capacity处

!!其实clear的字面意思就是清空准备装载;

         iii. 之后调用put系列方法向块中装载数据即可;

    4) 卸载:

         i. 必须先调用flip方法:Buffer Buffer.flip();

         ii. 该方法会将limit移到当前指针处,然后将指针移到0处,表示已经准备好卸载数据,卸载范围就是0 ~ limit,limit之外的不会被卸载出去;

         iii. 接着调用get系列方法从块中取(卸载)数据即可;

    5) 为什么要分装和卸?理由很简单,Buffer即可读也可写,同时读写会造成混乱,因此Java规定Buffer只能有一种状态,要么读要么写,因此调用clear和flip后还决定了当前Buffer所处的状态,clear后就不能使用get读了,flip后就不能使用put写了!

    6) put系列方法后面会详细讲解;


4. 查看和设置Buffer的属性信息:这些方法在抽象类Buffer中就已经定义

    1) 查看三要素:返回三要素的值

         i. int capacity(); 

         ii. int limit();  

         iii. int position();

    2) 修改limit和position(capacity不允许修改):均保留原来(修改前)的数据,返回的Buffer引用

         i. Buffer limit(int newLimit);  // newLimit当然不能大于capacity,否则会抛出异常

!如果修改前指针大于newLimit那么修改后会将指针移到newLimit的位置;

!!如果修改前的mark大于newLimit,那么修改后mark会被取消掉;

         ii. Buffer position(int newPosition); // 新位置不能大于limit,更不用说capacity,否则会抛出异常

!同样,如果修改前mark大于newPosition,那么那个mark将被丢弃;

    3) 设置标记:Buffer mark();  // 将当前位置设置成标记

    4) 撤回:撤回到标记处或者原始状态

         i. Buffer reset();  // 将指针移回到mark处

         ii. Buffer rewind(); // 将指针撤回到0并丢弃原有的mark

    5) 查看剩余:

         i. int remaining();  // 查看当前位置到limit之间还有多少单位没有处理完

         ii. boolean hasRemaining(); // 查看当前位置到limit之间是否还有没处理的数据


5. put装载数据:

    1) 用的最多的还是ByteBuffer和CharBuffer,因此就主要以这两个例子来介绍;

    2) 首先是ByteBuffer的put系列方法:

         i. 首先少不了传统的三大件:

            a. ByteBuffer put(byte b);

            b. ByteBuffer put(byte[] src);

            c. ByteBuffer put(byte[] src, int off, int len);

         ii. 装载任意基本类型的数据:

             a. ByteBuffer putXxx(xxx value);

             b. ByteBuffer putXxx(int index, xxx value);

!xxx表示Java基本类型,这里支持char、short、int、long、float、double;

!!第一个方法表示在当前位置处装载一个数据,而第二个方法表示在绝对位置index处装入value,index表示字节,比如index=4就表示在第5个字节处(以0开始)装入一个value,如果value是大于一个字节的类型,比如long,那么就会当成一个8字节的byte数组插入,如果index处已经有其它数据了,那么将会被覆盖!

         iii. 直接装载另一个Buffer:ByteBuffer put(ByteBuffer src);

              a. 会将src当前位置到limit之间的remaining个字节装载到本Buffer的当前位置处;

              b. 装载完后src和本buffer的位置指针都会向后移动src.remaining()的字节;

              c. 如果src.remaining() > this.remaining()就会抛出Buffer溢出的异常!

    3) CharBuffer的put系列方法:

         i. 首先还是三大件:

            a. CharBuffer put(char c);

            b. CharBuffer put(char[] src);

            c. CharBuffer put(char[] src, int off, int len);

         ii. 只不过CharBuffer就不能put各种基本类型了,限制只能put字符类型了,因此和ByteBuffer对应的put就成这样了:

            a. CharBuffer put(char c); // 就是三大件之一

            b. CharBuffer put(int index, char c);  // index是指单位而不是字节,表示第几+1个字符的位置

!!在put和get中的index的单位都是数据单位,如果是char就是单个字符,如果是long就是单个long,如果是byte就是单个byte!!不要弄错了!!

         iii. 直接装在另一个Buffer:CharBuffer put(CharBuffer src);

         iv. 只不过它还多了支持装载字符串的版本而已:

              a. CharBuffer put(String src);  // 当前位置处装入一个String

              b. CharBuffer put(String src, int start, int end); // 将指定String的[start, end]区间装入

    4) 其余的putXxx系列(long、int、float、double)都有CharBuffer的i.、ii.、iii.的系列版本,只不过类型改成相应的类型即可!


6. get卸载数据:

    1) ByteBuffer的get系列:

         i. 首先还是三大件:

            a. byte get();

            b. ByteBuffer get(byte[] dst);

            c. ByteBuffer get(byte[] dst, int off, int len);

         ii. 卸载各基础类型:

            a. xxx getXxx();  // 从当前位置获取一个相应类型的数据

            b. xxx getXxx(int index); // 从指定位置处,单位是数据单位!

!!支持的类型还是char、short、int、long、float、double;

!!注意没有byte、boolean、String!

    2) 其它的XxxBuffer的get系列:

         i. xxx get();

         ii. XxxBuffer get(xxx[] dst);

         iii. XxxBuffer get(xxx[] dst, int off, int len); // 首先还是这三大件

         iv. xxx get(index); // 其余能想到的也就只有它了,所以不用硬记


!!!!注意!!!!!定位的读取和写入不改变position(即含有index的方法都不改变position),其余方法都改变position!


7. Buffer的综合示例:

public class Test {
	
	CharBuffer cb;
	
	private void capacity() {
		System.out.println("capacity = " + cb.capacity());
	}
	
	private void limit() {
		System.out.println("limit = " + cb.limit());
	}
	
	private void position() {
		System.out.println("position = " + cb.position());
	}
	
	public void init() {
		cb = CharBuffer.allocate(8);
		
		// 初始化
		capacity(); // 8
		limit(); // 8
		position(); // 0
		
		cb.put('a');
		cb.put('b');
		cb.put('c');
		position(); // 3
		
		cb.flip(); // 准备卸载
		limit(); // 3,准备卸载limit变为卸载前的position
		position(); // 0,归位
		
		System.out.println(cb.get()); // 'a'
		position(); // 1
		
		cb.clear(); // 清空准备重新装载
		limit(); // 8,清空后limit = capacity
		position(); // 0,归位
		
		System.out.println(cb.get(2)); // 'c'
		position(); // 0,定位读取不改变当前位置
	}
	
	public static void main(String[] args) {
		new Test().init();		
	}
}


8. 更高效的直接Buffer:

    1) 可以看到ByteBuffer还有一种创建方式:static ByteBuffer ByteBuffer.allocateDirect(int capacity);

    2) 其创建得到的ByteBuffer和普通的ByteBuffer用法没有任何区别!

    3) 该方法创建的ByteBuffer叫做直接ByteBuffer,其读写效率远高于普通的任何Buffer(其它普通的Buffer都有一层Java的抽象,而直接ByteBuffer操作的就是底层字节);

    4) 但不过该方法创建直接ByteBuffer的代价极高,也比较耗费资源,因此该方法适用于长期生存、多次反复利用的ByteBuffer,其余情况则不适用;

    5) 由于其高效且创建代价极大,因此就只有ByteBuffer支持,其它类型的Buffer不支持,因为ByteBuffer是字节层面的,更加适用于这种需求!

你可能感兴趣的:(nio,buffer,疯狂Java)