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),在处理大量并发请求时具有优势,特别是在网络编程和高并发场景下,表现得更为出色。
IO和NIO的区别
Java IO和NIO的主要区别在于两者的处理方式不同。Java IO是面向流(Stream)的,它将输入输出数据直接传输到目标设备或文件中,以流的形式进行读写;而NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
特别是在网络编程和高并发场景下,Java NIO表现得更为出色。Java IO在进行网络通信时,每个客户端连接都需要创建一个线程来进行处理,这样会导致系统资源的浪费。Java NIO则只需要一个线程就可以完成对多个客户端连接的处理,大大减少系统资源的占用。
二、NIO核心原理
主要包括:缓冲区(Buffer)、通道(Channel)和选择器(Selector)、字符集(Charset);首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。 NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。
- 缓冲区Buffer
缓冲区是Java NIO中一个非常重要的概念,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,用于保存读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。
缓冲区可以在内存中创建,并可以通过通道(Channel)进行读写操作,也可以作为参数传递给其他方法。缓冲区在java NIO中负责数据的存取,底层缓冲区其实就是数组,用于存储不同数据类型的数据,根据不同的数据类型(Boolean除外),提供了相应类型的缓冲区:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。
缓冲区的四个核心属性:
capacity:容量,表示缓冲区的最大容量,声明后不能改变
position:表示缓冲区中正在操作数据的位置。当前位置,下一个要被读取或写入的位置;
limit:界限,缓冲区中可以操作数据的大小,表示可以读写的元素数量;
mark:标志,可以让缓冲区记住一个position或limit的值。可以通过调用reset()恢复到mark的位置。
缓冲区的读写操作都会修改position和limit属性,例如在从缓冲区中读取数据时,position属性会自动向后移动,而limit属性则不会更改,因此读取操作只能读取到limit位置之前的数据。
四者的关系:0<mark<=position<=limit<=capacity
缓冲区的三个核心操作方法:
put():存数据到缓存区,写数据模式。
flip():切换到读数据模式(position和limit改变,capacity不变)
get():从缓冲区中读取数据。
通过:static ByteBuffe allocate(int capacity)创建指定大小的缓冲区,在JVM内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JJVM内存开销,处理过程中有复杂的操作。
- 通道Channel
通道(Channel)是Java NIO的核心概念,是网络或文件IO操作的抽象,它表示一个数据通讯的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。在java NIO中Channel本身不负责存储数据,通道可以和缓冲区一起使用,让数据直接在缓冲区之间进行传输。
通道类似于标准IO中的输入输出流,但通道更加灵活和高效。与输入输出流不同的是,通道可以使用Selector选择器实现非阻塞IO操作,并且可以同时进行读写操作。
通道的主要实现类:
FileChannel:用于文件读写操作;
DatagramChannel:用于UDP协议的网络通信;
SocketChannel:用于TCP协议的网络通信;
ServerSocketChannel:用于监听TCP连接请求。
通道的获取方式
java针对支持通道的类提供了getChannel()方法。
支持通道的类如下:
(一)提供本地文件IO的Channel类有:
FileInputStream
FileOutputStream
RandomAccessFile
(二)提供网络套接字IO的Channel类:
DatagramSocket
Socket
ServerSocket
(三)获取通道的其他方式:
在JDK7.0中的AIO针对各个通道提供静态方法open()可打开并返回指定通道;
在JDK7.0中的AIO的Files类可使用Files类的静态方法newByteChannel()获取字节通道。
在使用NIO进行网络编程时,我们常常使用SocketChannel和ServerSocketChannel来实现客户端与服务器之间的通信。使用FileChannel可以完成对本地文件的读写操作,使用DatagramChannel可以发送和接收UDP协议的数据包。
我们来看一个从文件读数据的例程,其中方法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/TestBIO.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;
try{
aFile = new RandomAccessFile("D:/temp/TestNIO.txt","rw");
FileChannel 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();
}catch (IOException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
fileReadBIO();
System.out.println();
fileReadNIO();
}
}
- 选择器Selector和选择键SelectionKey
选择器(Selector)和选择键(SelectionKey)是Java NIO提供的另外两个核心组件。选择器用于检测通道的状态,并且可以根据通道状态进行非阻塞选择操作。而选择键则是一种将通道和选择器进行关联的机制。
使用选择器可以实现单线程管理多个通道的方式,以此实现高并发IO操作。在选择器的模型中,每个通道都会注册到一个选择器上,并且每个通道都有一个其唯一的选择键对象来代表这个通道。选择键对象包含几个标志位,表示通道的当前状态等信息。
选择器可以监听多个通道的事件,例如连接就绪、读取数据就绪、写入数据就绪等等。当有一个或多个通道的事件就绪时,选择器就会自动返回这些通道的选择键,我们可以通过选择键获取到对应的通道,然后进行相应的操作。
选择器是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。
- 字符集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只能被InputStream、OutputStream、RandomAccessFile所创建。
使用fileChannel.transferTo()可极大提高文件的复制效率,为进行大容量文件的读和写,直接把读通道和写通道建立了连接,还能有效避免因文件过大而导致内存溢出。
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)强制将所有对此通道的文件更新写入到存储设备中
这里我们提供一个文件读、写和拷贝复制的例程:
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:
我们以“写文件例程”方法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中读取多少个元素。
关于NIO网络通讯编程的应用请参见博客:
Java 输入与输出之 NIO【非阻塞式IO】【NIO网络编程】探索之【二】
参考文献&博客:
- 参考文献之一
攻破JAVA NIO技术壁垒 - 参考文献之二
Java NIO全面详解(看这篇就够了) - 参考文献之三
Java NIO详解 - 参考文献之四
Java FileChannel文件的读写实例