☰
Java 文件操作全解:File 类与字节流 InputStream/OutputStream 用法指南
2026/11/5 3:54:29 网站建设 项目流程

在 Java 开发中,文件操作是最基础也最常用的能力之一:小到读取配置文件、写入日志,大到文件上传下载、数据备份,都离不开文件系统交互。Java 将文件系统操作拆分为两大块:File类负责文件元信息与目录管理,InputStream/OutputStream字节流负责文件内容的读写。

本文从基础概念到核心 API,再到实战案例,带你系统掌握 Java 文件操作的完整用法。


一、文件系统基础概念

在写代码之前,我们先理清几个文件系统的核心概念。

1. 文件与目录

硬盘上的数据以独立单位持久化存储,这个单位就是文件;文件除了内容本身,还包含文件名、大小、修改时间等元信息。 为了方便管理大量文件,系统用树形结构组织文件,专门存放管理信息的特殊文件就是目录(文件夹)。

2. 绝对路径与相对路径

定位文件的路径分为两种:

  • 绝对路径:从系统根目录出发,唯一确定一个文件。例如 Windows 下的C:\Program Files\test.txt。
  • 相对路径:以当前工作目录为基准,描述文件位置。例如.\test.txt表示当前目录下的文件,..\test.txt表示上级目录下的文件。

3. 文本文件与二进制文件

按照存储格式划分:

  • 文本文件:保存经过字符集编码的内容,比如.txt、.java文件,可以用记事本直接打开阅读。
  • 二进制文件:按标准格式存储非字符数据,比如图片、视频、exe 程序,直接打开会看到乱码。

字节流可以处理所有类型的文件,字符流仅适合处理文本文件。


二、File 类:文件与目录的抽象描述

java.io.File是 Java 对文件和目录的抽象表示。注意:File 对象只是路径的封装,不代表文件一定真实存在,它不负责读写文件内容,只操作文件元信息。

1. 类中的构造方法和方法

构造方法
签名说明
File(File parent, String child)根据父目录和子文件路径,创建一个新的 File 实例
File(String pathname)根据文件路径创建一个新的 File 实例,路径可以是绝对路径或者相对路径
File(String parent, String child)根据父目录和子文件路径,创建一个新的 File 实例,父目录用路径表示
方法
修饰符及返回值类型方法签名说明
StringgetParent()返回 File 对象的父目录文件路径
StringgetName()返回 File 对象的纯文件名称
StringgetPath()返回 File 对象的文件路径
StringgetAbsolutePath()返回 File 对象的绝对路径
StringgetCanonicalPath()返回 File 对象的修饰过的绝对路径
booleanexists()判断 File 对象描述的文件是否真实存在
booleanisDirectory()判断 File 对象代表的文件是否是一个目录
booleanisFile()判断 File 对象代表的文件是否是一个普通文件
booleancreateNewFile()根据 File 对象,自动创建一个空文件,成功创建后返回 true
booleandelete()根据 File 对象,删除该文件,成功删除后返回 true
voiddeleteOnExit()根据 File 对象,标注文件将被删除,删除动作会到 JVM 运行结束时才会进行
String[]list()返回 File 对象代表的目录下的所有文件名
File[]listFiles()返回 File 对象代表的目录下的所有文件,以 File 对象表示
booleanmkdir()创建 File 对象代表的目录
booleanmkdirs()创建 File 对象代表的目录,如果必要,会创建中间目录
booleanrenameTo(File dest)进行文件改名,也可以视为我们平时的剪切、粘贴操作
booleancanRead()判断用户是否对文件有可读权限
booleancanWrite()判断用户是否对文件有可写权限

2. 核心方法详解

(1)获取路径信息

表格

方法作用
String getParent()获取父目录路径
String getName()获取纯文件名
String getPath()获取构造时传入的路径
String getAbsolutePath()获取绝对路径
String getCanonicalPath()获取规范化的绝对路径(自动解析..等相对路径)

示例:

File file = new File("..\\test.txt"); System.out.println(file.getParent()); // .. System.out.println(file.getName()); // test.txt System.out.println(file.getAbsolutePath());// 带..的完整路径 System.out.println(file.getCanonicalPath());// 解析后的标准路径
(2)文件状态判断

表格

方法作用
boolean exists()判断文件 / 目录是否真实存在
boolean isDirectory()判断是否为目录
boolean isFile()判断是否为普通文件
boolean canRead()判断是否有可读权限
boolean canWrite()判断是否有可写权限
(3)文件创建与删除

① 创建空文件:createNewFile ()

createNewFile()用于根据 File 对象的路径创建一个空文件,是原子操作,仅当文件不存在时才会创建成功。

代码示例

import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("hello-world.txt"); System.out.println(file.exists()); // 判断文件是否存在 System.out.println(file.isDirectory()); // 判断是否为目录 System.out.println(file.isFile()); // 判断是否为普通文件 System.out.println(file.createNewFile());// 创建空文件 System.out.println(file.exists()); // 创建后再次判断是否存在 System.out.println(file.isDirectory()); System.out.println(file.isFile()); System.out.println(file.createNewFile());// 重复创建 } }

运行结果

false false false true true false true false

结果解读

  1. 初始状态文件不存在,exists()返回false,且既不是目录也不是普通文件;
  2. 调用createNewFile()成功创建空文件,返回true;
  3. 创建完成后,exists()与isFile()均返回true,确认普通文件已生成;
  4. 再次调用createNewFile(),因文件已存在,返回false,不会重复创建。

注意:该方法是原子操作,多线程并发场景下,能保证同一文件只会被创建一次。


② 文件删除的两种方式

File 类提供了两种删除方案,核心差异在于执行时机不同,分别适配不同的业务场景。

方式一:立即删除 delete ()

调用后同步执行删除操作,当场返回结果,删除成功返回true;文件不存在、文件被占用、目录非空等情况返回false。

代码示例

import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("some-file.txt"); System.out.println(file.exists()); System.out.println(file.createNewFile()); System.out.println(file.exists()); System.out.println(file.delete()); // 立即删除 System.out.println(file.exists()); } }

运行结果

false true true true false

结果解读

  1. 先创建文件,确认文件存在;
  2. 调用delete()后立即执行删除,返回true表示删除成功;
  3. 最终exists()返回false,文件已被移除。
方式二:退出时删除 deleteOnExit ()

调用时不会立刻删除文件,仅向 JVM 注册一个 “退出删除” 的标记,等 JVM 正常退出时才会按注册顺序执行删除。

  • 方法签名:void deleteOnExit()
  • 执行时机:JVM 正常退出时才执行删除
  • 返回值:无返回值,仅做登记,无法即时获知删除结果

代码示例

import java.io.File; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { File file = new File("some-file.txt"); System.out.println(file.exists()); System.out.println(file.createNewFile()); System.out.println(file.exists()); file.deleteOnExit(); // 注册:JVM退出时自动删除 System.out.println(file.exists()); } }

运行结果

false true true true

结果解读

  1. 调用deleteOnExit()时,文件不会立刻消失,因此最后一行exists()仍然返回true;
  2. 只有当main方法执行完毕、JVM 正常结束后,文件才会被自动删除。
(4)目录操作
File dir = new File("a/b/c"); // 创建单级目录,父目录不存在则创建失败 boolean res1 = dir.mkdir(); // 创建多级目录,自动创建所有不存在的父目录 boolean res2 = dir.mkdirs(); // 列出目录下所有文件的名称 String[] names = dir.list(); // 列出目录下所有文件的File对象 File[] files = dir.listFiles();
(5)重命名与移动
File src = new File("old.txt"); File dest = new File("new.txt"); // 重命名文件,也可实现剪切粘贴效果 boolean result = src.renameTo(dest);

3. 常见易错点

  1. mkdir()只能创建单级目录,多级目录必须用mkdirs();
  2. delete()删除目录时必须为空,否则删除失败;
  3. deleteOnExit()仅在 JVM 正常退出时生效,强制杀进程不会执行删除;
  4. list()/listFiles()可能返回null(路径不存在、不是目录、无权限),使用前必须判空。

那么无论是delete()还是deleteOnExit(),都要求目录必须是空的(里面没有任何文件和子文件夹),才能删除成功。


三、字节流:文件内容的读写基础

Java 用「流」的概念处理数据读写:数据像水流一样从一端流向另一端。按数据方向划分:

  • 输入流(InputStream):数据从文件流向程序,对应读文件;
  • 输出流(OutputStream):数据从程序流向文件,对应写文件。

字节流以字节为单位操作,可以处理所有类型的文件(文本、图片、视频等)。

1. InputStream:字节输入流

InputStream是所有字节输入流的抽象父类,FileInputStream是其针对文件的实现类。

核心方法

表格

方法作用
int read()读取一个字节,返回字节值;读到文件末尾返回-1
int read(byte[] b)批量读取字节到数组,返回实际读到的字节数;末尾返回-1
int read(byte[] b, int off, int len)读取字节到数组的指定区间
void close()关闭流,释放系统资源
FileInputStream概述

构造方法

签名

说明

FileInputStream(Filefile)

利⽤File构造⽂件输⼊流

FileInputStream(Stringname)

利⽤⽂件路径构造⽂件输⼊流

将文件完全读完的方式
① 单字节逐个读取
try (InputStream is = new FileInputStream("e:/test.txt")) { while (true) { int b = is.read(); if (b == -1) { // 读到文件末尾 break; } System.out.printf("%x ", b); // 以十六进制打印字节 } } catch (IOException e) { e.printStackTrace(); }

特点:代码简单,但频繁 IO 操作,性能差,仅适合小文件。

②字节数组批量读取
try (InputStream is = new FileInputStream("e:/test.txt")) { byte[] buf = new byte[1024]; // 缓冲区 int len; while ((len = is.read(buf)) != -1) { // 只处理实际读到的len个字节 for (int i = 0; i < len; i++) { System.out.printf("%x ", buf[i]); } } } catch (IOException e) { e.printStackTrace(); }

特点:一次读取一批字节,大幅减少 IO 次数,性能远高于单字节读取,是开发中的标准写法。

示例:字节流手动解码输出中文

在批量读取字节的基础上,可以手动按编码规则将字节解码为中文字符。以下为教学演示写法,仅适配纯 UTF-8 中文小文件,不是通用解决方案:

import java.io.*; // 需要先在项目目录下准备好一个 hello.txt 的文件,里面填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len == -1) { // 代表文件已经全部读完 break; } // 每次使用 3 字节进行 utf-8 解码,得到中文字符 // 利用 String 中的构造方法完成 // 注意:这个方法了解下即可,不是通用的解决办法 for (int i = 0; i < len; i += 3) { String s = new String(buf, i, 3, "UTF-8"); System.out.printf("%s", s); } } } } }
③字符流批量读取

使用Reader字符流读取文本文件,底层内置解码器,自动按照编码规则将字节拼接为完整字符,中英文混合也能正常输出,是文本文件读取的通用标准方案。

package io; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo4 { public static void main(String[] args) { try(Reader reader=new FileReader("e:/test2.txt")){ char[] chars = new char[1024]; // 数组只创建一次 int n; while (true) { n = reader.read(chars); if (n == -1) { break; // 读到文件末尾,退出循环 } // 只打印实际读到的n个字符 for (int i = 0; i < n; i++) { System.out.print(chars[i]); } } } catch (IOException e) { e.printStackTrace(); } } }

2. OutputStream:字节输出流

OutputStream是所有字节输出流的抽象父类,FileOutputStream是其文件实现类。

核心方法

表格

方法作用
void write(int b)写入一个字节
void write(byte[] b)写入整个字节数组
void write(byte[] b, int off, int len)写入数组的指定区间
void flush()刷新缓冲区,将内存数据强制写入磁盘
void close()关闭流
写入示例
import java.io.*; public class Demo { public static void main(String[] args) { try (OutputStream os = new FileOutputStream("e:/output.txt")) { // 1. 写入单个字节(仅适用于ASCII字符,比如英文、数字) os.write('H'); // 2. 批量写入英文内容 String enStr = "Hello World"; byte[] enData = enStr.getBytes("UTF-8"); os.write(enData); // 3. 批量写入中文内容 String cnStr = "你好中国"; byte[] cnData = cnStr.getBytes("UTF-8"); // 中文必须指定编码转成字节数组 os.write(cnData); os.flush(); // 强制刷新缓冲区 } catch (IOException e) { e.printStackTrace(); } } }
flush () 的作用

为了减少磁盘 IO 次数,输出流内部有缓冲区:数据先写入内存缓冲区,缓冲区满了才真正写入磁盘。flush()会强制将缓冲区数据刷入磁盘,避免数据遗留在内存中丢失。

关闭流时会自动执行 flush,但建议关键写入后手动调用一次。

3. try-with-resources 自动关流

所有 IO 流都属于系统资源,使用完毕必须关闭,否则会造成资源泄漏。 Java 7 引入的try-with-resources语法可以自动关闭实现了AutoCloseable接口的流,无需手动写close(),也不会因为异常导致关流失败,是官方推荐的标准写法。

语法:

try (InputStream is = new FileInputStream("test.txt"); OutputStream os = new FileOutputStream("copy.txt")) { // 读写逻辑 } catch (IOException e) { e.printStackTrace(); } // 跳出try块后,两个流会自动关闭

示例:

package io; import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo4 { public static void main(String[] args) { // try-with-resources:自动关闭流,无需手动close try (Reader reader = new FileReader("e:/test2.txt")) { // 方式2:字符数组批量读取 char[] chars = new char[1024]; // 数组只创建一次 int n; while (true) { n = reader.read(chars); if (n == -1) { break; // 读到文件末尾,退出循环 } // 只打印实际读到的n个字符 for (int i = 0; i < n; i++) { System.out.print(chars[i]); } } } catch (IOException e) { e.printStackTrace(); } } }

四、扩展:字节流转字符流

经上可知字节流处理文本文件不够方便,Java 提供了转换流,可以将字节流包装成字符流,更适合处理文本内容。

1. 字符读取:Scanner 包装 InputStream

上述例子中,我们看到了对字符类型直接使用 InputStream 进⾏读取是⾮常⿇烦且困难的,所以,我们使用⼀种我们之前⽐较熟悉的类来完成该工作,就是 Scanner 类。

构造⽅法

说明

Scanner(InputStreamis,Stringcharset)

使⽤charset字符集进行is的扫描读取

import java.io.*; import java.util.*; // 需要先在项⽬⽬录下准备好⼀个 hello.txt 的⽂件,⾥⾯填充 "你好中国" 的内容 public class Main { public static void main(String[] args) throws IOException { try (InputStream is = new FileInputStream("hello.txt")) { try (Scanner scanner = new Scanner(is, "UTF-8")) { while (scanner.hasNext()) { String s = scanner.next(); System.out.print(s); } } } } }

2. 字符写入:PrintWriter 包装 OutputStream

import java.io.*; public class Main { public static void main(String[] args) throws IOException { // try-with-resources 自动关闭所有流 try (OutputStream os = new FileOutputStream("output.txt"); OutputStreamWriter osWriter = new OutputStreamWriter(os, "UTF-8"); PrintWriter writer = new PrintWriter(osWriter)) { writer.println("我是第一行"); writer.print("我的第二行\r\n"); writer.printf("%d: 我的第三行\r\n", 1 + 1); writer.flush(); } } }

五、实战案例

案例 1:递归遍历目录查找文件

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验根目录 System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): "); String rootDirPath = scanner.next(); File rootDir = new File(rootDirPath); if (!rootDir.isDirectory()) { System.out.println("您输入的根目录不存在或者不是目录,退出"); return; } // 2. 输入要查找的文件名关键词 System.out.print("请输入要找出的文件名中的字符: "); String token = scanner.next(); List<File> result = new ArrayList<>(); // 3. 递归遍历目录,收集符合条件的文件 scanDir(rootDir, token, result); // 4. 遍历结果,逐个询问是否删除 System.out.println("共找到了符合条件的文件 " + result.size() + " 个"); for (File file : result) { System.out.print(file.getCanonicalPath() + " 请问您是否要删除该文件(y/n): "); String in = scanner.next(); if (in.toLowerCase().equals("y")) { file.delete(); System.out.println("已删除"); } } scanner.close(); } /** * 递归扫描目录,收集文件名包含关键词的普通文件 */ private static void scanDir(File rootDir, String token, List<File> result) { File[] files = rootDir.listFiles(); // 空目录、无权限、路径无效时直接返回,避免空指针 if (files == null || files.length == 0) { return; } for (File file : files) { if (file.isDirectory()) { // 是目录,递归进入继续扫描 scanDir(file, token, result); } else { // 是普通文件,判断文件名是否包含关键词 if (file.getName().contains(token)) { result.add(file); } } } } }

案例 2:文件复制工具

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验源文件 System.out.print("请输入要复制的文件(绝对路径 OR 相对路径): "); String sourcePath = scanner.next(); File sourceFile = new File(sourcePath); if (!sourceFile.exists()) { System.out.println("文件不存在,请确认路径是否正确"); return; } if (!sourceFile.isFile()) { System.out.println("文件不是普通文件,请确认路径是否正确"); return; } // 2. 输入并校验目标路径 System.out.print("请输入要复制到的目标路径(绝对路径 OR 相对路径): "); String destPath = scanner.next(); File destFile = new File(destPath); if (destFile.exists()) { if (destFile.isDirectory()) { System.out.println("目标路径已经存在,并且是一个目录,请确认路径是否正确"); return; } if (destFile.isFile()) { System.out.print("目标文件已经存在,是否要进行覆盖?y/n: "); String ans = scanner.next(); if (!ans.toLowerCase().equals("y")) { System.out.println("停止复制"); return; } } } // 3. 执行文件复制 try (InputStream is = new FileInputStream(sourceFile); OutputStream os = new FileOutputStream(destFile)) { byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len == -1) { break; } os.write(buf, 0, len); } os.flush(); } System.out.println("复制已完成"); scanner.close(); } }

案例3:对名称与内容两个维度进行检索

扫描指定目录,并找到名称或者内容中包含指定字符的所有普通文件(不包含目录)
注意:我们现在的方案性能较差,所以尽量不要在太复杂的目录下或者大文件下实验

import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); // 1. 输入并校验根目录 System.out.print("请输入要扫描的根目录(绝对路径 OR 相对路径): "); String rootDirPath = scanner.next(); File rootDir = new File(rootDirPath); if (!rootDir.isDirectory()) { System.out.println("您输入的根目录不存在或者不是目录,退出"); return; } // 2. 输入要搜索的内容关键词 System.out.print("请输入要查找的文件内容关键词: "); String token = scanner.next(); List<File> result = new ArrayList<>(); // 3. 递归遍历,按内容筛选文件 scanDirWithContent(rootDir, token, result); // 4. 输出结果 System.out.println("共找到了符合条件的文件 " + result.size() + " 个,它们分别是:"); for (File file : result) { System.out.println(file.getCanonicalPath()); } scanner.close(); } /** * 递归扫描目录,收集内容包含关键词的普通文件 */ private static void scanDirWithContent(File rootDir, String token, List<File> result) { File[] files = rootDir.listFiles(); // 空目录、无权限、路径无效直接返回,避免空指针 if (files == null || files.length == 0) { return; } for (File file : files) { if (file.isDirectory()) { // 是目录,递归进入继续扫描 scanDirWithContent(file, token, result); } else { // 是普通文件,判断内容是否包含关键词 if (isContentContains(file, token)) { result.add(file); } } } } /** * 判断单个文本文件的内容是否包含指定关键词 * 统一按 UTF-8 编码处理 */ private static boolean isContentContains(File file, String token) throws IOException { StringBuilder sb = new StringBuilder(); // 字节流 + Scanner 按行读取文本 try (InputStream is = new FileInputStream(file); Scanner scanner = new Scanner(is, "UTF-8")) { while (scanner.hasNextLine()) { sb.append(scanner.nextLine()); sb.append("\r\n"); // 保留换行,避免跨行关键词匹配失效 } } // 判断内容中是否包含关键词 return sb.indexOf(token) != -1; } }

六、总结与选型建议

  1. File 类:只负责文件元信息操作(创建、删除、重命名、遍历目录),不读写内容。
  2. 字节流:
    • InputStream/OutputStream是字节流的抽象父类,文件操作使用FileInputStream/FileOutputStream实现;
    • 批量读写(字节数组缓冲区)性能远高于单字节读写,是标准写法;
    • 必须使用 try-with-resources 自动关流,避免资源泄漏。
  3. 选型建议:
    • 操作图片、视频、压缩包等二进制文件 → 直接用字节流;
    • 操作纯文本文件 → 优先用字符流(包装字节流),避免编码问题;
    • 大文件读写务必使用缓冲区,减少磁盘 IO 次数。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询