IO 流
File 类
File 是文件或目录路径的抽象表示。创建 File 对象只是在内存中创建路径对象,不代表磁盘上的文件或目录一定存在。
构造器
File file1 = new File("d:/lesson/java2601/a.txt");
File file2 = new File("d:/lesson/java2601", "a.txt");
File parent = new File("d:/lesson/java2601");
File file3 = new File(parent, "a.txt");
在 Windows 路径字符串中可以使用转义后的反斜杠,也可以使用正斜杠。
创建文件和目录
创建文件
File file = new File("d:/lesson/java2601/test/a.txt");
try {
boolean created = file.createNewFile();
System.out.println(created ? "创建成功" : "文件已存在");
} catch (IOException e) {
e.printStackTrace();
}
createNewFile() 不会自动创建不存在的父目录。
文件扩展名通常用于帮助操作系统和应用程序识别文件类型,但扩展名本身不会改变文件内容。
创建目录
mkdir() 只能在父目录已经存在时创建单级目录。
File directory = new File("d:/lesson/java2601/test/a");
directory.mkdir();
mkdirs() 可以同时创建缺失的多级父目录。
File directory = new File("d:/lesson/java2601/test/a/b/c");
directory.mkdirs();
创建临时文件
File directory = new File("d:/lesson/java2601/test");
try {
File tempFile = File.createTempFile("temp", ".txt", directory);
System.out.println(tempFile);
} catch (IOException e) {
e.printStackTrace();
}
临时文件名前缀至少需要三个字符。
常用方法
| 方法 | 作用 |
|---|---|
exists() | 判断路径是否存在 |
isFile() | 判断是否为普通文件 |
isDirectory() | 判断是否为目录 |
delete() | 删除文件或空目录 |
getName() | 获取文件或目录名称 |
getParentFile() | 获取父路径对应的 File 对象 |
getPath() | 获取构造对象时使用的路径形式 |
getAbsolutePath() | 获取绝对路径字符串 |
length() | 获取普通文件长度,单位为字节 |
listFiles() | 获取目录下的直接子项 |
listFiles() 在路径不是目录、发生 IO 错误或没有访问权限时可能返回 null,使用前应检查。
File directory = new File("d:/lesson/java2601");
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
System.out.println(file);
}
}
File.equals() 比较抽象路径名是否相等,不会自动把相对路径、符号链接或包含 .. 的路径全部规范化。需要比较实际规范路径时,可以使用 getCanonicalFile(),并处理 IOException。
递归
递归是方法直接或间接调用自身。递归必须具有明确的终止条件,并确保每次调用都逐步接近终止条件。
阶乘
public static long factorial(int number) {
if (number < 0) {
throw new IllegalArgumentException("阶乘参数不能为负数");
}
if (number <= 1) {
return 1;
}
return number * factorial(number - 1);
}斐波那契数列
public static long fibonacci(int number) {
if (number < 0) {
throw new IllegalArgumentException("参数不能为负数");
}
if (number <= 1) {
return number;
}
return fibonacci(number - 1) + fibonacci(number - 2);
}这种直接递归会重复计算大量子问题,只适合演示递归。处理较大数据时应使用循环或缓存结果。
递归查找文件
public static void findPdfFiles(File path, List<File> result) {
if (path == null || !path.exists()) {
return;
}
if (path.isFile()) {
if (path.getName().toLowerCase().endsWith(".pdf")) {
result.add(path);
}
return;
}
File[] children = path.listFiles();
if (children == null) {
return;
}
for (File child : children) {
findPdfFiles(child, result);
}
}递归删除目录
public static boolean deleteRecursively(File path) {
if (path == null || !path.exists()) {
return true;
}
if (path.isDirectory()) {
File[] children = path.listFiles();
if (children == null) {
return false;
}
for (File child : children) {
if (!deleteRecursively(child)) {
return false;
}
}
}
return path.delete();
}递归处理目录时还要注意访问权限、符号链接和过深目录可能导致的栈溢出。现代 Java 项目也可以使用 java.nio.file.Files 提供的文件树 API。
IO 流概述
IO 表示输入和输出。以程序内存为参照:
- 输入流把外部数据读入程序。
- 输出流把程序数据写到外部设备。
按数据单位分类
InputStream和OutputStream体系按字节处理,适合图片、音频、视频和其他二进制数据。Reader和Writer体系按字符处理,适合文本数据。
字符流需要把字节按照字符编码解码为字符,或者把字符编码为字节。
按连接目标分类
- 节点流直接连接文件、内存、网络等数据源或目标,例如
FileInputStream。 - 处理流包装其他流并增加缓冲、数据类型读写、对象序列化等功能,例如
BufferedInputStream和ObjectOutputStream。
处理流不一定都比节点流“更高级”或“更快”,应根据所需功能组合使用。
使用步骤
-
确定输入还是输出。
-
确定处理字节还是字符。
-
选择实际数据源对应的节点流。
-
根据需要增加缓冲、编码转换或对象处理流。
-
使用
try-with-resources及时关闭资源。
FileInputStream 和 FileOutputStream
读取字节
FileInputStream 的常用 read() 方法如下:
read():读取一个字节,返回0到255,到达末尾返回-1。read(byte[] buffer):最多读取数组长度个字节,返回实际读取数量。read(byte[] buffer, int offset, int length):把数据读取到数组指定范围。
批量读取时,只能处理本次实际读取到的长度。
try (FileInputStream input = new FileInputStream(
"d:/lesson/java2601/test/a.txt")) {
byte[] buffer = new byte[1024];
int length;
while ((length = input.read(buffer)) != -1) {
for (int i = 0; i < length; i++) {
System.out.println(buffer[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
}输出文本内容时,不应把任意字节逐个强制转换成字符,应使用字符流并指定正确编码。
写入字节
FileOutputStream 默认覆盖原文件,构造器第二个参数为 true 时表示追加。
try (FileOutputStream output = new FileOutputStream(
"d:/lesson/java2601/test/a.txt", true)) {
byte[] data = "ABCDEFG".getBytes(StandardCharsets.UTF_8);
output.write(data);
} catch (IOException e) {
e.printStackTrace();
}
复制文件
public static void copyFile(String source, String target) {
try (FileInputStream input = new FileInputStream(source);
FileOutputStream output = new FileOutputStream(target)) {
byte[] buffer = new byte[8192];
int length;
while ((length = input.read(buffer)) != -1) {
output.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}复制二进制文件必须使用字节流。
BufferedInputStream 和 BufferedOutputStream
缓冲字节流在内存中维护缓冲区,减少底层读写调用次数。
public static void copyFileWithBuffer(String source, String target) {
try (BufferedInputStream input = new BufferedInputStream(
new FileInputStream(source));
BufferedOutputStream output = new BufferedOutputStream(
new FileOutputStream(target))) {
byte[] buffer = new byte[8192];
int length;
while ((length = input.read(buffer)) != -1) {
output.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}关闭输出流时会先刷新缓冲区。需要在流保持打开的情况下立刻把数据提交到底层目标时,可以调用 flush()。
FileReader 和 FileWriter
FileReader 和 FileWriter 用于读写文本,但传统构造器通常使用平台默认字符集。文件编码固定时,建议使用 InputStreamReader 和 OutputStreamWriter 显式指定字符集。
try (FileReader reader = new FileReader(
"d:/lesson/java2601/test/a.txt")) {
char[] buffer = new char[1024];
int length;
while ((length = reader.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, length));
}
} catch (IOException e) {
e.printStackTrace();
}FileReader 本身不是 BufferedReader,不能把它描述为天然具有行缓冲功能。
try (FileWriter writer = new FileWriter(
"d:/lesson/java2601/test/a.txt")) {
writer.write("1234567890");
} catch (IOException e) {
e.printStackTrace();
}
BufferedReader 和 BufferedWriter
BufferedReader 提供 readLine(),可以按行读取文本。返回的字符串不包含行结束符,到达文件末尾时返回 null。
try (BufferedReader reader = new BufferedReader(
new FileReader("d:/lesson/java2601/test/a.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}BufferedWriter 提供 newLine() 写入平台对应的行分隔符。
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("d:/lesson/java2601/test/a.txt"))) {
writer.write("第一行");
writer.newLine();
writer.write("第二行");
} catch (IOException e) {
e.printStackTrace();
}InputStreamReader 和 OutputStreamWriter
这两个转换流连接字节流与字符流,并负责字符编码转换。
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("d:/lesson/java2601/test/a.txt"),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("d:/lesson/java2601/test/a.txt"),
StandardCharsets.UTF_8))) {
writer.write("使用 UTF-8 写入文本");
} catch (IOException e) {
e.printStackTrace();
}DataInputStream 和 DataOutputStream
数据流可以按照 Java 基本类型的二进制格式读写数据。
try (DataOutputStream output = new DataOutputStream(
new FileOutputStream("d:/lesson/java2601/test/data.bin"))) {
output.writeInt(1024);
output.writeDouble(100.0);
output.writeUTF("Java");
} catch (IOException e) {
e.printStackTrace();
}读取时必须使用相同的顺序和类型,否则会读到错误结果或抛出异常。
try (DataInputStream input = new DataInputStream(
new FileInputStream("d:/lesson/java2601/test/data.bin"))) {
int number = input.readInt();
double price = input.readDouble();
String text = input.readUTF();
System.out.println(number);
System.out.println(price);
System.out.println(text);
} catch (IOException e) {
e.printStackTrace();
}对象序列化与反序列化
序列化把对象及其可序列化对象图转换为字节序列,反序列化根据字节序列恢复对象。
类需要实现 Serializable 标记接口。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password;
}
transient 字段不会参与默认序列化。serialVersionUID 用于判断序列化数据与当前类版本是否兼容。
序列化到字节数组
public static byte[] serialize(Object object) throws IOException {
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(object);
return bytes.toByteArray();
}
}
从字节数组反序列化
public static Object deserialize(byte[] data)
throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bytes = new ByteArrayInputStream(data);
ObjectInputStream input = new ObjectInputStream(bytes)) {
return input.readObject();
}
}序列化数据不要求 JDK 小版本完全一致,但类名、字段结构、serialVersionUID 和相关类必须满足兼容条件。
反序列化不可信数据可能触发危险对象行为,真实项目中不得直接反序列化来源不可信的字节流。
PrintWriter
PrintWriter 提供 print()、println() 和 printf() 等便捷文本输出方法。
try (PrintWriter writer = new PrintWriter(
"d:/lesson/java2601/test/a.txt",
StandardCharsets.UTF_8.name())) {
writer.println("你好");
writer.println("中午吃什么");
} catch (FileNotFoundException | UnsupportedEncodingException e) {
e.printStackTrace();
}PrintWriter 的部分写入错误不会直接以 IOException 抛出,可以在需要时调用 checkError() 检查状态。关闭流会自动刷新缓冲区。
Properties 类
Properties 用于保存字符串键值配置,继承自 Hashtable<Object, Object>。读取配置时应优先使用 getProperty(),写入时使用 setProperty()。
配置文件适合保存可由部署环境调整、但不应硬编码在程序中的数据。
常见配置格式
- XML 具有明确结构,但编写和解析相对复杂。
- Properties 使用简单的键值形式,层级表达能力有限。
- YAML 具有较强的层级表达能力,常用于 Spring Boot 等框架。
读取配置
p1.name=Tom
p1.age=20
p2.name=Jerry
p2.age=22
Properties properties = new Properties();
try (Reader reader = new InputStreamReader(
new FileInputStream("src/myinfo.properties"),
StandardCharsets.UTF_8)) {
properties.load(reader);
Person person1 = new Person();
person1.name = properties.getProperty("p1.name");
person1.age = Integer.parseInt(properties.getProperty("p1.age"));
Person person2 = new Person();
person2.name = properties.getProperty("p2.name");
person2.age = Integer.parseInt(properties.getProperty("p2.age"));
} catch (IOException e) {
e.printStackTrace();
}使用 Reader 可以显式选择字符集。Java 8 中直接使用 InputStream 读取 Properties 时,传统格式按 ISO-8859-1 规则处理,非 ASCII 字符通常需要 Unicode 转义。
写入配置
properties.setProperty("p3.name", "Tom");
properties.setProperty("p3.age", "22");
try (Writer writer = new OutputStreamWriter(
new FileOutputStream("src/myinfo.properties"),
StandardCharsets.UTF_8)) {
properties.store(writer, "第三个人的信息");
} catch (IOException e) {
e.printStackTrace();
}
Apache POI
Apache POI 是 Apache 软件基金会的 Java 项目,可以读写 Microsoft Office 格式,例如 Excel 和 Word。
一个典型练习是读取学生成绩工作簿中的数据,计算平均成绩,再把结果写入另一个工作表。处理 Excel 时需要根据文件格式选择对应模型:
.xls通常使用 HSSF。.xlsx通常使用 XSSF。
使用 POI 读写工作簿、输入流和输出流时,也应通过 try-with-resources 正确关闭资源,并注意单元格类型、空单元格和公式单元格的处理。
喜欢的话,留下你的评论吧~