2. FileOutputStream 简介

FileOutputStreamjava.io 包中的基础类,提供了最简单直接的二进制文件写入方式。特别适合处理小文件或简单写入场景,它的API设计直观易用,是Java文件I/O的入门级选择。

以下代码演示了如何使用 FileOutputStream 写入字节数组:

byte[] data = "This is some data to write".getBytes();

try (FileOutputStream outputStream = new FileOutputStream("output.txt")) {
    outputStream.write(data);
} catch (IOException e) {
    // 异常处理
}

这段代码的核心逻辑:

  1. 将字符串转换为字节数组
  2. 通过 try-with-resources 自动管理资源
  3. 调用 write() 方法一次性写入数据

✅ 优点:

  • API 简单直观
  • 适合小文件操作
  • 自动资源管理(try-with-resources)

❌ 局限:

  • 不支持随机访问
  • 大文件处理性能一般

3. FileChannel 简介

FileChannel 属于 java.nio.channels 包,提供了更强大的文件操作能力。特别适合大文件处理、随机访问和高性能场景,通过缓冲区机制实现高效数据传输。

使用 FileChannel 写入文件的示例:

byte[] data = "This is some data to write".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);

try (FileChannel fileChannel = FileChannel.open(Path.of("output.txt"), 
  StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
    fileChannel.write(buffer);
} catch (IOException e) {
    // 异常处理
}

关键步骤解析:

  1. 创建 ByteBuffer 并包装字节数组
  2. 使用 FileChannel.open() 打开文件通道
  3. 指定写入和创建选项
  4. 通过通道写入缓冲区数据

⚠️ 注意点:

  • 需要理解缓冲区操作
  • 文件打开选项组合使用

4. 数据访问模式对比

4.1. FileOutputStream 的顺序访问

FileOutputStream 采用严格的顺序写入模式,只能从文件开头开始连续写入数据,不支持跳转到指定位置操作。

顺序写入示例:

byte[] data1 = "This is the first line.\n".getBytes();
byte[] data2 = "This is the second line.\n".getBytes();

try (FileOutputStream outputStream = new FileOutputStream("output.txt")) {
    outputStream.write(data1);
    outputStream.write(data2);
} catch (IOException e) {
    // 异常处理
}

执行结果:

This is the first line.
This is the second line.

❌ 限制:

  • 无法在文件中间插入数据
  • 修改文件内容需要重写整个文件

4.2. FileChannel 的随机访问

FileChannel 支持随机访问,可通过 position() 方法自由定位读写位置,实现灵活的文件操作。

随机写入示例:

ByteBuffer buffer1 = ByteBuffer.wrap(data1);
ByteBuffer buffer2 = ByteBuffer.wrap(data2);

try (FileChannel fileChannel = FileChannel.open(Path.of("output.txt"), 
  StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
    fileChannel.write(buffer1);

    fileChannel.position(10); // 定位到第10字节
    fileChannel.write(buffer2);
} catch (IOException e) {
    // 异常处理
}

✅ 优势:

  • 精确控制写入位置
  • 支持文件内容局部修改
  • 适合结构化文件操作

5. 并发与线程安全

5.1. FileOutputStream 的线程安全问题

FileOutputStream 本身不提供线程安全机制,多线程并发写入会导致数据混乱,必须通过外部同步控制。

线程安全写入方案:

final Object lock = new Object();

void writeToFile(String fileName, byte[] data) {
    synchronized (lock) {
        try (FileOutputStream outputStream = new FileOutputStream(fileName, true)) {
            outputStream.write(data);
            log.info("Data written by " + Thread.currentThread().getName());
        } catch (IOException e) {
            // 异常处理
        }
    }
}

多线程调用示例:

Thread thread1 = new Thread(() -> writeToFile("output.txt", data1));
Thread thread2 = new Thread(() -> writeToFile("output.txt", data2));

thread1.start();
thread2.start();

⚠️ 踩坑点:

  • 忘记同步会导致数据错乱
  • 同步粒度控制不当影响性能

5.2. FileChannel 的文件锁机制

FileChannel 内置文件锁功能,可通过 FileLock 实现文件区域的并发控制,避免数据竞争。

文件锁写入示例:

void writeToFileWithLock(String fileName, ByteBuffer buffer, int position) {
    try (FileChannel fileChannel = FileChannel.open(Path.of(fileName), 
        StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
        
        // 获取文件区域锁
        try (FileLock lock = fileChannel.lock(position, buffer.remaining(), false)) {
            fileChannel.position(position);
            fileChannel.write(buffer);
            log.info("Data written by " + Thread.currentThread().getName() + " at position " + position);
        } catch (IOException e) {
            // 异常处理
        }
    } catch (IOException e) {
        // 异常处理
    }
}

多线程调用示例:

Thread thread1 = new Thread(() -> writeToFileWithLock("output.txt", buffer1, 0));
Thread thread2 = new Thread(() -> writeToFileWithLock("output.txt", buffer2, 20));

thread1.start();
thread2.start();

✅ 优势:

  • 细粒度区域锁定
  • 跨进程同步支持
  • 避免全局锁的性能损耗

6. 性能对比分析

使用 JMH 基准测试对比两种方案处理大文件(1GB数据)的性能:

@Setup
public void setup() {
    largeData = new byte[1000 * 1024 * 1024]; // 1GB数据
    Arrays.fill(largeData, (byte) 1);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testFileOutputStream() {
    try (FileOutputStream outputStream = new FileOutputStream("largeOutputStream.txt")) {
        outputStream.write(largeData);
    } catch (IOException e) {
        // 异常处理
    }
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testFileChannel() {
    ByteBuffer buffer = ByteBuffer.wrap(largeData);
    try (FileChannel fileChannel = FileChannel.open(Path.of("largeFileChannel.txt"), 
        StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
        fileChannel.write(buffer);
    } catch (IOException e) {
        // 异常处理
    }
}

测试执行代码:

Options opt = new OptionsBuilder()
  .include(FileIOBenchmark.class.getSimpleName())
  .forks(1)
  .build();

new Runner(opt).run();

测试结果(平均耗时):

Benchmark                             Mode  Cnt    Score    Error  Units
FileIOBenchmark.testFileChannel       avgt    5  431.414 ± 52.229  ms/op
FileIOBenchmark.testFileOutputStream  avgt    5  556.102 ± 91.512  ms/op

性能分析结论:

  • FileOutputStream 采用阻塞式I/O,大文件处理时性能损耗明显
  • FileChannel 支持内存映射文件(Memory-Mapped I/O),可实现零拷贝数据传输
  • 高频I/O场景下 FileChannel 优势更显著

7. 总结

两种文件写入方案的核心差异总结:

特性 FileOutputStream FileChannel
适用场景 小文件/简单写入 大文件/高性能需求
访问模式 顺序访问 随机访问
并发控制 需外部同步 内置文件锁
性能表现 一般(大文件较慢) 优秀(尤其大文件)
API复杂度 简单直观 需理解缓冲区机制

选择建议

  1. 简单脚本/小文件处理 → FileOutputStream
  2. 大文件/高性能需求 → FileChannel
  3. 需要随机访问 → 必选 FileChannel
  4. 多线程环境 → 优先 FileChannel + 文件锁

源码示例可在 GitHub仓库 获取完整实现。


原始标题:Guide to FileOutputStream vs. FileChannel | Baeldung