JDK1.4开始,加入了Java.nio.*包,在这个包中加入了新的JAVA I/O类库,以便加快I/O操作的速度。在nio中读写之所以提高,只要是采用了更接近操作系统执行I/O操作的结构——通道和缓冲区。在《Thinking in Java》中有举了一个例子来说明通道和缓冲区的作用。
可以想象一个煤矿,通道时一个包含煤层(数据)的矿藏,而缓冲区则是派送到矿藏的卡车。卡车载满煤炭而归,而我们再从卡车上获得煤炭。也就就说,我们并没有与通道直接交互,我们只是和缓冲区交互,并把缓冲区派送到通道。通道要么从缓冲区获得数据,要么像缓冲区发送数据。
唯一直接与通道交互的缓冲区是ByteBuffer,从名字上就可以看出,这是一个可以存储字节的存储器。下面是ByteBuffer的部分JDK源码(重要的方法)
public abstract class ByteBuffer extends Bufferimplements Comparable<ByteBuffer>
/**
* Allocates a new byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, and its mark will be undefined. It will have a {@link #array
* </code>backing array<code>}, and its {@link #arrayOffset </code>array
* offset<code>} will be zero.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
/**
* 静态方法,得到原始字节形式的缓冲区。。
*/
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
/**
* 下面这些方法用于得到其他基本类型的缓冲区
*/
public abstract ShortBuffer asShortBuffer();
public abstract CharBuffer asCharBuffer();
public abstract IntBuffer asIntBuffer();
public abstract long getLong();
public abstract DoubleBuffer asDoubleBuffer();
public abstract FloatBuffer asFloatBuffer();
从上面的代码片段中,我们可以看到,这个类其实一个很基础的类:通过告知分配多少存储空间来创建一个BuyeBuffer对象,并且有一些方法,用于产生成其他基本类型的缓冲区,从而以字节或者其他基本类型方式读取数据或者输出数据,但是,要注意的是,没有办法输出或者读取对象(即使是String字符串对象也不行,但是可以通过String.getBytes()转换为字节类型处理),这样的处理方式虽然比较低级,但是,这正是大多数操作系统中更有效的映射方式。
旧有的I/O类库中有三个类被修改,用以产生FileChannel。这三个被修改的类是FileInputStream,FileOutStream,RandomAccessFile。
下面是一个简单的文件复制程序
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelCopy {
public static final int BSIZE = 1024;
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
String arg1 = "E:/test/in.txt";
String arg2 = "E:/test/out.txt";
FileChannel inChannel = new FileInputStream(new File(arg1)).getChannel();
FileChannel outChannel = new FileOutputStream(new File(arg2),false).getChannel();//false:覆盖写,true:追加写
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
while(inChannel.read(buffer)!=-1) {
buffer.flip();//一定要加上 ,read操作后需要次方法
outChannel.write(buffer);
buffer.clear();/一定要加上,如果read一次后,还需要对缓冲区进一步read,那么也必须要加上次方法
}
System.out.println("Sucess!");
}
}
这个程序很简单,就不要做过多的介绍,但是需要注意的是,是最后几行代码。
1)一旦调用read来告知FileChannel向ByteBuffer存储字节,那就必须调用缓冲区的flip方法,让别人做好读取字节的准备,在上述程序中,如果没有flip()方法,那么就无法把内容写到目标文件中(其实写了,但是内容是空白)。为什么会出现这种情况呢,下面是加上flip()方法和没有加上flip()方法ByteBuffer对象的状态。
java.nio.HeapByteBuffer[pos=0 lim=21 cap=1024] (加上flip()方法)
java.nio.HeapByteBuffer[pos=21 lim=1024 cap=1024](没有加上flip()方法)
我们可以很清楚的看到,如果不加上flip方法,那么读取的内容其实是缓冲区当前位置往后的内容(之后的内容当然是空白了,缓冲区还没数据写到那呢。),而真正的内容应该是当前位置之前的内容。 个人觉得read操作后,必须就跟上flip()方法。
2)write()操作后,信息仍然在缓冲区中,接着clear()操作会重新设置缓冲区内部的指针,以便缓冲区在另一个read()操作期间能够做好接受数据的准备。换句话说,如果我们打算使用缓冲区执行进一步的read()操作,我们也必须得调用clear()方法来为每个read()做好准备。为什么?我代码没有跟下去,如果没有clear()方法,inChnanel.read(buffer)会一直等于0,导致了死循环,有高人解释下为什么?
ByteBuffer缓冲区的细节
1)ByteBuffer是唯一能将数据写入或读出的方式,我们只能使用通过创建一个独立的基本类型缓冲器,或者使用“as”方法从ByteBuffer中获得。也就说,我们不能
把基本类型的缓冲器转换成ByteBuffer(这个其实看源码就知道了,他们直接表示继承关系,不能强制转化)
2)Buffer由数据和可以高效地访问及操纵这些数据的四个索引组成,这四个索引分别是:mark(标记,其实我没明白这个的作用),position(位置),limit(界限),capacity(容量)。下面是用于设置和复位索引以及查询它们值的方法。
capacity() 返回缓冲区容量clear() 清空缓冲区,将position置为0,limit设置为容量大小(其实数据并没有清楚,只是次方法后,每次重头开始写入数据,覆盖了原有数据)
flip() 将limit置为position,position置为0.
limit() 返回limit值
limit(int lim) 设置limit值
mark() 将mark设置为position
position() 返回position的值
position(int pos) 设置position的值
remaining() 返回(limit-position)
hasRemaining() return position < limit;
reset() 把postion设置为mark
内存映射文件
内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件,有了内存映射文件,我们就可以假定整个文件都在内存中,而且完全可以把他们当做非常大的数组来访问。
我们可以看到ByteBuffer是一个abstract class.其有两个子类,一个是HeapByteBuffer,我们之前将的nio的操作,就是通过其来实现的。另一个就是MappedByteBuffer这个就是我们内存映射文件需要用到的。我们看到在FileChannel有一个map方法,如下所示,该方法就可以得到相应的MappedByteBuffer
* @see java.nio.channels.FileChannel.MapMode
* @see java.nio.MappedByteBuffer
*/
public abstract MappedByteBuffer map(MapMode mode,
long position, long size)
throws IOException; //mode = FileChannel.MapMode.READ_WRITE等等,position = 映射文件的起始位置,size = 映射区域大小
注意:“映射读"可以通过FileInpuStream.getChannel().map()来处理,但是”映射写(读写)“ 必须得用RandomAccesssFile().getChannel().map(),而不能通过FileOutputStream得到缓冲器。
补充内容:
不同的机器可能会使用不同的字节排序方法来存储数据,"big endian"(高位优先)将最重要的字节地址存放地址最低的存储器单元( 最低地址存放高位字节),"big endian"存放方式正是我们的书写方式,大数先写(比如,总是按照千、百、十、个位来书写数字)。而“little endian”(低位优先)则是将最重要的字节放在地址最高的存储单元( 最低地址存放低位字节)。当存储量大雨一个字节是(如short,int,float)就要考虑字节的顺序问题了。ByteBuffer是以高位优先的形式存储数据的,但是可以通过order()方法来改变顺序。参数是ByteOrder.BIG_ENDIAN ,ByteOrder.LITTLE_ENDIAN。
看下面的例子
0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 1如果我们以short方式读取数据
1)以默认的高位优先方式:则得到的数字时97(二进制为00000000 01100001)
2)低位优先的方式,则得到的数字是24832(二进制为01100001 00000000)