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是字节层面的,更加适用于这种需求!