Java NIO(New IO)是从Java1.4版本开始引入的一个新的IO API,可以代替标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(No Blocking IO) |
无 | 选择器(Selectors) |
3.1通道和缓冲区简介
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
3.2缓冲区(Buffer)
(1)缓冲区一个用于特定基本数据类型的容器。有Java.nio报定义的,所有缓冲区都是Buffer抽象类的子类。
(2)Java NIo中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中。
(3)Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean除外),有以下Buffer常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述Buffer类他们都采用相识的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
public static XXXBuffer allocate(int capacity):创建一个容量为capcaity的Buffer对象
3.3缓冲区的基本属性
3.3.1Buffer中的重要概念
(1)容量(capacity):表示Buffer最大容量,缓冲区容量不能为负,并且创建不能更改
(2)限制(limt):第一个不应该读取或写入的数据的索引,即位于limit后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
(3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且其不能大于其限制。
(4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
(5)标记、位置、限制、容量遵守以下不变式:0<=mark<=position<=limit<=capacity
3.4Buffer常用的方法
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为0 |
int capacity() | 返回Buffer的capacity大小 |
boolean hashRemaining() | 判断缓冲区中是否还有元素 |
int limit() | 返回Buffer的界限(limit)的位置 |
Buffer limit(int n) | 将设置缓冲区界限为n,并返回一个具有新limit的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置position |
Buffer position(int n) | 将设置缓冲区的当前位置为n,并返回修改后的Buffer对象 |
int remaining() | 返回position和limit之间的元素个数 |
Buffer reset() | 将位置position转到以前设置的mark所在的位置 |
Buffer rewind() | 将位置设为0,取消设置的mark |
3.5Buffer方法代码演示
public class TestBuffer {
@Test
public void test01() {
//1.分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("capacity:" + byteBuffer.capacity());//1024
System.out.println("limit:" + byteBuffer.limit());//1024
System.out.println("position:" + byteBuffer.position());//0
//2.利用put()存入数据
byteBuffer.put(new byte[]{97, 98, 99, 100, 101});
System.out.println("capacity:" + byteBuffer.capacity());//1024
System.out.println("limit:" + byteBuffer.limit());//1024
System.out.println("position:" + byteBuffer.position());//5
/*
*3.flip()将缓冲区的界限设置为当前位置,并将当前位置充值为0
* public final Buffer flip() {
* limit = position;
* position = 0;
* mark = -1;
* return this;
* }
*/
byteBuffer.flip();
System.out.println("capacity:" + byteBuffer.capacity());//1024
System.out.println("limit:" + byteBuffer.limit());//5
System.out.println("position:" + byteBuffer.position());//0
//4.利用get()读取缓冲区的数据
byte[] data = new byte[byteBuffer.limit()];
byteBuffer.get(data);
System.out.println(Arrays.toString(data));//[97, 98, 99, 100, 101]
System.out.println("capacity:" + byteBuffer.capacity());//1024
System.out.println("limit:" + byteBuffer.limit());//5
System.out.println("position:" + byteBuffer.position());//5
//5.rewind(),将位置设置为0,取消设置的mark
byteBuffer.rewind();
System.out.println("capacity:" + byteBuffer.capacity());//1024
System.out.println("limit:" + byteBuffer.limit());//5
System.out.println("position:" + byteBuffer.position());//0
/*
* 6.clear(),清空缓冲区,但是缓冲区中的数据依据存在
* public final Buffer clear() {
* position = 0;
* limit = capacity;
* mark = -1;
* return this;
* }
*/
byteBuffer.clear();
}
@Test
public void test02() {
String str = "abcd";
//1.分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//2.利用put()存入数据
byteBuffer.put(str.getBytes());
byteBuffer.flip();
byte[] data = new byte[1024];
byteBuffer.get(data,0,2);
System.out.println(new String(data,0,2));//ab
System.out.println("position:"+byteBuffer.position());//2
//3.标记当前position的位置
byteBuffer.mark();
byteBuffer.get(data,0,2);
System.out.println(new String(data,0,2));//cd
System.out.println("position:"+byteBuffer.position());//4
//4.将position位置设置为mark标记的位置
byteBuffer.reset();
System.out.println("position:"+byteBuffer.position());//2
}
}
3.6直接与非直接缓冲区
(1)字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作。也就是说,在每次调用基础操作系统的一个本机I/O操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
(2)直接字节缓冲区可以通过调用此类的allocateDirect()工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需要成功通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给哪些易受基础系统的本机I/O操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
(3)直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer。Java平台的实现有助于通过JNI从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的时不可访问的内存区域,则试图访问该区域不会更改缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定异常。
(4)字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。提高此方法是为了能够在性能关键代码中执行显示缓冲区管理。
(5)非直接缓冲区通过allocate()反复分配缓冲区,将缓冲区建立在JVM的内存中,直接缓冲区通过allocateDirect()反复分配直接缓冲区,将缓冲区简历在物理内存中,可以提高效率。
3.8通道(Channel)
由java.nio.channels包定义的。channel表示IO源与目标打开的连接。Channel类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。
3.8.1Java为Channel借口提供的最主要实现类如下:
(1)FileChannel:用于读取、写入、映射和操作文件的通道。
(2)DatagramChannel:通过UDP读写网络中的数据通道。
(3)SocketChannel:通过TCP读写网络中的数据。
(4)ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SockerChannel。
3.8.2获取通道的一种方式是对支持通道的对象调用
getChannel()反复。支持通道的类如下:
FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket、ServerSocket
获取通道的其它方式是使用Files类的静态反复newByteChannel()获取字节通道。或者通过通道的静态反复open()打开并返回指定的通道。
3.9代码演示
public class TestChannel {
//非直接缓复制87M大小文件存耗时:694毫秒
@Test
public void test01() {
Instant start = Instant.now();
//利用(非直接缓冲区)通道完成文件的复制
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fis = new FileInputStream("1.exe");
fos = new FileOutputStream("2.exe");
//获取通道
inChannel = fis.getChannel();
outChannel = fos.getChannel();
//分配缓冲区的大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将通道中的数据存入缓冲区中
while (inChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
//将缓冲区的数据写入通道
outChannel.write(byteBuffer);
//清空缓冲区
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Instant end = Instant.now();
System.out.println("非直接缓复制87M大小文件存耗时:"+ ChronoUnit.MILLIS.between(start, end)+"毫秒");
}
//直接缓复制87M大小文件存耗时:192毫秒
@Test
public void test02() {
Instant start = Instant.now();
//利用(直接缓冲区)通道完成文件的复制(内存映射文件)
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
/**
* public static FileChannel open(Path path, OpenOption... options)
* @param path 文件路劲
* @param options 做什么操作
*/
inChannel = FileChannel.open(Paths.get("1.exe"), StandardOpenOption.READ);
//StandardOpenOption.CREATE,如果文件存在就覆盖,如果不存在就创建
//StandardOpenOption.CREATE_NEW,如果文件存在就报错,如果不存在就创建
outChannel = FileChannel.open(Paths.get("3.exe"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
//将缓冲区创建在物理内存中
MappedByteBuffer inBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inBuffer.limit()];
inBuffer.get(dst);
outBuffer.put(dst);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Instant end = Instant.now();
System.out.println("直接缓复制87M大小文件存耗时:"+ ChronoUnit.MILLIS.between(start, end)+"毫秒");
}
}
3.10分散(Scatter)和聚集(Gather)
3.10.2代码演示
@Test
public void test04() {
//分散,把通道内的数据读取到多个缓冲区中
RandomAccessFile inRaf = null;
RandomAccessFile outRaf = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inRaf = new RandomAccessFile("1.txt", "rw");
outRaf = new RandomAccessFile("2.txt", "rw");
//1.获取通道
inChannel = inRaf.getChannel();
//2.分配缓冲区
ByteBuffer byteBuffer1 = ByteBuffer.allocate(512);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
//3.分散读取
ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};
inChannel.read(byteBuffers);
//4.把buffer切换到读模式
Arrays.stream(byteBuffers).forEach(Buffer::flip);
System.out.println("第一个buffer读取的内容是:"+new String(byteBuffer1.array(),0,byteBuffer1.limit()));
System.out.println("第二个buffer读取的内容是:"+new String(byteBuffer2.array(),0,byteBuffer2.limit()));
//5.聚集写入
outChannel = outRaf.getChannel();
outChannel.write(byteBuffers);
} catch (IOException e) {
e.printStackTrace();
}finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outRaf != null) {
try {
outRaf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inRaf != null) {
try {
inRaf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.11transferFrom()与transferTo()
transferFrom()表示这个通道的数据来之哪个通道;
transferTo()表示将这个通道的数据传输给其它通道。
//使用通道之间的数据传输(直接缓冲区)87M大小文件存耗时:680毫秒
@Test
public void test03() {
Instant start = Instant.now();
//利用(直接缓冲区)通道完成文件的复制(内存映射文件)
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
/**
* public static FileChannel open(Path path, OpenOption... options)
* @param path 文件路劲
* @param options 做什么操作
*/
inChannel = FileChannel.open(Paths.get("1.exe"), StandardOpenOption.READ);
//StandardOpenOption.CREATE,如果文件存在就覆盖,如果不存在就创建
//StandardOpenOption.CREATE_NEW,如果文件存在就报错,如果不存在就创建
outChannel = FileChannel.open(Paths.get("4.exe"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Instant end = Instant.now();
System.out.println("使用通道之间的数据传输(直接缓冲区)87M大小文件存耗时:" + ChronoUnit.MILLIS.between(start, end) + "毫秒");
}