Skip to content

三、文件编程

一、FileChannel

FileChannel 仅支持阻塞模式,不支持非阻塞模式(区别于 SocketChannel/DatagramChannel);

1、获取

  • FileInputStream.getChannel() → 仅读(Read-only),调用写入方法会抛异常;
  • FileOutputStream.getChannel() → 仅写(Write-only),调用读取方法会抛异常;
  • RandomAccessFile.getChannel() → 读写权限由创建 RandomAccessFile 时的模式参数决定(r= 只读,rw= 读写,rws/rwd= 带同步的读写)。
java
// 1.1 FileInputStream → 仅读Channel
try (FileInputStream fis = new FileInputStream(FILE_PATH);
     FileChannel readOnlyChannel = fis.getChannel()) {
    System.out.println("✅ FileInputStream获取的Channel:仅读");
}

// 1.2 FileOutputStream → 仅写Channel
try (FileOutputStream fos = new FileOutputStream(FILE_PATH);
     FileChannel writeOnlyChannel = fos.getChannel()) {
    System.out.println("✅ FileOutputStream获取的Channel:仅写");
}

// 1.3 RandomAccessFile → 读写Channel(rw模式)
try (RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw");
     FileChannel rwChannel = raf.getChannel()) {
    System.out.println("✅ RandomAccessFile(rw)获取的Channel:读写");
}

2、读取

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

java
int readBytes = channel.read(buffer);

3、写入

  • ✅ 关键坑点:write() 方法不保证一次性写入所有数据(受缓冲区 / 系统缓存影响),可能出现「部分写入」;
  • ✅ 正确姿势:必须在 while 循环中调用 write(),结合 buffer.hasRemaining() 判断,确保缓冲区数据全部写入
java
ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip();   // 切换读模式

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

4、关闭

  • FileChannel 是资源型对象,必须关闭,否则会造成文件句柄泄漏;
  • ✅ 便捷方式:调用 FileInputStream/FileOutputStream/RandomAccessFileclose() 方法,会间接调用对应 Channel 的 close() 方法(推荐,无需手动关 Channel);
  • ✅ 最佳实践:使用 try-with-resources 语法(JDK7+),自动关闭资源,无需手动调用 close。

5、位置

获取当前位置

java
long pos = channel.position();

设置当前位置

java
long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)

6、文件大小获取

  • 方法:long size() → 返回文件的实际字节大小,精准可靠;
java
long fileSize = channel.size();

7、强制写入

  • 底层原理:操作系统为提升性能,会将写入数据缓存到内存,并非立刻刷写到磁盘,断电 / 宕机可能导致数据丢失;

  • 方法:

    java
    void force(boolean metaData) // 强制将内存中的数据刷写到磁盘;
    //true , 刷写「文件内容 + 文件元数据」(权限、修改时间等);
    //false,仅刷写「文件内容」;

8、传输数据

(1)小于2G

示例代码

java
public class TestFileChannel {
    public static void main(String[] args) {
        try (
                FileChannel from = new FileInputStream("word.txt").getChannel();
                FileChannel to = new FileOutputStream("word2.txt").getChannel();
        ) {
            from.transferTo(0, from.size(), to);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(2)大于2G

示例代码

java
public class TestFileChannel1 {
    public static void main(String[] args) {
        try (
                FileChannel from = new FileInputStream("word.txt").getChannel();
                FileChannel to = new FileOutputStream("word3.txt").getChannel();
        ) {
            long size = from.size();
            for (long left = size; left > 0; ) {
                left -= from.transferTo(size - left, left, to);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二、Path

jdk7 引入了 Path 和 Paths 类

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例
java
Path source = Paths.get("1.txt"); // 相对路径 使用 user.dir 环境变量来定位 1.txt

Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了  d:\1.txt

Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了  d:\1.txt

Path projects = Paths.get("d:\\data", "projects"); // 代表了  d:\data\projects
  • . 代表了当前路径
  • .. 代表了上一级路径

例如目录结构如下

d:
	|- data
		|- projects
			|- a
			|- b

代码示例

java
Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
System.out.println(path.normalize()); // 正常化路径

会输出

d:\data\projects\a\..\b
d:\data\projects\b

三、Files

1、检查文件是否存在

java
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

2、创建一级目录

java
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

3、创建多级目录用

java
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

4、拷贝文件

java
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");

Files.copy(source, target);
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

java
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

5、移动文件

java
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

6、删除文件

java
Path target = Paths.get("helloword/target.txt");

Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

7、删除目录

java
Path target = Paths.get("helloword/d1");

Files.delete(target);
  • 如果目录还有内容,会抛异常 DirectoryNotEmptyException

四、示例

1、查询目录的树形结构

java
public class TestFilesAndPath {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("C:\\study\\code\\blog");
        AtomicInteger dirInteger = new AtomicInteger();
        AtomicInteger fileInteger = new AtomicInteger();

        Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                System.out.println("目录" + dir);
                dirInteger.incrementAndGet();
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                System.out.println("文件" + file);
                fileInteger.incrementAndGet();
                return super.visitFile(file, attrs);
            }
        });
        System.out.println(dirInteger.get());
        System.out.println(fileInteger.get());
    }
}

2、查询目录中特定文件

java
public class TestFilesAndPath {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("C:\\study\\code\\blog");
        AtomicInteger fileInteger = new AtomicInteger();

        Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if(file.getFileName().toString().endsWith(".md")){
                    System.out.println("文件" + file);
                    fileInteger.incrementAndGet();
                }
                return super.visitFile(file, attrs);
            }
        });
        System.out.println(fileInteger.get());
    }
}

3、删除树形目录

java
public class TestFilesAndPath {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("C:\\study\\code\\blog");
        AtomicInteger fileInteger = new AtomicInteger();

        Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return super.visitFile(file, attrs);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return super.postVisitDirectory(dir, exc);
            }
        });
        System.out.println(fileInteger.get());
    }
}

4、拷贝多级目录

java
public class TestFilesAndPath {
    public static void main(String[] args) throws IOException {
        String source = "C:\\work\\作业一";
        String target = "C:\\caogao\\a";

        Files.walk(Paths.get(source)).forEach(path -> {
            try {
                String targetName = path.toString().replace(source, target);
                System.out.println(targetName);
//           如果是目录
                if (Files.isDirectory(path)) {
                    Files.createDirectory(Paths.get(targetName));
                } else if (Files.isRegularFile(path)) {
                    Files.copy(path, Paths.get(targetName));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}