Java IO流(二):文件操作与NIO入门

简介: 本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。

💡 摘要:你是否曾需要递归遍历目录结构?是否想知道Java NIO相比传统IO有什么优势?是否对Path、Files这些新API感到好奇?

别担心,文件操作和NIO是Java IO编程的重要进阶内容,能让你更高效地处理文件和网络IO。

本文将带你从文件操作的高级API讲起,学习Files类的各种实用方法。然后深入NIO的核心概念,理解Channel、Buffer、Selector的工作原理。

接着通过实战案例展示NIO在文件读写、目录遍历、非阻塞IO等方面的强大能力。最后对比传统IO与NIO的性能差异。从基础用法到高级特性,从同步阻塞到异步非阻塞,让你全面掌握Java现代IO编程技术。文末附性能测试和面试高频问题,助你写出更高效的IO代码。

一、Files类:现代文件操作API

1. Files类概述

Java 7引入的Files类提供了丰富的静态方法,用于文件操作,比传统的File类更强大、更易用。

基本文件操作

java

import java.nio.file.*;

import java.io.IOException;

import java.util.List;


public class FilesExample {

   public static void main(String[] args) {

       Path path = Paths.get("test.txt");

       

       // 检查文件是否存在

       boolean exists = Files.exists(path);

       System.out.println("文件存在: " + exists);

       

       // 检查是否是目录

       boolean isDirectory = Files.isDirectory(path);

       System.out.println("是目录: " + isDirectory);

       

       // 获取文件大小

       try {

           long size = Files.size(path);

           System.out.println("文件大小: " + size + " bytes");

       } catch (IOException e) {

           System.err.println("无法获取文件大小: " + e.getMessage());

       }

   }

}

2. 文件读写操作

读写文本文件

java

// 写入文件

Path outputPath = Paths.get("output.txt");

try {

   String content = "Hello, Files API!\n这是第二行";

   Files.write(outputPath, content.getBytes(StandardCharsets.UTF_8));

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

} catch (IOException e) {

   e.printStackTrace();

}


// 读取文件

try {

   List<String> lines = Files.readAllLines(outputPath, StandardCharsets.UTF_8);

   for (String line : lines) {

       System.out.println("行内容: " + line);

   }

} catch (IOException e) {

   e.printStackTrace();

}


// 追加内容

try {

   String appendText = "\n追加的内容";

   Files.write(outputPath, appendText.getBytes(StandardCharsets.UTF_8),

               StandardOpenOption.APPEND);

} catch (IOException e) {

   e.printStackTrace();

}

二进制文件操作

java

// 读写二进制文件

Path binaryPath = Paths.get("data.bin");

try {

   // 写入二进制数据

   byte[] data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"的字节表示

   Files.write(binaryPath, data);

   

   // 读取二进制数据

   byte[] readData = Files.readAllBytes(binaryPath);

   System.out.println("读取的数据: " + new String(readData));

   

} catch (IOException e) {

   e.printStackTrace();

}

3. 文件与目录管理

创建和删除操作

java

// 创建目录

Path dirPath = Paths.get("mydir/subdir");

try {

   Files.createDirectories(dirPath); // 创建多级目录

   System.out.println("目录创建成功");

} catch (IOException e) {

   e.printStackTrace();

}


// 创建临时文件

try {

   Path tempFile = Files.createTempFile("prefix_", ".txt");

   System.out.println("临时文件: " + tempFile);

   

   // 临时目录

   Path tempDir = Files.createTempDirectory("temp_");

   System.out.println("临时目录: " + tempDir);

   

} catch (IOException e) {

   e.printStackTrace();

}


// 删除文件/目录

try {

   Files.deleteIfExists(Paths.get("file_to_delete.txt"));

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

} catch (IOException e) {

   e.printStackTrace();

}

文件移动和复制

java

Path source = Paths.get("source.txt");

Path target = Paths.get("target.txt");


try {

   // 复制文件

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

   System.out.println("文件复制成功");

   

   // 移动文件(重命名)

   Path newLocation = Paths.get("backup/" + target.getFileName());

   Files.createDirectories(newLocation.getParent());

   Files.move(target, newLocation, StandardCopyOption.REPLACE_EXISTING);

   System.out.println("文件移动成功");

   

} catch (IOException e) {

   e.printStackTrace();

}

二、目录遍历与文件查找

1. 递归遍历目录

使用Files.walk()

java

Path startPath = Paths.get(".");

try {

   Files.walk(startPath)

       .filter(Files::isRegularFile) // 只处理普通文件

       .filter(path -> path.toString().endsWith(".java")) // 过滤Java文件

       .forEach(path -> {

           try {

               System.out.println("Java文件: " + path +

                                " 大小: " + Files.size(path) + " bytes");

           } catch (IOException e) {

               System.err.println("无法获取文件大小: " + path);

           }

       });

} catch (IOException e) {

   e.printStackTrace();

}

使用Files.list()

java

Path dirPath = Paths.get("src/main/java");

try {

   Files.list(dirPath)

       .filter(Files::isDirectory)

       .forEach(subDir -> System.out.println("子目录: " + subDir.getFileName()));

} catch (IOException e) {

   e.printStackTrace();

}

2. 文件查找

使用Files.find()

java

Path searchPath = Paths.get(".");

try {

   // 查找最近7天内修改过的.java文件

   Files.find(searchPath, 10, (path, attrs) -> {

       return path.toString().endsWith(".java") &&

              attrs.lastModifiedTime().toMillis() >

                  System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L;

   }).forEach(path -> System.out.println("最近修改: " + path));

   

} catch (IOException e) {

   e.printStackTrace();

}

三、NIO核心概念入门

1. NIO与传统IO的区别

主要区别对比

特性 传统IO NIO
数据流 面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞模式 阻塞IO(Blocking IO) 非阻塞IO(Non-blocking IO)
选择器 有(Selector)
性能 连接数多时性能差 连接数多时性能好
适用场景 连接数少,数据量大 连接数多,数据量小

2. Buffer:数据容器

Buffer的基本用法

java

import java.nio.*;


public class BufferExample {

   public static void main(String[] args) {

       // 创建ByteBuffer,容量为10字节

       ByteBuffer buffer = ByteBuffer.allocate(10);

       

       System.out.println("初始状态: " + buffer);

       // capacity:10, position:0, limit:10

       

       // 写入数据

       buffer.put((byte)'H');

       buffer.put((byte)'e');

       buffer.put((byte)'l');

       buffer.put((byte)'l');

       buffer.put((byte)'o');

       

       System.out.println("写入5字节后: " + buffer);

       // capacity:10, position:5, limit:10

       

       // 切换为读模式

       buffer.flip();

       System.out.println("flip之后: " + buffer);

       // capacity:10, position:0, limit:5

       

       // 读取数据

       while (buffer.hasRemaining()) {

           byte b = buffer.get();

           System.out.print((char) b);

       }

       System.out.println();

       

       System.out.println("读取完成后: " + buffer);

       // capacity:10, position:5, limit:5

       

       // 清空缓冲区(为写入做准备)

       buffer.clear();

       System.out.println("clear之后: " + buffer);

       // capacity:10, position:0, limit:10

   }

}

Buffer的三种模式

  1. 写模式:position指向下一个写入位置,limit等于capacity
  2. 读模式:调用flip()后,position重置为0,limit指向最后一个元素后一位
  3. 清空模式:调用clear()后,position重置为0,limit等于capacity

3. Channel:数据通道

FileChannel文件操作

java

public class FileChannelExample {

   public static void main(String[] args) {

       Path path = Paths.get("test.txt");

       

       // 使用FileChannel写入文件

       try (FileChannel channel = FileChannel.open(path,

               StandardOpenOption.CREATE,

               StandardOpenOption.WRITE)) {

           

           ByteBuffer buffer = ByteBuffer.allocate(1024);

           buffer.put("Hello, FileChannel!".getBytes(StandardCharsets.UTF_8));

           buffer.flip(); // 切换为读模式

           

           while (buffer.hasRemaining()) {

               channel.write(buffer); // 写入文件

           }

           

           System.out.println("文件写入完成");

           

       } catch (IOException e) {

           e.printStackTrace();

       }

       

       // 使用FileChannel读取文件

       try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {

           ByteBuffer buffer = ByteBuffer.allocate(1024);

           

           int bytesRead = channel.read(buffer);

           buffer.flip(); // 切换为读模式

           

           byte[] data = new byte[buffer.remaining()];

           buffer.get(data);

           System.out.println("文件内容: " + new String(data, StandardCharsets.UTF_8));

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

文件复制优化

java

public class FileCopyNIO {

   public static void copyFile(String source, String target) {

       Path sourcePath = Paths.get(source);

       Path targetPath = Paths.get(target);

       

       try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);

            FileChannel targetChannel = FileChannel.open(targetPath,

                    StandardOpenOption.CREATE,

                    StandardOpenOption.WRITE)) {

           

           // 使用transferTo进行高效的文件复制

           long transferred = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

           System.out.println("复制字节数: " + transferred);

           

           // 或者使用transferFrom

           // long transferred = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());

           

       } catch (IOException e) {

           System.err.println("文件复制失败: " + e.getMessage());

       }

   }

   

   public static void main(String[] args) {

       copyFile("source.jpg", "target.jpg");

   }

}

4. 内存映射文件

内存映射文件操作

java

public class MappedFileExample {

   public static void main(String[] args) {

       Path path = Paths.get("mapped_file.txt");

       

       try (FileChannel channel = FileChannel.open(path,

               StandardOpenOption.CREATE,

               StandardOpenOption.READ,

               StandardOpenOption.WRITE)) {

           

           // 创建内存映射缓冲区

           MappedByteBuffer mappedBuffer = channel.map(

               FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024); // 映射1MB

           

           // 写入数据

           String message = "Hello, Memory Mapped File!";

           mappedBuffer.put(message.getBytes(StandardCharsets.UTF_8));

           

           // 读取数据

           mappedBuffer.flip();

           byte[] data = new byte[mappedBuffer.remaining()];

           mappedBuffer.get(data);

           System.out.println("读取内容: " + new String(data, StandardCharsets.UTF_8));

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

四、NIO文件操作实战

1. 大文件处理

分块读取大文件

java

public class LargeFileProcessor {

   public static void processLargeFile(String filePath, int bufferSize) {

       Path path = Paths.get(filePath);

       

       try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {

           ByteBuffer buffer = ByteBuffer.allocate(bufferSize);

           long fileSize = channel.size();

           long position = 0;

           

           while (position < fileSize) {

               int bytesRead = channel.read(buffer, position);

               if (bytesRead == -1) break;

               

               buffer.flip();

               processBuffer(buffer, bytesRead);

               buffer.clear();

               

               position += bytesRead;

               System.out.printf("处理进度: %.2f%%\n",

                   (double)position / fileSize * 100);

           }

           

       } catch (IOException e) {

           System.err.println("处理文件失败: " + e.getMessage());

       }

   }

   

   private static void processBuffer(ByteBuffer buffer, int bytesRead) {

       // 处理缓冲区数据

       byte[] data = new byte[bytesRead];

       buffer.get(data);

       // 这里可以添加具体的处理逻辑

       System.out.println("处理了 " + bytesRead + " 字节数据");

   }

   

   public static void main(String[] args) {

       processLargeFile("large_file.dat", 8192); // 8KB缓冲区

   }

}

2. 文件锁

文件锁机制

java

public class FileLockExample {

   public static void main(String[] args) {

       Path path = Paths.get("locked_file.txt");

       

       try (FileChannel channel = FileChannel.open(path,

               StandardOpenOption.CREATE,

               StandardOpenOption.WRITE)) {

           

           // 获取排他锁

           FileLock lock = channel.lock();

           System.out.println("获得文件锁");

           

           try {

               // 执行需要加锁的操作

               ByteBuffer buffer = ByteBuffer.wrap("Locked content".getBytes());

               channel.write(buffer);

               

               // 模拟耗时操作

               Thread.sleep(3000);

               

           } finally {

               lock.release();

               System.out.println("释放文件锁");

           }

           

       } catch (IOException | InterruptedException e) {

           e.printStackTrace();

       }

   }

}

五、性能对比:NIO vs 传统IO

1. 文件复制性能测试

java

public class PerformanceComparison {

   public static void main(String[] args) {

       String source = "large_file.iso"; // 1GB+的大文件

       String target1 = "copy_io.iso";

       String target2 = "copy_nio.iso";

       

       // 传统IO性能测试

       long startIO = System.currentTimeMillis();

       copyFileIO(source, target1);

       long timeIO = System.currentTimeMillis() - startIO;

       

       // NIO性能测试

       long startNIO = System.currentTimeMillis();

       copyFileNIO(source, target2);

       long timeNIO = System.currentTimeMillis() - startNIO;

       

       System.out.printf("传统IO耗时: %d ms\n", timeIO);

       System.out.printf("NIO耗时: %d ms\n", timeNIO);

       System.out.printf("性能提升: %.2f%%\n",

           (double)(timeIO - timeNIO) / timeIO * 100);

   }

   

   // 传统IO文件复制

   private static void copyFileIO(String source, String target) {

       try (InputStream is = new FileInputStream(source);

            OutputStream os = new FileOutputStream(target)) {

           

           byte[] buffer = new byte[8192];

           int bytesRead;

           

           while ((bytesRead = is.read(buffer)) != -1) {

               os.write(buffer, 0, bytesRead);

           }

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

   

   // NIO文件复制

   private static void copyFileNIO(String source, String target) {

       try (FileChannel sourceChannel = new FileInputStream(source).getChannel();

            FileChannel targetChannel = new FileOutputStream(target).getChannel()) {

           

           sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

2. 内存映射文件性能优势

随机访问性能测试

java

public class RandomAccessPerformance {

   public static void main(String[] args) {

       Path path = Paths.get("random_access_test.bin");

       int fileSize = 100 * 1024 * 1024; // 100MB

       int accessCount = 10000;

       

       // 准备测试文件

       createTestFile(path, fileSize);

       

       // 传统RandomAccessFile测试

       long startRAF = System.currentTimeMillis();

       testRandomAccessFile(path, accessCount);

       long timeRAF = System.currentTimeMillis() - startRAF;

       

       // 内存映射文件测试

       long startMapped = System.currentTimeMillis();

       testMappedFile(path, accessCount);

       long timeMapped = System.currentTimeMillis() - startMapped;

       

       System.out.printf("RandomAccessFile耗时: %d ms\n", timeRAF);

       System.out.printf("内存映射文件耗时: %d ms\n", timeMapped);

       System.out.printf("性能提升: %.2f%%\n",

           (double)(timeRAF - timeMapped) / timeRAF * 100);

   }

   

   private static void createTestFile(Path path, int size) {

       try (FileChannel channel = FileChannel.open(path,

               StandardOpenOption.CREATE,

               StandardOpenOption.WRITE)) {

           

           ByteBuffer buffer = ByteBuffer.allocate(size);

           for (int i = 0; i < size; i++) {

               buffer.put((byte)(i % 256));

           }

           buffer.flip();

           channel.write(buffer);

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

   

   private static void testRandomAccessFile(Path path, int accessCount) {

       try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r")) {

           Random random = new Random();

           for (int i = 0; i < accessCount; i++) {

               long position = random.nextInt((int) raf.length());

               raf.seek(position);

               raf.readByte();

           }

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

   

   private static void testMappedFile(Path path, int accessCount) {

       try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {

           MappedByteBuffer mappedBuffer = channel.map(

               FileChannel.MapMode.READ_ONLY, 0, channel.size());

           

           Random random = new Random();

           for (int i = 0; i < accessCount; i++) {

               int position = random.nextInt(mappedBuffer.capacity());

               mappedBuffer.get(position);

           }

           

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

六、总结:NIO最佳实践

1. 选择指南

使用传统IO的场景

  • ✅ 简单的文件读写操作
  • ✅ 小文件处理
  • ✅ 需要兼容老代码
  • ✅ 简单的网络通信

使用NIO的场景

  • ✅ 大文件处理(内存映射文件)
  • ✅ 高性能文件复制(transferTo/transferFrom)
  • ✅ 非阻塞网络编程
  • ✅ 需要文件锁机制
  • ✅ 随机访问大文件

2. 性能优化建议

缓冲区大小选择

java

// 合适的缓冲区大小(根据具体场景调整)

int bufferSize = 8192; // 8KB - 通用场景

int largeBufferSize = 65536; // 64KB - 大文件处理

int networkBufferSize = 4096; // 4KB - 网络传输

内存映射文件使用

  • ✅ 处理超大文件(超过内存容量)
  • ✅ 随机访问频繁的场景
  • ✅ 进程间共享数据
  • ❌ 小文件(开销可能不划算)

七、面试高频问题

❓1. NIO和传统IO的主要区别是什么?

:主要区别:NIO面向缓冲区、支持非阻塞IO、有选择器机制,适合高并发场景;传统IO面向流、阻塞式,适合连接数少的场景。

❓2. Buffer的flip()方法有什么作用?

:flip()方法将Buffer从写模式切换到读模式,将limit设置为当前位置,position重置为0。

❓3. 内存映射文件有什么优势?

:内存映射文件将文件直接映射到内存空间,避免了用户态和内核态的数据拷贝,提高了大文件随机访问的性能。

❓4. FileChannel的transferTo()有什么优势?

:transferTo()方法在很多操作系统上会使用零拷贝技术,避免了数据在用户态和内核态之间的多次拷贝,大幅提高文件复制性能。

❓5. 什么时候使用Files类而不是传统File类?

:Files类提供了更丰富、更易用的API,支持更多的文件操作,并且通常有更好的性能。在新代码中推荐使用Files类。

相关文章
|
Prometheus 负载均衡 监控
详解Gateway
详解Gateway
1813 0
|
存储 Java Maven
如何使用Spring Boot和MinIO实现文件上传、读取、下载和删除的功能?
如何使用Spring Boot和MinIO实现文件上传、读取、下载和删除的功能?
2148 5
|
数据安全/隐私保护
关于 OAuth 2.0 统一认证授权
随着互联网的巨头大佬逐渐积累了海量的用户与数据,用户的需求越来越多样化,为了满足用户在不同平台活动的需求,平台级的厂商则需要以接口的形式开放给第三方开发者,这样满足了用户的多样性需求,也可以让自己获得利益,让数据流动起来,形成给一个良性的生态环境,最终达到用户、平台商、第三方开发者共赢。
3176 0
|
3天前
|
人工智能 监控 安全
紧急!!慎用Cursor V1.5.7版本!!!存在恶意大规模攻击用户项目文件行为
Cursor v1.5.7 利用DeepSeek 3.1的架构感知和代码能力,对用户项目文件进行多批次恶意攻击
56 12
|
5天前
|
设计模式 安全 Java
Java设计模式(一):单例模式与工厂模式
本文详解单例模式与工厂模式的核心实现及应用,涵盖饿汉式、懒汉式、双重检查锁、工厂方法、抽象工厂等设计模式,并结合数据库连接池与支付系统实战案例,助你掌握设计模式精髓,提升代码专业性与可维护性。
|
20天前
|
运维 安全 网络安全
VMware NSX 4.2.3 - 网络安全虚拟化平台
VMware NSX 4.2.3 - 网络安全虚拟化平台
44 0
|
easyexcel Java API
Apache POI、EasyPoi、EasyExcel 三种区别,如何选择
Apache POI、EasyPoi、EasyExcel 三种区别,如何选择
1779 0
|
JSON 前端开发 安全
前端开发中的跨域问题及解决方案
在前端开发中,跨域是一个常见但又令人头疼的问题。本文将深入探讨跨域产生的原因以及一些常见的解决方案,帮助开发者更好地理解和处理跨域情况。
|
12月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
865 30
什么是线程池?从底层源码入手,深度解析线程池的工作原理