NIO采用内存映射文件的方式处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入输出比传统的输入输出要快得多.
Java中与NIO相关的包如下:
java.nio包:主要包含各种与Buffer相关的类.
java.nio.channels包:与Channle和Selector相关的类.
java.nio.charset包:主要包含与字符集相关的类
java.nio.channels.spi包:与Channel相关的服务提供编程接口
java.nio.charset.spi包:包含与字符集想的服务提供者编程接口
NIO中的新特点:Channel(通道)和Buffer(缓冲),Channle与传统的InputStream,OutputStream最大的区别在于它提供了一个map()方法,通过该方法可以海子街将"一块数据"映射到内存中.如果说传统的输入/输出系统是面向流的处理,则NIO则是面向块的处理.
Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中.
除了ByteBuffer之外,它们都采用相同或相似的方法来管理数据,只是各自管理的数据类型不同而已.
NIO还提供了加工Unicode字符串映射成字节序列以及逆映射操作的Charset类, 也提供了用于支持费阻塞式输入输出的Selector类.
15.9.2使用Buffer
Buffer就像是一个数组,可以保存多个类型相同的数据.Buffer是一个抽象类,最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作. 也有CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer.
这些实现类都没提供构造器,使用如下方式是构造:
ByteBuffer byteBuffer = ByteBuffer.allocate(160);
或者
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(160);//ByteBuffer 独有的创建方法,称为直接BUffer
直接Buffer的创建成本比普通的Buffer创建成本高,但效率也高,所以只适用于长生存期的Buffer,不适用于短生存期,一次用完就丢弃的Buffer.
但实际使用较多的是ByteBuffer和CharBuffer,其他Buffer子类则较少用到.其中ByteBuffer还有一个子类: MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回.
Buffer中三个重要的概念: 容量(capacity),界限(limit),位置(position)
Buffer的主要作用就是装入数据,然后输出数据. 开始时,Buffer的Position为0,limit为capacity, 程序可通过put()方法向Buffer中放入一些数据(或者从Channel中获取一些数据),每放入一些数据,Buffer的position相应地向后移动一些位置.
当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0,这就使得Buffer的读写指针又移动到了开始的位置.也就是说,Buffer调用filp()方法之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear()方法,clear方法不是清空Buffer的数据,它仅仅将position指为0,将limit置为capacity,这样为再次向Buffe热衷装入数据做好准备.
Buffer包含的一些常用方法:
这些方法大概可以分类
1.标记功能: mark()和reset()
2.position操作: position() position(int newPs) rewind() reset()
3.limit操作: limit() limit(int newLt)
4.剩余元素操作: hasRemaining() remaining()
5.容器大小: capacity()
重点:
put()方法:用于向Buffer中放入数据
get()方法:用于从Buffer中取出数据
这两个方法,既支持对单个数据的访问,也支持对批量数据的访问.
而且,分为相对和绝对两种.
相对(relative):从Buffer的当前position处开始读取或写入数据, 然后将位置(position)的值按处理的元素的个数增加.
绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时,并不会影响位置(position)的值.
public static void bufferTest(){ 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=0):" + 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()); }
15.9.3 使用Channel
Channel类似于传统的流对象,但与传统的流对象有两个主要区别
1. Channel可以直接将指定文件的部分或全部直接映射成Buffer.
2. 程序不能直接访问Channel中的数据,包括读取, 写入,都不行,Channel中取出一些数据,然后让程序从Buffer中取出这些数据:如果要将程序中的数据写入Channel, 一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中.
Channle接口的实现类很多,这里只介绍FileChannel的用法.
所有的Channle都不应该通过构造器来直接创建,而是通过传统的节点InputStream,OutputStream的getChannle()方法来返回对应的Channle.
Channle中最常用的三类方法是map(),read(),write(),其中map()方法用于将Channle对应的部分或全部数据映射成ByteBuffer,而read()和write()方法都有一系列重载形式,这些方法用于从Buffer中读取数据或者向Buffer中写入数据.
public static void fileChannelTest(){ File f = new File(".project"); try( // 创建FileInputStream,以该文件输入流创建FileChannel FileChannel inChannel = new FileInputStream(f).getChannel(); // 以文件输出流创建FileBuffer,用以控制输出 FileChannel outChannel = new FileOutputStream("aw.txt") .getChannel()) { // 将FileChannel里的全部数据映射成ByteBuffer MappedByteBuffer buffer = inChannel.map(FileChannel .MapMode.READ_ONLY , 0 , f.length()); // ① // 直接将buffer里的数据全部输出 outChannel.write(buffer); // ② // 再次调用buffer的clear()方法,复原limit、position的位置 buffer.clear(); // 使用GBK的字符集来创建解码器 Charset charset = Charset.forName("UTF-8"); // 创建解码器(CharsetDecoder)对象 CharsetDecoder decoder = charset.newDecoder(); // 使用解码器将ByteBuffer转换成CharBuffer CharBuffer charBuffer = decoder.decode(buffer); // CharBuffer的toString方法可以获取对应的字符串 System.out.println(charBuffer); } catch (IOException ex) { ex.printStackTrace(); } }
下面给一个出问题的例子,这个是
RandomAccessFile生成的FileChannle,可是与疯狂讲义里说的有偏差,主要问题是当插入的数据超出FileChannle的范围时,FileChannle不会自动扩大,反而变成等待状态,
所访问的文件直接挂掉,一点那个文件,eclipse也挂掉,最后Mac都无法关机,只能强制关机。也不知道哪里没用对。
public static void randomFileChannelTest() throws FileNotFoundException, IOException{ File f = new File("write.txt"); try( // 创建一个RandomAccessFile对象 FileChannel randomChannel = new RandomAccessFile(f, "rw").getChannel(); ) { // 将Channel中所有数据映射成ByteBuffer randomChannel.position(1000); System.out.println(f.length()); System.out.println(randomChannel.size()); ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0 ,1); System.out.println(buffer.limit()); System.out.println(buffer.capacity()); // 使用GBK的字符集来创建解码器 Charset charset = Charset.forName("UTF-8"); // 创建解码器(CharsetDecoder)对象 CharsetDecoder decoder = charset.newDecoder(); // 使用解码器将ByteBuffer转换成CharBuffer CharBuffer charBuffer = decoder.decode(buffer); // CharBuffer的toString方法可以获取对应的字符串 System.out.println(charBuffer); // 把Channel的记录指针移动到最后 randomChannel.position(10); System.out.println("randomChannel.position():"+randomChannel.position()); // 将buffer中所有数据输出 buffer.flip(); randomChannel.write(buffer); buffer.clear(); randomChannel.close(); }catch(Exception e){ e.printStackTrace(); } }
编码解码(字符集和Charset),(在String类里也提供了一个getBytes(String charset)方法,该方法返回byte[],也是使用指定的字符集将字符串转换成字节序列,跟这个Charset的功能类似).
public class CharsetTransform { public static void main(String[] args) throws Exception { // 创建简体中文对应的Charset Charset cn = Charset.forName("GBK"); // 获取cn对象对应的编码器和解码器 CharsetEncoder cnEncoder = cn.newEncoder(); CharsetDecoder cnDecoder = cn.newDecoder(); // 创建一个CharBuffer对象 CharBuffer cbuff = CharBuffer.allocate(8); cbuff.put('孙'); cbuff.put('悟'); cbuff.put('空'); cbuff.flip(); // 将CharBuffer中的字符序列转换成字节序列 ByteBuffer bbuff = cnEncoder.encode(cbuff); // 循环访问ByteBuffer中的每个字节 for (int i = 0; i < bbuff.capacity() ; i++) { System.out.print(bbuff.get(i) + " "); } // 将ByteBuffer的数据解码成字符序列 System.out.println(" " + cnDecoder.decode(bbuff)); } }
15.9.5 文件锁