跳到主要内容

Java IO 与 NIO

Java 提供了丰富的 I/O(输入/输出)操作 API。从 Java 1.4 开始,引入了 NIO(New I/O),提供了更高效的 I/O 操作方式。

IO 概述

IO 分类

Java IO 主要分为以下几类:

分类字节流字符流
输入InputStreamReader
输出OutputStreamWriter

字节流 vs 字符流

  • 字节流:以字节为单位,处理二进制数据(图片、视频、音频等)
  • 字符流:以字符为单位,处理文本数据,自动处理编码转换

IO 类层次结构

字节输入流:
InputStream
├── FileInputStream
├── ByteArrayInputStream
├── PipedInputStream
├── FilterInputStream
│ ├── BufferedInputStream
│ └── DataInputStream
└── ObjectInputStream

字节输出流:
OutputStream
├── FileOutputStream
├── ByteArrayOutputStream
├── PipedOutputStream
├── FilterOutputStream
│ ├── BufferedOutputStream
│ └── DataOutputStream
└── ObjectOutputStream

字符输入流:
Reader
├── FileReader
├── CharArrayReader
├── PipedReader
├── BufferedReader
├── InputStreamReader
└── StringReader

字符输出流:
Writer
├── FileWriter
├── CharArrayWriter
├── PipedWriter
├── BufferedWriter
├── OutputStreamWriter
└── StringWriter

文件操作

File 类

File 类表示文件或目录的路径:

import java.io.File;

// 创建 File 对象
File file1 = new File("test.txt");
File file2 = new File("/path/to", "file.txt");
File dir = new File("/path/to/directory");

// 文件信息
System.out.println("文件名: " + file1.getName());
System.out.println("路径: " + file1.getPath());
System.out.println("绝对路径: " + file1.getAbsolutePath());
System.out.println("父目录: " + file1.getParent());
System.out.println("是否存在: " + file1.exists());
System.out.println("是否文件: " + file1.isFile());
System.out.println("是否目录: " + file1.isDirectory());
System.out.println("文件大小: " + file1.length() + " 字节");
System.out.println("最后修改: " + new Date(file1.lastModified()));
System.out.println("可读: " + file1.canRead());
System.out.println("可写: " + file1.canWrite());
System.out.println("可执行: " + file1.canExecute());

// 文件操作
file1.createNewFile(); // 创建文件
file1.delete(); // 删除文件
file1.renameTo(new File("new_name.txt")); // 重命名

// 目录操作
dir.mkdir(); // 创建单级目录
dir.mkdirs(); // 创建多级目录
String[] children = dir.list(); // 列出子文件名
File[] files = dir.listFiles(); // 列出子文件对象

// 遍历目录
public static void listFiles(File dir) {
if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
listFiles(file); // 递归
} else {
System.out.println(file.getAbsolutePath());
}
}
}
}
}

文件过滤器

import java.io.File;
import java.io.FilenameFilter;

// 使用 FilenameFilter
File dir = new File(".");
String[] txtFiles = dir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".txt");
}
});

// 使用 Lambda
String[] javaFiles = dir.list((d, name) -> name.endsWith(".java"));

// 使用 FileFilter
File[] files = dir.listFiles(file -> file.isFile() && file.length() > 1024);

字节流

FileInputStream / FileOutputStream

用于读写二进制文件:

import java.io.*;

// 写入文件
try (FileOutputStream fos = new FileOutputStream("test.bin")) {
byte[] data = {65, 66, 67, 68, 69};
fos.write(data);
fos.write(70); // 写入单个字节
} catch (IOException e) {
e.printStackTrace();
}

// 读取文件
try (FileInputStream fis = new FileInputStream("test.bin")) {
// 方式1:逐字节读取
int b;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}

// 方式2:读取到字节数组
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}

// 方式3:读取全部(小文件)
byte[] allBytes = fis.readAllBytes(); // Java 9+
}

// 复制文件
public static void copyFile(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
}

BufferedInputStream / BufferedOutputStream

使用缓冲提高性能:

import java.io.*;

// 使用缓冲流复制文件
public static void copyWithBuffer(String src, String dest) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
}
}

// 读取文件
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.bin"))) {
int b;
while ((b = bis.read()) != -1) {
// 处理字节
}
}

// 写入文件
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.bin"))) {
bos.write("Hello".getBytes());
bos.flush(); // 显式刷新缓冲区(try-with-resources 会自动调用)
}

DataInputStream / DataOutputStream

读写基本数据类型:

import java.io.*;

// 写入基本数据类型
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
dos.writeInt(123);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("Hello");
}

// 读取基本数据类型(必须按写入顺序读取)
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
int i = dis.readInt(); // 123
double d = dis.readDouble(); // 3.14
boolean b = dis.readBoolean(); // true
String s = dis.readUTF(); // "Hello"
}

ByteArrayInputStream / ByteArrayOutputStream

内存中的字节流:

import java.io.*;

// 写入到内存
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("Hello".getBytes());
baos.write(" World".getBytes());

byte[] data = baos.toByteArray();
System.out.println(new String(data)); // Hello World

// 从内存读取
ByteArrayInputStream bais = new ByteArrayInputStream(data);
int b;
while ((b = bais.read()) != -1) {
System.out.print((char) b);
}

字符流

FileReader / FileWriter

用于读写文本文件:

import java.io.*;

// 写入文件
try (FileWriter fw = new FileWriter("test.txt")) {
fw.write("Hello, World!\n");
fw.write("你好,世界!");
} catch (IOException e) {
e.printStackTrace();
}

// 追加写入
try (FileWriter fw = new FileWriter("test.txt", true)) {
fw.write("\n追加的内容");
}

// 读取文件
try (FileReader fr = new FileReader("test.txt")) {
// 方式1:逐字符读取
int c;
while ((c = fr.read()) != -1) {
System.out.print((char) c);
}

// 方式2:读取到字符数组
char[] buffer = new char[1024];
int len;
while ((len = fr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}

BufferedReader / BufferedWriter

带缓冲的字符流,可以按行读写:

import java.io.*;

// 写入文件
try (BufferedWriter bw = new BufferedWriter(new FileWriter("test.txt"))) {
bw.write("第一行");
bw.newLine(); // 写入换行符
bw.write("第二行");
bw.newLine();
bw.write("第三行");
}

// 读取文件
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
// 方式1:逐行读取
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

// 方式2:读取到字符数组
char[] buffer = new char[1024];
int len;
while ((len = br.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}

// Java 8+ 读取所有行
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
br.lines().forEach(System.out::println);
}

InputStreamReader / OutputStreamWriter

字节流与字符流的桥梁,可以指定编码:

import java.io.*;

// 使用指定编码写入
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("test.txt"), "UTF-8")) {
osw.write("你好,世界!");
}

// 使用指定编码读取
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("test.txt"), "UTF-8")) {
char[] buffer = new char[1024];
int len;
while ((len = isr.read(buffer)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}

// 结合 BufferedReader 使用
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("test.txt"), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}

PrintWriter

提供格式化输出:

import java.io.*;

// 写入文件
try (PrintWriter pw = new PrintWriter(new FileWriter("output.txt"))) {
pw.println("Hello, World!");
pw.printf("姓名: %s, 年龄: %d%n", "张三", 25);
pw.format("价格: %.2f%n", 123.456);

// 不抛出 IOException,可以检查错误
if (pw.checkError()) {
System.out.println("写入出错");
}
}

// 输出到控制台
PrintWriter console = new PrintWriter(System.out, true); // autoFlush
console.println("输出到控制台");

标准输入输出

System.in / System.out / System.err

import java.io.*;
import java.util.Scanner;

// 标准输出
System.out.println("标准输出");
System.err.println("错误输出");

// 标准输入(原始方式)
try (InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr)) {
System.out.print("请输入: ");
String line = br.readLine();
System.out.println("输入内容: " + line);
}

// 使用 Scanner(推荐)
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("请输入姓名: ");
String name = scanner.nextLine();

System.out.print("请输入年龄: ");
int age = scanner.nextInt();

System.out.println("姓名: " + name + ", 年龄: " + age);
}

// 重定向标准流
System.setIn(new FileInputStream("input.txt"));
System.setOut(new PrintStream(new FileOutputStream("output.txt")));
System.setErr(new PrintStream(new FileOutputStream("error.txt")));

序列化

Serializable 接口

Java 对象序列化可以将对象转换为字节流,便于存储或传输:

import java.io.*;

// 可序列化的类
public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 版本号

private String name;
private int age;
private transient String password; // transient 字段不参与序列化

public Person(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}

@Override
public String toString() {
return "Person{name=" + name + ", age=" + age + ", password=" + password + "}";
}
}

// 序列化对象
public static void serialize(Object obj, String filename) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(filename))) {
oos.writeObject(obj);
}
}

// 反序列化对象
public static Object deserialize(String filename)
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(filename))) {
return ois.readObject();
}
}

// 使用
Person person = new Person("张三", 25, "123456");
serialize(person, "person.ser");

Person restored = (Person) deserialize("person.ser");
System.out.println(restored); // Person{name=张三, age=25, password=null}

序列化的注意事项

public class Employee implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private Department department; // Department 也必须实现 Serializable

// 静态字段不参与序列化
private static int count = 0;

// transient 字段不参与序列化
private transient String password;

// 自定义序列化逻辑
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 默认序列化
// 可以在这里加密敏感数据
}

private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 默认反序列化
// 可以在这里解密敏感数据
}
}

NIO 概述

NIO(New I/O)是 Java 1.4 引入的 I/O API,提供了更高效的 I/O 操作。

NIO vs IO

特性IONIO
模型阻塞式非阻塞式/选择器
操作面向流面向缓冲区
效率较低较高
适用连接数少连接数多

NIO 核心组件

  1. Buffer(缓冲区):存储数据
  2. Channel(通道):传输数据
  3. Selector(选择器):多路复用器

Buffer 缓冲区

Buffer 是一个容器,用于存储基本数据类型。

Buffer 类型

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

Buffer 使用

import java.nio.*;

// 创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024); // 堆内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 直接内存

// 写入数据
buffer.put((byte) 65);
buffer.put(new byte[]{66, 67, 68});

// 切换到读模式
buffer.flip();

// 读取数据
byte b1 = buffer.get(); // 65
byte[] arr = new byte[3];
buffer.get(arr); // [66, 67, 68]

// 重置 position
buffer.rewind(); // position = 0,可以重新读取

// 标记和重置
buffer.mark();
buffer.get(); // 移动 position
buffer.reset(); // 回到 mark 位置

// 清空缓冲区
buffer.clear(); // position = 0, limit = capacity(数据未清除,只是指针重置)

// 常用属性
System.out.println("容量: " + buffer.capacity());
System.out.println("当前位置: " + buffer.position());
System.out.println("限制: " + buffer.limit());
System.out.println("剩余: " + buffer.remaining());
System.out.println("是否有剩余: " + buffer.hasRemaining());

Buffer 工作流程

写入模式:
capacity
┌───────────────────────────────────┐
│ 数据 │ 数据 │ 数据 │ 空闲 │
└───────────────────────────────────┘
position limit

flip() 后(读取模式):
capacity
┌───────────────────────────────────┐
│ 数据 │ 数据 │ 数据 │ 空闲 │
└───────────────────────────────────┘
position limit

Channel 通道

Channel 是双向的,可以同时进行读写操作。

FileChannel

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

// 从 FileInputStream 获取 Channel
try (FileChannel channel = new FileInputStream("input.txt").getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 读取到 Buffer
while (channel.read(buffer) != -1) {
buffer.flip();

while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}

buffer.clear();
}
}

// 写入文件
try (FileChannel channel = new FileOutputStream("output.txt").getChannel()) {
ByteBuffer buffer = ByteBuffer.wrap("Hello, NIO!".getBytes());
channel.write(buffer);
}

// 使用 RandomAccessFile 读写
try (RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel channel = file.getChannel()) {

// 读取
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
channel.read(readBuffer);

// 写入
ByteBuffer writeBuffer = ByteBuffer.wrap("新数据".getBytes());
channel.write(writeBuffer);

// 获取/设置位置
long position = channel.position();
channel.position(0); // 移动到开头

// 获取文件大小
long size = channel.size();

// 截断文件
channel.truncate(1024);
}

// 使用 transferTo/transferFrom 复制文件(零拷贝)
try (FileChannel src = new FileInputStream("src.txt").getChannel();
FileChannel dest = new FileOutputStream("dest.txt").getChannel()) {

// 方式1:transferTo
src.transferTo(0, src.size(), dest);

// 方式2:transferFrom
dest.transferFrom(src, 0, src.size());
}

Path 和 Files(Java 7+ NIO.2)

Java 7 引入了 NIO.2,提供了更好的文件操作 API。

Path 类

import java.nio.file.*;

// 创建 Path
Path path1 = Path.of("test.txt"); // Java 11+
Path path2 = Paths.get("test.txt"); // Java 7+
Path path3 = FileSystems.getDefault().getPath("test.txt");

Path absPath = Path.of("/home/user/docs/file.txt");
Path relPath = Path.of("docs/file.txt");

// Path 信息
System.out.println("文件名: " + path1.getFileName());
System.out.println("父路径: " + path1.getParent());
System.out.println("根路径: " + path1.getRoot());
System.out.println("绝对路径: " + path1.toAbsolutePath());
System.out.println("元素数量: " + path1.getNameCount());

// 遍历路径元素
for (Path element : path1) {
System.out.println(element);
}

// 路径组合
Path combined = Path.of("/home", "user", "docs"); // /home/user/docs
Path resolved = path1.resolve("subdir/file.txt"); // 在当前路径后添加
Path sibling = path1.resolveSibling("other.txt"); // 替换文件名

// 路径规范化
Path normalized = Path.of("/home/user/../docs/./file.txt").normalize();
// 结果: /home/docs/file.txt

// 路径比较
Path p1 = Path.of("/home/user");
Path p2 = Path.of("/home/user");
System.out.println(p1.equals(p2)); // true
System.out.println(p1.startsWith(p2)); // true

Files 工具类

import java.nio.file.*;
import java.nio.file.attribute.*;

// 文件操作
Path file = Path.of("test.txt");

// 创建文件/目录
Files.createFile(file);
Files.createDirectory(Path.of("mydir"));
Files.createDirectories(Path.of("a/b/c/d")); // 创建多级目录
Files.createTempFile("prefix", ".tmp"); // 创建临时文件
Files.createTempDirectory("temp"); // 创建临时目录

// 读写文件
// 小文件读写
List<String> lines = Files.readAllLines(file); // 读取所有行
String content = Files.readString(file); // Java 11+,读取全部内容
byte[] bytes = Files.readAllBytes(file); // 读取字节数组

Files.write(file, "Hello".getBytes()); // 写入字节
Files.writeString(file, "Hello\nWorld"); // Java 11+,写入字符串
Files.write(file, List.of("Line1", "Line2")); // 写入行列表

// 追加内容
Files.write(file, "追加内容".getBytes(), StandardOpenOption.APPEND);

// 复制、移动、删除
Files.copy(Path.of("src.txt"), Path.of("dest.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.move(Path.of("old.txt"), Path.of("new.txt"),
StandardCopyOption.REPLACE_EXISTING);
Files.delete(file); // 文件不存在会抛异常
Files.deleteIfExists(file); // 文件不存在返回 false

// 文件信息
System.out.println("大小: " + Files.size(file));
System.out.println("是否存在: " + Files.exists(file));
System.out.println("是否文件: " + Files.isRegularFile(file));
System.out.println("是否目录: " + Files.isDirectory(file));
System.out.println("是否可读: " + Files.isReadable(file));
System.out.println("是否可写: " + Files.isWritable(file));
System.out.println("是否隐藏: " + Files.isHidden(file));
System.out.println("最后修改: " + Files.getLastModifiedTime(file));
System.out.println("所有者: " + Files.getOwner(file));

// 文件属性
BasicFileAttributes attrs = Files.readAttributes(file, 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.isRegularFile());
System.out.println("大小: " + attrs.size());

// 遍历目录
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("."))) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}

// 过滤目录内容
try (DirectoryStream<Path> stream = Files.newDirectoryStream(
Path.of("."), "*.java")) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}

// 递归遍历(深度优先)
Files.walkFileTree(Path.of("."), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("文件: " + file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("目录: " + dir);
return FileVisitResult.CONTINUE;
}
});

// 使用 Stream 遍历(Java 8+)
Files.list(Path.of(".")).forEach(System.out::println); // 当前目录
Files.walk(Path.of(".")).forEach(System.out::println); // 递归遍历
Files.find(Path.of("."), Integer.MAX_VALUE,
(path, attrs) -> path.toString().endsWith(".java")) // 查找文件
.forEach(System.out::println);

// 读写文件流
try (BufferedReader reader = Files.newBufferedReader(file);
BufferedWriter writer = Files.newBufferedWriter(file)) {
// ...
}

// 文件通道
try (SeekableByteChannel channel = Files.newByteChannel(file,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// ...
}

小结

本章我们学习了:

  1. IO 概述:字节流与字符流、IO 类层次结构
  2. 文件操作:File 类、文件过滤器
  3. 字节流:FileInputStream/FileOutputStream、BufferedInputStream/BufferedOutputStream、DataInputStream/DataOutputStream
  4. 字符流:FileReader/FileWriter、BufferedReader/BufferedWriter、InputStreamReader/OutputStreamWriter
  5. 标准输入输出:System.in/out/err、Scanner
  6. 序列化:Serializable 接口、ObjectInputStream/ObjectOutputStream
  7. NIO 基础:Buffer、Channel、FileChannel
  8. NIO.2:Path、Files 工具类

练习

  1. 实现一个文件复制工具,支持大文件复制
  2. 使用 BufferedReader 和 BufferedWriter 实现文件的行处理
  3. 使用 Files API 实现目录遍历,统计各类文件数量
  4. 实现一个简单的对象序列化和反序列化工具
  5. 使用 NIO 的 FileChannel 实现高效的文件复制