引言:数据流动的艺术
在Java中,输入输出(I/O)操作是程序与外部世界沟通的桥梁。无论是读取配置文件、写入日志,还是处理用户上传的文件,I/O操作无处不在。理解Java的I/O体系,不仅能写出更高效的代码,还能避免许多常见的陷阱。
Java I/O 体系结构概览
I/O流的分类
Java I/O流主要分为两大类:
Java
Java I/O 流
├── 按数据方向分
│ ├── 输入流 (InputStream/Reader)
│ └── 输出流 (OutputStream/Writer)
│
├── 按数据类型分
│ ├── 字节流 (处理二进制数据)
│ │ ├── InputStream
│ │ └── OutputStream
│ │
│ └── 字符流 (处理文本数据)
│ ├── Reader
│ └── Writer
│
└── 按功能分
├── 节点流 (直接连接数据源)
└── 处理流/包装流 (装饰器模式,增强功能)
字节流操作:处理二进制数据
FileInputStream 和 FileOutputStream
Java
public class ByteStreamExample {
// 复制文件的基本方法(字节流)
public static void copyFile(String sourcePath, String targetPath) throws IOException {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(sourcePath);
fos = new FileOutputStream(targetPath);
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成");
} finally {
// 确保资源被关闭
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
}
}
// 使用try-with-resources改进版本
public static void copyFileModern(String sourcePath, String targetPath) throws IOException {
try (FileInputStream fis = new FileInputStream(sourcePath);
FileOutputStream fos = new FileOutputStream(targetPath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制完成");
}
}
// 读取图像文件信息
public static void readImageInfo(String imagePath) throws IOException {
try (FileInputStream fis = new FileInputStream(imagePath)) {
byte[] header = new byte[4];
fis.read(header);
// 简单判断文件类型
if (header[0] == (byte) 0xFF && header[1] == (byte) 0xD8) {
System.out.println("JPEG 图像文件");
} else if (header[0] == (byte) 'G' && header[1] == (byte) 'I'
&& header[2] == (byte) 'F') {
System.out.println("GIF 图像文件");
} else {
System.out.println("未知文件类型");
}
long fileSize = new File(imagePath).length();
System.out.println("文件大小: " + fileSize + " 字节");
}
}
}
字符流操作:处理文本数据
FileReader 和 FileWriter
Java
public class CharacterStreamExample {
// 读取文本文件内容(字符流)
public static String readTextFile(String filePath) throws IOException {
StringBuilder content = new StringBuilder();
try (FileReader reader = new FileReader(filePath)) {
char[] buffer = new char[4096];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
}
return content.toString();
}
// 写入文本文件
public static void writeTextFile(String filePath, String content) throws IOException {
try (FileWriter writer = new FileWriter(filePath)) {
writer.write(content);
}
}
// 按行读取文本文件
public static List<String> readLines(String filePath) throws IOException {
List<String> lines = new ArrayList<>();
try (FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
// 处理不同编码的文本文件
public static String readFileWithEncoding(String filePath, String charsetName)
throws IOException {
StringBuilder content = new StringBuilder();
// 指定编码读取文件
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream(filePath), charsetName)) {
char[] buffer = new char[4096];
int charsRead;
while ((charsRead = reader.read(buffer)) != -1) {
content.append(buffer, 0, charsRead);
}
}
return content.toString();
}
}
缓冲流:提升I/O性能的关键
缓冲流的威力
Java
public class BufferedStreamExample {
public static void performanceComparison() throws IOException {
String sourceFile = "largefile.dat";
String target1 = "copy1.dat";
String target2 = "copy2.dat";
// 1. 无缓冲复制
long startTime = System.currentTimeMillis();
copyWithoutBuffer(sourceFile, target1);
long time1 = System.currentTimeMillis() - startTime;
// 2. 有缓冲复制
startTime = System.currentTimeMillis();
copyWithBuffer(sourceFile, target2);
long time2 = System.currentTimeMillis() - startTime;
System.out.println("无缓冲复制耗时: " + time1 + "ms");
System.out.println("有缓冲复制耗时: " + time2 + "ms");
System.out.println("性能提升: " + (time1 - time2) + "ms");
}
private static void copyWithoutBuffer(String source, String target) throws IOException {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target)) {
int byteData;
while ((byteData = fis.read()) != -1) { // 逐字节读取,效率极低!
fos.write(byteData);
}
}
}
private static void copyWithBuffer(String source, String target) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(target))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
}
}
// 带缓冲的字符流
public static void processTextFileWithBuffer(String inputFile, String outputFile)
throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));
BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// 处理每一行,例如添加行号
String processedLine = String.format("%04d: %s", lineNumber, line);
writer.write(processedLine);
writer.newLine(); // 跨平台换行符
}
}
}
}
对象序列化:保存和恢复对象状态
Serializable接口
Java
public class SerializationExample {
// 可序列化的对象
public static class Person implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
private String name;
private transient int age; // transient修饰的字段不会被序列化
private List<String> hobbies;
public Person(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age +
", hobbies=" + hobbies + "}";
}
}
// 序列化对象到文件
public static void serializeObject(Object obj, String filePath) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filePath))) {
oos.writeObject(obj);
System.out.println("对象序列化完成: " + filePath);
}
}
// 从文件反序列化对象
public static Object deserializeObject(String filePath)
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filePath))) {
return ois.readObject();
}
}
public static void main(String[] args) throws Exception {
List<String> hobbies = Arrays.asList("阅读", "游泳", "编程");
Person person = new Person("张三", 25, hobbies);
// 序列化
serializeObject(person, "person.dat");
// 反序列化
Person restoredPerson = (Person) deserializeObject("person.dat");
System.out.println("恢复的对象: " + restoredPerson);
// 注意:age字段为0,因为被transient修饰
}
// 自定义序列化过程
public static class CustomSerializable implements Serializable {
private String data;
private transient String sensitiveData; // 敏感数据,不直接序列化
public CustomSerializable(String data, String sensitiveData) {
this.data = data;
this.sensitiveData = sensitiveData;
}
// 自定义序列化逻辑
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化
// 对敏感数据进行加密后再序列化
String encrypted = encrypt(sensitiveData);
out.writeObject(encrypted);
}
// 自定义反序列化逻辑
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化
// 读取并解密敏感数据
String encrypted = (String) in.readObject();
this.sensitiveData = decrypt(encrypted);
}
private String encrypt(String data) {
// 简单的加密示例(实际项目应使用安全的加密算法)
return Base64.getEncoder().encodeToString(data.getBytes());
}
private String decrypt(String encrypted) {
byte[] bytes = Base64.getDecoder().decode(encrypted);
return new String(bytes);
}
}
}
现代Java I/O:NIO与NIO.2
Path API:更优雅的文件路径操作
Java
public class ModernIOExample {
public static void pathOperations() {
// 1. 创建Path对象
Path path1 = Paths.get("/Users/test/documents", "file.txt");
Path path2 = Path.of("data", "input", "config.properties");
// 2. 获取路径信息
System.out.println("文件名: " + path1.getFileName());
System.out.println("父目录: " + path1.getParent());
System.out.println("根目录: " + path1.getRoot());
System.out.println("绝对路径: " + path1.toAbsolutePath());
// 3. 路径操作
Path resolved = path2.resolve("subfolder/file.txt");
Path relativized = path1.relativize(Paths.get("/Users/test"));
Path normalized = Paths.get("/a/./b/../c").normalize();
// 4. 文件系统操作
try {
// 检查文件属性
boolean exists = Files.exists(path1);
boolean isDirectory = Files.isDirectory(path1);
boolean isReadable = Files.isReadable(path1);
long size = Files.size(path1);
FileTime lastModified = Files.getLastModifiedTime(path1);
// 创建目录(包括父目录)
Path newDir = Paths.get("new/directory");
Files.createDirectories(newDir);
// 创建文件
Path newFile = newDir.resolve("test.txt");
Files.createFile(newFile);
} catch (IOException e) {
e.printStackTrace();
}
}
// 使用Files类进行高效文件操作
public static void filesClassExamples() throws IOException {
// 1. 读取所有行
Path filePath = Paths.get("data.txt");
List<String> allLines = Files.readAllLines(filePath, StandardCharsets.UTF_8);
// 2. 流式处理大文件
try (Stream<String> lines = Files.lines(filePath, StandardCharsets.UTF_8)) {
List<String> filtered = lines
.filter(line -> line.contains("error"))
.collect(Collectors.toList());
System.out.println("找到 " + filtered.size() + " 个错误行");
}
// 3. 复制文件
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
// 4. 移动/重命名文件
Files.move(target, Paths.get("renamed.txt"),
StandardCopyOption.REPLACE_EXISTING);
// 5. 删除文件
Files.deleteIfExists(Paths.get("temp.txt"));
// 6. 遍历目录
Path dir = Paths.get("src/main/java");
try (Stream<Path> paths = Files.walk(dir, 3)) { // 最大深度3层
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
}
// 文件属性操作
public static void fileAttributes() throws IOException {
Path path = Paths.get("test.txt");
// 基本属性
BasicFileAttributes attrs = Files.readAttributes(
path, BasicFileAttributes.class);
System.out.println("创建时间: " + attrs.creationTime());
System.out.println("最后访问时间: " + attrs.lastAccessTime());
System.out.println("最后修改时间: " + attrs.lastModifiedTime());
System.out.println("是否目录: " + attrs.isDirectory());
System.out.println("大小: " + attrs.size() + " 字节");
// 设置文件属性
Files.setAttribute(path, "dos:hidden", true);
Files.setLastModifiedTime(path,
FileTime.fromMillis(System.currentTimeMillis()));
}
}
高级I/O技巧与实践
1. 文件锁:多进程/多线程安全访问
Java
public class FileLockExample {
public static void writeWithLock(String filePath, String content) throws IOException {
Path path = Paths.get(filePath);
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
FileLock lock = channel.lock()) { // 获取独占锁
System.out.println("获取文件锁,开始写入...");
// 写入内容
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = ByteBuffer.wrap(bytes);
channel.write(buffer);
Thread.sleep(2000); // 模拟长时间操作
System.out.println("写入完成,释放锁");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void tryReadLock(String filePath) throws IOException {
Path path = Paths.get(filePath);
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
FileLock lock = channel.tryLock(0, Long.MAX_VALUE, true)) { // 尝试获取共享锁
if (lock != null) {
System.out.println("成功获取读锁");
// 读取文件内容
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer) > 0) {
buffer.flip();
System.out.print(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
}
lock.release();
} else {
System.out.println("文件被锁定,无法读取");
}
}
}
}
2. 内存映射文件:超大文件高效处理
Java
public class MemoryMappedFileExample {
public static void processLargeFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
long fileSize = channel.size();
long position = 0;
long chunkSize = 1024 * 1024 * 100; // 每次映射100MB
while (position < fileSize) {
long size = Math.min(chunkSize, fileSize - position);
// 映射文件的一部分到内存
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, position, size);
// 在内存中直接操作数据
processBuffer(buffer);
position += size;
}
}
}
private static void processBuffer(ByteBuffer buffer) {
// 示例:搜索特定字节模式
while (buffer.hasRemaining()) {
byte b = buffer.get();
// 处理逻辑...
}
}
}
3. 管道通信:线程间数据传输
Java
public class PipeExample {
public static void threadCommunication() throws IOException {
// 创建管道
Pipe pipe = Pipe.open();
// 写入线程
Thread writerThread = new Thread(() -> {
try (Pipe.SinkChannel sinkChannel = pipe.sink()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i = 0; i < 10; i++) {
String message = "Message " + i + "\n";
buffer.put(message.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
sinkChannel.write(buffer);
}
buffer.clear();
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
// 读取线程
Thread readerThread = new Thread(() -> {
try (Pipe.SourceChannel sourceChannel = pipe.source()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int bytesRead = sourceChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.print(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
readerThread.start();
writerThread.start();
}
}
I/O最佳实践与性能优化
最佳实践总结
Java
public class IOBestPractices {
// 1. 总是使用try-with-resources
public static void goodPractice1() throws IOException {
// ✅ 正确做法
try (BufferedReader reader = Files.newBufferedReader(Paths.get("file.txt"))) {
// 使用reader
}
// ❌ 错误做法
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
// 使用reader
} finally {
if (reader != null) {
reader.close(); // 可能忘记关闭
}
}
}
// 2. 选择合适的缓冲区大小
public static void bufferSizeSelection() {
// 根据场景选择合适的缓冲区大小
int[] bufferSizes = {
1024, // 小文件或网络操作
8192, // 默认推荐值
65536, // 大文件处理
1024 * 1024 // 超大文件或数据库操作
};
// 测试找到最佳缓冲区大小
for (int bufferSize : bufferSizes) {
try {
long time = testBufferPerformance(bufferSize);
System.out.printf("缓冲区大小 %d: %d ms%n", bufferSize, time);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 3. 处理大文件的正确方式
public static void processLargeFileEfficiently(String inputFile, String outputFile)
throws IOException {
// 使用流式处理,避免一次性加载到内存
try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFile));
BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputFile))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理每一行
String processed = processLine(line);
writer.write(processed);
writer.newLine();
// 定期flush,但不频繁
if (Math.random() < 0.001) {
writer.flush();
}
}
}
}
// 4. 安全的临时文件处理
public static void safeTempFileUsage() throws IOException {
// 创建临时文件(会自动删除)
Path tempFile = Files.createTempFile("prefix_", ".tmp");
try {
// 使用临时文件
Files.write(tempFile, "临时数据".getBytes());
// 处理文件...
} finally {
// 确保临时文件被删除
Files.deleteIfExists(tempFile);
}
// 或者使用deleteOnExit(程序退出时删除)
File temp = File.createTempFile("temp", ".txt");
temp.deleteOnExit();
}
// 5. 监控I/O操作进度
public static void copyWithProgress(String source, String target)
throws IOException {
long totalSize = Files.size(Paths.get(source));
long copied = 0;
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
copied += bytesRead;
// 显示进度
double progress = (double) copied / totalSize * 100;
if ((int) progress % 10 == 0) {
System.out.printf("复制进度: %.1f%%%n", progress);
}
}
}
}
private static long testBufferPerformance(int bufferSize) throws IOException {
// 性能测试逻辑
return System.currentTimeMillis(); // 示例返回值
}
private static String processLine(String line) {
// 示例处理逻辑
return line.toUpperCase();
}
}
实战:实现一个简单的文件管理器
Java
public class SimpleFileManager {
public static class FileInfo {
private String name;
private String type;
private long size;
private LocalDateTime lastModified;
private boolean isDirectory;
// 构造函数、getter、setter省略
@Override
public String toString() {
return String.format("%-20s %-10s %12d %s",
name,
type,
size,
lastModified.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
}
}
public List<FileInfo> listFiles(String directory) throws IOException {
List<FileInfo> fileList = new ArrayList<>();
try (Stream<Path> paths = Files.list(Paths.get(directory))) {
paths.forEach(path -> {
try {
FileInfo info = new FileInfo();
info.setName(path.getFileName().toString());
BasicFileAttributes attrs = Files.readAttributes(
path, BasicFileAttributes.class);
info.setDirectory(attrs.isDirectory());
info.setType(attrs.isDirectory() ? "DIR" : getFileExtension(path));
info.setSize(attrs.size());
info.setLastModified(LocalDateTime.ofInstant(
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault()));
fileList.add(info);
} catch (IOException e) {
System.err.println("无法读取文件信息: " + path);
}
});
}
return fileList.stream()
.sorted(Comparator.comparing(FileInfo::isDirectory).reversed()
.thenComparing(FileInfo::getName))
.collect(Collectors.toList());
}
public void searchFiles(String directory, String pattern) throws IOException {
Path startDir = Paths.get(directory);
if (!Files.exists(startDir) || !Files.isDirectory(startDir)) {
throw new IllegalArgumentException("目录不存在: " + directory);
}
try (Stream<Path> paths = Files.walk(startDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().matches(pattern))
.forEach(System.out::println);
}
}
public long calculateDirectorySize(String directory) throws IOException {
Path dirPath = Paths.get(directory);
try (Stream<Path> paths = Files.walk(dirPath)) {
return paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (IOException e) {
return 0L;
}
})
.sum();
}
}
public void backupDirectory(String sourceDir, String backupDir) throws IOException {
Path source = Paths.get(sourceDir);
Path backup = Paths.get(backupDir);
if (!Files.exists(backup)) {
Files.createDirectories(backup);
}
try (Stream<Path> paths = Files.walk(source)) {
paths.forEach(sourcePath -> {
try {
Path targetPath = backup.resolve(source.relativize(sourcePath));
if (Files.isDirectory(sourcePath)) {
Files.createDirectories(targetPath);
} else {
Files.copy(sourcePath, targetPath,
StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
System.err.println("备份失败: " + sourcePath);
}
});
}
}
private String getFileExtension(Path path) {
String fileName = path.getFileName().toString();
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1).toUpperCase();
}
public static void main(String[] args) throws IOException {
SimpleFileManager manager = new SimpleFileManager();
// 示例使用
System.out.println("=== 当前目录文件列表 ===");
List<FileInfo> files = manager.listFiles(".");
files.forEach(System.out::println);
System.out.println("\n=== 目录大小 ===");
long size = manager.calculateDirectorySize(".");
System.out.println("总大小: " + size + " 字节 (" + (size/1024.0/1024.0) + " MB)");
System.out.println("\n=== 搜索Java文件 ===");
manager.searchFiles(".", ".*\.java$");
}
}
总结与进阶学习
核心要点回顾
- 理解流的概念:区分字节流和字符流,节点流和处理流
- 正确管理资源:始终使用try-with-resources
- 使用缓冲提高性能:特别是处理大文件时
- 掌握NIO.2 API:Path和Files类提供了更现代的接口
- 处理编码问题:明确指定字符编码,避免乱码
常见陷阱与解决方案
Java
// 陷阱1:未指定编码
// ❌
new FileReader("file.txt");
// ✅
new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8);
// 陷阱2:忘记flush缓冲区
// ❌ 数据可能丢失
writer.write("data");
// ✅
writer.write("data");
writer.flush(); // 或使用try-with-resources自动flush
// 陷阱3:不处理符号链接
// ❌ 可能陷入循环
Files.walk(dirPath);
// ✅
Files.walk(dirPath, FileVisitOption.FOLLOW_LINKS);