文件读写操作:`FileInputStream/FileOutputStream` 与 `FileReader/FileWriter`

文件读写:字节流与字符流的爱恨情仇

各位看官,今天咱不聊风花雪月,也不谈人生理想,就来扒一扒编程世界里一对看似亲密,实则性格迥异的冤家:FileInputStream/FileOutputStreamFileReader/FileWriter。 它们都负责文件读写,但就像性格迥异的兄弟姐妹,一个耿直粗犷,一个细腻温柔,选择哪个,可得好好掂量掂量。

开场白:文件读写,程序的必修课

话说,咱们的程序就像一个勤劳的小蜜蜂,整天嗡嗡嗡地忙碌着。但光忙活不行啊,得把成果保存下来,或者从之前的“记忆”中读取数据,才能更好地工作。 这时候,文件读写就派上用场了。 它可以让程序和磁盘上的文件进行交流,读取文件内容,或者把程序产生的数据写入文件。

那么,FileInputStream/FileOutputStreamFileReader/FileWriter 到底有什么区别呢? 简单来说,前者是字节流,后者是字符流。 就像一个是处理原始数据的“壮汉”,一个是处理文字信息的“文人”,各有各的用武之地。

字节流:耿直的搬运工 FileInputStream/FileOutputStream

字节流,顾名思义,是以字节(byte)为单位进行读写的。 它们就像耿直的搬运工,不管文件里是啥,都一股脑地按字节搬运。 FileInputStream 负责从文件读取字节, FileOutputStream 负责把字节写入文件。

1. FileInputStream:从文件中“抠”字节

FileInputStream 用于从文件中读取字节数据。 它的使用方法很简单,先创建一个 FileInputStream 对象,指定要读取的文件,然后就可以通过 read() 方法一个字节一个字节地读取了。

import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamExample {

    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            // 1. 创建 FileInputStream 对象
            fis = new FileInputStream("test.txt");

            // 2. 读取数据
            int data;
            while ((data = fis.read()) != -1) {
                // read() 方法返回读取到的字节,如果到达文件末尾,则返回 -1
                System.out.print((char) data); // 将字节转换为字符打印
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流 (重要!)
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解析:

  • fis = new FileInputStream("test.txt");: 创建一个 FileInputStream 对象,指定要读取的文件为 "test.txt"。如果文件不存在,会抛出 FileNotFoundException 异常。
  • while ((data = fis.read()) != -1): 循环读取文件内容, fis.read() 方法每次读取一个字节,并将其转换为 int 类型返回。 如果到达文件末尾,则返回 -1,循环结束。
  • System.out.print((char) data);: 将读取到的字节转换为字符打印出来。 注意,这里假设文件内容是文本文件,如果文件是二进制文件,直接转换成字符可能会出现乱码。
  • fis.close();: 关闭输入流。 务必在 finally 块中关闭流,确保即使发生异常也能正确关闭,释放资源。

2. FileOutputStream:向文件中“塞”字节

FileOutputStream 用于向文件中写入字节数据。 同样,先创建一个 FileOutputStream 对象,指定要写入的文件,然后就可以通过 write() 方法一个字节一个字节地写入了。

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamExample {

    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
            // 1. 创建 FileOutputStream 对象
            fos = new FileOutputStream("output.txt");

            // 2. 写入数据
            String message = "Hello, FileOutputStream!";
            byte[] bytes = message.getBytes(); // 将字符串转换为字节数组
            fos.write(bytes); // 将字节数组写入文件

            // 或者一个字节一个字节的写入
            // for (byte b : bytes) {
            //     fos.write(b);
            // }

            // 3. flush() 刷新缓冲区 (重要!)
            fos.flush(); // 确保所有数据都写入磁盘

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流 (重要!)
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解析:

  • fos = new FileOutputStream("output.txt");: 创建一个 FileOutputStream 对象,指定要写入的文件为 "output.txt"。 如果文件不存在,会自动创建。 如果文件已存在,默认会覆盖原有内容。 如果要追加内容,可以使用 FileOutputStream("output.txt", true) 构造函数。
  • String message = "Hello, FileOutputStream!";: 定义要写入的字符串。
  • byte[] bytes = message.getBytes();: 将字符串转换为字节数组。 不同的字符编码会影响字节数组的内容。 通常使用 UTF-8 编码。
  • fos.write(bytes);: 将字节数组写入文件。 也可以使用循环一个字节一个字节的写入。
  • fos.flush();: 刷新缓冲区。 FileOutputStream 内部有一个缓冲区,数据会先写入缓冲区,当缓冲区满了或者调用 flush() 方法时,才会真正写入磁盘。 务必调用 flush() 方法,确保所有数据都写入磁盘,避免数据丢失。
  • fos.close();: 关闭输出流。 同样,务必在 finally 块中关闭流,确保即使发生异常也能正确关闭,释放资源。

字节流的适用场景:

字节流适合处理二进制文件,例如图片、音频、视频等。 因为它们不需要进行字符编码的转换,直接按字节读写即可。 也可以处理文本文件,但需要自己处理字符编码的问题。

字符流:温柔的翻译官 FileReader/FileWriter

字符流,则以字符(char)为单位进行读写。 它们就像温柔的翻译官,会根据指定的字符编码,将字节数据转换为字符,或者将字符转换为字节数据。 FileReader 负责从文件读取字符, FileWriter 负责把字符写入文件。

1. FileReader:从文件中“读”字符

FileReader 用于从文件中读取字符数据。 它会根据系统默认的字符编码,将文件中的字节数据转换为字符。

import java.io.FileReader;
import java.io.IOException;

public class FileReaderExample {

    public static void main(String[] args) {
        FileReader fr = null;
        try {
            // 1. 创建 FileReader 对象
            fr = new FileReader("test.txt");

            // 2. 读取数据
            int data;
            while ((data = fr.read()) != -1) {
                // read() 方法返回读取到的字符,如果到达文件末尾,则返回 -1
                System.out.print((char) data);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流 (重要!)
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解析:

  • fr = new FileReader("test.txt");: 创建一个 FileReader 对象,指定要读取的文件为 "test.txt"。 FileReader 会使用系统默认的字符编码来读取文件。
  • while ((data = fr.read()) != -1): 循环读取文件内容, fr.read() 方法每次读取一个字符,并将其转换为 int 类型返回。 如果到达文件末尾,则返回 -1,循环结束。
  • System.out.print((char) data);: 将读取到的字符打印出来。
  • fr.close();: 关闭输入流。 务必在 finally 块中关闭流,确保即使发生异常也能正确关闭,释放资源。

注意: FileReader 使用系统默认的字符编码来读取文件,如果文件使用的字符编码与系统默认的字符编码不一致,可能会出现乱码。

2. FileWriter:向文件中“写”字符

FileWriter 用于向文件中写入字符数据。 它会根据系统默认的字符编码,将字符转换为字节数据,然后写入文件。

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterExample {

    public static void main(String[] args) {
        FileWriter fw = null;
        try {
            // 1. 创建 FileWriter 对象
            fw = new FileWriter("output.txt");

            // 2. 写入数据
            String message = "Hello, FileWriter!";
            fw.write(message); // 将字符串写入文件

            // 或者一个字符一个字符的写入
            // for (int i = 0; i < message.length(); i++) {
            //     fw.write(message.charAt(i));
            // }

            // 3. flush() 刷新缓冲区 (重要!)
            fw.flush(); // 确保所有数据都写入磁盘

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流 (重要!)
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码解析:

  • fw = new FileWriter("output.txt");: 创建一个 FileWriter 对象,指定要写入的文件为 "output.txt"。 FileWriter 会使用系统默认的字符编码来写入文件. 如果要追加内容,可以使用 FileWriter("output.txt", true) 构造函数。
  • String message = "Hello, FileWriter!";: 定义要写入的字符串。
  • fw.write(message);: 将字符串写入文件。 也可以使用循环一个字符一个字符的写入。
  • fw.flush();: 刷新缓冲区。 FileWriter 内部也有一个缓冲区,数据会先写入缓冲区,当缓冲区满了或者调用 flush() 方法时,才会真正写入磁盘。 务必调用 flush() 方法,确保所有数据都写入磁盘,避免数据丢失。
  • fw.close();: 关闭输出流。 同样,务必在 finally 块中关闭流,确保即使发生异常也能正确关闭,释放资源。

注意: FileWriter 使用系统默认的字符编码来写入文件,如果需要指定字符编码,可以使用 OutputStreamWriter 来代替 FileWriter

字符流的适用场景:

字符流适合处理文本文件,例如 .txt.java.html 等。 因为它们会自动进行字符编码的转换,避免出现乱码问题。

字节流 vs 字符流:一场友谊赛

特性 字节流 (FileInputStream/FileOutputStream) 字符流 (FileReader/FileWriter)
处理单位 字节 (byte) 字符 (char)
适用场景 二进制文件 (图片、音频、视频) 文本文件 (txt, java, html)
编码转换 不进行编码转换 进行编码转换
效率 通常更高 相对较低
使用方便性 相对复杂,需要自己处理编码问题 相对简单,自动处理编码问题

总结:

  • 字节流 就像一个朴实的农民工,啥都能搬,但需要你自己负责分类整理 (处理编码)。
  • 字符流 就像一个贴心的管家,只负责处理文字,而且会帮你翻译整理 (自动处理编码)。

进阶篇:使用缓冲流提升效率

无论是字节流还是字符流,一个字节/字符一个字节/字符地读写效率都比较低。 就像搬家,如果每次只搬一件东西,效率肯定不高。 为了提升效率,我们可以使用缓冲流

缓冲流在内部维护一个缓冲区,可以一次性读取/写入多个字节/字符,减少了磁盘 I/O 的次数,从而提升了效率。

1. 字节缓冲流: BufferedInputStream/BufferedOutputStream

import java.io.*;

public class BufferedStreamExample {

    public static void main(String[] args) {
        try (
            FileInputStream fis = new FileInputStream("input.txt");
            BufferedInputStream bis = new BufferedInputStream(fis);
            FileOutputStream fos = new FileOutputStream("output.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos)
        ) {

            byte[] buffer = new byte[1024]; // 创建一个缓冲区
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead); // 将缓冲区中的数据写入文件
            }

            bos.flush(); // 确保所有数据都写入磁盘

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码解析:

  • 使用 try-with-resources 语句,可以自动关闭流,无需手动关闭。
  • BufferedInputStreamBufferedOutputStream 分别是对 FileInputStreamFileOutputStream 的包装,提供了缓冲功能。
  • bis.read(buffer) 从输入流读取数据到缓冲区,返回实际读取的字节数。
  • bos.write(buffer, 0, bytesRead) 将缓冲区中的数据写入输出流。

2. 字符缓冲流: BufferedReader/BufferedWriter

import java.io.*;

public class BufferedReaderWriterExample {

    public static void main(String[] args) {
        try (
            FileReader fr = new FileReader("input.txt");
            BufferedReader br = new BufferedReader(fr);
            FileWriter fw = new FileWriter("output.txt");
            BufferedWriter bw = new BufferedWriter(fw)
        ) {

            String line;
            while ((line = br.readLine()) != null) { // 一行一行地读取
                bw.write(line); // 写入一行
                bw.newLine(); // 写入换行符
            }

            bw.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码解析:

  • BufferedReader 提供了 readLine() 方法,可以一次性读取一行文本。
  • BufferedWriter 提供了 newLine() 方法,可以写入换行符。

总结: 使用缓冲流可以显著提升文件读写的效率,建议在实际开发中使用。

编码问题:不得不说的痛

字符流涉及到字符编码的问题,如果文件使用的字符编码与程序使用的字符编码不一致,可能会出现乱码。

常见的字符编码有:

  • ASCII: 只能表示英文字符,占用 1 个字节。
  • GBK: 中文编码,包含简体中文和繁体中文,占用 2 个字节。
  • UTF-8: Unicode 的一种实现方式,可以表示世界上所有的字符,占用 1-4 个字节。

为了避免乱码问题,建议:

  • 统一使用 UTF-8 编码。
  • 明确指定字符编码。

可以使用 InputStreamReaderOutputStreamWriter 来指定字符编码。

import java.io.*;

public class EncodingExample {

    public static void main(String[] args) {
        try (
            FileInputStream fis = new FileInputStream("input.txt");
            InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); // 指定字符编码为 UTF-8
            BufferedReader br = new BufferedReader(isr);
            FileOutputStream fos = new FileOutputStream("output.txt");
            OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8"); // 指定字符编码为 UTF-8
            BufferedWriter bw = new BufferedWriter(osw)
        ) {

            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
            }

            bw.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结:选择适合自己的才是最好的

FileInputStream/FileOutputStreamFileReader/FileWriter 就像两把剑,各有千秋。 选择哪一把,取决于你要处理的文件类型。

  • 如果处理的是二进制文件,或者对效率要求较高,可以选择字节流。
  • 如果处理的是文本文件,并且希望自动处理字符编码,可以选择字符流。

当然,实际开发中,我们通常会使用缓冲流来提升效率,并且要注意字符编码的问题,避免出现乱码。

希望这篇文章能帮助你更好地理解文件读写操作,在编程的道路上更上一层楼! 记住,熟练掌握文件读写,才能让你的程序拥有“记忆”,变得更加强大!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注