Java文件操作和IO(三)——深入理解数据流与文件内容读写

目录

一、流的基本概念:数据的水管

1.1流的分类

二、字节流:一切数据的基石

2.1抽象基类:InputStream和OutputStream

2.2文件字节流:FileInputStream和FileOutputStream

2.3高效读写:使用缓冲区

三、字符流:处理文本的利器

3.1为什么需要字符流?

3.2使用Scanner读取文本

3.3使用PrintWriter写入文本

四、资源管理:try-with-resources

五、综合实践:三个实用小工具

5.1文件复制(字节流)

5.2扫描目录并删除指定文件

5.3按文件内容搜索(字符流)


一、流的基本概念:数据的水管

在计算机中,流(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;
}