Java IO
IO
IO 是指 Input/Output,即输入和输出。以内存为中心:
- Input 只从外部读入数据到内存。例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
- Output 指把数据从内存输出到外部。例如,把数据从内存写入文件,把数据从内存输出到网络等等。
为什么要把数据读到内存才能处理数据?
因为代码是在内存中运行的,数据也必须读到内存。
InputStream/OutputStream
IO 流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为 IO 流。
IO 流以 byte为最小单位,因此也称为 字节流。例如,我们要从磁盘读入一个文件,包含 6 个字节,就相当于读入了 6 个字节的数据。
在 Java 中,InputStream代表输入字节流,OutputStream代表输出字节流,这是最基本的两种 IO 流。
Reader/Writer
如果我们需要读写的是字符,并且字符不全是单字节表示的 ASCII 字符,那么,按照 char来读写显然更方便,这种流称为 字符流。
Java 提供了 Reader和 Writer表示字符流,字符流传输的最小数据单位是 char。
究竟使用 Reader 还是 InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用 InputStream,如果数据源是文本,使用 Reader 更方便一些。
同步和异步
同步 IO 是指,读写 IO 时代码必须等待数据返回后才继续执行后续代码。
- 优点是代码编写简单
- 缺点是 CPU 执行效率低
异步 IO 是指,读写 IO 时仅发出请求,然后立即执行后续代码。 - 优点是 CPU 执行效率高
- 缺点是代码编写复杂
Java 标准库的包 java.io提供了同步 IO,而 java.nio提供了异步 IO。
上面讨论的 InputStream,OutputStream,Reader和 Writer都是同步 IO 的抽象类,对应的具体实现类,以文件为例,有 FileInputStream,FileOutputStream,FileReader 和 FileWriter。
File
在计算机系统中,文件是非常重要的存储方式。
Java 标准库 java.io提供了 File对象来操纵文件和目录。
构造一个 File 对象
File f = new File("index.html");
System.out.println(f);
文件和目录
File 对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个 File 对象,即使传入的文件或目录不存在,代码也不会出错。因为构造一个 File 对象,并不会导致任何磁盘操作。只有当我们调用 File 对象的某些方法时,才真正进行磁盘操作。
关于 isFile() 或 isDirectory() 方法始终返回 false问题,检查文件路径是否正确,读取路径与实际文件路径不一致,导致读取不到,因此返回 false。
InputStream
InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。
FileInputStream是 InputStream的一个子类。从文件流中读取数据。
package com.chen.file;
import java.io.*;
public class TestFile {
public static void main(String[] args) throws IOException {
InputStream input = null;
try {
input = new FileInputStream("index.txt");
while (true) {
int n = input.read();
if (n == -1) {
break;
}
System.out.println(n);
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (input != null) {
input.close();
}
}
}
}
注意:在计算机中,类似文件,网络端口这些资源,都是由操作系统统一管理。应用程序在运行过程中,如果打开了一个文件进行读写,完成后要及时关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其它应用程序的运行。
InputStream和 OutputStream都是通过 close()方法来关闭流。关闭流就会释放对应的底层资源。
缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
- int read(byte[] b)读取若干字节并填充到 byte[]数组,返回读取的字节数。
- int read(byte[] b,int off,int len):指定 byte[]数组的偏移量和最大填充数。
利用上述方法一次读取多个字节时,需要先定义一个 byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区,但不会超过缓冲区的大小。read()方法的返回值不再是字节的 int值,而是返回实际读取了多少字节。如果返回 -1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
package com.chen.file;
import java.io.*;
public class TestFile {
public static void main(String[] args) throws IOException {
InputStream input = null;
try {
input = new FileInputStream("index.txt");
//定义 1000 字节的一个缓冲区
byte[] buffer = new byte[1000];
int n;
while (true) {
n = input.read(buffer);
if (n == -1) {
break;
}
System.out.println(n);
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (input != null) {
input.close();
}
}
}
}
阻塞
在调用 InputStream的 read()方法读取数据时,我们说 read()方法是阻塞的。它的意思是,对于下面的代码:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
执行到第二行代码时,必须等 read()方法返回后才能继续。因为读取 IO 流相比执行普通代码,速度会慢很多,因此,无法确定 read()方法调用到底要花费多长时间。
OutputStream
OutputStream是 Java 标准库提供的最基本的输出流。
OutputStream也提供了 close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个 flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有 flush()?
因为向磁盘,网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立即写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个 byte[]数组),等到缓冲区满了,再一次性写入文件或者网络。
对于很多 IO 设备来说,一次写一个字节和一次写 1000 个字节,花费的时间几乎是完全一样的,所有 OutputStream有个 flush()方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个 flush()方法,因为缓冲区写满了会自动调用它,并且,在关闭资源之前,也会调用一次。
Reader
Reader是 Java 的 IO 库提供的另一个输入流接口。和 InputStream的区别是,InputStream是一个字节流,即以 byte为单位读取,而 Reader是一个字符流,即以 char为单位读取。
java.io.Reader是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
这个方法读取字符流的下一个字符,并返回字符表示的 int,范围是 0 ~ 65535。如果已读到末尾,返回 -1。
FileReader
FileReader是 Reader的一个子类,它可以打开文件并获取 Reader。
package com.chen.file;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class TestFile {
public static void main(String[] args) throws IOException {
Reader reader = new FileReader("./index.txt", StandardCharsets.UTF_8);//避免乱码
for (;;) {
int n = reader.read();
if (n == -1) {
break;
}
System.out.println((char)n); //打印 char
}
reader.close();
}
}
Writer
Reader是带编码转换器的 InputStream,它把 byte转换为 char,而 Writer就是带编码转换器的 OutputStream,它把 char转换为 byte并输出。
Writer是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):void write(int c);
- 写入字符数组的所有字符:void write(char[] c);
- 写入 String 表示的所有字符:void write(String s);
FileWriter
FileWriter就是向文件中写入字符流的 Writer。它的使用方法和 FileReader类似:
package com.chen.file;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class TestFile {
public static void main(String[] args) throws IOException {
try (Writer writer = new FileWriter("index.txt", StandardCharsets.UTF_8)) {
writer.write('H'); //写入单个字符
writer.write('\n');
writer.write("Hello\n".toCharArray()); //写入 char[]
writer.write("hello 你好,世界"); //写入 String
}
}
}
使用 Files
从 Java 7 开始,提供了 Files这个工具类,能极大地方便我们读写文件。
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));
// 写入二进制文件:
byte[] data = ...
Files.write(Path.of("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Path.of("/path/to/file.txt"), lines);
特别注意:Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个 G 的大文件。读写大文件仍然需要使用文件流,每次只读写一部分文件内容。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?