一、Buffer类
java.nio.Buffer这个类是用来干什么的?有怎样的结构?
"Core Java"中是这样定义的“A buffer is array of values of the same type”。所以,我们可以感性的认识到:buffer就像数组一样,存放的是相同类型的数据。还有一个重要的事情就是:Buffer是一种随机存储类型的数据结构,就像普通数组一样(用下标的方式)能够用索引号定位到buffer中的任何一个位置的数据上)。
Buffer类是一个抽象类,其子类有(注意:StringBuffer类和这里的buffer没有什么联系):
Buffer类中的属性和方法用于管理和控制buffer的状态,其子类则有get或put方法用于“读出”或“写入”不同类型的数据;我们还应该注意到:ByteBuffer、CharBuffer等易燃是一个抽象类。所以,我们是不能够通过new的方式得到一个ByteBuffer或CharBuffer。现在,万一我们想得到一个CharBuffer怎么办呢(ByteBuffer可以用同样的方式得到)?
答案:可以利用CharBuffer的allocate(int capacity)方法,或者是对现有的char[] array进行包装的wrap方法,如下:
在我们实际的编程中使用最多的是ByteBuffer和CharBuffer,如下图所示,Buffer具有以下4个属性:
①、capacity,一个Buffer建立以后,它就固定不变了;
②、position,它指示了下一个要读或写数据的位置;
③、limit,超过limit位置的数据是没有意义的;
④、mark,它标记某个重要的位置,用于后面能够返回到该位置进行重新读或写;
它们四个的关系是:
0≤mark≤position≤limit≤capacity
Buffer最重要的一个作用就是:循环的用于“先写,后读”。下面是一个先写后读的过程:
①、最开始的时候,position=0 && limit=capacity;
②、调用put方法向buffer中写数据,当数据写完了或者是position到达了capacity的位置,下面就开始从buffer中读取数据;
③、从“写”状态向“读”状态转变,需要调用flip方法。其作用就是:先让limit等于当前的position,然后将position设置为0;
④、读取数据的时候,只要remaining方法(limit-position)返回一个正数,那么我们就可以持续的调用get方法从buffer中读取数据;
⑤、当读取数据过程完毕了,我们可以调用clear方法,将buffer从“读”状态转换为“写”状态,进入下一个“先读,后写”循环;
⑥、clear方法时设置position=0 && limit=capacity;
⑦、如果想重新读取buffer,可以调用其rewind或者是mark/reset方法,API中有详细介绍;
API:java.nio.Buffer
①、Buffer clear()
设置buffer进入到写状态,设置position=0 && limit=capacity;
②、Buffer flip()
设置buffer进入到读状态,设置limit等于当前的position && position=0;
③、Buffer rewind() //rewind可以翻译为“倒回”,“倒带”等
准备重新读取buffer中相同的数据,设置position=0 && limit保持不变;
④、Buffer mark()
将当前的position设置为mark,其可以配合reset()方法实现buffer的重读/写;
⑤、Buffer reset()
设置position=mark,从而可以从mark位置重新开始读或写;
⑥、int remaining()
返回buffer中“可读”数据的个数,或者是还可以“写入”多少个新的数据,返回limit-position;
⑦、int position() 返回当前position的值;
⑧、int capacity() 返回buffer的capacity的值;
二、nio的文件映射
下面我们看java.nio包相对于旧的IO而言有哪些增强的新特性:
nio主要是支持一下四个增强的特性:
①、字符集的编码和解码;
②、非阻塞IO(nonblocking I/O);
③、Memmory-mapped files;
④、文件锁;
对字符集的编码和解码可以单独拿出来讲。非阻塞IO主要用在网络通信中。文件锁是一个复杂却不怎么靠得住的东西(依赖于具体操作系统对锁的支持),在并发的情况下,通常可以借助于数据库的锁机制,将文件存入数据库中即可实现文件的同步。
这里主要总结Memmory-mapped files。
大多数操作系统可以利用虚拟内存(virtual memory)的优势,将整个文件或者是文件的一部分映射到内存中。然后,我们就可以像内存数组一样访问映射文件了(主要是其随机访问特性),这样会比传统的文件操作(RandomAccessFile)要快很多。
【讨论:对文件的操作有大体的三种方式File流、RandomAccessFile、Memmory-mapped files,但是各有特色:①文件流和缓冲流结合起来会很快,但是不具有随机访问特性;②、RandomAccessFile有随机访问特性,但是它效率十分低下;③、Memmory-mapped files具有随机访问特性,其效率甚至要比缓冲流还高,它主要是用于对“大文件”的操作上】
文件的映射操作比较简单,依据下面的步骤即可:
①、从文件中获取到一个channel。其中channel是磁盘文件的一个抽象,通过它能够获取到操作系统的一些特性,比如:内存映射、文件锁、文件间的快速数据传递。在jak1.4中已经重写了FileInputStream、FileOutputStream和RandomAccessFile类,为它们添加了getChannel方法。所以,我们可以通过调用getChannel方法获取到磁盘文件的channel。如下:
FileInputStream in = new FileInputStream(...); FileChannel channel = in.getChannel();
②、从channel中获取到MappedByteBuffer。我们可以通过调用Channel类的map方法进行文件的映射,此方法会返回一个MappedByteBuffer对象。在map方法中我们可以指定文件映射的范围(全部或者是部分),还可以指定映射的模式,共支持三种模式:
----FileChannel.MapMode.READ_ONLY:只能从buffer中读取数据,不能像buffer中写入数据。当调用写方法的时候会抛出一个ReadOnlyBufferException异常。
----FileChannel.MapMode.READ_WRITE:buffer是可写的,同时buffer中改变的数据会在某个时候写回到文件中。注意,其它也映射了该文件的程序并不能马上感知到这一改变(所以,有了文件同步锁机制)。
----FileChannel.MapMode.PRIVATE:buffer是可写的,但是任何改变都不会写回到文件中去。
③、一旦我们得到了一个buffer,那么我们就可以调用Buffer或者是其子类的方法对buffer进行数据的访问。注意,Buffer同时支持顺序访问和随机访问两种方式。比如,下面的两个例子:
//使用顺序访问buffer while(buffer.hasRemaining()){ byte b = buffer.get(); ... } //使用随机访问buffer for(int i = 0; i < buffer.limit(); i++){ byte b = buffer.get(i); //Buffer这个抽象类没有get和put方法,它只负责管理和控制buffer的状态 ... }
buffer.order(ByteOrder.LITTLE_ENDIAN); 指定小端存储
ByteOrder b = buffer.order(); 找出当前buffer中存放byte的模式
下面的一个例子中,分别使用了FileInputStream,BufferedInputStream,RandomAccessFile和MappedFile来读取rt.jar文件(59.8MB),并计算器CRC32值。我们可以对比四种处理方式的效率,得到一个感性的认识:
package nio; import java.io.*; import java.nio.*; import java.nio.channels.FileChannel; import java.util.Scanner; import java.util.zip.CRC32; public class NIOtest { public static long checksumInputStream(String filename) throws Exception{ CRC32 crc32; InputStream in = null; try { crc32 = new CRC32(); in = new FileInputStream(filename); int c; while((c = in.read()) != -1){ crc32.update(c); } } finally{ if(in != null) in.close(); } return crc32.getValue(); } public static long checksumBufferedInputStream(String filename) throws Exception{ CRC32 crc32 = new CRC32(); InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(filename)); int b; while((b = in.read()) != -1){ crc32.update(b); } } finally{ if(in != null) in.close(); } return crc32.getValue(); } public static long checksumRandomAccessFile(String filename) throws Exception{ RandomAccessFile file = null; CRC32 crc = new CRC32(); try { file = new RandomAccessFile(filename, "r");//只读模式 long length = file.length(); for(int i = 0; i < length; i++){ file.seek(i); int b = file.read(); crc.update(b); } } finally{ if(file != null) file.close(); } return crc.getValue(); } public static long checksumMappedFile(String filename) throws Exception{ CRC32 crc = new CRC32(); FileInputStream in = null; FileChannel channel = null; try { in = new FileInputStream(filename); channel = in.getChannel(); int size = (int) channel.size(); MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size); for(int p = 0; p < size; p++){ int b = buffer.get(p); crc.update(b); } } finally{ if(in != null) in.close(); } return crc.getValue(); } public static void main(String[] args) throws Exception { System.out.println("输入测试文件路径:"); InputStream in = System.in; Scanner scanner = new Scanner(in); String filename = scanner.nextLine(); System.out.println("开始计算..."); System.out.println(); long start = System.currentTimeMillis(); long crcValue = checksumInputStream(filename); long end = System.currentTimeMillis(); System.out.println("---InputStream----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumBufferedInputStream(filename); end = System.currentTimeMillis(); System.out.println("---BufferedInputStream----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumRandomAccessFile(filename); end = System.currentTimeMillis(); System.out.println("---RandomAccessFile----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); start = System.currentTimeMillis(); crcValue = checksumMappedFile(filename); end = System.currentTimeMillis(); System.out.println("---MappedFile----> " + (end - start ) + " 毫秒。 crc32 值: " + Long.toHexString(crcValue)); if(scanner != null) scanner.close(); } }
执行结果:
输入测试文件路径: E:\Java\jdk1.8.0_25\jre\lib\rt.jar 开始计算... ---InputStream----> 222721 毫秒。 crc32 值: 2a57ac2 ---BufferedInputStream----> 3020 毫秒。 crc32 值: 2a57ac2 ---RandomAccessFile----> 377263 毫秒。 crc32 值: 2a57ac2 ---MappedFile----> 4323 毫秒。 crc32 值: 2a57ac2