Java IO 与 NIO
Java 提供了丰富的 I/O(输入/输出)操作 API。从 Java 1.4 开始,引入了 NIO(New I/O),提供了更高效的 I/O 操作方式。
IO 概述
IO 分类
Java IO 主要分为以下几类:
| 分类 | 字节流 | 字符流 |
|---|---|---|
| 输入 | InputStream | Reader |
| 输出 | OutputStream | Writer |
字节流 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
| 特性 | IO | NIO |
|---|---|---|
| 模型 | 阻塞式 | 非阻塞式/选择器 |
| 操作 | 面向流 | 面向缓冲区 |
| 效率 | 较低 | 较高 |
| 适用 | 连接数少 | 连接数多 |
NIO 核心组件
- Buffer(缓冲区):存储数据
- Channel(通道):传输数据
- Selector(选择器):多路复用器
Buffer 缓冲区
Buffer 是一个容器,用于存储基本数据类型。
Buffer 类型
ByteBufferCharBufferShortBufferIntBufferLongBufferFloatBufferDoubleBuffer
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)) {
// ...
}
小结
本章我们学习了:
- IO 概述:字节流与字符流、IO 类层次结构
- 文件操作:File 类、文件过滤器
- 字节流:FileInputStream/FileOutputStream、BufferedInputStream/BufferedOutputStream、DataInputStream/DataOutputStream
- 字符流:FileReader/FileWriter、BufferedReader/BufferedWriter、InputStreamReader/OutputStreamWriter
- 标准输入输出:System.in/out/err、Scanner
- 序列化:Serializable 接口、ObjectInputStream/ObjectOutputStream
- NIO 基础:Buffer、Channel、FileChannel
- NIO.2:Path、Files 工具类
练习
- 实现一个文件复制工具,支持大文件复制
- 使用 BufferedReader 和 BufferedWriter 实现文件的行处理
- 使用 Files API 实现目录遍历,统计各类文件数量
- 实现一个简单的对象序列化和反序列化工具
- 使用 NIO 的 FileChannel 实现高效的文件复制