文件读写:字节流与字符流的爱恨情仇
各位看官,今天咱不聊风花雪月,也不谈人生理想,就来扒一扒编程世界里一对看似亲密,实则性格迥异的冤家:FileInputStream/FileOutputStream
和 FileReader/FileWriter
。 它们都负责文件读写,但就像性格迥异的兄弟姐妹,一个耿直粗犷,一个细腻温柔,选择哪个,可得好好掂量掂量。
开场白:文件读写,程序的必修课
话说,咱们的程序就像一个勤劳的小蜜蜂,整天嗡嗡嗡地忙碌着。但光忙活不行啊,得把成果保存下来,或者从之前的“记忆”中读取数据,才能更好地工作。 这时候,文件读写就派上用场了。 它可以让程序和磁盘上的文件进行交流,读取文件内容,或者把程序产生的数据写入文件。
那么,FileInputStream/FileOutputStream
和 FileReader/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
语句,可以自动关闭流,无需手动关闭。 BufferedInputStream
和BufferedOutputStream
分别是对FileInputStream
和FileOutputStream
的包装,提供了缓冲功能。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 编码。
- 明确指定字符编码。
可以使用 InputStreamReader
和 OutputStreamWriter
来指定字符编码。
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/FileOutputStream
和 FileReader/FileWriter
就像两把剑,各有千秋。 选择哪一把,取决于你要处理的文件类型。
- 如果处理的是二进制文件,或者对效率要求较高,可以选择字节流。
- 如果处理的是文本文件,并且希望自动处理字符编码,可以选择字符流。
当然,实际开发中,我们通常会使用缓冲流来提升效率,并且要注意字符编码的问题,避免出现乱码。
希望这篇文章能帮助你更好地理解文件读写操作,在编程的道路上更上一层楼! 记住,熟练掌握文件读写,才能让你的程序拥有“记忆”,变得更加强大!