详解Java NIO
写在前面:
在学习完IO后,又深入学习了NIO,这篇笔记花了笔者挺长时间,文章较长,可根据目录各区所需,如果觉得写的不错,点个赞吧,如果有什么问题,请不吝赐教,多多交流学习
Java NIO
1.Java NIO简介
NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API.NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区、基础通道的IO操作。NIO将以更高效的方式进行文件的读写操作
2.JavaNIO与IO的主要区别
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
无 | 选择器(Selector) |
3.缓冲区(Buffer) 和通道(channel)
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备的连接,若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。
那什么是缓冲区呢?
缓冲区底层就是数组,用于存储不同类型的数据,根据数据类型,根据数据类型的不同(除Boolean外),都提供了对应的缓冲区类型:ByteBuffer
,CharBuffer
,shortBuffer
,FloatBuffer
等等,但管理方式都相同,通过allocate()
进行操作,会在下文中详解其各种方法
缓冲区和通道是什么关系呢?
Java NIO中的Buffer
主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入到通道中的,,通道负责存储数据,缓冲区负责数据传输
如图所示,通道就像轨道,缓冲区就像列车,往返在程序和文件之间,“承载”数据在两者之间传输,在普通的IO中,流分为输入流输出流是单向的,但是NIO是双向的不区分方向
4.详解缓冲区的创建及常见方法
4.1详解Buffer类
抽象Buffer类是所有数据类型Buffer的父类,源码中有四个属性
capacity
:表示创建缓冲区的容量limit
:当前写到的位置position
:当前读到的位置mark
:用来标记position
的位置,使用reset()
方法会是指position=mark
,复位position
接下来我们通过源码来看看常见的方法(如果是第一次接触这些方法可能无法理解具体细节,可以结合接下来的实例代码进一步理解各个方法):
allocate(int capacity)
: 创建一个对应数据类型的缓冲类,不同数据类型对应的Buffer的创建方式相同,使用allocate()方法,同时创建时指名容量capacity()
:返回当前类型Buffer的容量position
:返回当前读到的位置limit()
:返回当前写到的位置mark()
:标记当前的position,使mark = positionreset()
:复位position,使position = mark,通过源码得知,mark初始值为-1,所以我们使用reset()方法前必须先使用mark()方法,重置mark,否则会抛出异常flip()
:转换模式,由读模式转换到写模式,此时执行limit = position,position = 0,mark = -1;rewind()
:重读操作,position = 0,mark = -1,重新读取数据clear()
:清空操作,此时会将四种属性变为初始值,此时缓冲区的数据处在“迷失”状态put()
:各种写操作get()
:各种读操作
还有许多方法,在此就不一一写出,如果感兴趣,可以点进Buffer类中查看源码
4.2接下来一个实例来解释执行各种方法时四种变量的变化
我们编写一个实例来看一下各个变量的具体变化:
public class TestBufffer {
public static void main(String[] args) {
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println("-------------初始状态----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
String str = "hello";
allocate.put(str.getBytes());
System.out.println("-------------put()----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
allocate.flip();
System.out.println("-------------flip()----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
byte[] dst = new byte[allocate.limit()];
allocate.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("-------------get()----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
allocate.rewind();
System.out.println("-------------rewind()----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
allocate.clear();//此时缓冲区中的数据依然存在,处于被遗忘状态,仍然可以读取到
System.out.println("-------------clear()----------------");
System.out.println("position:"+allocate.position());
System.out.println("limit:"+allocate.limit());
System.out.println("capacity:"+allocate.capacity());
}
}
运行结果:
5.直接缓冲区和非直接缓冲区
直接缓冲区:通过allocateDirect()
方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率
非直接缓冲区:通过allocate()
方法分配缓冲区,将缓冲区建立在JVM的内存中
可以看到直接缓冲区跳过了copy操作,直接建立在物理内存中,加快了运行效率,但同时也带来了很多问题,首先程序会直接占用物理内存,并且数据不易控制,抛出不确定的异常
6.文件通道(FileChannel)
Java为Channel接口提供的最主要实现类包括本地IO和网络IO
FileChannel
:用于读取,写入,映射和操作文件的通道DatagramChannel
:通过UDP 读写网络中的数据通道。SocketChannel
:通过TCP 读写网络中的数据。ServerSocketChannel
:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。
6.1获取通道
获取通道的一种方式就是对支持通道的对象调用getChannel()方法。支持通道的类如下:
FileInputStream
FIleOutStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
只有byte型缓冲区才有通道,根据流对象调用getChannel()方法可以获取到对应的channel对象
6.2通道的数据传输
- 将Buffer中数据写入Channel
例如:将buffer中数据写入到Channel中,write方法的返回值和基本IO的返回值相同
int bytesWritten = inChannel,write(buf);
- 从Channel读取数据到Buffer
例如:从Channle读取数据到Buffer,read方法的返回值和基本IO的返回值相同,当你像缓冲区写数据时,记得将缓冲区切换为写模式
int bytesRead = inChannel,read(buf);
6.3利用通道复制文件的实例
public class TestChannel {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src\\pic1.png");
FileOutputStream fos = new FileOutputStream("src\\pic4.png");
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
//获取缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
while((inChannel.read(buf))!=-1){//将通道中的数据写入到buf中,此时buf为读模式
buf.flip();//将buf切换到写模式
outChannel.write(buf);//将缓冲区数据写入到通道中
buf.clear();//清空缓冲区,进行下一次读取
}
inChannel.close();
outChannel.close();
fis.close();
fos.close();
}
}
7.分散(Scatter)和聚集(Gather)
7.1什么是分散读取和聚集写入
- 分散读取(Scattering Reads):将通道中的数据分到到多个缓冲区中
其中按照缓冲区的顺序,从Channel 中读取的数据依次将Buffer 填满
- 聚集写入(Gather Writes):将多个缓冲区中的数据聚集到通道中
按照缓冲区的顺序,写入position 和limit 之间的数据到Channel
7.2具体的代码示例
下面的代码中我创建了两个缓冲区,有不同的大小,将演示从文本中将数据读入到两个缓冲区中
public class TestScatter {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src\\hello.txt");
// 创建通道
FileChannel inChannel = fis.getChannel();
// 创建多个缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(10);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
// 创建缓冲区集合,作为参数
ByteBuffer[] buffers = {buf1,buf2};
inChannel.read(buffers);
// 此时缓冲区处于写状态,切换成读状态,读取缓冲区中的数据
for(ByteBuffer byteBuffer:buffers){
byteBuffer.flip();
}
// 输出两个缓冲区的数据
System.out.println("------------------------buffer1------------------------");
System.out.println(new String(buffers[0].array(),0,buffers[0].limit()));
System.out.println("------------------------buffer2------------------------");
System.out.println(new String(buffers[1].array(),0,buffers[1].limit()));
}
}
关于聚集写入道理相同,在此就不再重复展示
8.NIO_阻塞与非阻塞
关于NIO非阻塞性质以及选择器的使用详见我的另一篇博客:https://blog.csdn.net/CPrimer0/article/details/113919771