Java NIO Tutorial
Java NIO
Java NIO 诞生于JDK1.4, 意味着代替标准Java IO和 Java NetWorking 相关的IO,Java NIO提供了与标准IO API不同的IO使用方式。
Java NIO Overview
Java NIO 由三个核心的组件构成:
- Channels;
- Buffers;
- Selectors
Java NIO API 当然远不止这些类, 但这三个类在我看来是Java NIO API的核心类. 其余的组件,比如说Pipe和FileLock, 仅仅是与三个核心组件一起使用的实用程序类。因此,在Overview里对这三个类进行些简单介绍.
Channels 和 Buffers
通常, NIO的所有IO源头都是Channel. Channel跟Stream类似. 数据能从Channel读进Buffer, 数据也能从Buffer写进Channel. 如图所示:
有多种类型的Channel和Buffer. 下面列举了在Java NIO实现的一些主要的Channel:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
正如你所见, 这些Channel基本覆盖了UDP + TCP network IO, 和file IO.
下面列举的是在Java NIO里的主要Buffer实现:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些Buffer覆盖了基本的数据类型.
Selectors
Selector使得单个线程可以处理多个Channel. 当应用程序打开了很多连接(即会有多个Channel),但每个连接却只有较低的流量,使用Selector来处理将变得非常方便. 如图为Selector的工作模式:
Java NIO Channel
Java NIO Channel类似于Stream, 但有些差异:
- Channel是双向的, 但Stream通常是单向的;
- Channel可以异步读写;
- Channel总是从Buffer写入数据,将数据读入Buffer;
Channel Implementations
下面是Java NIO里最重要的Channel实现:
- FileChannel: 文件读写相关;
- DatagramChannel: UDP NetWorking IO 相关
- SocketChannel: TCP NetWorking IO 相关
- ServerSocketChannel: ServerSocketChannel允许您监听传入的TCP连接, 就像Web服务器一样. 对于每个传入连接, 都会创建一个SocketChannel。
Basic Channel Example
下面是一个使用FileChannel将数据读入Buffer的简单例子:
package test;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
File path = new File("f://javaDemo/java_nio_study/src/test", "data.txt");
RandomAccessFile file = new RandomAccessFile(path, "rw");
FileChannel inChannel = file.getChannel();
ByteBuffer buf = ByteBuffer.allocate(8);
int bytesRead = inChannel.read(buf);
while(bytesRead != -1) {
System.out.println("Read " + bytesRead);
//Buffer一开始是读入,要换成写出要调用flip()
buf.flip();
while(buf.hasRemaining()) {
System.out.println((char)buf.get());
}
//后续Buffer章节将会介绍
buf.clear();
bytesRead = inChannel.read(buf);
}
file.close();
}
}
Java NIO Buffer
Buffer是被用于Channel交互的. 数据从Channel读出时先读到Buffer, 数据要写入Channel时只能将Buffer的数据写入.
Buffer本质上是一个可以写入数据的内存块,然后可以再次读取。 此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用该内存块。
Basic Buffer Usage
通常使用以下4个步骤对Buffer进行读写操作:
- 将Channel里的数据写入Buffer;
- 调用buffer.flip(), 转换为读模式;
- 将Buffer里的数据读出;
- 调用buffer.clear()或者buffer.compact()整理内存块;
将数据写入Buffer时,Buffer会跟踪您写入的数据量。 一旦需要读取数据,就需要使用flip()方法调用将Buffer从写入模式切换到读取模式。 在读取模式下,Buffer允许您读取写入Buffer的所有数据。
一旦读完所有数据,就需要清除缓冲区,以便再次写入。 您可以通过两种方式执行此操作:通过调用clear()或调用compact()。 clear()方法清除整个缓冲区。 compact()方法仅清除您已读取的数据。 任何未读数据都会移动到缓冲区的开头,现在数据将在未读数据之后写入缓冲区。
这是一个简单的ByteBuffer用法示例:
package test;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
*
* @author LuoQiang
* @version 1.0
*
*/
public class ByteBufferDemo {
public static void main(String[] args) throws IOException {
//获取一个FileChannel
RandomAccessFile afile = new RandomAccessFile("data.txt", "r");
FileChannel fileChannel = afile.getChannel();
//获取一个capacity为8的ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
// FileChannle --> ByteBuffer
int bytesRead = fileChannel.read(byteBuffer);
while(bytesRead != -1) {
System.out.println("Read " + bytesRead + "byte(s)");
//将byteBuffer切换为读模式
byteBuffer.flip();
while(byteBuffer.hasRemaining()) {
System.out.print((char)byteBuffer.get());//每次读一个字节
}
System.out.println();
//清除缓冲区
byteBuffer.clear();
//再次读入
bytesRead = fileChannel.read(byteBuffer);
}
//关闭文件流
afile.close();
}
}
Buffer Capacity, Position and Limit
Buffer本质上是一个可以写入数据的内存块,然后可以再次读取。 此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。
Buffer有三个重要的属性:
- capacity
- position
- limit
position和limit的含义取决于缓冲区时处于读还是写模式, capacity表示内存块的大小.
以下是三者的概念图:
Capacity
作为存储器块,Buffer具有一定的固定大小,也称为“容量”。 您只能将容量个字节,长整数,字符等写入Buffer。 Buffer已满后,您需要清空它(读取数据或清除它),然后才能将更多数据写入其中。
Position
将数据写入缓冲区时,在Position执行此操作。 最初位置为0.当一个字节,长整数等已写入缓冲区时,位置被提前指向缓冲区中的下一个单元以插入数据。 Position最大可以变为容量 - 1。
Limit
在写入模式下,Limit是您可以写入缓冲区的数据量的限制。 在写入模式下,限制等于缓冲区的容量。
将Buffer翻转为读取模式时,limit意味着可以从数据中读取的数据量的限制。 因此,当将Buffer翻转到读取模式时,limit被设置为写入模式的写入位置。 换句话说,您可以读取写入的字节数(限制设置为写入的字节数,由位置标记)。
Buffer Types
Java NIO 有以下Buffer类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
如您所见,这些Buffer类型代表不同的数据类型。 换句话说,它们允许您使用char,short,int,long,float或double来处理缓冲区中的字节。
MappedByteBuffer后面将进行介绍.
Allocating a Buffer
要获取Buffer对象,必须先分配它。 每个Buffer类都有一个allocate()方法来执行此操作。 下面是一个显示ByteBuffer分配的示例,容量为48字节:
ByteBuffer buf = ByteBuffer.allocate(48);
下面是一个为1024个字符分配空间的CharBuffer示例:
CharBuffer buf = CharBuffer.allocate(1024);
Writing Date to a Buffer
您可以通过两种方式将数据写入Buffer:
- 从一个Channel将数据写入Buffer
- 通过Buffer的put()方法
下面是一个示例,显示了Channel如何将数据写入Buffer:
int bytesRead = inChannel.read(buf); //read into buffer.
这是一个通过put()方法将数据写入Buffer的示例:
buf.put(127);
put()方法还有许多其他版本,允许您以多种不同方式将数据写入Buffer。 例如,在特定位置写入,或将一个字节数组写入缓冲区。 有关更多详细信息,请参阅JavaDoc以获取具体的缓冲区实现。
flip()
flip()方法将Buffer从写入模式切换到读取模式。 调用flip()会先将limit置为Position,再将Position置回0.
Reading Data from a Buffer
有两种方法可以从Buffer读取数据:
- 将Buffer的数据读入Channel
- 使用get()方法
以下是如何将Buffer中的数据读入Channel的示例:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
以下是使用get()方法从Buffer读取数据的示例:
byte aByte = buf.get();
get()方法还有许多其他版本,允许您以多种不同方式从Buffer中读取数据。 例如,读取特定位置,或从缓冲区读取字节数组。 有关更多详细信息,请参阅JavaDoc以获取具体的Buffer实现。
rewind()
Buffer.rewind()将位置设置回0,因此您可以重读Buffer中的所有数据。 limit保持不变,因此仍然标记可以从Buffer读取多少元素(字节,字符等)。
clear() and compact()
完成从Buffer读取数据后,必须使Buffer准备好再次写入。 你可以通过调用clear()或调用compact()来实现。
如果调用clear(), 则position置为0, limit置为最大容量处, 换句话说, Buffer将被清除
如果缓冲区中仍有未读数据,并且您想稍后读取它,但您需要先进行一些写入,请调用compact()而不是clear()。
compact()将所有未读数据复制到Buffer的开头。 然后它将position设置在最后一个未读元素之后。 limit属性仍设置为capacity,就像clear()一样。 现在Buffer已准备好写入,但不会覆盖未读数据。
mark() and reset()
您可以通过调用Buffer.mark()方法在Buffer中标记给定位置。 然后,您可以通过调用Buffer.reset()方法将位置重置回标记位置。 这是一个例子:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() and compareTo()
可以使用equals()和compareTo()比较两个缓冲区。
equals()
两个Buffer相等当且仅当:
- 他们是同样的Buffer类型;
- 有同样多的数据
- 数据都一样
如您所见,equals仅比较Buffer的一部分,而不是它内部的每个元素。 实际上,它只是比较缓冲区中的其余元素。
compareTo()
ompareTo()方法比较两个缓冲区的剩余元素(字节,字符等),用于例如 排序例程。 在以下情况下,缓冲区被视为“小于”另一个缓冲区:
- 与另一个Buffer中的对应元素相等的第一个元素小于另一个Buffer中的元素。
- 所有元素都相等,但第一个Buffer在第二个Buffer之前耗尽了元素(它有更少的元素)。