Java中的IO(输入输出)是用于在程序中读取和写入数据的一种机制。Java提供了两种不同的IO模型:传统的IO模型和NIO(New IO)模型。
1. 传统IO模型
在传统的IO模型中,输入和输出是通过字节流或字符流进行处理的。字节流是以8位字节为单位读写数据,而字符流则是以16位字符为单位读写数据。常见的字节流包括InputStream和OutputStream,而常见的字符流包括Reader和Writer。
另外,在传统的IO模型中,还有缓冲流,它们可以提高IO的效率。BufferedInputStream和BufferedOutputStream是字节流的缓冲流,而BufferedReader和BufferedWriter则是字符流的缓冲流。
传统IO模型的优点在于它简单易用,而缺点则是它的效率较低,特别是在处理大量数据时。
以下是一个简单的使用传统IO模型进行文件读写的Java代码示例:
import java.io.*;
public class TraditionalIOExample {
public static void main(String[] args) {
File inputFile = new File("input.txt");
File outputFile = new File("output.txt");
try {
// 使用字节流读取文件
FileInputStream fis = new FileInputStream(inputFile);
// 使用字节流写入文件
FileOutputStream fos = new FileOutputStream(outputFile);
// 使用缓冲流提高效率
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
// 读取数据并写入文件
int len;
byte[] buffer = new byte[1024];
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
// 关闭流
bos.close();
bis.close();
fos.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,我们首先创建了一个File
对象,指定了输入文件和输出文件。然后使用FileInputStream
和FileOutputStream
来创建字节流,对文件进行读写操作。为了提高效率,我们还使用了缓冲流BufferedInputStream
和BufferedOutputStream
,将其包装在字节流之上。
接下来,在while循环中,我们不断从输入文件中读取数据,并将其写入到输出文件中。最后,我们关闭所有的流以释放资源。如果在读写过程中发生异常,我们就在catch块中捕获并打印异常信息。
这是一个简单的使用传统IO模型进行文件读写的例子,但实际上,传统IO模型还可以用于网络编程、对象序列化等方面。
字节流
字节流是Java IO中的一种流,它以字节为单位进行读写操作,用于处理二进制数据,如图像、音频等。字节流主要有两种类型:InputStream和OutputStream,分别用于从输入流中读取字节和向输出流中写入字节。
InputStream是所有字节输入流的超类,它定义了读取字节的基本方法,如read()、read(byte[] b)、read(byte[] b, int off, int len)等。其中,read()方法每次读取一个字节,返回一个整数表示实际读取的字节数,如果已经读到流的末尾,则返回-1。read(byte[] b)方法会尝试从输入流中读取b.length个字节,并将其存储在字节数组b中,返回值为实际读取的字节数。read(byte[] b, int off, int len)方法与read(byte[] b)类似,只不过指定了起始偏移量off和读取长度len。
OutputStream是所有字节输出流的超类,它定义了写入字节的基本方法,如write(int b)、write(byte[] b)、write(byte[] b, int off, int len)等。其中,write(int b)方法会将指定的字节写入输出流,write(byte[] b)方法会将字节数组b中的所有字节写入输出流,write(byte[] b, int off, int len)方法则只写入数组b中从偏移量off开始的len个字节。
除了基本的读写方法,字节流还提供了一些其他的常用方法。例如,InputStream中的available()方法返回可以从输入流中读取的估计字节数,mark(int readlimit)方法在输入流中标记当前位置,reset()方法将输入流重新定位到最后一次标记的位置,skip(long n)方法跳过指定的字节数等。而OutputStream中也有类似的方法,如flush()方法将输出流缓冲区的内容强制刷新到目标设备上,close()方法关闭流等。
下面是一个简单的示例代码,演示如何使用字节流进行文件读写操作:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamDemo {
public static void main(String[] args) throws IOException {
// 从文件中读取数据,写入到另一个文件中
FileInputStream fis = new FileInputStream("source.bin");
FileOutputStream fos = new FileOutputStream("dest.bin");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
fos.close();
}
}
上述代码通过创建FileInputStream和FileOutputStream来实现文件的读写操作,使用read()方法从源文件中读取字节,并使用write()方法将字节写入目标文件中。注意,在使用完流之后,需要及时调用close()方法关闭流。
字符流
字符流是Java IO中的一种流,它以字符为单位进行读写操作,用于处理文本数据,如文本文件、XML等。Java字符流主要有两种类型:Reader和Writer,分别用于从输入流中读取字符和向输出流中写入字符。
Reader是所有字符输入流的超类,它定义了读取字符的基本方法,如read()、read(char[] cbuf)、read(char[] cbuf, int off, int len)等。其中,read()方法每次读取一个字符,返回一个整数表示实际读取的字符数,如果已经读到末尾,则返回-1;read(char[] cbuf)方法会尝试从输入流中读取cbuf.length个字符,并将其存储在字符数组cbuf中,返回值为实际读取的字符数;read(char[] cbuf, int off, int len)方法与read(char[] cbuf)类似,只不过指定了起始偏移量off和读取长度len。
Writer是所有字符输出流的超类,它定义了写入字符的基本方法,如write(int c)、write(char[] cbuf)、write(String str)等。其中,write(int c)方法会将指定的字符写入输出流;write(char[] cbuf)方法会将字符数组cbuf中的所有字符写入输出流;write(String str)方法会将字符串str中的所有字符写入输出流。
除了基本的读写方法,字符流也提供了一些其他的常用方法。例如,Reader中的skip(long n)方法跳过n个字符,mark(int readAheadLimit)方法在输入流中标记当前位置,reset()方法将输入流重新定位到最后一次标记的位置等;而Writer中的flush()方法将输出流缓冲区的内容强制刷新到目标设备上,close()方法关闭流等。
下面是一个简单的示例代码,演示如何使用字符流进行文件读写操作:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CharacterStreamDemo {
public static void main(String[] args) throws IOException {
// 从文件中读取数据,写入到另一个文件中
FileReader fr = new FileReader("source.txt");
FileWriter fw = new FileWriter("dest.txt");
char[] buffer = new char[1024];
int len;
while ((len = fr.read(buffer)) != -1) {
fw.write(buffer, 0, len);
}
fr.close();
fw.close();
}
}
上述代码通过创建FileReader和FileWriter来实现文件的读写操作,使用read()方法从源文件中读取字符,并使用write()方法将字符写入目标文件中。注意,在使用完流之后,需要及时调用close()方法关闭流。
2. NIO模型
NIO是Java 1.4引入的新IO模型,它的目标是提高IO的效率,特别是在处理大量数据时。NIO的设计中引入了三个重要的概念:通道、缓冲区和选择器。
通道是NIO中的抽象概念,它类似于传统IO模型中的流。但是,通道可以同时进行读写操作,并且可以使用选择器来实现多路复用,从而提高效率。
缓冲区是NIO中的另一个重要概念,它用于存储读取或写入的数据。与传统IO模型不同的是,NIO中的缓冲区可以直接与通道交互,从而避免了频繁地进行字节或字符的拷贝操作,提高了效率。
选择器是NIO中的另一个重要概念,它可以监听多个通道上的事件并在有事件发生时及时地处理它们。这样,一个线程就可以同时处理多个通道上的IO操作,从而避免了线程阻塞等问题,提高了系统的效率和可扩展性。
下面是一个基于NIO模型的简单示例代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioServer {
private Selector selector;
public void init() throws IOException {
// 创建一个选择器
selector = Selector.open();
// 创建一个ServerSocketChannel并绑定到指定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false);
// 将ServerSocketChannel注册到选择器上,监听ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() throws IOException {
while (true) {
// 阻塞等待就绪的Channel
selector.select();
// 获取所有已就绪的Channel
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接请求
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println("接受新的连接:" + socketChannel.getRemoteAddress());
// 将新连接的SocketChannel注册到选择器上,监听READ事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读取数据
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String content = new String(bytes, "UTF-8");
System.out.println("接收到客户端消息:" + content);
// 回写数据
buffer.clear();
buffer.put("Hello client!".getBytes());
buffer.flip();
socketChannel.write(buffer);
} else if (readBytes < 0) {
// 连接关闭
key.cancel();
socketChannel.close();
}
}
// 移除当前已处理的事件
keyIterator.remove();
}
}
}
public static void main(String[] args) throws IOException {
NioServer server = new NioServer();
server.init();
server.start();
}
}
上述代码中,首先创建了一个选择器(Selector)对象。然后创建了一个ServerSocketChannel并将其注册到选择器上,监听ACCEPT事件。接着进入主循环,调用select()方法阻塞等待就绪的Channel,然后使用selectedKeys()方法获取所有已就绪的Channel,遍历每个已就绪的Channel,根据其状态进行相应的处理。如果是新连接请求,就接受连接并将新的SocketChannel注册到选择器上,监听READ事件;如果是数据读取请求,就读取数据并回写数据。
需要注意的是,在NIO模型中,数据的读写操作是通过缓冲区(Buffer)对象完成的,所以需要在代码中使用ByteBuffer等缓冲区对象来处理数据。另外,NIO模型还需要特别注意内存泄漏的问题,因为它很容易发生,需要及时释放资源和取消注册。
总的来说,NIO比传统的IO模型更适合处理大量数据时的IO操作,并且具有更好的效率和可扩展性。但是,NIO的学习曲线相对较陡峭,需要掌握一定的底层知识和技能。