游乐游手机版
首页/编程语言/文章详情

Java字节流FileInputStream与FileOutputStream入门到实践

时间:2026-06-14 06:51
FileInputStream和FileOutputStream是Java字节流中处理文件读写的核心类,支持所有类型文件。批量读写(字节数组)比逐字节读写性能高数倍。需用try-with-resources确保流关闭。写入时应用write(buffer,0,len)避免脏数据。纯文本场景优先使用字符流或NIO工具类。

一、前言

谈到 Java 的文件读写操作,字节流可以说是最基础也最高频的 I/O 场景之一。作为 Java I/O 体系的两大核心分支,字节流专门负责处理所有二进制数据的传输——包括图片、音频、视频以及任何非文本文件。而 FileInputStreamFileOutputStream 正是这个体系中最关键的两个实现类。深入理解它们,就等于拿到了 Java 文件 I/O 的入门钥匙,为后续的高阶操作打下坚实基础。

Java字节流FileInputStream和FileOutputStream从入门到实践

本文将围绕这两个类的构造方法、核心 API、使用方式、底层原理以及常见陷阱展开详细讲解,确保你学完后能够编写出既健壮又高效的文件读写代码,从容应对日常开发中的字节流操作需求。

二、字节流简介

Java 的 I/O 流主要分为两大类:

  • 字节流(Byte Stream):以 InputStream / OutputStream 为抽象父类,按字节(8 bit)进行读写,适用于所有类型的文件,包括二进制文件。
  • 字符流(Character Stream):以 Reader / Writer 为抽象父类,按字符(16 bit)进行读写,更适合纯文本文件。

FileInputStreamFileOutputStream 分别继承自 InputStreamOutputStream,是操作文件最直接、最常用的入口类。

 InputStream (抽象)
    └── FileInputStream    ← 从文件读取字节
 OutputStream (抽象)
    └── FileOutputStream   ← 向文件写入字节

三、核心 API 速览

3.1 FileInputStream

方法签名说明
FileInputStream(String name)根据文件路径创建输入流
FileInputStream(File file)根据 File 对象创建输入流
int read()读取一个字节,返回 0~255 的 int,读到末尾返回 -1
int read(byte[] b)读取最多 b.length 个字节到数组,返回实际读取的字节数
int read(byte[] b, int off, int len)读取最多 len 个字节到数组的指定偏移位置
long skip(long n)跳过并丢弃 n 个字节
int available()返回可读的字节数估计值
void close()关闭流,释放系统资源

3.2 FileOutputStream

方法签名说明
FileOutputStream(String name)根据路径创建输出流(覆盖模式)
FileOutputStream(String name, boolean append)指定是否追加模式
FileOutputStream(File file)根据 File 对象创建输出流
void write(int b)写入一个字节(只取 int 的低 8 位)
void write(byte[] b)写入整个字节数组
void write(byte[] b, int off, int len)写入数组的一部分
void flush()刷新缓冲区(字节流 flush 是空实现,但遵循约定)
void close()关闭流,释放系统资源

四、代码实现

4.1 基础用法:单个字节读写(不推荐用于大文件)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileInputStreamDemo {

    public static void main(String[] args) throws IOException {
        // 用 try-with-resources 自动关闭流(JDK 7+)
        try (FileInputStream fis = new FileInputStream("source.txt");
             FileOutputStream fos = new FileOutputStream("dest.txt")) {

            int data;
            while ((data = fis.read()) != -1) {
                fos.write(data);
            }
        }
        System.out.println("文件复制完成");
    }
}

缺点:每次只读写一个字节,每次 read()/write() 都涉及一次 native 调用,处理大文件时性能极差,不推荐用于生产环境。

4.2 推荐用法:字节数组批量读写

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyWithBuffer {

    public static void main(String[] args) {
        String source = "photo.jpg";
        String dest = "photo_copy.jpg";

        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(dest)) {

            byte[] buffer = new byte[8192]; // 8KB 缓冲区
            int len;

            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
                // 注意:必须使用 write(buffer, 0, len) 而不是 write(buffer)
                // 因为最后一次读取可能不足 8192 字节
            }

            System.out.println("复制完成");

        } catch (IOException e) {
            System.err.println("文件操作失败: " + e.getMessage());
        }
    }
}

4.3 追加模式写入

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class FileAppendDemo {

    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
            String line = "[" + System.currentTimeMillis() + "] 用户登录成功\n";
            fos.write(line.getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

FileOutputStream 的第二个参数 appendtrue 时,写入内容会追加到文件末尾而非覆盖原内容,非常适合日志记录等场景。

五、执行流程与底层原理

read(byte[]) 为例,一次读取的完整链路如下:

  1. Java 应用层 调用 FileInputStream.read(byte[])
  2. JDK 内部 调用 native readBytes 方法
  3. JNI 进入 C 层,调用操作系统的 ReadFile(Windows)或 read()(Linux/POSIX)
  4. 内核 从磁盘读取数据到内核空间缓冲区
  5. 内核 → 用户空间 数据从内核缓冲区拷贝到 Java 传入的 byte 数组
  6. 返回实际读取的字节数(-1 表示 EOF)

这个过程中涉及一次用户态 ↔ 内核态切换和一次数据拷贝。这也是为什么批量读(减少 native 调用次数)比逐字节读性能高出几个数量级的根本原因。

六、常见问题与注意事项

6.1 流必须关闭

未关闭流会导致文件句柄泄漏,最终可能触发 Too many open files 异常,影响系统稳定性。

✅ 正确做法:优先使用 try-with-resources(JDK 7+),它会自动调用 close() 方法,即使在异常发生时也能正确释放资源。

6.2 write(byte[]) 和 write(byte[], 0, len) 的区别

fos.write(buffer) 会把整个 buffer 写入文件。如果最后一次读取的数据不足 buffer 长度,就会写入上一次残留的脏数据。因此始终使用 write(buffer, 0, len) 来确保只写入实际读取到的字节。

6.4 字符编码问题

字节流本身不涉及编码转换,写入什么字节就原样读出什么字节。如果需要在字节流上处理文本,必须在外部显式指定字符集

// 写入时指定编码
fos.write("中文".getBytes(StandardCharsets.UTF_8));

// 读取时指定编码
String text = new String(bytes, StandardCharsets.UTF_8);

6.5 FileNotFoundException 不等于文件不存在

FileInputStream 构造时如果文件不存在会抛出 FileNotFoundException。但如果文件是一个目录,或者路径中包含不可读的目录,同样的异常也会抛出。不要简单将它等同于“文件不存在”来处理,应当根据具体异常信息做更精细的错误处理。

七、最佳实践

  1. 能用字符流的场景(纯文本)优先用字符流,否则用字节流。FileReader / FileWriter 内部封装了字节转换,使用起来更省心。
  2. 大文件优先使用 BufferedInputStream / BufferedOutputStream 进行包装,它们内部会自动维护更大的缓冲区,进一步减少 native 调用次数,提升性能。
  3. 版本兼容性无需担心 — FileInputStream 和 FileOutputStream 从 JDK 1.0 就已存在,不存在版本兼容问题。
  4. Files.copy() 可简化操作 — 对于单纯的文件复制任务,使用 java.nio.file.Files.copy() 一行代码即可搞定,底层利用更高效的 FileChannel 实现。
// NIO 一行复制
Files.copy(Path.of("source.txt"), Path.of("dest.txt"), StandardCopyOption.REPLACE_EXISTING);

八、总结

  • FileInputStreamFileOutputStream 是 Java 字节流体系中最基础的文件读写类,支持所有文件类型,是学习 Java I/O 的必知必会内容。
  • 批量读写(通过字节数组)比逐字节读写性能高出几个数量级,日常开发务必采用批量方式。
  • 始终使用 try-with-resources 确保流被正确关闭,避免资源泄漏。
  • 写入时注意使用 write(buffer, 0, len) 而非 write(buffer),防止脏数据写入文件。
  • 纯文本场景优先考虑 FileReader / FileWriter 或 NIO 的 Files 工具类,操作更简洁。

掌握字节流是深入理解 Java I/O 体系的第一步,也是编写高可靠文件操作代码的必备基础。希望本文能帮助你熟练运用 FileInputStream 和 FileOutputStream,从容应对各类文件读写需求。

推荐标签:

  • Java IO
  • FileInputStream
  • FileOutputStream
  • 字节流
  • Java 文件操作
来源:https://www.jb51.net/program/365261itj.htm
上一篇JavaSE程序逻辑控制完整代码实例 下一篇C#一维、多维与交错数组对比分析
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
AWS RDS 数据库配置入门与基础操作指南
编程语言 · 2026-07-10

AWS RDS 数据库配置入门与基础操作指南

本文介绍了AWSRDS的基本概念与核心价值,即提供托管式关系数据库服务,简化运维。详细阐述了创建RDS实例的关键配置步骤,包括引擎选择、实例规格、存储与网络设置。最后,指导读者如何通过多种方式安全连接至数据库实例,并开始进行数据操作,为后续应用开发奠定基础。

PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案
编程语言 · 2026-07-10

PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案

PHPMVC中AJAX请求返回整页HTML的常见原因是控制器方法未正确输出响应或未终止执行,导致框架渲染视图。解决方法是在控制器中设置JSON响应头、输出数据后调用exit()明确终止,同时前端使用小写url和dataType: "json "。

Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南
编程语言 · 2026-07-10

Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南

手动构造RSA公钥时,模数N为*big Int类型,不能直接使用超长十进制字面量,需通过SetString或UnmarshalText方法解析字符串。公钥指数E可直接赋值,推荐65537。生产环境应使用rsa GenerateKey生成密钥对,避免手动构造引发的安全和格式错误。

Go语言实现HTTP定时轮询监控多URL响应时间与状态检测
编程语言 · 2026-07-10

Go语言实现HTTP定时轮询监控多URL响应时间与状态检测

使用Go语言实现HTTP定时轮询监控,通过按行分割与Tab解析URL列表,避免闭包陷阱和nil指针,每个URL启动独立ticker安全并发请求,并配置超时控制与资源关闭,确保响应时间与状态码准确检测。

Tkinter中Label标签在主循环动态更新的正确方法
编程语言 · 2026-07-10

Tkinter中Label标签在主循环动态更新的正确方法

在Tkinter中正确动态更新标签的方法:将标签组件的textvariable参数绑定到一个StringVar变量,然后通过调用该变量的 set()方法更新其值,界面会自动刷新。这样避免直接修改text属性或调用update()。此做法实现数据与界面的解耦,代码更简洁,响应更及时,避免手动同步的闪烁,推荐做法。