定义
在JDK 1.4中定义了一个全新的API,即New IO,与标准的IO API有不同的工作方式,NIO的核心组件包括Channel(通道)、Buffer(缓冲区)、Selectors(选择器)。
传统的IO是基于字节流和字符流操作,是阻塞式,而NIO是基于Channel和Buffer,是非阻塞式,Selector用于监听多个通道的事件。单个线程可以监听多个数据通道。
Channel和Buffer
Channel和IO中的流相似,只不过流是单向的,InputStream只能输入不能输出,OutputStream又只能输出不能做输入,而Channel就厉害了,既可以用输出,又可以输入,又当爹又当妈的。
Channel的主要实现有:
FileChannel: 文件的数据读写
DatagramChannel: UDP的数据读写
SocketChannel: TCP的数据读写,一般客户端实现
ServerSocketChannel: 一般是服务器实现。
FileChannel
FileChannel读取写入依靠ByteBuffer来完成,调用read把数据读到ByteBuffer中,调用write把ByteBuffer中数据写回文件中。
写入一个文件,使用ByteBuffer.wrap把字节数据包装成ByteBuffer,也可以调用Charset类来编码一个字符串,返回ByteBuffer对象。
private static void test0(String path) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(path);
FileChannel channel = fileOutputStream.getChannel();
byte[] bytes ="abc听风逝夜def".getBytes();
ByteBuffer byteBuffer =ByteBuffer.wrap(bytes);
channel.write(byteBuffer);
channel.write(Charset.forName("utf-8").encode("abc听风逝夜def"));
}catch (IOException e){
e.toString();
}
}
读取一个文件,要从FileInputStream获取FileChannel,否则会报java.nio.channels.NonReadableChannelException
异常。读取操作就比较有学问了。首先定义一个缓存区,使用read方法读取到缓存区,read()返回读到的大小,如果返回-1,则表示读玩了。通过array()方法获取读到的字节数据。最后要清除缓存区中已读到的数据,准备下一次读。
try {
FileInputStream fileInputStream = new FileInputStream(path);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer =ByteBuffer.allocate(1024);
while (channel.read(byteBuffer)>0){
System.out.println(new String(byteBuffer.array()));
byteBuffer.clear();
}
}catch (IOException e){
e.toString();
}
但是上述代码会出现这种情况。
会有很多空数据被输出,这是因为ByteBuffer大小是1024,而文件大小是18字节,其余的1024-18的地方没有存放东西。解决办法很简单,就是截取ByteBuffer中前N个数据。
private static void test1(String path) {
try {
FileInputStream fileInputStream = new FileInputStream(path);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer =ByteBuffer.allocate(1024);
int post=0;
while (( post=channel.read(byteBuffer))>0){
byte[] data =new byte[post];
System.arraycopy(byteBuffer.array(),0,data,0,post);
System.out.println(new String(data));
byteBuffer.clear();
}
}catch (IOException e){
e.toString();
}
}
还有一种骚操作,里面又有一个学问,flip()方法和limit,在下面会说。
private static void test1(String path) {
try {
FileInputStream fileInputStream = new FileInputStream(path);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (channel.read(byteBuffer) > 0) {
byteBuffer.flip();
int limit = byteBuffer.limit();
byte[] datas = new byte[limit];
byteBuffer.get(datas);
System.out.println(new String(datas));
byteBuffer.clear();
}
} catch (IOException e) {
e.toString();
}
flip()
ByteBuffer有两种模式,一种要写,一种要读。
写的时候,内部的pos属性会随着写入的大小移动,相当于一个指针,如下代码,随着put一个字节,pos会往后移1,但是在putInt的时候,会移动4字节,因为int就是4字节,其他类型也一样,putLong会移动8字节。内部limit=capacity。当put的大小超过limit时,将报错。
ByteBuffer byteBuffer =ByteBuffer.allocate(100);
System.out.println(byteBuffer);
byteBuffer.put("1".getBytes());
System.out.println(byteBuffer);
byteBuffer.putInt(1);
System.out.println(byteBuffer);
byteBuffer.putLong(1);
System.out.println(byteBuffer);
java.nio.HeapByteBuffer[pos=0 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=1 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=5 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=13 lim=100 cap=100]
切换读的时候,就是通过flip方法,此时limit会变成写入后的大小,并且pos为0,每get数据的时候,post往下移,下次从这个位置后读取,直到多余limit时就报错。
ByteBuffer byteBuffer =ByteBuffer.allocate(100);
System.out.println(byteBuffer);
byteBuffer.put("1".getBytes());
System.out.println(byteBuffer);
byteBuffer.putInt(1);
System.out.println(byteBuffer);
byteBuffer.putLong(1);
System.out.println(byteBuffer);
byteBuffer.flip();
System.out.println(byteBuffer);
java.nio.HeapByteBuffer[pos=0 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=1 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=5 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=13 lim=100 cap=100]
java.nio.HeapByteBuffer[pos=0 lim=13 cap=100]
但是上述代码有一个通病,面对中文时候可能乱码,别看上面没出现,如下例子。只是把缓存区调小了,输出就会乱码。原因是缓冲区大小是2,文件中数据是“abc听风逝夜def”,第一次读取是ab,第二次读取是c和听字的一半,所以就乱码了。有人说缓存区调大啊,其实也没用。算一下就知道了,比如缓冲区大小是1024,一个汉字3字节,可以一次读取<=341个汉字,但是3*341=1023啊,如果此时是342个汉字,肯定读到乱码。因为剩余一个字节不够存一个汉字,这个道理其实和FileInputStream直接read一样,也会出现乱码,通过FileReader或者BufferedReader能解决。
try {
FileInputStream fileInputStream = new FileInputStream(path);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(2);
while (channel.read(byteBuffer) > 0) {
byteBuffer.flip();
int limit = byteBuffer.limit();
byte[] datas = new byte[limit];
byteBuffer.get(datas);
System.out.println(new String(datas));
byteBuffer.clear();
}
} catch (IOException e) {
e.toString();
}
网上解决这个办法更离谱,没几个有用的,也是在极端情况下会出现乱码,或者修改一下缓冲区大小就乱码。来上我写的,在这里不管文件有多少个字符,或者缓冲区有多大,都通吃!!!,思路还简单。
private static void test4(String path) {
try {
FileInputStream fileOutputStream = new FileInputStream(path);
FileChannel channel = fileOutputStream.getChannel();
List<byte[]> data = new ArrayList<>();
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
int totalSize = 0;
while (channel.read(byteBuffer) > 0) {
byteBuffer.flip();
byte[] soure = new byte[byteBuffer.limit()];
System.arraycopy(byteBuffer.array(), 0, soure, 0, soure.length);
totalSize += soure.length;
data.add(soure);
byteBuffer.clear();
}
byte[] bytesData = new byte[totalSize];
int destPos = 0;
for (int i = 0; i < data.size(); i++) {
System.arraycopy(data.get(i), 0, bytesData, destPos, data.get(i).length);
destPos += data.get(i).length;
}
System.out.println(new String(bytesData));
} catch (
IOException e) {
}
}
Buffer
Buffer有很多数据类型实现,当然对Stirng的操作就是StringBuffer了。
在上面说了Buffer的工作状态,但是有个问题要说明一下,如果缓冲区大小为10,先往里面put了5个数据,并切换到读模式,当读取了2个数后,在往里面存放几个数会报错?
答案是三个,当切换到读模式后,pos=0,limit=5,get两次后pos是2,紧接着put是不报错的,但是超过limit后就会报。
其他API就简单了,多练就懂了。