Java IO流教程:从入门到最佳实践

Java的输入/输出(IO)系统是程序与外部世界(如文件系统、网络、内存)进行数据交互的核心。它提供了一套强大但略显复杂的类库。本教程将带你从基础概念入手,逐步掌握Java IO的核心用法和最佳实践。

一、核心概念:流(Stream)

在Java中,流(Stream) 是对数据传输通道的抽象。你可以把它想象成一根管道,数据从源头(Source)通过这根管道流入程序(输入流),或者从程序通过这根管道流向目的地(Destination)(输出流)。

根据处理数据的类型,Java IO流主要分为两大类:

  1. 字节流(Byte Stream)
    • 处理单位 :8位字节(byte)。
    • 核心抽象类InputStream(输入)和 OutputStream(输出)。
    • 适用场景 :处理所有类型的数据,尤其是二进制数据,如图片、音频、视频、可执行文件等。字节流是"万能"的,因为它不关心数据内容,只负责搬运字节。
  2. 字符流(Character Stream)
    • 处理单位 :16位字符(char)。
    • 核心抽象类Reader(输入)和 Writer(输出)。
    • 适用场景 :专门用于处理文本数据.txt, .java, .xml等)。字符流在底层会自动处理字节与字符之间的编码转换,使得读写文本更加方便,但必须注意字符编码问题。

一句话总结:处理文本用字符流,处理其他所有文件用字节流。

二、基石:File类

java.io.File 类是文件和目录路径名的抽象表示。它本身用于读写文件内容,而是用于操作文件和目录的元数据,如创建、删除、重命名、判断是否存在等。

常用方法一览

方法分类 方法签名 说明
创建 boolean createNewFile() 原子性地创建一个新文件。
boolean mkdir() 创建一个目录。
boolean mkdirs() 创建一个目录,包括任何必需但不存在的父目录。
删除 boolean delete() 删除此抽象路径名表示的文件或目录。
判断 boolean exists() 测试此抽象路径名表示的文件或目录是否存在。
boolean isFile() 测试此抽象路径名表示的文件是否是一个标准文件。
boolean isDirectory() 测试此抽象路径名表示的文件是否是一个目录。
获取信息 String getName() 返回由此抽象路径名表示的文件或目录的名称。
String getAbsolutePath() 返回此抽象路径名的绝对路径名字符串。
long length() 返回由此抽象路径名表示的文件的长度(字节)。

代码示例

复制代码
import java.io.File;
import java.io.IOException;

public class FileDemo {
    public static void main(String[] args) {
        // 创建一个File对象,指向 "test_dir/test_file.txt"
        File file = new File("test_dir", "test_file.txt");

        // 1. 获取并打印父目录的绝对路径
        File parentDir = file.getParentFile();
        System.out.println("父目录路径: " + parentDir.getAbsolutePath());

        // 2. 如果父目录不存在,则创建它(包括所有不存在的父级)
        if (!parentDir.exists()) {
            boolean dirCreated = parentDir.mkdirs();
            System.out.println("目录创建" + (dirCreated ? "成功" : "失败"));
        }

        // 3. 如果文件不存在,则创建它
        try {
            if (!file.exists()) {
                boolean fileCreated = file.createNewFile();
                System.out.println("文件创建" + (fileCreated ? "成功" : "失败"));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 4. 打印文件信息
        System.out.println("文件名: " + file.getName());
        System.out.println("是否是一个文件: " + file.isFile());
    }
}
三、字节流实战:复制任意文件

字节流是处理二进制数据的首选。下面我们通过复制一张图片来演示 FileInputStreamFileOutputStream 的用法。

关键点 :使用缓冲区(byte[] buffer)可以极大地提高IO效率,避免频繁的磁盘读写。

复制代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamCopy {
    public static void main(String[] args) {
        // 源文件和目标文件路径
        String sourcePath = "source_image.jpg";
        String destPath = "dest_image.jpg";

        // 使用 try-with-resources 语句,自动关闭流
        try (FileInputStream fis = new FileInputStream(sourcePath);
             FileOutputStream fos = new FileOutputStream(destPath)) {

            byte[] buffer = new byte[8192]; // 创建一个8KB的缓冲区
            int bytesRead;
            // 从输入流中读取数据,直到文件末尾(read返回-1)
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 将读取到的数据写入输出流
                fos.write(buffer, 0, bytesRead);
            }
            System.out.println("文件复制成功!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
四、字符流实战:读写文本文件

字符流专为文本设计。我们使用 FileReaderFileWriter 来读写一个简单的文本文件。

重要警告FileReaderFileWriter 会使用操作系统的默认字符编码 (例如在中文Windows上是GBK)。如果文件是UTF-8编码,直接读写会导致中文乱码

最佳实践 :使用 InputStreamReaderOutputStreamWriter 作为"桥梁",并显式指定字符编码(如 StandardCharsets.UTF_8)。

复制代码
import java.io.*;
import java.nio.charset.StandardCharsets;

public class CharacterStreamDemo {
    public static void main(String[] args) {
        String filePath = "test.txt";
        String contentToWrite = "你好,Java IO!\nHello, World!";

        // --- 写入文件 ---
        // 使用 OutputStreamWriter 并指定 UTF-8 编码,避免乱码
        try (OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream(filePath), StandardCharsets.UTF_8);
             BufferedWriter writer = new BufferedWriter(osw)) {

            writer.write(contentToWrite);
            System.out.println("文本写入成功!");

        } catch (IOException e) {
            e.printStackTrace();
        }

        // --- 读取文件 ---
        // 使用 InputStreamReader 并指定 UTF-8 编码,确保正确读取
        try (InputStreamReader isr = new InputStreamReader(
                new FileInputStream(filePath), StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {

            String line;
            System.out.println("--- 文件内容 ---");
            // readLine() 方法可以按行读取,非常方便
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
五、性能优化:缓冲流(Buffered Stream)

无论是字节流还是字符流,直接使用底层的 FileInputStreamFileReader 进行逐字节/字符的读写,效率都非常低下,因为每次读写都可能触发一次昂贵的磁盘I/O操作。

缓冲流BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter)通过在内存中维护一个缓冲区来解决这个问题。它们一次性从磁盘读取一大块数据到缓冲区,后续的读取操作直接从内存中获取,极大地减少了磁盘访问次数。

性能对比:复制一个10MB的文件,使用缓冲流比不使用快数百倍。

如何使用 :非常简单,只需将节点流(如 FileInputStream)作为参数传递给缓冲流的构造函数即可。

复制代码
// 字节缓冲流示例
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.jpg"));
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dest.jpg"))) {
    // ... 读写逻辑与之前相同,但效率极高
}

// 字符缓冲流示例
try (BufferedReader br = new BufferedReader(new FileReader("source.txt"));
     BufferedWriter bw = new BufferedWriter(new FileWriter("dest.txt"))) {
    String line;
    while ((line = br.readLine()) != null) { // readLine() 是BufferedReader的独有方法
        bw.write(line);
        bw.newLine(); // 写入一个平台相关的换行符
    }
}
六、资源管理:try-with-resources

在Java 7之前,我们必须在一个 finally 块中手动关闭所有打开的流,代码冗长且容易出错。

Java 7引入了 try-with-resources 语句,它确保每个声明的资源在语句结束时都会被自动关闭。任何实现了 java.lang.AutoCloseable 接口的对象都可以在此语句中使用,而所有的IO流类都实现了这个接口。

语法

复制代码
try (ResourceType resource = new ResourceType()) {
    // 使用资源
} catch (Exception e) {
    // 处理异常
}

优势

  • 代码简洁 :无需编写 finally 块。
  • 安全可靠 :即使 try 块中发生异常,资源也会被自动、正确地关闭,有效防止资源泄漏(如"Too many open files"错误)。

本教程中所有代码示例均已采用此最佳实践。

七、高级主题:对象序列化

Java提供了一种机制,可以将对象的状态(即对象的成员变量值)转换为字节流,以便保存到文件中或通过网络传输。这个过程称为序列化(Serialization) 。反之,从字节流恢复对象的过程称为反序列化(Deserialization)

如何使一个类可序列化

只需让该类实现 java.io.Serializable 接口。这是一个标记接口,内部没有方法。

复制代码
import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    // 建议声明一个序列版本号,用于验证序列化与反序列化版本的兼容性
    private static final long serialVersionUID = 1L;

    private String username;
    private transient Date lastLoginTime; // transient关键字表示此字段不参与序列化

    public User(String username) {
        this.username = username;
        this.lastLoginTime = new Date();
    }

    // ... getter, setter, toString 等方法
}

序列化与反序列化示例

复制代码
import java.io.*;

public class SerializationDemo {
    public static void main(String[] args) {
        User user = new User("alice");
        String filePath = "user.ser";

        // --- 序列化:将对象写入文件 ---
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) {
            oos.writeObject(user);
            System.out.println("对象序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // --- 反序列化:从文件读取对象 ---
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) {
            User loadedUser = (User) ois.readObject();
            System.out.println("对象反序列化成功!");
            System.out.println("用户名: " + loadedUser.username);
            // transient字段在反序列化后为null
            System.out.println("上次登录时间: " + loadedUser.lastLoginTime);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
总结与最佳实践
  1. 明确数据流类型 :处理文本用字符流(Reader/Writer),处理二进制文件用字节流(InputStream/OutputStream)。
  2. 始终指定字符编码 :使用 InputStreamReader/OutputStreamWriter 并显式传入 StandardCharsets.UTF_8,彻底告别中文乱码。
  3. 拥抱缓冲流 :永远使用 Buffered* 流来包装节点流,这是提升IO性能最简单有效的方法。
  4. 使用 try-with-resources:这是管理IO资源的标准方式,能自动关闭流,保证代码的健壮性。
  5. 了解NIO :对于更复杂、高性能的IO需求(如网络服务器、大文件映射),可以进一步学习Java NIO(New IO)和NIO.2(java.nio.file 包)。
相关推荐
好家伙VCC2 小时前
**发散创新:用 Rust实现数据编织(DataWrangling)的高效流式处理架构**在现
java·开发语言·python·架构·rust
要开心吖ZSH2 小时前
MP4 转 WAV 音频转码方案详解(ProcessBuilder + FFmpeg)
java·ffmpeg·音视频
Memory_荒年2 小时前
Netty深度解构:高性能背后的核心机制与实战精要
java·后端
红云梦2 小时前
互联网三高-高性能之多级缓存架构
java·redis·缓存·架构·cdn
222you2 小时前
线程池的三个方法,七个参数,四个拒绝策略
java·开发语言
m0_716765232 小时前
C++提高编程--仿函数、常用遍历算法(for_each、transform)详解
java·开发语言·c++·经验分享·算法·青少年编程·visual studio
平平无奇的开发仔2 小时前
通过@Transational注解的对象,简单了解SpringAop是如何执行的
后端
元俭2 小时前
【Eino 框架入门】用 Agent 实现多轮对话
后端
斯瓦辛武2 小时前
linux系统安装skywalking
后端