Java IO详解:File、FileInputStream与FileOutputStream

文章目录

    • [引言:Java IO体系与文件操作](#引言:Java IO体系与文件操作)
    • 第一章:IO流基础概念
      • [1.1 什么是流(Stream)](#1.1 什么是流(Stream))
      • [1.2 Java IO流的分类](#1.2 Java IO流的分类)
      • [1.3 文件IO的核心类](#1.3 文件IO的核心类)
    • 第二章:File类深度剖析
      • [2.1 类的定义与核心字段](#2.1 类的定义与核心字段)
      • [2.2 构造方法:创建File对象](#2.2 构造方法:创建File对象)
      • [2.3 常用API详解](#2.3 常用API详解)
        • [2.3.1 获取文件和目录基本信息](#2.3.1 获取文件和目录基本信息)
        • [2.3.2 判断功能](#2.3.2 判断功能)
        • [2.3.3 列出目录内容](#2.3.3 列出目录内容)
        • [2.3.4 创建与删除](#2.3.4 创建与删除)
        • [2.3.5 重命名与移动](#2.3.5 重命名与移动)
      • [2.4 应用场景与最佳实践](#2.4 应用场景与最佳实践)
      • [2.5 File类的局限性](#2.5 File类的局限性)
    • 第三章:FileInputStream源码剖析与使用
      • [3.1 类定义与继承体系](#3.1 类定义与继承体系)
      • [3.2 核心字段与构造方法](#3.2 核心字段与构造方法)
        • [3.2.1 核心字段](#3.2.1 核心字段)
        • [3.2.2 构造方法](#3.2.2 构造方法)
      • [3.3 核心方法详解](#3.3 核心方法详解)
        • [3.3.1 read():读取单个字节](#3.3.1 read():读取单个字节)
        • [3.3.2 read(byte[] b):读取到字节数组](#3.3.2 read(byte[] b):读取到字节数组)
        • [3.3.3 skip(long n):跳过字节](#3.3.3 skip(long n):跳过字节)
        • [3.3.4 available():可用字节数估计](#3.3.4 available():可用字节数估计)
        • [3.3.5 close():关闭流](#3.3.5 close():关闭流)
        • [3.3.6 getChannel():获取文件通道](#3.3.6 getChannel():获取文件通道)
      • [3.4 线程安全性分析](#3.4 线程安全性分析)
      • [3.5 使用示例与最佳实践](#3.5 使用示例与最佳实践)
      • [3.6 性能优化建议](#3.6 性能优化建议)
    • 第四章:FileOutputStream源码剖析与使用
      • [4.1 类定义与继承体系](#4.1 类定义与继承体系)
      • [4.2 核心字段与构造方法](#4.2 核心字段与构造方法)
        • [4.2.1 核心字段](#4.2.1 核心字段)
        • [4.2.2 构造方法](#4.2.2 构造方法)
      • [4.3 核心方法详解](#4.3 核心方法详解)
        • [4.3.1 write(int b):写入单个字节](#4.3.1 write(int b):写入单个字节)
        • [4.3.2 write(byte[] b):写入字节数组](#4.3.2 write(byte[] b):写入字节数组)
        • [4.3.3 close():关闭流](#4.3.3 close():关闭流)
        • [4.3.4 getChannel()和getFD()](#4.3.4 getChannel()和getFD())
      • [4.4 使用示例与最佳实践](#4.4 使用示例与最佳实践)
      • [4.5 注意事项与常见陷阱](#4.5 注意事项与常见陷阱)
    • 第五章:综合实战与最佳实践
    • 第六章:总结与展望
      • [6.1 三大核心类对比](#6.1 三大核心类对比)
      • [6.2 从字节流到高级IO的演进](#6.2 从字节流到高级IO的演进)
      • [6.3 未来趋势](#6.3 未来趋势)
      • [6.4 给开发者的建议](#6.4 给开发者的建议)
    • 附录:常用代码片段速查

引言:Java IO体系与文件操作

在Java应用程序开发中,文件输入输出(I/O)是最基础且最常见的操作之一。无论是读取配置文件、处理用户上传的文档、记录日志信息,还是进行数据持久化,都离不开对文件的操作。Java的I/O体系通过"流"(Stream)的抽象,为开发者提供了一套统一而强大的API来处理各种设备间的数据传递。

本文将深入剖析Java文件IO的三大基石:

  • File类:文件和目录路径名的抽象表示,用于文件和目录的创建、删除、查询等操作
  • FileInputStream:字节文件输入流,用于从文件中读取原始字节数据
  • FileOutputStream:字节文件输出流,用于将原始字节数据写入文件

我们将从源码层面解读其设计原理,探讨核心方法的使用技巧,分析性能优化的关键点,并提供最佳实践指南。全文预计超过8000字,力求让你彻底掌握Java文件IO的精髓。


第一章:IO流基础概念

1.1 什么是流(Stream)

在Java中,流是一个抽象的概念,代表了数据的"流动"。可以将其想象为连接数据源(源端)和程序(目的端)的一条管道,数据在管道中按顺序传输。

输入流(Input Stream):数据从外部源(如文件、网络、键盘)流入程序(内存)。程序从输入流中读取数据。

输出流(Output Stream):数据从程序(内存)流向外部目的地(如文件、网络、屏幕)。程序向输出流中写入数据。

1.2 Java IO流的分类

Java的IO流体系可以从三个维度进行分类:

分类维度 类别 说明 典型类
数据流向 输入流 读取数据到程序 InputStream, Reader
输出流 从程序写出数据 OutputStream, Writer
操作单位 字节流 以字节(8位)为单位 InputStream, OutputStream
字符流 以字符(16位)为单位 Reader, Writer
角色分工 节点流 直接从数据源读写 FileInputStream, FileOutputStream
处理流 包装节点流,提供增强功能 BufferedInputStream, DataInputStream

字节流 vs 字符流

  • 字节流:处理所有类型的文件(文本、图片、视频、音频等)。因为所有文件在底层都是以字节形式存储的,字节流是通用的。
  • 字符流:专门处理文本文件。它内部处理字符编码和解码,方便处理人类可读的文本。

节点流 vs 处理流

  • 节点流:直接连接数据源,是IO操作的基础
  • 处理流:在节点流之上进行包装,提供缓冲、转换、对象序列化等高级功能(装饰器模式)

1.3 文件IO的核心类

本文聚焦于文件IO的基础字节流:

  • File:代表文件或目录路径,提供文件系统操作(创建、删除、重命名、查询属性等)
  • FileInputStream:字节文件输入流,从文件中读取字节数据
  • FileOutputStream:字节文件输出流,向文件中写入字节数据

这三者构成了Java进行原始文件操作的基础。理解它们的工作原理,是掌握更高级IO(如缓冲流、对象流、NIO)的前提。


第二章:File类深度剖析

java.io.File类是Java IO体系中唯一代表文件和目录路径名的类,但它不负责文件内容的读写。它更像是一个文件或目录的"名片",记录了路径信息,并提供了对文件元数据(属性)的操作方法。

2.1 类的定义与核心字段

java 复制代码
public class File implements Serializable, Comparable<File> {
    // 文件系统相关的操作接口(与具体操作系统交互)
    private static final FileSystem fs = DefaultFileSystem.getFileSystem();
    
    // 核心字段:存储文件的路径
    private final String path;
    
    // 路径的规范化状态
    private transient volatile PathStatus status = null;
    
    // 文件路径的规范化字符串
    private volatile transient String prefixLength;
    
    // 构造方法
    public File(String pathname) {
        if (pathname == null) {
            throw new NullPointerException();
        }
        this.path = fs.normalize(pathname);
    }
    // ...
}

关键分析

  • File类实现了Serializable接口,意味着File对象可以被序列化
  • 实现了Comparable<File>接口,提供了compareTo方法,可以按路径名字典序比较
  • 核心字段pathfinal修饰,说明File对象是不可变的------一旦创建,其代表的抽象路径名就不能改变
  • FileSystem fs是与底层操作系统交互的关键,所有与文件系统相关的操作(如检查文件是否存在、获取文件属性)最终都委托给它

2.2 构造方法:创建File对象

File类提供了多种构造方法,灵活适应不同的使用场景:

java 复制代码
public class FileConstructorDemo {
    public static void main(String[] args) {
        // 1. 通过完整路径名字符串创建
        File file1 = new File("D:\\data\\document.txt");  // Windows路径需转义
        File file2 = new File("/home/user/document.txt"); // Linux/Mac路径
        
        // 2. 通过父路径和子路径字符串创建
        String parent = "D:\\data";
        String child = "document.txt";
        File file3 = new File(parent, child);
        
        // 3. 通过父File对象和子路径字符串创建
        File parentDir = new File("D:\\data");
        File file4 = new File(parentDir, "document.txt");
        
        // 4. 通过URI创建(略)
    }
}

重要说明

  • 路径分隔符 :Windows使用反斜杠\,在Java字符串中需要转义为\\;Unix/Linux/Mac使用正斜杠/。更好的做法是使用File.separator常量,它会根据运行平台自动适配:

    java 复制代码
    File file = new File("D:" + File.separator + "data" + File.separator + "document.txt");
  • 创建时机new File()只是在内存中创建了一个对象,代表一个路径,并不会在硬盘上实际创建文件或目录 。文件是否真正存在,需要通过exists()方法验证。

  • 相对路径 :相对路径相对于当前工作目录(可通过System.getProperty("user.dir")获取)。在IDEA中,单元测试方法的相对路径相对于当前module,main方法的相对路径相对于当前工程。

2.3 常用API详解

File类的API主要分为四大类:获取基本信息、判断功能、列出目录内容、创建删除操作。

2.3.1 获取文件和目录基本信息
java 复制代码
import java.io.File;
import java.util.Date;

public class FileGetInfoDemo {
    public static void main(String[] args) {
        File file = new File("test.txt");
        
        System.out.println("文件名: " + file.getName());           // test.txt
        System.out.println("路径: " + file.getPath());             // test.txt(构造时的路径)
        System.out.println("绝对路径: " + file.getAbsolutePath()); // 完整绝对路径
        try {
            System.out.println("规范路径: " + file.getCanonicalPath()); // 解析.和..的绝对路径
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("父目录: " + file.getParent());          // null(如果没有父目录)
        System.out.println("文件大小: " + file.length() + " 字节"); // 文件实际大小
        System.out.println("最后修改时间: " + new Date(file.lastModified())); // 毫秒值
    }
}

关键点

  • length()返回0的情况:文件不存在,或者文件确实为空
  • lastModified()返回的是从1970-01-01 UTC开始的毫秒数
  • getAbsolutePath()getCanonicalPath()的区别:绝对路径可能包含...,规范路径会解析这些相对引用
2.3.2 判断功能
java 复制代码
import java.io.File;

public class FileCheckDemo {
    public static void main(String[] args) {
        File file = new File("test.txt");
        
        System.out.println("是否存在: " + file.exists());       // true/false
        System.out.println("是否是文件: " + file.isFile());     // true
        System.out.println("是否是目录: " + file.isDirectory()); // false
        System.out.println("是否可读: " + file.canRead());      // 权限检查
        System.out.println("是否可写: " + file.canWrite());     // 权限检查
        System.out.println("是否可执行: " + file.canExecute()); // 对于文件是可执行权限,对于目录是可遍历权限
        System.out.println("是否隐藏: " + file.isHidden());     // 平台相关
    }
}

注意isFile()isDirectory()在文件不存在时都返回false,不是抛出异常。

2.3.3 列出目录内容
java 复制代码
import java.io.File;

public class FileListDemo {
    public static void main(String[] args) {
        File dir = new File("D:\\data");
        
        if (dir.exists() && dir.isDirectory()) {
            // 方法1:返回字符串数组(仅名称)
            String[] fileNames = dir.list();
            System.out.println("目录中的文件和子目录:");
            for (String name : fileNames) {
                System.out.println("  " + name);
            }
            
            // 方法2:返回File对象数组(完整路径)
            File[] files = dir.listFiles();
            System.out.println("\nFile对象列表:");
            for (File f : files) {
                System.out.println("  " + f.getPath() + (f.isDirectory() ? " [目录]" : " [文件]"));
            }
            
            // 带文件名过滤器的版本
            File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
            System.out.println("\n文本文件:");
            for (File f : txtFiles) {
                System.out.println("  " + f.getName());
            }
        }
    }
}

源码分析listFiles()最终会调用FileSystemlist()方法,这是一个native方法,与操作系统交互获取目录内容。

2.3.4 创建与删除
java 复制代码
import java.io.File;
import java.io.IOException;

public class FileCreateDeleteDemo {
    public static void main(String[] args) {
        // 1. 创建文件
        File file = new File("newfile.txt");
        try {
            if (file.createNewFile()) {
                System.out.println("文件创建成功: " + file.getName());
            } else {
                System.out.println("文件已存在,无需创建");
            }
        } catch (IOException e) {
            System.out.println("创建文件时发生IO错误");
            e.printStackTrace();
        }
        
        // 2. 创建单级目录
        File singleDir = new File("mydir");
        if (singleDir.mkdir()) {
            System.out.println("目录创建成功: " + singleDir.getName());
        } else {
            System.out.println("目录创建失败(可能已存在或父目录不存在)");
        }
        
        // 3. 创建多级目录
        File multiDir = new File("parent/child/grandchild");
        if (multiDir.mkdirs()) {
            System.out.println("多级目录创建成功: " + multiDir.getPath());
        } else {
            System.out.println("多级目录创建失败");
        }
        
        // 4. 删除文件或空目录
        if (file.delete()) {
            System.out.println("文件删除成功");
        } else {
            System.out.println("文件删除失败(可能不存在或无权限)");
        }
        
        // 5. 程序退出时自动删除
        File tempFile = new File("temp.txt");
        try {
            if (tempFile.createNewFile()) {
                tempFile.deleteOnExit(); // JVM退出时删除
                System.out.println("临时文件将在程序退出时删除");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

重要说明

  • createNewFile():原子操作,检查文件是否存在并创建,返回boolean表示是否成功创建
  • mkdir() vs mkdirs():前者要求父目录必须存在,后者会创建所有不存在的父目录
  • delete():直接删除,不走回收站,需谨慎操作
  • deleteOnExit():注册一个钩子,在JVM正常退出时删除文件,适用于临时文件清理
2.3.5 重命名与移动
java 复制代码
import java.io.File;

public class FileRenameDemo {
    public static void main(String[] args) {
        File oldFile = new File("oldname.txt");
        File newFile = new File("newname.txt");
        
        // 确保原文件存在
        try {
            oldFile.createNewFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // 重命名/移动
        if (oldFile.renameTo(newFile)) {
            System.out.println("重命名成功");
            System.out.println("新文件存在: " + newFile.exists());
            System.out.println("旧文件存在: " + oldFile.exists());
        } else {
            System.out.println("重命名失败");
        }
    }
}

关键点

  • renameTo(File dest)的行为依赖于平台
  • 在同一文件系统内,相当于重命名(原子操作)
  • 在不同文件系统间,可能表现为复制+删除,不是原子操作
  • 目标文件不能已存在(某些平台会覆盖)

2.4 应用场景与最佳实践

场景1:递归遍历目录树
java 复制代码
import java.io.File;

public class DirectoryTraversal {
    
    public static void traverse(File dir, String indent) {
        if (!dir.exists() || !dir.isDirectory()) {
            System.out.println(indent + dir.getPath() + " (不是有效目录)");
            return;
        }
        
        File[] files = dir.listFiles();
        if (files == null) return;
        
        for (File file : files) {
            if (file.isDirectory()) {
                System.out.println(indent + "[DIR] " + file.getName());
                traverse(file, indent + "  "); // 递归
            } else {
                System.out.println(indent + "[FILE] " + file.getName() + " (" + file.length() + " 字节)");
            }
        }
    }
    
    public static void main(String[] args) {
        File root = new File("D:\\data");
        traverse(root, "");
    }
}
场景2:文件过滤器实现
java 复制代码
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;

public class FileFilterDemo {
    public static void main(String[] args) {
        File dir = new File("D:\\data");
        
        // 使用FilenameFilter(过滤文件名)
        String[] images = dir.list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".jpg") || name.endsWith(".png");
            }
        });
        
        // 使用FileFilter(过滤File对象)
        File[] largeFiles = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile() && pathname.length() > 1024 * 1024; // > 1MB
            }
        });
        
        // Lambda简化版
        File[] hiddenFiles = dir.listFiles(f -> f.isHidden());
    }
}

2.5 File类的局限性

尽管File类是文件操作的基础,但它存在一些局限性:

  1. 不能访问文件内容:File类只操作元数据,不能读写文件内容
  2. 操作失败处理:很多方法只返回boolean,不提供详细的失败原因
  3. 符号链接处理:对符号链接的支持有限
  4. 文件属性:无法设置文件所有者、权限等高级属性
  5. 大文件支持:length()返回long,理论上支持大文件,但一些方法如lastModified()精度有限

JDK 7引入了java.nio.file.PathFiles类,提供了更强大的文件操作功能,但在基础学习中,File类仍然是入门文件IO的第一步。


第三章:FileInputStream源码剖析与使用

java.io.FileInputStream是字节文件输入流,用于从文件中读取原始字节数据。它是InputStream抽象类的直接子类,适用于读取二进制文件(如图片、音频、视频)或任何需要按字节处理的文件。

3.1 类定义与继承体系

java 复制代码
public class FileInputStream extends InputStream

继承关系

复制代码
java.lang.Object
  └── java.io.InputStream
       └── java.io.FileInputStream

FileInputStream继承了InputStream,因此它拥有所有输入流的基本方法:read()read(byte[])close()等,并根据文件读取的特性进行了实现。

3.2 核心字段与构造方法

3.2.1 核心字段
java 复制代码
public class FileInputStream extends InputStream {
    /* 文件描述符,用于打开文件的句柄 */
    private final FileDescriptor fd;
    
    /* 引用文件的路径,如果流是通过文件描述符创建时该值为null */
    private final String path;
    
    /* 文件通道,用于NIO操作 */
    private FileChannel channel = null;
    
    /* 关闭锁,确保close操作的线程安全性 */
    private final Object closeLock = new Object();
    
    /* 标记流是否已关闭 */
    private volatile boolean closed = false;
    
    // ... 其他代码
}

字段解读

  • FileDescriptor fd:文件描述符,是操作系统用于管理打开文件的句柄。它包含了打开文件的关键信息,后续所有读写操作都通过它进行。
  • String path :记录文件的路径,用于错误信息和跟踪。如果流是通过已有的FileDescriptor创建的,则此字段为null
  • FileChannel channel :NIO中的通道,提供了与文件关联的通道,可以进行内存映射、文件锁定等高级操作。它是懒加载的,只有在调用getChannel()时才会创建。
  • closeLock :用于同步close()方法,防止多个线程同时关闭导致的问题。
  • closedvolatile修饰,确保多线程间的可见性。
3.2.2 构造方法

FileInputStream提供了三种重载的构造方法:

java 复制代码
// 1. 通过文件路径名字符串创建
public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}

// 2. 通过File对象创建(最常用)
public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name); // 安全检查:是否有读取权限
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    fd = new FileDescriptor();
    fd.attach(this); // 将当前流关联到文件描述符
    path = name;
    open(name); // 调用native方法打开文件
}

// 3. 通过已有的FileDescriptor创建
public FileInputStream(FileDescriptor fdObj) {
    SecurityManager security = System.getSecurityManager();
    if (fdObj == null) {
        throw new NullPointerException();
    }
    if (security != null) {
        security.checkRead(fdObj);
    }
    fd = fdObj;
    path = null;
    fd.attach(this); // 将当前流关联到已存在的文件描述符
}

构造过程分析

  1. 参数校验和权限检查(SecurityManager
  2. 创建(或使用已有的)FileDescriptor对象
  3. 调用attach(this)将文件描述符与当前流关联,便于后续资源释放
  4. 如果是通过文件路径创建,调用open(name)本地方法,真正与操作系统交互打开文件
  5. 如果文件不存在、是目录或无法打开,抛出FileNotFoundException

核心native方法

java 复制代码
private native void open(String name) throws FileNotFoundException;

这个native方法会调用操作系统的API(如Windows的CreateFile,Linux的open)来打开文件,获取文件句柄存储在fd中。

3.3 核心方法详解

3.3.1 read():读取单个字节
java 复制代码
public int read() throws IOException {
    return read0();
}

private native int read0() throws IOException;

工作原理

  • 每次调用读取一个字节(8位)
  • 返回值范围:0到255(无符号字节)
  • 如果到达文件末尾,返回-1
  • 该方法会阻塞,直到有数据可读、到达文件末尾或发生异常
  • 底层通过native方法直接调用操作系统读文件的系统调用

性能考量:每次读取一个字节意味着每个字节都要进行一次系统调用,对于大文件来说效率极低。实际开发中几乎不使用此方法读取大量数据。

3.3.2 read(byte[] b):读取到字节数组
java 复制代码
public int read(byte b[]) throws IOException {
    return readBytes(b, 0, b.length);
}

public int read(byte b[], int off, int len) throws IOException {
    return readBytes(b, off, len);
}

private native int readBytes(byte b[], int off, int len) throws IOException;

工作原理

  • 尝试读取最多b.length个字节到数组中
  • 返回实际读取的字节数,可能小于请求的长度
  • 返回-1表示文件末尾
  • native方法readBytes会尽可能多地读取数据,但受限于文件剩余字节数
  • 可以指定偏移量off,将数据存入数组的指定位置

示例代码

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

public class FileInputStreamReadDemo {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("test.dat")) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 处理读取到的数据,注意只处理bytesRead个字节
                // buffer中可能只有部分数据是有效的
                processData(buffer, bytesRead);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void processData(byte[] data, int length) {
        // 处理前length个字节
        System.out.println("读取到 " + length + " 字节");
    }
}

重要提示fis.read(buffer)返回的是实际读取的字节数,这个数值可能小于buffer的长度(特别是在接近文件末尾时)。处理数据时必须使用返回的长度,而不是buffer.length

3.3.3 skip(long n):跳过字节
java 复制代码
public native long skip(long n) throws IOException;

工作原理

  • 跳过并丢弃输入流中的n个字节
  • 返回实际跳过的字节数(可能小于n)
  • 如果n为负数,某些平台支持回退(如例子中skip(-1)
  • 跳过文件末尾不会抛出异常,但后续read会返回-1

示例

java 复制代码
FileInputStream fis = new FileInputStream("data.bin");
fis.skip(10); // 跳过前10个字节
int b = fis.read(); // 读取第11个字节
3.3.4 available():可用字节数估计
java 复制代码
public native int available() throws IOException;

工作原理

  • 返回估计的剩余可读取字节数(不受阻塞)
  • 这是一个估计值,不保证精确
  • 通常用于判断是否需要创建缓冲区,但不能依赖它作为文件总长度的准确值
  • 文件超过EOF时返回0

常见误用

java 复制代码
// 错误:用available()确定文件大小
FileInputStream fis = new FileInputStream("file.txt");
byte[] data = new byte[fis.available()]; // 可能太小,也可能太大
fis.read(data); // 可能无法填满缓冲区

正确用法:仅用于非阻塞场景的提示,不能替代循环读取。

3.3.5 close():关闭流
java 复制代码
public void close() throws IOException {
    synchronized (closeLock) {
        if (closed) {
            return;
        }
        closed = true;
    }
    if (channel != null) {
        channel.close();
    }
    // 关闭文件描述符
    fd.closeAll(new Closeable() {
        public void close() throws IOException {
            close0();
        }
    });
}

private native void close0() throws IOException;

设计要点

  • 使用closeLock保证线程安全,防止重复关闭
  • 关闭时,如果有关联的FileChannel,一并关闭
  • 通过fd.closeAll()最终调用native方法close0()释放系统资源
  • 推荐使用try-with-resources确保自动关闭
3.3.6 getChannel():获取文件通道
java 复制代码
public FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, false, this);
        }
        return channel;
    }
}

作用 :返回与此文件输入流关联的唯一的FileChannel对象。这是Java NIO的入口,可以进行更高效的文件操作(如内存映射文件、文件锁定等)。

3.4 线程安全性分析

FileInputStream的实例方法本身不是线程安全的,但它的某些操作具有原子性。

通过多线程测试可以发现:单次read操作本身不会被其他线程抢占而中断,它会完整地读取这次要读取的内容 。但是,由于其他线程可以改变输入流的位置(通过skipread),每个线程读取时开始的位置是不可预知的。

java 复制代码
// 来自并发编程网的测试代码
public class FileThreadTest implements Runnable {
    private int type;// 0做skip操作,1做读取操作
    private int gap;
    private FileInputStream in;

    // ... 构造方法

    @Override
    public void run() {
        byte[] body = new byte[gap];
        if (this.type == 0) {
            // 执行skip操作
        } else {
            // 执行read操作
            try {
                for(int i = 0; i < 10; i++) {
                    in.read(body);
                    System.out.println(Thread.currentThread().getName() + "-" + new String(body));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

测试结果表明:

  • 每个read()调用是原子的,会读取完整的一组字节
  • 但由于流的位置是共享的,多个线程交替执行会导致读取位置混乱
  • 如果需要线程安全,必须在外部进行同步,或每个线程使用自己的流实例

3.5 使用示例与最佳实践

示例1:基本文件读取(try-with-resources)
java 复制代码
import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamExample {
    public static void main(String[] args) {
        // JDK 7+ try-with-resources 自动关闭流
        try (FileInputStream fis = new FileInputStream("input.dat")) {
            byte[] buffer = new byte[4096]; // 4KB缓冲区
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 处理数据
                System.out.println("读取了 " + bytesRead + " 字节");
                // 例如:将字节写入其他流、解析数据等
            }
        } catch (IOException e) {
            System.err.println("文件读取错误: " + e.getMessage());
        }
    }
}
示例2:复制文件(结合FileOutputStream)
java 复制代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyExample {
    public static void copyFile(String source, String dest) throws IOException {
        try (FileInputStream fis = new FileInputStream(source);
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            byte[] buffer = new byte[8192]; // 8KB缓冲区
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
        } // 自动关闭两个流
    }
    
    public static void main(String[] args) {
        try {
            copyFile("source.jpg", "copy.jpg");
            System.out.println("文件复制成功");
        } catch (IOException e) {
            System.err.println("复制失败: " + e.getMessage());
        }
    }
}
示例3:读取部分数据(指定偏移量)
java 复制代码
import java.io.FileInputStream;
import java.io.IOException;

public class ReadPartialExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("data.bin")) {
            byte[] header = new byte[10];  // 读取文件头10字节
            byte[] body = new byte[100];   // 读取接下来的100字节
            
            int headerRead = fis.read(header);
            if (headerRead == 10) {
                System.out.println("文件头读取成功");
            }
            
            int bodyRead = fis.read(body);
            System.out.println("读取了 " + bodyRead + " 字节的正文");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.6 性能优化建议

  1. 使用缓冲流FileInputStream每次读取都会触发系统调用。包装为BufferedInputStream可大幅减少系统调用次数:

    java 复制代码
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.dat"))) {
        // 读取效率更高
    }
  2. 合理设置缓冲区大小:通常4KB-64KB之间,具体取决于应用场景

  3. 避免在循环中使用单字节读取while ((b = fis.read()) != -1)是性能杀手

  4. 考虑使用NIO :对于大文件或需要高吞吐量的场景,FileChannel和内存映射文件性能更好


第四章:FileOutputStream源码剖析与使用

java.io.FileOutputStream是字节文件输出流,用于将原始字节数据写入文件。它是OutputStream抽象类的直接子类,与FileInputStream对应。

4.1 类定义与继承体系

java 复制代码
public class FileOutputStream extends OutputStream

继承关系

复制代码
java.lang.Object
  └── java.io.OutputStream
       └── java.io.FileOutputStream

4.2 核心字段与构造方法

4.2.1 核心字段
java 复制代码
public class FileOutputStream extends OutputStream {
    /* 文件描述符 */
    private final FileDescriptor fd;
    
    /* 文件路径 */
    private final String path;
    
    /* 是否以追加模式打开 */
    private final boolean append;
    
    /* 文件通道 */
    private FileChannel channel;
    
    /* 关闭锁 */
    private final Object closeLock = new Object();
    
    /* 是否已关闭 */
    private volatile boolean closed = false;
    
    // ...
}

与FileInputStream不同的字段

  • boolean append :标记是否为追加模式。true表示写入的数据追加到文件末尾,false表示覆盖文件开头。
4.2.2 构造方法

FileOutputStream提供了5个重载的构造方法:

java 复制代码
// 1. 通过文件名创建(覆盖模式)
public FileOutputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null, false);
}

// 2. 通过文件名创建,指定是否追加
public FileOutputStream(String name, boolean append) throws FileNotFoundException {
    this(name != null ? new File(name) : null, append);
}

// 3. 通过File对象创建(覆盖模式)
public FileOutputStream(File file) throws FileNotFoundException {
    this(file, false);
}

// 4. 通过File对象创建,指定是否追加
public FileOutputStream(File file, boolean append) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkWrite(name); // 安全检查:是否有写入权限
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    this.fd = new FileDescriptor();
    this.append = append;
    this.path = name;
    
    // 根据追加模式打开文件
    open(name, append);
}

// 5. 通过FileDescriptor创建
public FileOutputStream(FileDescriptor fdObj) {
    SecurityManager security = System.getSecurityManager();
    if (fdObj == null) {
        throw new NullPointerException();
    }
    if (security != null) {
        security.checkWrite(fdObj);
    }
    this.fd = fdObj;
    this.path = null;
    this.append = false;
    fd.attach(this);
}

核心native方法

java 复制代码
private native void open(String name, boolean append) throws FileNotFoundException;

打开模式说明

  • append = false(默认):文件指针定位到文件开头。如果文件已存在,原有内容会被新写入的内容覆盖
  • append = true:文件指针定位到文件末尾,新写入的内容追加到原内容之后

4.3 核心方法详解

4.3.1 write(int b):写入单个字节
java 复制代码
public void write(int b) throws IOException {
    write(b, append);
}

private native void write(int b, boolean append) throws IOException;

工作原理

  • 写入一个字节(参数b的低8位,高24位被忽略)
  • 如果流以追加模式打开,写入位置在文件末尾
  • 否则在文件开头
  • native方法直接调用操作系统写文件系统调用

性能考量 :与FileInputStream.read()类似,单字节写入效率极低,不推荐大量使用。

4.3.2 write(byte[] b):写入字节数组
java 复制代码
public void write(byte b[]) throws IOException {
    writeBytes(b, 0, b.length, append);
}

public void write(byte b[], int off, int len) throws IOException {
    writeBytes(b, off, len, append);
}

private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;

工作原理

  • 将字节数组中的全部或部分数据写入文件
  • 建议使用批量写入提高性能
  • native方法一次性写入多个字节,减少系统调用次数
4.3.3 close():关闭流
java 复制代码
public void close() throws IOException {
    synchronized (closeLock) {
        if (closed) {
            return;
        }
        closed = true;
    }
    
    if (channel != null) {
        channel.close();
    }
    
    fd.closeAll(new Closeable() {
        public void close() throws IOException {
            close0();
        }
    });
}

private native void close0() throws IOException;

与FileInputStream类似:使用锁保证线程安全,关闭关联的通道,释放系统资源。

4.3.4 getChannel()和getFD()
java 复制代码
public FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, false, true, append, this);
        }
        return channel;
    }
}

public final FileDescriptor getFD() throws IOException {
    if (fd != null) return fd;
    throw new IOException();
}

4.4 使用示例与最佳实践

示例1:基本文件写入
java 复制代码
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamBasicExample {
    public static void main(String[] args) {
        String data = "Hello, Java IO!";
        
        try (FileOutputStream fos = new FileOutputStream("output.txt")) {
            // 将字符串转换为字节数组写入
            fos.write(data.getBytes());
            
            // 也可以逐字节写入(不推荐)
            // for (byte b : data.getBytes()) {
            //     fos.write(b);
            // }
            
            System.out.println("数据写入成功");
        } catch (IOException e) {
            System.err.println("写入失败: " + e.getMessage());
        }
    }
}
示例2:追加模式 vs 覆盖模式
java 复制代码
import java.io.FileOutputStream;
import java.io.IOException;

public class AppendVsOverwrite {
    public static void main(String[] args) {
        // 覆盖模式
        try (FileOutputStream fos = new FileOutputStream("test.txt")) {
            fos.write("First line\n".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 再次写入(覆盖模式)
        try (FileOutputStream fos = new FileOutputStream("test.txt")) {
            fos.write("Second line\n".getBytes()); // 原内容被覆盖
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 追加模式
        try (FileOutputStream fos = new FileOutputStream("test.txt", true)) {
            fos.write("Third line\n".getBytes()); // 追加到末尾
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

执行后,test.txt内容为:

复制代码
Second line
Third line
示例3:写入二进制数据
java 复制代码
import java.io.FileOutputStream;
import java.io.IOException;

public class WriteBinaryExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("binary.dat")) {
            // 写入int类型(4字节)
            int value = 12345678;
            fos.write((value >>> 24) & 0xFF); // 高位字节
            fos.write((value >>> 16) & 0xFF);
            fos.write((value >>> 8) & 0xFF);
            fos.write(value & 0xFF);           // 低位字节
            
            // 或者使用DataOutputStream简化
            // try (DataOutputStream dos = new DataOutputStream(fos)) {
            //     dos.writeInt(value);
            // }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.5 注意事项与常见陷阱

  1. 自动创建文件 :如果输出文件不存在,FileOutputStream会自动创建它(前提是父目录存在)

  2. 目录 vs 文件 :如果指定的路径是一个已存在的目录,会抛出FileNotFoundException

  3. 权限问题 :如果没有写入权限,也会抛出FileNotFoundException

  4. 覆盖 vs 追加 :默认为覆盖模式,需要追加时务必使用带boolean append参数的构造方法

  5. 数据持久性write()方法返回时,数据不一定已经持久化到磁盘,可能还在操作系统缓存中。需要确保数据真正写入可调用getFD().sync()

  6. 多线程写入 :与FileInputStream类似,多线程共享同一个FileOutputStream会导致数据交错,需要外部同步或使用每个线程独立的流


第五章:综合实战与最佳实践

5.1 文件复制工具完整实现

java 复制代码
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

/**
 * 文件复制工具 - 展示多种实现方式
 */
public class FileCopyUtil {
    
    /**
     * 方式1:使用FileInputStream/FileOutputStream(基础字节流)
     */
    public static void copyByStream(String src, String dest) throws IOException {
        File srcFile = new File(src);
        File destFile = new File(dest);
        
        // 检查源文件是否存在
        if (!srcFile.exists()) {
            throw new FileNotFoundException("源文件不存在: " + src);
        }
        
        // 确保目标文件父目录存在
        File parent = destFile.getParentFile();
        if (parent != null && !parent.exists()) {
            parent.mkdirs();
        }
        
        // 使用try-with-resources自动关闭资源
        try (FileInputStream fis = new FileInputStream(srcFile);
             FileOutputStream fos = new FileOutputStream(destFile)) {
            
            byte[] buffer = new byte[8192]; // 8KB缓冲区
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            
            fos.flush(); // 确保所有数据写入
        }
    }
    
    /**
     * 方式2:使用BufferedInputStream/BufferedOutputStream(缓冲流优化)
     */
    public static void copyByBufferedStream(String src, String dest) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
            
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
            
            bos.flush();
        }
    }
    
    /**
     * 方式3:使用FileChannel(NIO,适合大文件)
     */
    public static void copyByChannel(String src, String dest) throws IOException {
        try (FileInputStream fis = new FileInputStream(src);
             FileOutputStream fos = new FileOutputStream(dest);
             FileChannel inChannel = fis.getChannel();
             FileChannel outChannel = fos.getChannel()) {
            
            // 直接传输,无需手动缓冲区
            inChannel.transferTo(0, inChannel.size(), outChannel);
        }
    }
    
    /**
     * 方式4:使用Files工具类(Java 7+,最简单)
     */
    public static void copyByFiles(String src, String dest) throws IOException {
        Path sourcePath = Paths.get(src);
        Path destPath = Paths.get(dest);
        Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING);
    }
    
    /**
     * 性能测试
     */
    public static void performanceTest(String src, String dest) {
        long start, end;
        
        try {
            // 测试基础流
            start = System.currentTimeMillis();
            copyByStream(src, dest + ".stream");
            end = System.currentTimeMillis();
            System.out.println("基础流耗时: " + (end - start) + "ms");
            
            // 测试缓冲流
            start = System.currentTimeMillis();
            copyByBufferedStream(src, dest + ".buffered");
            end = System.currentTimeMillis();
            System.out.println("缓冲流耗时: " + (end - start) + "ms");
            
            // 测试NIO通道
            start = System.currentTimeMillis();
            copyByChannel(src, dest + ".channel");
            end = System.currentTimeMillis();
            System.out.println("NIO通道耗时: " + (end - start) + "ms");
            
            // 测试Files工具类
            start = System.currentTimeMillis();
            copyByFiles(src, dest + ".files");
            end = System.currentTimeMillis();
            System.out.println("Files工具类耗时: " + (end - start) + "ms");
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("用法: java FileCopyUtil <源文件> <目标文件>");
            return;
        }
        try {
            copyByStream(args[0], args[1]);
            System.out.println("文件复制成功");
        } catch (IOException e) {
            System.err.println("复制失败: " + e.getMessage());
        }
    }
}

5.2 文件加密/解密示例(异或算法)

java 复制代码
import java.io.*;

/**
 * 简单的文件加密/解密工具(使用异或算法)
 * 相同的程序执行两次即可解密(异或的特性:a XOR key XOR key = a)
 */
public class FileCipher {
    
    private static final byte DEFAULT_KEY = 0x7F; // 加密密钥
    
    /**
     * 加密/解密文件
     */
    public static void processFile(String src, String dest, byte key) throws IOException {
        try (FileInputStream fis = new FileInputStream(src);
             FileOutputStream fos = new FileOutputStream(dest)) {
            
            byte[] buffer = new byte[4096];
            int bytesRead;
            
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 对每个字节进行异或运算
                for (int i = 0; i < bytesRead; i++) {
                    buffer[i] ^= key;
                }
                fos.write(buffer, 0, bytesRead);
            }
        }
    }
    
    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("用法: java FileCipher <源文件> <目标文件> [密钥]");
            return;
        }
        
        String src = args[0];
        String dest = args[1];
        byte key = args.length > 2 ? Byte.parseByte(args[2]) : DEFAULT_KEY;
        
        try {
            processFile(src, dest, key);
            System.out.println("文件处理完成");
        } catch (IOException e) {
            System.err.println("处理失败: " + e.getMessage());
        }
    }
}

5.3 配置文件读取示例

java 复制代码
import java.io.*;
import java.util.Properties;

/**
 * 读取配置文件示例
 */
public class ConfigReader {
    
    private Properties props = new Properties();
    
    /**
     * 从文件加载配置
     */
    public void loadConfig(String configFile) throws IOException {
        try (FileInputStream fis = new FileInputStream(configFile)) {
            props.load(fis); // Properties提供了load(InputStream)方法
        }
    }
    
    /**
     * 保存配置到文件
     */
    public void saveConfig(String configFile) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(configFile)) {
            props.store(fos, "Configuration File");
        }
    }
    
    /**
     * 手动解析配置文件(演示FileInputStream用法)
     */
    public void manualParseConfig(String configFile) throws IOException {
        try (FileInputStream fis = new FileInputStream(configFile)) {
            byte[] buffer = new byte[1024];
            int bytesRead = fis.read(buffer);
            
            if (bytesRead > 0) {
                String content = new String(buffer, 0, bytesRead);
                String[] lines = content.split("\n");
                
                for (String line : lines) {
                    line = line.trim();
                    if (line.isEmpty() || line.startsWith("#")) {
                        continue; // 忽略空行和注释
                    }
                    
                    int eqIndex = line.indexOf('=');
                    if (eqIndex > 0) {
                        String key = line.substring(0, eqIndex).trim();
                        String value = line.substring(eqIndex + 1).trim();
                        props.setProperty(key, value);
                    }
                }
            }
        }
    }
    
    public String getProperty(String key) {
        return props.getProperty(key);
    }
    
    public static void main(String[] args) {
        ConfigReader reader = new ConfigReader();
        try {
            reader.loadConfig("config.properties");
            System.out.println("db.url = " + reader.getProperty("db.url"));
            System.out.println("db.username = " + reader.getProperty("db.username"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5.4 资源管理最佳实践:try-with-resources

JDK 7引入的try-with-resources语句是处理IO资源的最佳实践:

java 复制代码
// 传统方式(JDK 6及以前)
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // 读取操作
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// try-with-resources方式(JDK 7+)
try (FileInputStream fis = new FileInputStream("file.txt");
     FileOutputStream fos = new FileOutputStream("out.txt")) {
    // 读取和写入操作
} catch (IOException e) {
    e.printStackTrace();
} // 自动关闭fis和fos,无需finally

优点

  • 代码简洁,避免嵌套的try-catch-finally
  • 自动处理关闭顺序(按照资源声明相反的顺序)
  • 如果try块和close()都抛出异常,try块的异常会抑制close()的异常

5.5 常见问题与解决方案

问题1:文件被占用(Windows平台)

现象 :在Windows上,如果文件已被其他程序打开,再尝试写入会抛出FileNotFoundException

解决方案

  • 确保程序逻辑正确释放资源
  • 使用FileChanneltryLock()尝试获取文件锁
  • 考虑使用随机访问文件RandomAccessFile
问题2:路径不存在

现象 :写入文件时父目录不存在,抛出FileNotFoundException

解决方案

java 复制代码
File file = new File("parent/child/data.txt");
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
    parent.mkdirs(); // 创建所有不存在的父目录
}
try (FileOutputStream fos = new FileOutputStream(file)) {
    // 写入数据
}
问题3:编码问题

现象 :使用FileOutputStream写入文本时,出现乱码。

解决方案

  • 明确指定字符编码:

    java 复制代码
    String text = "中文内容";
    try (FileOutputStream fos = new FileOutputStream("file.txt")) {
        fos.write(text.getBytes(StandardCharsets.UTF_8));
    }
  • 使用字符流FileWriter(可指定编码):

    java 复制代码
    try (FileWriter fw = new FileWriter("file.txt", StandardCharsets.UTF_8)) {
        fw.write(text);
    }
问题4:性能瓶颈

现象:读写大文件时速度很慢。

解决方案

  • 使用缓冲流包装:new BufferedInputStream(new FileInputStream(...))
  • 增加缓冲区大小(4KB-64KB)
  • 使用NIO的FileChannel.transferTo()或内存映射文件
  • 考虑异步IO(NIO.2)

第六章:总结与展望

6.1 三大核心类对比

特性 File FileInputStream FileOutputStream
主要作用 文件和目录路径名操作 从文件读取字节数据 向文件写入字节数据
核心功能 创建、删除、重命名、查询属性 读取字节数组/单个字节 写入字节数组/单个字节
是否处理内容 否(只处理元数据)
数据流向 N/A 文件 → 程序 程序 → 文件
线程安全 是(不可变对象) 否(需要外部同步) 否(需要外部同步)
异常处理 部分方法返回boolean IOException IOException
JDK版本 1.0 1.0 1.0

6.2 从字节流到高级IO的演进

本文深入讲解了File、FileInputStream和FileOutputStream这三个基础的IO类。在实际开发中,我们通常不会直接使用它们,而是基于它们构建更高级的IO处理:

  1. 缓冲流BufferedInputStream/BufferedOutputStream - 减少系统调用次数
  2. 数据流DataInputStream/DataOutputStream - 读写Java基本数据类型
  3. 对象流ObjectInputStream/ObjectOutputStream - 对象序列化
  4. 字符流FileReader/FileWriter - 专门处理文本文件
  5. NIO.2FilesPathFileChannel - 更现代、更强大的文件操作

6.3 未来趋势

随着Java的发展,文件IO也在不断演进:

  • JDK 7 NIO.2 :引入了PathFiles类,提供了更全面的文件操作API
  • JDK 8 :增强了Files类的方法,支持Stream API遍历目录
  • JDK 11Files.readString()Files.writeString()简化文本文件读写
  • 未来:可能进一步增强异步IO、内存访问等特性

6.4 给开发者的建议

  1. 掌握基础:深刻理解File、FileInputStream、FileOutputStream,它们是所有Java文件IO的基石

  2. 使用缓冲:除非处理极小的文件,否则始终用缓冲流包装字节流

  3. 明确资源管理:始终使用try-with-resources确保资源释放

  4. 区分字节与字符:处理文本优先考虑字符流或指定编码,处理二进制使用字节流

  5. 了解NIO:对于高性能要求的场景,学习并应用NIO.2 API

  6. 阅读源码:通过阅读JDK源码,理解设计模式(装饰器模式)和底层实现


附录:常用代码片段速查

读取文件所有字节

java 复制代码
byte[] allBytes = Files.readAllBytes(Paths.get("file.dat"));

读取文件所有行

java 复制代码
List<String> lines = Files.readAllLines(Paths.get("file.txt"), StandardCharsets.UTF_8);

写入字符串到文件

java 复制代码
Files.write(Paths.get("file.txt"), "Hello".getBytes(StandardCharsets.UTF_8));

遍历目录(Java 8 Stream)

java 复制代码
Files.list(Paths.get(".")).forEach(System.out::println);

递归遍历目录树

java 复制代码
Files.walk(Paths.get(".")).filter(Files::isRegularFile).forEach(System.out::println);

临时文件创建

java 复制代码
Path tempFile = Files.createTempFile("prefix", ".tmp");

文件复制

java 复制代码
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"), StandardCopyOption.REPLACE_EXISTING);

获取文件大小

java 复制代码
long size = Files.size(Paths.get("file.txt"));

检查文件是否存在

java 复制代码
boolean exists = Files.exists(Paths.get("file.txt"));
相关推荐
Hello.Reader1 小时前
Tauri vs Qt跨平台桌面(与移动)应用选型的“底层逻辑”与落地指南
开发语言·qt·tauri
山北雨夜漫步1 小时前
点评day05 秒杀优化-利用消息队列实现异步写入数据库
jvm
xyq20241 小时前
R语言连接MySQL数据库的详细指南
开发语言
追随者永远是胜利者1 小时前
(LeetCode-Hot100)647. 回文子串
java·算法·leetcode·职场和发展·go
春和景明3602 小时前
mysql复习
java
宇木灵2 小时前
C语言基础-六、指针
c语言·开发语言·学习·算法
百锦再2 小时前
Java InputStream和OutputStream实现类完全指南
java·开发语言·spring boot·python·struts·spring cloud·kafka
Vic101012 小时前
链表算法三道
java·数据结构·算法·链表
mjhcsp2 小时前
C++区间 DP解析
开发语言·c++