Java 输入与输出之 NIO【非阻塞式IO】【NIO核心原理】探索之【一】

Java标准的输入/输出(Input/Output,简称I/O)是Java程序与外部世界进行交互的重要机制,它允许程序读取和写入数据到各种类型的源,如文件、网络套接字、管道、内存缓冲区等。Java I/O API主要位于java.io包中,提供了丰富的类和接口来处理不同类型的输入输出操作。
Java 的 I/O 类库位于 java.io 包中,JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
Java标准的输入/输出(I/O)体系一些实现类的层次关系:
在这里插入图片描述

以下是一个简单的 Java I/O 示例,它展示了如何使用 FileInputStream 和 FileOutputStream 读取和写入文件:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class IOExample {
    public static void main(String[] args) {
        try {
            // 创建输入流以读取文件
            FileInputStream fis = new FileInputStream("input.txt");
            // 创建输出流以写入文件
            FileOutputStream fos = new FileOutputStream("output.txt");
 
            int content;
            // 读取并写入文件直到文件末尾
            while ((content = fis.read()) != -1) {
                fos.write(content);
            }
 
            // 关闭流
            fis.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

一、Java I/O发展史
JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
JDK 1.4 引入了一套全新的IO处理机制,引入了缓存区(Buffer)、通道(Channel)等概念,与之前的标准IO(BIO)相比,NIO具有更高的可扩展性和灵活性,特别是在网络编程和高并发场景下,表现得更为出色。这是非阻塞式IO(NIO)模式。提供了更强大的文件处理功能和更高效的IO操作,如内存映射文件等的功能。Java NIO(New I/O)是一种高性能的I/O处理机制,它提供了对标准Java I/O API的替代方案,以支持更高效的文件和网络数据传输。
在JDK 1.7 版本中对NIO进行了完善,推出了NIO.2,也称为AIO(异步IO),在处理大量并发请求时具有优势,特别是在网络编程和高并发场景下,表现得更为出色。
在这里插入图片描述
BIO、NIO和AIO比较
Java的输入输出(I/O)模型,可以分为以下三种:
BIO(Blocking IO)是标准的IO模式,也是最传统的一种IO模型,它采用阻塞模式,在读/写数据如果没有可以读取/写入时会发生阻塞。标准的IO是基于字节流和字符流进行操作的。BIO 的读写是面向流(Stream)的,,流(Stream)是单向的,一次性只能从流(Stream)中读取一个或者多个字节,并且读完之后流(Stream)无法再读取,需要我们自己将数据缓存起来,BIO位于java.io包中。
NIO(New IO)采用非阻塞模式,它通常是基于通道(Channel)和缓冲区(Buffer)的。通道(Channel)是双向的,既可进行读操作又可进行写操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Java NIO引入了选择器的概念,选择器可同时监听多个通道事件(比如:连接打开,数据到达),因此,单个线程可以监听多个数据通道。NIO的IO调用可以是非阻塞的,NIO位于java.nio包中。NIO是面向缓冲区(Buffer)的,它会使用缓存去管理数据,使得读写操作更加快速和灵活。
AIO 就是Asynchronous IO(异步IO)是 NIO 的优化版本。Java 7 引入了 NIO 的改进版 NIO 2,它是异步非阻塞 的 IO 模型。AIO允许异步地进行IO操作,例如:当线程从通道读取数据到缓冲区时,线程还可以进行其他事情。AIO 是基于事件和回调机制实现的,也就是说 AIO 模式不需要selector 操作,而是事件驱动形式,也就是当客户端发送数据之后,会主动通知服务器,接着服务器再进行读写操作。AIO位于java.nio包中。

特别是在网络编程和高并发场景下,Java NIO和AIO表现得更为出色。Java BIO在进行网络通信时,每个客户端连接都需要创建一个线程来进行处理,这样会导致系统资源的浪费。Java NIO则只需要一个线程就可以完成对多个客户端连接的处理,大大减少系统资源的占用。
在这里插入图片描述

二、NIO核心原理
主要包括:缓冲区(Buffer)、通道(Channel)和选择器(Selector)、字符集(Charset);首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。 NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。

  1. 缓冲区Buffer
    缓冲区是Java NIO中一个非常重要的概念,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,用于保存读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。

实际上,在BIO类库中的BufferedInputStream、BufferedOutputStream、BufferedReader和BufferedWriter在其实现中都间接运用了缓冲区。直到java.nio包公开引入了Buffer API,使得Java程序可以直接控制和运用缓冲区。

在Java NIO中,缓冲区的作用用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,利用缓冲区来操作数据能提高处理的效率。
缓冲区可以在内存中创建,并可以通过通道(Channel)进行读写操作,也可以作为参数传递给其他方法。缓冲区负责数据的缓存,底层缓冲区其实就是数组。
在NIO中,根据不同的数据类型(Boolean除外),提供了相应类型的缓冲区,共有八种缓冲区类(其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer):
在这里插入图片描述

缓冲区的四个核心属性:

(1). position属性
表示缓冲区中正在操作数据的位置。它表示当前位置是要读取或写入的下一个元素的索引。在buffer进行读写模式改变时,position的值会进行相应调整
写模式
在刚进入写模式时,position的值为0,表示当前写入位置为从头开始,每当一个数据写入到缓冲区之后,position会向后移动到下一个可写位置,当position的值达到limit时,缓冲区就已经无空间可写了。
读模式
当缓冲区刚进入读模式时,position会被重置为0,每读一个数据,position会向后移动到下一个可读为止,当position的值达到limit时,缓冲区就已经无数据可读了。

(2). limit属性
limit:界限,缓冲区中可以操作数据的大小,表示可以写入或者读取的数据最大上限。
写模式
limit属性值的含义为可以写入数据的最大上限位置,在刚进入写模式时,limit的值会别设置成Buffer的capacity值,表示一直可以将缓冲区内容写满
读模式
limit值的含义为最多能从buffer读取数据的最大上限,flip方法将缓冲区切换到读模式时,limit的值会被设置为上次写模式的position。

(3). mark属性
mark:标记,在缓冲区操作过程中,可以将当前position的值临时存入mark属性中,需要的时候可以通过调用reset()把mark的位置恢复到position属性中

(4). capacity属性
capacity是缓冲区的容量,表示缓冲区的最大容量,声明后不能改变。
一旦写入数据达到了capacity,缓冲区就满了,不能再写入。

Buffer在读模式与写模式下4个属性的关系如下图所示:
在这里插入图片描述

缓冲区的读写操作都会修改position和limit属性,例如在从缓冲区中读取数据时,position属性会自动向后移动,而limit属性则不会更改,因此读取操作只能读取到limit位置之前的数据。

四者的关系:0<mark<=position<=limit<=capacity

缓冲区相关的方法

缓冲区存取数据的两个核心方法:

1)put():当写数据模式时,存入数据到缓冲区

put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

2)get():当读数据模式时,从缓冲区中读取数据

get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)

缓冲区相关的其他方法:
flip():切换操作模式。当将缓冲区从写模式“切换”到读数据模式,重置 position 为 0(position和limit改变,capacity不变)
rewind():将 position 设置为 0,限制保持不变,可以重新读取缓冲区中的数据。
remaining():返回缓冲区中剩余数据的字节数。ByteBuffer.remaining() 方法可确定在当前缓冲区的 position 和 limit 之间还有多少字节可以读取。它表示“从当前位置到缓冲区结束,可以读取的字节数”。
compact():其功能有点像磁盘碎片整理的作用。compact()方法将所有未读的数据拷贝到Buffer起始处,将 position 设置为 0。
clear():清空缓冲区,重置 position 和 limit 为初始位置。缓冲区中原有数据不会被清除,但处于“已遗忘”状态。

缓冲区的创建
ByteBuffer是一个抽象类,所以无法直接使用new关键字来创建对象。创建ByteBuffer对象主要使用ByteBuffer类中的两个静态方法来创建:用ByteBuffer.allocate(int capacity)方法可创建非直接缓冲区;用ByteBuffer.allocateDirect(int capacity)方法创建的则是直接缓冲区。

直接缓冲区和非直接缓冲区:
直接缓冲区可以通过调用工厂方法 allocateDirect()来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区还可以通过 FileChannel 的 map()方法将文件区域直接映射到操作系统的系统内存中来创建。该方法返回MappedByteBuffer 。
非直接缓冲区,是在JVM内存中创建的,缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复杂的操作。
缓冲区创建样例代码:

import java.nio.ByteBuffer; //导入包
public class ByteBufferTest {
    public static void main(String ... args) {
 
        //非直接缓冲区:使用allocate创建缓冲区,
        //其内存分配由jvm负责,受到jvm堆内存机制的影响。
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
 
        //直接缓冲区:使用allocateDirect方法创建
        //是在操作系统的本地内存中进行的,不受 Java 堆内存管理机制的影响
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);
    }
}

缓冲区可以存储多条数据,并且逐条读出后显示输出。
一个基本数据类型的数据有固定的大小,所以ByteBuffer提供了相关写入和读取 方法。
int 类型占用 4 个字节
long 类型占用 8 个字节
float 类型占用 4 个字节
double 类型占用 8 个字节
在下面的例程中我们用基本数据类型来演示一条定长的记录,记录格式:长整型+整型+长整型。每条记录的长度是20个字节。
缓冲区的用法示例:

package nio;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ByteBufferExample {
	/***用基本数据类型测试***/
	public static void primitiveDataType() {
	    ByteBuffer buffer = ByteBuffer.allocate(1024);
	    //第一条数据
	    buffer.putLong(5288L);
	    buffer.putInt(1234);
	    buffer.putLong(6789L);
	    //第二条数据
	    buffer.putLong(45678L);
	    buffer.putInt(1235);
	    buffer.putLong(98765L);
	    
	    buffer.flip(); //切换读模式
	    while (buffer.hasRemaining()){
	        //偏移量
	        long offset = buffer.getLong();
	        //msg大小
	        int msgSize = buffer.getInt();
	        //标签大小
	        long tagsSize = buffer.getLong();
	        //拼接一条完整的数据
	        String msgIndex = offset + "\t" + msgSize + "\t" + tagsSize;
	        System.out.println("信息:" + msgIndex);
	    }
	}
	/***用文本数据类型测试***/
	public static void textDataType() {
	    String text = "时光不老,岁月静好。";
	    String text2 = "陌上花开,可缓缓归矣。";
	    // 创建一个 ByteBuffer
	    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
	    // 将字符串转换为字节数组并逐个字节写入
	    byteBuffer.put(text.getBytes(StandardCharsets.UTF_8));
	    byteBuffer.put("\n".getBytes(StandardCharsets.UTF_8));
	    byteBuffer.put(text2.getBytes(StandardCharsets.UTF_8));
	    // 准备读取
	    byteBuffer.flip(); //切换
	    // 读取并输出 ByteBuffer 中的字节
	    byte[] outputArray = new byte[byteBuffer.remaining()];
	    byteBuffer.get(outputArray);
	    String outputText = new String(outputArray, StandardCharsets.UTF_8);
	    System.out.println("存储并检索文本: \n" + outputText);
	}
	
	public static void main(String[] args) {
		ByteBufferExample.primitiveDataType();
		ByteBufferExample.textDataType();
	}
}

测试结果图:
在这里插入图片描述

  1. 通道Channel
    通道(Channel)是Java NIO的核心概念,是网络或文件IO操作的抽象,它表示一个数据通讯的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。在java NIO中Channel本身不负责存储数据,通道可以和缓冲区一起使用,让数据直接在缓冲区之间进行传输。

在这里插入图片描述

通道(Channel)可以对应到BIO中的流(Stream)。Channel与Stream的区别是:Stream是单向的阻塞式的,Channel是双向的,可以使用Selector选择器实现非阻塞IO操作。在使用流(Stream)的时候有InputStream,OutputStream等都只支持单向操作;而Channel是双向的,它即可用来进行读操作,又可以进行写操作。

通道的主要实现类:
FileChannel:是一个连接到文件的通道,用于文件读写操作。FileChannel不支持选择器Selector;无法设置为非阻塞模式,它总是运行在阻塞模式下。
DatagramChannel:用于UDP协议的网络通信;
SocketChannel:用于TCP协议的网络通信;
ServerSocketChannel:用于网络通信监听TCP连接请求。

三个与网络通讯相关的通道实现类都继承自“可选择通道”SelectableChannel超类,都可与Selector组合使用。Selector只支持SelectableChannel通道的实现类;FileChannel不是SelectableChannel通道的实现类,Selector不支持。

通道的获取方式
java针对支持通道的类提供了getChannel()方法。
支持通道的类如下:
(一)提供本地文件IO的Channel类有(FileChannel专用):
FileInputStream
FileOutputStream
RandomAccessFile
(二)提供网络套接字IO的Channel类:
DatagramSocket
Socket
ServerSocket
(三)获取通道的其他方式:
在JDK7.0中的AIO针对各个通道提供静态方法open()可打开并返回指定通道;
在JDK7.0中的AIO的Files类可使用Files类的静态方法newByteChannel()获取字节通道。

在使用NIO进行网络编程时,我们常常使用SocketChannel和ServerSocketChannel以TCP/IP协议来实现客户端与服务器之间的通信。
当使用DatagramChannel来实现客户端与服务器之间的通信时,可以发送和接收UDP协议的数据包。Java NIO 中的 DatagramChannel 是一个能收发 UDP 包的通道。因为 UDP 是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

  1. 选择器Selector和选择键SelectionKey
    选择器(Selector)和选择键(SelectionKey)是Java NIO提供的另外两个核心组件。
    选择器(Selector)提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。单个的线程可以监听多个数据通道。
    在这里插入图片描述

Selector是SelectableChannel的选择器,又称为多路复用器,一般用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。

选择器(Selector)用于检测通道的状态,并且可以根据通道状态进行非阻塞选择操作。而选择键(SelectionKey)则是一种将通道和选择器(Selector)进行关联的机制。

使用选择器(Selector)可以实现单线程管理多个通道的方式,以此实现高并发IO操作。在选择器的模型中,每个通道都会注册到一个选择器上,并且每个通道都有一个唯一的选择键对象来代表这个通道。选择键对象包含几个标志位,表示通道的当前状态等信息。
在这里插入图片描述

选择器(Selector)可以监听多个通道的事件,例如连接就绪、读取数据就绪、写入数据就绪等等。当有一个或多个通道的事件就绪时,选择器就会自动返回这些通道的选择键,我们可以通过选择键获取到对应的通道,然后进行相应的操作。

选择器(Selector)是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。

  1. 字符集Charset(编码解码)
    编码(按指定Charset编码方案编码)
    字符串转成字节数组
    解码(按指定Charset编码方案解码)
    字节数组转成字符串

请看一个编码和解码的演示例程:

package nio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Map;
import java.util.Set;

/**
 * 字符集(Charset)
 * 编码:字符串-->字节数组
 * 解码:字节数组-->字符串
 */

public class CharsetDemo {
	static String infStr = "绿水青山就是金山银山!";
    public static void charSetEncoderAndDecoder() throws CharacterCodingException {
        Charset charset=Charset.forName("UTF-8");
        //1.获取编码器
        CharsetEncoder charsetEncoder=charset.newEncoder();
        //2.获取解码器
        CharsetDecoder charsetDecoder=charset.newDecoder();

        //3.获取需要解码编码的数据
        CharBuffer charBuffer=CharBuffer.allocate(1024);
        charBuffer.put(infStr);
        charBuffer.flip();

        //4.编码
        ByteBuffer byteBuffer=charsetEncoder.encode(charBuffer);
        System.out.println("编码后:");
        for (int i=0;i<byteBuffer.limit();i++) {
            System.out.println(byteBuffer.get());
        }
        //5.解码
        byteBuffer.flip();
        CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);
        System.out.println("\n解码后:");
        System.out.println(charBuffer1.toString());
        System.out.println("\n使用不正确的编码格式解码,解码结果:");
        Charset charset1=Charset.forName("GBK");
        byteBuffer.flip();
        CharBuffer charBuffer2 =charset1.decode(byteBuffer);
        System.out.println(charBuffer2.toString());
    }
    /***查询系统可用的字符编码***/
    public static void getAvailableCharsets() {
        //6.获取Charset所支持的字符编码
        System.out.println("\n系统可用的字符编码:");
         Map<String ,Charset> map= Charset.availableCharsets();
         Set<Map.Entry<String,Charset>>  set=map.entrySet();
        for (Map.Entry<String,Charset> entry: set
             ) {
            System.out.println(entry.getKey()+"="+entry.getValue().toString());
        }
    }
    
    public static void main(String[] args) throws IOException {
    	/****字符集编码和解码演示****/
    	charSetEncoderAndDecoder();
    	/***查询系统可用的字符编码***/
    	getAvailableCharsets();
	}
}

一、通道的主要实现类文件通道FileChannel介绍:

FileChannel:用于文件读写操作;它是用于读取和写入文件的通道。

FileChannel的常用方法:
int read(ByteBuffer dst) 从Channel当中读取数据至ByteBuffer
long read(ByteBuffer[] dsts)将channel当中的数据“分散”至ByteBuffer[]
int write(Bytesuffer src)将ByteBuffer当中的数据写入到Channel
long write(ByteBuffer[] srcs)将Bytesuffer[]当中的数据“聚集”到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中
long transferTo() 可极大提高文件的复制效率,为进行大容量文件的读和写,直接把读通道和写通道建立了连接。

使用FileChannel可以完成对本地文件的读写操作。
打开FileChannel的方式:
在使用FileChannel读写文件之前,必须先打开FileChannel。但是,我们无法直接打开一个FileChannel,可以使用FileChannel.open()方法来打开一个通道;也可以通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。

从FileChannel读取数据的示例代码

    	RandomAccessFile file = new RandomAccessFile("data/nio-data.txt", "rw");
    	FileChannel inChannel = file.getChannel(); //打开通道
    	 
    	ByteBuffer buf = ByteBuffer.allocate(48); //分配缓冲区

    	int bytesR = inChannel.read(buf); //从Channel读取数据,保存在缓冲区

向FileChannel写入数据的示例代码

    	String newData = "写入文件的测试字符串..." + System.currentTimeMillis();
    	FileChannel outC = FileChannel.open(Paths.get("data/wData.txt"), WRITE, CREATE); //打开通道
    	ByteBuffer buf2 = ByteBuffer.allocate(48); //分配缓冲区

    	buf2.clear();

    	buf2.put(newData.getBytes()); //把字符串放入缓冲区
    	buf2.flip(); //切换
    	while(buf2.hasRemaining()) { //循环调用,防止未完全写入
    		outC.write(buf2);  //向通道写入数据
    	}

注意: FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

关闭FileChannel
用完FileChannel后必须将其关闭。例如: channel.close();

文件读写演示例程(BIO方式与NIO对比)
下面的例程演示使用RandomAccessFile来获取一个FileChannel实例。
我们来看一个从文件读数据的例程,其中方法fileReadBIO()是用BIO的输入流方式读文件信息;方法fileReadNIO()使用NIO的FileChannel和字节缓冲区ByteBuffer来读文件信息。

package nio;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileReadTest {
    public static void fileReadBIO(){
        InputStream in = null;
        System.out.println("BIO模式读文件测试***");
        try{
        	String path = "D:/temp/test00.txt";
            in = new BufferedInputStream(new FileInputStream(path));
 
            byte [] buf = new byte[1024];
            int bytesRead = in.read(buf);
            while(bytesRead != -1)
            {
                for(int i=0;i<bytesRead;i++)
                    System.out.print((char)buf[i]);
                bytesRead = in.read(buf);
            }
        }catch (IOException e)
        {
            e.printStackTrace();
        }finally{
            try{
                if(in != null){
                    in.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    
    public static void fileReadNIO(){
    	System.out.println("NIO模式读文件测试***");
        RandomAccessFile aFile = null;
        FileChannel fileChannel = null;
        try{
            aFile = new RandomAccessFile("D:/temp/test00.txt","rw");
            fileChannel = aFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
 
            int bytesRead = fileChannel.read(buf);
            System.out.println(bytesRead);
            while(bytesRead != -1)
            {
                buf.flip();
                while(buf.hasRemaining())
                {
                    System.out.print((char)buf.get());
                }
 
                buf.compact();
                bytesRead = fileChannel.read(buf);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile != null) aFile.close();
                if(fileChannel != null) fileChannel.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
    	fileReadBIO();
    	System.out.println();
    	fileReadNIO();
	}
    
}

使用fileChannel.transferTo()可极大提高文件的复制效率,为进行大容量文件的读和写,直接把读通道和写通道建立了连接,还能有效避免因文件过大而导致内存溢出。此时通道的作用非常类似于流(Stream),此时甚至不需要借助于缓冲区(Buffer)。
下面是一个文件读、写和拷贝复制的例程:

package nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
    public static void readFile(){ //读文件例程
        try {
            //1.定义一个文件字节输入流与源文件接通
            FileInputStream fos = new FileInputStream(new File("D:/temp/test01.txt"));
            //2.获取文件字节输入流的文件通道
            FileChannel channel = fos.getChannel();
            //3.定义一个缓存区
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int bytesRead = channel.read(buf); //4.从通道读取数据写入缓存区
            String str = null;
            while(bytesRead != -1) {
                //5、切换为读数据
                buf.flip();
                //6.读取缓存区中的数据并输出即可
                str = new String(buf.array(), 0, buf.remaining());
                System.out.println("读取内容..." + str);
                bytesRead = channel.read(buf); //4.从通道读取数据写入缓存区
            }
            channel.close();    
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void writeFile(){ //写文件例程
        try {
            //1.字节输出流通向目标文件
            FileOutputStream fos = new FileOutputStream(new File("D:/temp/test01.txt"));
            //2.得到字节输出流对应的通道Channel
            FileChannel channel = fos.getChannel();
            //3.分配缓存区
            ByteBuffer bf = ByteBuffer.allocate(1024);
            bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());
            //4.把缓存区切换为写模式
            bf.flip();
            //5.输出数据到文件
            channel.write(bf);
            channel.close();
            System.out.println("完成数据写入....");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void copyFile(){ //复制文件例程
        try {
            long starTime = System.currentTimeMillis();
            //1、创建输入文件流
            FileInputStream fis = new FileInputStream(new File("D:/temp/test01.txt"));
            //2、得到输入channel
            FileChannel fisChannel = fis.getChannel();
            //3、创建输出文件流
            FileOutputStream fos = new FileOutputStream(new File("D:/temp/test02.txt"));
            //4、得到输出channel
            FileChannel fosChannel = fos.getChannel();
            //5、使用输入channel将文件转到fosChannel
            fisChannel.transferTo(0, fisChannel.size(), fosChannel);
            fis.close();
            fos.close();
            fisChannel.close();
            fosChannel.close();
            long endTime = System.currentTimeMillis();
            System.out.println("耗时=" + (endTime - starTime) + "ms");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

	public static void main(String[] args) throws IOException {
		writeFile(); 
		System.out.println("从文件读信息:");
		readFile();
	}
}

例程说明:
方法readFile()演示如何从文件读入数据信息;
方法writeFile()演示如何把数据信息写入磁盘文件中。
方法copyFile()则是一个文件复制演示程序。
由于例程中没有显式指定数据信息的字符集编码方案,如果读入一个其他文本编辑器编辑的文本文件,显示出来也可能是乱码的。
因此,本例程中最后测试时,先调用writeFile(); 然后再调用readFile();,这样可确保读写测试都使用默认的字符集编码方案。由于写入的文本信息太少,第一次无法测试到循环读信息的效果。可在第一次测试后,再用文本编辑器随意增加足够文本信息,再进行测试,才可测试到循环读信息。

二、缓冲区Buffer的用法分解说明:
Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。通道Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须在缓冲区Buffer进行缓存。
可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态:capacity, position, limit, mark:

在这里插入图片描述

从FileChannel读取数据

我们以“写文件例程”方法writeFile()为例来进行说明。我们来分析这几行源代码:

            //3.分配缓存区
            ByteBuffer bf = ByteBuffer.allocate(1024);
            bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());
            //4.把缓存区切换为写模式
            bf.flip();
            //5.输出数据到文件
            channel.write(bf);
            channel.close();

//3.分配缓存区
ByteBuffer bf = ByteBuffer.allocate(1024);
当执行完上面这行代码后,程序分配了缓冲区Buffer,此时Buffer的初始状态如下图,position的位置为0,capacity和limit默认都是数组长度。
在这里插入图片描述
bf.put(“最近公司有个需求,就是上传产品详情图。” .getBytes());
执行完上面这行代码,缓冲区Buffer写入了数据后,position的位置移到写入数据的后面,缓冲区的状态如下:
在这里插入图片描述
//4.把缓冲区切换为写模式
bf.flip();
执行完上面这行代码flip()后,缓冲区的position的位置移到0,limit则移到了原来position的位置。
在这里插入图片描述
在下一次再往Buffer写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
调用clear()方法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”。
如果Buffer中仍有未读的数据,且后续还需要这些数据,那么可使用compact()方法,其功能有点像磁盘碎片整理的作用。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position,之后可以通过调用Buffer.reset()方法恢复到这个position。
Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。

关于Java 输入与输出之 NIO的其他博客:
Java 输入与输出之 NIO【非阻塞式IO】【NIO网络编程】探索之【二】
Java 输入与输出之 NIO.2【AIO】【Path、Paths、Files】【walkFileTree接口】探索之【三】

NIO 的应用场景:

网络通信:NIO 可以用于开发高并发的网络应用,例如 Web 服务器、游戏服务器等。
文件操作:NIO 可以用于开发高性能的文件操作应用,例如文件传输、文件压缩等。
进程间通信:NIO 可以用于实现进程间通信,例如共享内存、管道等。
数据库操作:NIO 可以用于提高数据库操作的性能,例如批量插入、批量查询等。

NIO 的优缺点
1、NIO 的优势
NIO 相对于传统 IO 具有以下优势:
提高并发性:NIO 可以使用多路复用器来监听多个通道的事件,提高并发性。
提高性能:NIO 支持非阻塞 IO,可以提高性能。
简化编程:NIO 的 API 更加简洁,易于理解和使用。

2、NIO 的缺点
NIO 相对于传统 IO 具有以下缺点:
学习成本较高:由于NIO固有的复杂性,NIO 的概念和 API 与传统 IO 不同,学习成本较高。
调试困难:因为多路利用导致调试的复杂性。
不兼容性:NIO 与传统 IO 存在不兼容性,需要注意兼容性问题。

NIO性能优势局限:
Java NIO 在高并发和高吞吐量的场景下可以提供性能优势,但对于许多常规应用程序而言,传统的阻塞式I/O 已经足够了。只有需要处理大量并发连接或需要高度定制化的网络通信时,Java NIO 才会显得更有价值。由于上述的各种原因NIO的推广不及预期。

参考文献&博客:

  1. 参考文献之一
    攻破JAVA NIO技术壁垒
  2. 参考文献之二
    Java NIO全面详解(看这篇就够了)
  3. 参考文献之三
    Java NIO详解
  4. 参考文献之四
    Java FileChannel文件的读写实例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值