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的学习曲线相对较陡峭,需要掌握一定的底层知识和技能。
/