目录
2.1抽象基类:InputStream和OutputStream
2.2文件字节流:FileInputStream和FileOutputStream
一、流的基本概念:数据的水管
在计算机中,流(Stream)被定义为数据在时间上和空间上的连续传输序列。可以把它想象成一根水管:
输入流:数据从外部源流进程序,就像水从水龙头流入水杯。
输出流:数据从程序流向外部,就像水杯里的水倒进水槽。
Java将所有的I/O抽象为流,无论数据源是文件、内存、网络还是其他设备,开发者都可以使用统一的API进行读写。
1.1流的分类
按传输的单位:
字节流:以字节为单位,适合处理任何类型的数据(图像、音频、视频、文本等)。
字符流:以字符为单位,专门处理文本数据,支持字符集编码。
按功能角色:
节点流:直接连接数据源
处理流:对节点流进行包装,增加额外功能
二、字节流:一切数据的基石
2.1抽象基类:InputStream和OutputStream
java.io.InputStream是所有字节输入流的父类,定义了最基本的读取方法
|----------------------------------------|----------------------------------------|
| 方法 | 说明 |
| int read() | 读取一个字节(0~255),若到到达末尾返回-1 |
| int read(byte\[\] b) | 读取最多b.length个字节放入数组,返回实际读取的字节数; 末尾返回-1 |
| int read(byte\[\] b, int off ,int len) | 读取最多len个字节放入b的off起始位置 |
| void close | 释放底层资源,必须调用 |
java.io.OutputStream定义了写入方法:
|-------------------------------------------|-------------------|
| 方法 | 说明 |
| void write(int b) | 写入一个字节(int 的低8位) |
| void write(byte\[\] b) | 写入整个字节数组 |
| void write(byte\[\] b, int off , int len) | 写入数组从off开始的len个字节 |
| void flush() | 强制将缓冲区的数据写入底层设备 |
| void close() | 释放资源(通常先flush) |
当你调用write时,数据往往没有真正写在硬盘或网络上,而是被放在了缓冲区,只有缓冲区满了或者流被关闭了,数据才会真正被flush到目标设备上。
出于防御型编程,建议在close前显式调用一次flush
2.2文件字节流:FileInputStream和FileOutputStream
这两个类直接连接到文件,是典型的节点流。
构造方法
java
// 从 File 对象或路径字符串构造
FileInputStream fis = new FileInputStream("data.dat");
FileOutputStream fos = new FileOutputStream("out.dat");
// 第二个参数表示是否追加,默认 false 覆盖
FileOutputStream fosAppend = new FileOutputStream("out.dat", true);
2.3高效读写:使用缓冲区
单字节读写效率极低(每次调用操作系统底层),通常我们使用固定大小的字节数组作为缓冲区,批量读写。
读取模板:
java
try (InputStream is = new FileInputStream("source.dat")) {
byte[] buffer = new byte[1024]; // 1KB 缓冲区
int len;
while ((len = is.read(buffer)) != -1) {
// 处理 buffer 中 [0, len) 的数据
}
}
写入模板:
java
try (OutputStream os = new FileOutputStream("dest.dat")) {
byte[] buffer = ...; // 准备数据
int len = ...;
os.write(buffer, 0, len);
os.flush(); // 确保所有数据写入磁盘
}
三、字符流:处理文本的利器
3.1为什么需要字符流?
字节流虽然通用,但直接处理文本会遇到编码问题。例如,中文字符在UTF-8中可能占3个字节,若用字节流著俄国字节读取,无法得到正确的字符。Java提供了字符流(Reader/Writer),它们以字符(16-bit Unicode)为单位,自动处理字节与字符之间额转换,前提是必须指定正确的字符集。
3.2使用Scanner读取文本
在基础阶段,我们最熟悉的文本读取工具莫过于Scanner。它不仅能直接从标准输入读取,还可以从任何InputStream读取,并指定字符集
java
try (InputStream is = new FileInputStream("hello.txt");
Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
}
这种方式简洁直观,适合读取结构化或按行分割的文本。但对于大文件,Scanner有一定的性能开销,此时可以考虑更底层的BufferReader。
3.3使用PrintWriter写入文本
相比于FileOutputStream.write(byte\[\] ),PrintWriter提供了我们熟悉的print()、println()和printf()方法,支持格式化输出,且自动处理字符编码。
通常的包装链:
java
try (OutputStream os = new FileOutputStream("output.txt");
OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
PrintWriter writer = new PrintWriter(osw)) {
writer.println("我是第一行");
writer.printf("数值:%d%n", 123);
writer.flush();
}
四、资源管理:try-with-resources
Java 7引入的try-with-resources语法大大简化了资源释放代码。实现类AtuoCloseable接口的类(包括所有I/O流)都可以放在try后的括号中,系统会在try块结束时自动调用close(),无需我们显式写finally。
java
// 旧方式(易错)
InputStream is = null;
try {
is = new FileInputStream("test.txt");
// ...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) { /* 忽略 */ }
}
}
// 新方式(推荐)
try (InputStream is = new FileInputStream("test.txt")) {
// ...
} catch (IOException e) {
e.printStackTrace();
}
五、综合实践:三个实用小工具
5.1文件复制(字节流)
java
public static void copyFile(String srcPath, String destPath) throws IOException {
File src = new File(srcPath);
File dest = new File(destPath);
if (!src.exists() || !src.isFile()) {
throw new IllegalArgumentException("源文件不存在或不是普通文件");
}
if (dest.exists() && dest.isFile()) {
// 询问是否覆盖(此处省略交互)
}
try (InputStream is = new FileInputStream(src);
OutputStream os = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
}
}
5.2扫描目录并删除指定文件
java
private static void scanDir(File dir, String token, List<File> result) {
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
scanDir(f, token, result);
} else {
if (f.getName().contains(token)) {
result.add(f);
}
}
}
}
5.3按文件内容搜索(字符流)
java
private static boolean isContentContains(File file, String token) throws IOException {
StringBuilder sb = new StringBuilder();
try (InputStream is = new FileInputStream(file);
Scanner scanner = new Scanner(is, "UTF-8")) {
while (scanner.hasNextLine()) {
sb.append(scanner.nextLine());
sb.append("\r\n");
}
}
return sb.indexOf(token) != -1;
}