新IO和传统IO有相同的目的,都是用于进行输入输出,但新IO使用了不同的方式来处理输入输出,新IO采用内存映射文件的方式来处理输入输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,通过这种方式来进行输入输出比传统的输入输出要快得多。Java中与新IO相关的包:
java.nio 主要包含各种与Buffer相关的类
java.nio.channels 主要包含与Channel和Selector相关的类
java.nio.charset 主要包含与字符集相关的类
java.nio.channels.spi 主要包含与Channel相关的服务提供者编程接口
java.nio.charset.spi 包含与字符集相关的服务提供者编程接口
Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过该方法可以直接将“一块数据”映射到内存中。如果说传统的IO是面向流的处理,则新IO则是面向块的处理。
Buffer可以理解为一个容器,它的本质是一个数组,发送到Channel中所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。
Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作。除了ByteBuffer之外,对应于其他基本数据类型(boolean除外)都有相应的Buffer类:CharBuffer...
static xxxBuffer allocate(int capacity)
实际上使用较多的是ByteBuffer和CharBuffer,其他用得少。其中ByteBuffer类还有一个子类:MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。
在Buffer中有三个重要的概念:容量(capacity)、界限(limit)、位置(position)
0<=mark<=position<=limit<=capacity
Buffer的主要作用就是装入数据,然后输出数据。开始时Buffer的position为0,limit为capacity,程序可以通过put()方法向Buffer中放入一些数据(或从Channel中获取一些数据),每放入一些数据。Buffer的position相应地向后移动一些位置。
当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在的位置,并将position设为0,这就使得Buffer的读写指针又移到了开始位置。也就是说,Buffer调用flip()方法之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear()方法,clear()方法将position置0,将limit置capacity,这样为再次向Buffer中装入数据做好准备。
Buffer包含以下常用的方法:
int capacity()
boolean hasRemaining() 判断当前位置和界限之间是否还有元素可供处理
int limit()
Buffer limit(int newLt) 重新设置界限的值,并返回一个具有新的limit的缓冲区对象。
Buffer mark() 设置Buffer的mark位置,它只能在0和位置之间做mark
int position()
Buffer position(int newPs) 设置Buffer的position,并返回position被修改后的Buffer对象
int remaining() 返回当前位置和界限之间的元素个数
Buffer reset() 将position转到mark所在的位置
Buffer rewind() 将position设置为0,取消设置的mark
此外,Buffer还提供两个重要的方法:put()、get(),用于向Buffer中放入数据和从Buffer中取数据。其既可以单个数据访问,也可以进行批量数据操作。
当使用put()和get()来访问Buffer中的数据时,分为相对和绝对两种:
相对:从Buffer的当前位置处开始处理,然后将位置移动
绝对:直接根据索引向Buffer中读取或写入数据,position不变
通过allocate()方法创建的Buffer对象时普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本比普通Buffer的创建成本高,但直接Buffer的读取效率更高。
Channel类似于传统的流对象,但与传统的流对象有两个区别:
(1)Channel可以直接将制定文件的部分或全部直接映射成Buffer
(2)程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。即如果从Channel中去数据,必须先用Buffer从Channel中取出数据,然后让程序从Buffer中取出这些数据;如果要向Channel中写数据,同样先往Buffer中放入数据,再将Buffer中的数据写入Channel中。
Java为Channel接口提供了DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel、SocketChannel等实现类。
所有Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。
Channel中最常用的三类方法是map()、read()和write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;而read()或write()方法都有一系列重载形式,这些方法用于从Buffer中读取或写入数据。
map()方法的签名为:MappedByBuffer map(FileChannel.MapMode mode, long position, long size),第一个参数为执行映射时的模式,分别有只读、写等模式;而第二、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer。
JDK 1.4提供了Charset来处理字节序列和字符序列之间的转换,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持的字符集的方法,Charset类是不可变的。
Charset类提供了一个availableCharsets()静态方法来获取当前JDK所支持的所有字符集。
一旦知道了字符集的别名后,就可以调用Charset的forName()方法来创建对应的Charset对象,forName()方法的参数就是相应字符集的别名:
Charset cs = Charset.forName(“utf-8”)
获取Charset对象之后,就可以通过该对象的newDecoder()、newEncoder()两个方法分别返回CharsetDecoder和CharsetEncoder对象,代表该Charset对象的解码器和编码器。调用CharsetDecoder的decode()方法就可以将ByteBuffer(字节序列)转换为CharBuffer(字符序列),调用CharsetEncoder的encode()方法就可以将CharBuffer或String(字符序列)转换为ByteBuffer(字节序列)。
实际上,Charset类提供了如下三个方法:
CharBuffer decode(ByteBuffer bb)
ByteBuffer encode(CharBuffer cb)
ByteBuffer encode(String str)
在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁的FileLock对象,从而锁定文件。
lock()和tryLock()方法的区别为:当lock()试图锁定某个文件时,如果无法获得文件锁,程序将一直阻塞;而tryLock()方法是尝试获取文件锁,它将直接返回而不阻塞,如果获得文件锁,该方法则返回该文件锁,否则返回null。
如果FileChannel只想锁定文件的部分内容,而不是锁定文件的全部内容,则可以使用如下的lock()或tryLock()方法:
lock(long position, long size, boolean shared)
tryLock(long position, long size, boolean shared)
参数shared为true时,表示该锁为一个共享锁,它将允许多个进程来读取文件,但阻止其他进程获得对该文件的排它锁。当shared为false时,表明该锁是一个排它锁,它将锁住对该文件的读写。程序可以通过调用FileLock的isShared来判断它获得的锁是否为共享锁。
处理完文件后需要调用release()方法来释放文件锁。
关于文件锁需要注意:
(1)在某些平台上,文件锁仅仅是建议性的,并不是强制性的。这意味着即使一个程序不能获得文件锁,它可以对该文件进行读写。
(2)在某些平台上,不能同步地锁定一个文件并把它映射到内存中
(3)文件锁由Java虚拟机所支持,如果两个Java程序使用同一个Java虚拟机允许,则它们不能对同一个文件加锁
(4)在某些平台上关闭FileChannel时,会释放Java虚拟机在该文件上的所有锁,因此应该避免对同一个被锁定的文件打开多个FileChannel。
下面给出NIO数据读写的代码:
(1)读数据
package cn.edu.hust.io.nio;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIORead {
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream("test.txt");
FileChannel channel = inputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
StringBuffer stringBuffer = new StringBuffer();
while (buffer.remaining() > 0) {
byte b = buffer.get();
stringBuffer.append((char) b);
}
System.out.println(stringBuffer);
channel.close();
inputStream.close();
}
}
(2)写数据
package cn.edu.hust.io.nio;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOWrite {
public static void main(String[] args) throws IOException {
File file = new File("test.txt");
FileOutputStream outputStream = new FileOutputStream(file);
FileChannel channel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
String data = "Benz Audi BMW Toyota Honda Nissan Mazda Volvo";
buffer.put(data.getBytes());
buffer.flip(); //准备好数据
channel.write(buffer);
channel.close();
outputStream.close();
}
}