在 Java 开发中,文件操作和数据读写是核心技能之一,无论是数据持久化、配置文件处理还是日志记录,都离不开对文件和 IO 流的掌握。

一、数据存储:从临时到持久

在程序运行过程中,数据的存储方式决定了其生命周期。了解不同存储方案的特点,能帮助我们选择合适的方式保存数据。

1. 内存中的临时存储

我们日常使用的变量、对象、数组、集合等都是内存中的数据容器,例如:

double money = 9999.5; // 变量

Student s = new Student(); // 对象

int[] age = new int[100]; // 数组

List<Student> students = new ArrayList<>(); // 集合

特点:这些数据仅在程序运行期间存在,一旦程序结束或发生断电,数据会立即丢失,无法长期保存。

2. 文件:持久化存储方案

文件是存储在磁盘上的持久化数据载体,具有以下优势:

  • 持久性:即使程序终止或断电,数据依然保存在磁盘中,不会丢失。
  • 通用性:可存储文本、图片、视频、音频等多种类型的数据。

在 Java 中,我们通过 File 类操作文件本身(如创建、删除、获取信息),通过 IO 流读写文件内容,实现数据的持久化管理。

二、File 类:操作文件与文件夹的工具

java.io.File 类是 Java 操作文件系统的基础,它用于代表磁盘中的文件或文件夹,但只能对文件 / 文件夹本身进行操作,不能读写文件内容

1. 创建 File 对象

File 类提供了三种常用构造器,用于根据路径创建文件对象:

构造器

说明

public File(String pathname)

根据文件路径(绝对路径或相对路径)创建对象

public File(String parent, String child)

根据父路径字符串和子路径字符串创建对象

public File(File parent, String child)

根据父路径对象和子路径字符串创建对象

示例

// 绝对路径:从盘符开始的完整路径

File file1 = new File("D:\\test\\a.txt");

// 相对路径:从当前工程目录开始的路径

File file2 = new File("src\\demo\\b.txt");

// 父路径+子路径

File parent = new File("D:\\test");

File file3 = new File(parent, "c.txt");

注意:File 对象仅封装路径信息,路径可以是真实存在的,也可以是不存在的(后续可通过方法创建)。

2. 路径的两种类型

  • 绝对路径:从盘符开始的完整路径,例如 D:\\java\\file.txt,在任何环境下都能准确定位文件。
  • 相对路径:不包含盘符,默认从当前工程的根目录开始查找,例如 src\\test.txt,更灵活,适合项目内文件操作。

3. File 类的核心方法

File 类提供了丰富的方法用于操作文件和文件夹,可分为以下几类:

(1)判断与获取信息

方法

说明

exists()

判断路径对应的文件 / 文件夹是否存在,返回 boolean

isFile()

判断是否为文件,返回 boolean

isDirectory()

判断是否为文件夹,返回 boolean

getName()

获取文件 / 文件夹的名称(包含后缀)

length()

获取文件大小(单位:字节),文件夹无意义

lastModified()

获取最后修改时间(毫秒值,可转换为日期)

getAbsolutePath()

获取绝对路径字符串

示例

File file = new File("a.txt");

if (file.exists()) {

    System.out.println("是否为文件:" + file.isFile());

    System.out.println("文件名:" + file.getName());

    System.out.println("文件大小:" + file.length() + "字节");

} else {

    System.out.println("文件不存在");

}

(2)创建与删除

方法

说明

createNewFile()

创建新文件,返回 boolean(true 表示创建成功)

mkdir()

创建一级文件夹(父文件夹必须存在)

mkdirs()

创建多级文件夹(父文件夹不存在时自动创建,推荐使用)

delete()

删除文件或空文件夹,返回 boolean(删除后不进入回收站)

示例

// 创建文件

File file = new File("newFile.txt");

boolean isCreated = file.createNewFile();

System.out.println("文件创建成功?" + isCreated);

// 创建多级文件夹

File dir = new File("dir1\\dir2\\dir3");

boolean isDirCreated = dir.mkdirs();

System.out.println("文件夹创建成功?" + isDirCreated);

// 删除文件

boolean isDeleted = file.delete();

System.out.println("文件删除成功?" + isDeleted);

(3)遍历文件夹

方法

说明

list()

返回文件夹下所有一级文件 / 文件夹的名称数组(String[]

listFiles()

返回文件夹下所有一级文件 / 文件夹的对象数组(File[],重点)

listFiles() 注意事项

  • 若调用者是文件或路径不存在,返回 null
  • 若调用者是空文件夹,返回长度为 0 的数组;
  • 若调用者是有内容的文件夹,返回一级文件 / 文件夹对象数组(包含隐藏文件);
  • 若没有权限访问,返回 null

示例:遍历文件夹下的所有文件

File dir = new File("D:\\test");

if (dir.isDirectory()) {

    File[] files = dir.listFiles();

    if (files != null) {

        for (File f : files) {

            System.out.println(f.getAbsolutePath());

        }

    }

}

三、递归算法:自调用的编程思想

递归是一种重要的算法思想,指方法在执行过程中调用自身的过程,广泛应用于文件夹遍历、树形结构处理等场景。

1. 递归的两种形式

  • 直接递归:方法 A 直接调用方法 A 自身。
  • 间接递归:方法 A 调用方法 B,方法 B 又调用方法 A(需谨慎使用,避免死循环)。

核心警告:递归必须有明确的终止条件,否则会导致「栈内存溢出错误」(递归死循环)。

2. 递归算法三要素

要写出正确的递归代码,需满足三个核心条件:

  1. 递归公式:将原问题分解为更小的子问题的数学关系(如 f(n) = n * f(n-1))。
  2. 终结点:递归停止的条件(最小子问题的解,如 f(1) = 1)。
  3. 方向:递归过程必须逐步靠近终结点(如 n 从大到小递减)。

3. 经典递归案例解析

(1)计算 n 的阶乘

阶乘定义:n! = 1 × 2 × 3 × ... × n,递归公式为 f(n) = n × f(n-1),终结点为 f(1) = 1

public class FactorialDemo {

    public static void main(String[] args) {

        int result = factorial(5);

        System.out.println("5的阶乘:" + result); // 输出:120

    }

    // 递归计算阶乘

    public static int factorial(int n) {

        if (n == 1) { // 终结点

            return 1;

        } else {

            return n * factorial(n - 1); // 递归公式,方向:n递减

        }

    }

}

执行流程:从 factorial(5) 开始,逐步分解为 5×factorial(4)5×4×factorial(3)→...→5×4×3×2×1,最终返回结果。

(2)递归求和(1~n 的和)

求和公式:f(n) = 1 + 2 + ... + n,递归公式为 f(n) = f(n-1) + n,终结点为 f(1) = 1

public class SumDemo {

    public static void main(String[] args) {

        int sum = sum(5);

        System.out.println("1~5的和:" + sum); // 输出:15

    }

    public static int sum(int n) {

        if (n == 1) {

            return 1; // 终结点

        } else {

            return sum(n - 1) + n; // 递归公式

        }

    }

}

(3)文件搜索(多级目录)

需求:在指定文件夹中搜索目标文件(如 test.txt),利用递归遍历所有子目录。

public class FileSearch {

    public static void main(String[] args) {

        searchFile(new File("D:\\test"), "test.txt");

    }

    // 递归搜索文件

    public static void searchFile(File dir, String target) {

        // 1. 校验目录合法性

        if (dir == null || !dir.exists() || !dir.isDirectory()) {

            return;

        }

        // 2. 获取一级文件对象

        File[] files = dir.listFiles();

        if (files == null) {

            return; // 无权限或异常

        }

        // 3. 遍历文件/文件夹

        for (File file : files) {

            if (file.isFile()) {

                // 是文件,判断是否为目标

                if (file.getName().equals(target)) {

                    System.out.println("找到文件:" + file.getAbsolutePath());

                }

            } else {

                // 是文件夹,递归搜索子目录

                searchFile(file, target);

            }

        }

    }

}

四、字符集:字符与字节的映射规则

字符集(Charset)定义了字符与二进制字节之间的转换规则,是解决中文乱码问题的核心知识。

1. 常见字符集及特点

不同字符集对字符的编码方式不同,直接影响数据的存储和传输:

字符集

特点

适用场景

ASCII

1 个字节存储 1 个字符,仅包含英文、数字和符号(共 128 个字符)

纯英文环境

GBK

中文占 2 字节,英文 / 数字占 1 字节,兼容 ASCII,包含 2 万 + 汉字

中文环境(如 Windows 系统默认)

UTF-8

中文占 3 字节,英文 / 数字占 1 字节,兼容 ASCII,支持全球语言

国际化场景(推荐,如网页、跨平台项目)

Unicode

所有字符占 2 或 4 字节,不兼容 ASCII,是字符集标准而非编码方式

统一字符编码标准

2. 编码与解码

  • 编码:将字符转换为字节(String → byte[]),通过 String 类的 getBytes() 方法实现。
  • 解码:将字节转换为字符(byte[] → String),通过 String 类的构造器实现。

关键原则:编码与解码必须使用相同的字符集,否则会出现乱码。

3. Java 中的编码与解码操作

import java.io.UnsupportedEncodingException;

public class CharsetDemo {

    public static void main(String[] args) throws UnsupportedEncodingException {

        String str = "我爱 Java";

        // 编码:字符 → 字节

        byte[] gbkBytes = str.getBytes("GBK"); // 使用 GBK 编码

        byte[] utf8Bytes = str.getBytes("UTF-8"); // 使用 UTF-8 编码

        // 解码:字节 → 字符

        String gbkStr = new String(gbkBytes, "GBK"); // 正确解码(无乱码)

        String utf8Str = new String(utf8Bytes, "UTF-8"); // 正确解码(无乱码)

        // 错误示例:编码与解码字符集不一致,导致乱码

        String errorStr = new String(gbkBytes, "UTF-8");

        System.out.println("乱码结果:" + errorStr);

    }

}

注意:英文和数字在大多数字符集中编码规则一致(兼容 ASCII),因此很少出现乱码;中文乱码多因编码和解码字符集不一致导致。

五、IO 流:数据读写的核心工具

IO 流(Input/Output Stream)用于实现内存与外部设备(文件、网络等)之间的数据传输,是 Java 数据持久化的核心技术。

1. IO 流的分类

IO 流可按「数据流向」和「操作单位」分为四大类:

分类标准

类型

说明

数据流向

输入流

从外部设备读数据到内存(Input)

输出流

从内存写数据到外部设备(Output)

操作单位

字节流

以字节为单位读写数据,可操作所有类型文件(文本、图片、视频等)

字符流

以字符为单位读写数据,仅适合操作文本文件(如 .txt.java

2. 字节流:万能的文件操作工具

字节流以字节为单位处理数据,是最基础的 IO 流,可操作任意类型的文件。核心抽象类为 InputStream(输入)和 OutputStream(输出),常用实现类为 FileInputStream 和 FileOutputStream

(1)文件字节输入流(FileInputStream)

用于从文件读取字节数据到内存,常用构造器和方法:

构造器

说明

FileInputStream(File file)

基于文件对象创建输入流

FileInputStream(String path)

基于文件路径创建输入流

核心方法

说明

read()

读取 1 个字节,返回字节值(-1 表示读取结束)

read(byte[] buffer)

读取多个字节到缓冲区数组,返回实际读取的字节数(-1 表示结束)

readAllBytes()

一次性读取所有字节(JDK 9+,适合小文件)

close()

关闭流,释放资源

示例:读取文件内容

import java.io.FileInputStream;

import java.io.IOException;

public class FileInputStreamDemo {

    public static void main(String[] args) {

        // try-with-resource 语法:自动关闭流(资源需实现 AutoCloseable 接口)

        try (FileInputStream fis = new FileInputStream("test.txt")) {

            byte[] buffer = new byte[1024]; // 缓冲区(减少 IO 次数)

            int len; // 记录每次读取的字节数

            // 循环读取,直到结束(len = -1)

            while ((len = fis.read(buffer)) != -1) {

                // 将字节转换为字符串(指定字符集避免乱码)

                System.out.print(new String(buffer, 0, len, "UTF-8"));

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(2)文件字节输出流(FileOutputStream)

用于从内存写入字节数据到文件,常用构造器和方法:

构造器

说明

FileOutputStream(File file)

基于文件对象创建输出流(覆盖模式:新内容覆盖旧内容)

FileOutputStream(String path, boolean append)

基于路径创建输出流(append = true 为追加模式)

核心方法

说明

write(int b)

写入 1 个字节(取 int 低 8 位)

write(byte[] buffer)

写入整个字节数组

write(byte[] buffer, int off, int len)

写入字节数组的一部分(从 off 开始,写 len 个字节)

close()

关闭流,释放资源

示例:写入数据到文件

import java.io.FileOutputStream;

import java.io.IOException;

public class FileOutputStreamDemo {

    public static void main(String[] args) {

        // 追加模式写入(第二个参数为 true)

        try (FileOutputStream fos = new FileOutputStream("output.txt", true)) {

            String data = "Hello, IO 流!\r\n"; // \r\n 是 Windows 换行符

            // 将字符串编码为字节数组(指定 UTF-8 字符集)

            byte[] bytes = data.getBytes("UTF-8");

            fos.write(bytes); // 写入字节数组

            System.out.println("写入成功!");

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(3)文件复制案例

利用字节流实现任意文件的复制(核心逻辑:边读边写):

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

public class FileCopyDemo {

    public static void main(String[] args) {

        long start = System.currentTimeMillis(); // 记录开始时间

        // 源文件和目标文件路径

        String srcPath = "D:\\source.mp4";

        String destPath = "D:\\copy.mp4";

        try (FileInputStream fis = new FileInputStream(srcPath);

             FileOutputStream fos = new FileOutputStream(destPath)) {

            byte[] buffer = new byte[1024 * 8]; // 8KB 缓冲区(提高效率)

            int len;

            // 边读边写

            while ((len = fis.read(buffer)) != -1) {

                fos.write(buffer, 0, len); // 写入读取到的有效字节

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

        long end = System.currentTimeMillis();

        System.out.println("复制完成,耗时:" + (end - start) + " 毫秒");

    }

}

3. 字符流:文本文件的最佳选择

字符流以字符为单位处理数据,专为文本文件设计,能自动处理字符编码问题(需确保编码一致)。核心抽象类为 Reader(输入)和 Writer(输出),常用实现类为 FileReader 和 FileWriter

(1)文件字符输入流(FileReader)

用于从文本文件读取字符数据到内存,常用构造器和方法:

构造器

说明

FileReader(File file)

基于文件对象创建输入流

FileReader(String path)

基于文件路径创建输入流

核心方法

说明

read()

读取 1 个字符,返回字符的 Unicode 码(-1 表示结束)

read(char[] buffer)

读取多个字符到缓冲区数组,返回实际读取的字符数(-1 表示结束)

close()

关闭流,释放资源

示例:读取文本文件

import java.io.FileReader;

import java.io.IOException;

public class FileReaderDemo {

    public static void main(String[] args) {

        try (FileReader fr = new FileReader("text.txt")) {

            char[] buffer = new char[1024]; // 字符缓冲区

            int len;

            while ((len = fr.read(buffer)) != -1) {

                // 直接将字符数组转换为字符串(无需额外编码)

                System.out.print(new String(buffer, 0, len));

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(2)文件字符输出流(FileWriter)

用于从内存写入字符数据到文本文件,常用构造器和方法:

构造器

说明

FileWriter(File file)

基于文件对象创建输出流(覆盖模式)

FileWriter(String path, boolean append)

基于路径创建输出流(append = true 为追加模式)

核心方法

说明

write(int c)

写入 1 个字符(取 int 低 16 位)

write(String str)

写入字符串

write(char[] buffer)

写入字符数组

flush()

刷新缓冲区(将内存缓冲数据强制写入文件)

close()

关闭流(包含刷新操作)

注意:字符流有缓冲区,数据会先存入内存缓冲区,需调用 flush() 或 close() 才能真正写入文件。

示例:写入文本文件

import java.io.FileWriter;

import java.io.IOException;

public class FileWriterDemo {

    public static void main(String[] args) {

        try (FileWriter fw = new FileWriter("output.txt", true)) {

            fw.write("字符流写入文本\n"); // 写入字符串

            fw.write("Hello World!\n");

            fw.flush(); // 手动刷新(可选,close() 会自动刷新)

            System.out.println("写入成功!");

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

4. 缓冲流:提升 IO 性能的利器

缓冲流(Buffered Stream)是对原始流的包装,自带 8KB 缓冲区,通过减少物理 IO 操作次数大幅提高读写性能。

(1)缓冲字节流
  • BufferedInputStream:包装字节输入流,提高读性能。
  • BufferedOutputStream:包装字节输出流,提高写性能。

构造器

// 包装字节输入流

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.txt"));

// 包装字节输出流

BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

示例:缓冲字节流复制文件

import java.io.*;

public class BufferedStreamDemo {

    public static void main(String[] args) {

        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src.mp4"));

             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dest.mp4"))) {

            byte[] buffer = new byte[1024];

            int len;

            while ((len = bis.read(buffer)) != -1) {

                bos.write(buffer, 0, len);

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(2)缓冲字符流
  • BufferedReader:包装字符输入流,新增 readLine() 方法按行读取。
  • BufferedWriter:包装字符输出流,新增 newLine() 方法跨平台换行。

构造器

// 包装字符输入流

BufferedReader br = new BufferedReader(new FileReader("a.txt"));

// 包装字符输出流

BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

新增方法

  • BufferedReader.readLine():读取一行文本(不含换行符),返回 null 表示结束。
  • BufferedWriter.newLine():根据当前系统自动添加换行符(跨平台兼容)。

示例:按行读写文本

import java.io.*;

public class BufferedCharStreamDemo {

    public static void main(String[] args) {

        try (BufferedReader br = new BufferedReader(new FileReader("source.txt"));

             BufferedWriter bw = new BufferedWriter(new FileWriter("dest.txt"))) {

            String line;

            // 按行读取

            while ((line = br.readLine()) != null) {

                bw.write(line); // 写入一行

                bw.newLine(); // 换行(跨平台)

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

5. 其他常用流

(1)字符输入转换流(InputStreamReader)

解决字符流读取不同编码文本的乱码问题,可指定字符集编码:

import java.io.*;

public class InputStreamReaderDemo {

    public static void main(String[] args) {

        try (

            // 原始字节流 + 指定字符集 = 字符流(解决乱码)

            InputStreamReader isr = new InputStreamReader(

                new FileInputStream("gbk.txt"), "GBK");

            BufferedReader br = new BufferedReader(isr)

        ) {

            String line;

            while ((line = br.readLine()) != null) {

                System.out.println(line); // 正确读取 GBK 编码文本

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(2)打印流(PrintStream/PrintWriter)

提供便捷的打印功能,支持任意类型数据输出,自动刷新,适合日志输出:

特点

PrintStream

继承自字节输出流,支持字节数据写入

PrintWriter

继承自字符输出流,支持字符数据写入

常用方法println(Xxx value) 打印任意类型数据并换行。

示例:使用 PrintWriter 写入日志

import java.io.*;

public class PrintStreamDemo {

    public static void main(String[] args) {

        try (

            // 指定字符集和自动刷新

            PrintWriter pw = new PrintWriter(

                new FileOutputStream("log.txt"), true, "UTF-8")

        ) {

            pw.println("日志时间:2024-08-11");

            pw.println("操作人:admin");

            pw.println("操作结果:成功");

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(3)数据特殊流(DataInputStream/DataOutputStream)

可读写基本数据类型(如 int、double)和字符串,并保留数据类型信息:

作用

DataOutputStream

写入基本类型和字符串(writeInt()writeDouble()writeUTF()

DataInputStream

读取对应类型数据(readInt()readDouble()readUTF()

注意:读取顺序必须与写入顺序一致。

示例:读写基本类型数据

import java.io.*;

public class DataStreamDemo {

    public static void main(String[] args) throws IOException {

        // 写入数据

        try (DataOutputStream dos = new DataOutputStream(

                new FileOutputStream("data.dat"))) {

            dos.writeInt(100); // 写入 int

            dos.writeDouble(3.14); // 写入 double

            dos.writeUTF("Java IO"); // 写入字符串

        }

        // 读取数据(顺序必须与写入一致)

        try (DataInputStream dis = new DataInputStream(

                new FileInputStream("data.dat"))) {

            int num = dis.readInt();

            double d = dis.readDouble();

            String str = dis.readUTF();

            System.out.println(num + ", " + d + ", " + str); // 输出:100, 3.14, Java IO

        }

    }

}

6. 资源释放的两种方式

IO 流属于稀缺资源,使用后必须关闭以释放资源,常用两种方式:

(1)try-catch-finally(传统方式)

FileInputStream fis = null;

try {

    fis = new FileInputStream("a.txt");

    // 读取操作

} catch (IOException e) {

    e.printStackTrace();

} finally {

    // 确保关闭资源

    if (fis != null) {

        try {

            fis.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

(2)try-with-resource(JDK 7+,推荐)

资源在 () 中声明,会自动关闭(需实现 AutoCloseable 接口):

try (FileInputStream fis = new FileInputStream("a.txt");

     FileOutputStream fos = new FileOutputStream("b.txt")) {

    // 读写操作(无需手动关闭资源)

} catch (IOException e) {

    e.printStackTrace();

}

六、IO 框架:Commons-IO 简化开发

Apache Commons-IO 是一款常用的 IO 工具框架,封装了复杂的 IO 操作,提供简单易用的 API,能大幅提高开发效率。

1. 框架导入步骤

  1. 下载 Commons-IO 框架(如 commons-io-2.11.0.jar);
  2. 在项目中创建 lib 文件夹,将 jar 包复制到该文件夹;
  3. 右键 jar 包,选择 Add as Library 完成导入。

2. 核心工具类及常用方法

(1)FileUtils:文件操作工具类

import org.apache.commons.io.FileUtils;

import java.io.File;

import java.io.IOException;

public class FileUtilsDemo {

    public static void main(String[] args) throws IOException {

        // 复制文件

        FileUtils.copyFile(new File("a.txt"), new File("b.txt"));

        // 复制文件夹(含子内容)

        FileUtils.copyDirectory(new File("dir1"), new File("dir2"));

        // 读取文件内容

        String content = FileUtils.readFileToString(new File("a.txt"), "UTF-8");

        // 写入文件内容(追加模式)

        FileUtils.writeStringToFile(

            new File("b.txt"), "追加内容", "UTF-8", true);

    }

}

(2)IOUtils:流操作工具类

import org.apache.commons.io.IOUtils;

import java.io.*;

public class IOUtilsDemo {

    public static void main(String[] args) throws IOException {

        try (InputStream is = new FileInputStream("a.txt");

             OutputStream os = new FileOutputStream("b.txt")) {

            // 复制流数据

            IOUtils.copy(is, os);

        }

    }

}

总结

本文从数据存储方案入手,详细讲解了 File 类的文件操作、递归算法的核心思想、字符集的编码解码规则,以及 IO 流的完整体系(字节流、字符流、缓冲流等),最后介绍了 Commons-IO 框架的简化用法。

掌握这些知识后,你将能够:

  • 使用 File 类创建、删除、遍历文件和文件夹;
  • 运用递归算法解决复杂问题(如多级文件搜索);
  • 理解字符集原理,避免中文乱码;
  • 熟练使用各类 IO 流读写不同类型的文件;
  • 利用框架简化 IO 操作,提高开发效率。

IO 流是 Java 开发的基础,建议通过大量实践加深理解,从简单的文件读写到复杂的项目应用,逐步积累经验。希望本文能帮助你快速掌握 Java 文件操作与 IO 流技术!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐