尧图精选

Java文件操作对比:从File到NIO.2,迁移指南与踩坑总结

🕒 发布时间:2026/10/1 22:42:03 📁 来源:尧图网络
先说明一下我写这篇对比的起因。虽然 Java 7 就把 NIO.2也就是 java.nio.file 这套 API带进来了但你去翻很多生产项目的代码java.io.File依然随处可见。不是老项目不敢动而是很多同学入行时学的就是File后面项目里new File()、file.exists()、file.delete()一路写下来没人提醒的话很难跳出这个惯性。这篇是 Java 文件操作对比系列的第 4 篇也是收尾篇。前面几篇拆了 IO 流的读写细节、字符编码处理和二进制操作这篇就专门把java.io.File和java.nio.file两大体系做一次全方位对照。重点不放在“哪个 API 更高级”这种口号上而是直接落到实际开发里你每天都会碰到的场景文件删不掉怎么办、目录怎么递归遍历、移动文件是否原子、符号链接怎么处理、文件监听怎么做。每一条都会给出可跑的示例和我会踩的坑。1. API 设计与使用体验对比为什么 File 用着别扭1.1 设计哲学一坨类 vs 一条路径加一个工具类java.io.File最大的问题在于它既是“路径的表示”又是“文件操作的入口”。你new File(a.txt)并没有真正触达文件系统它只包装了一个路径字符串但同一个对象上你又可以调用exists()、delete()、mkdirs()这些方法去修改真实文件系统。路径表示和状态操作混在一起职责非常散。而java.nio.file把这两件事拆开了。Path只负责“描述一个路径”不带任何文件系统操作真正的读写、复制、移动、删除、属性查询全部收敛到Files这个工具类里。也就是说你拿到一个Path对象它只是一个不可变的位置标记想对它做什么再通过Files静态方法传入这个Path。类名上也容易踩坑。java.io.File叫“文件”但它其实也能代表目录Path叫“路径”听上去好像是给文件用的实际指向文件或目录都行。理解这个职责分离之后写代码的思路会清晰很多先构造路径再决定操作而不是在一个对象上调来调去。1.2 失败模型静默 boolean 与显式异常这是两套 API 使用体验差异最大的地方也是从File迁到 NIO 后最先感觉“舒服”的点。java.io.File的写操作基本都是返回boolean比如File file new File(/tmp/data/report.txt); boolean deleted file.delete(); if (!deleted) { // 到底为什么失败权限不存在目录非空完全不知道 }delete()返回false的原因可能是不存在、没有权限、文件被占用但老 API 不会告诉你具体是哪一种。排查问题全靠猜。mkdir()、renameTo()同理。java.nio.file的做法完全不同删除操作要么成功要么抛出带具体类型的异常Path path Paths.get(/tmp/data/report.txt); Files.delete(path); // 文件不存在 - NoSuchFileExceptionNoSuchFileException是IOException的子类你能从异常类一眼看出问题文件不存在。权限问题抛AccessDeniedException目录非空删除失败会得到DirectoryNotEmptyException路径格式不对抛InvalidPathException。这些异常类本身就携带了足够多的排查信息。所以我的建议很直接新代码一律用Files老代码如果还在写boolean ok file.delete()这种逻辑至少加个日志把它为什么失败打印出来否则线上出问题没法定位。下面给一个简单的语义对照表操作java.io.Filejava.nio.file失败表现删除文件delete()Files.delete(path)返回 false / 抛异常删除存在才删delete()后判断返回值Files.deleteIfExists(path)返回 boolean / 返回 false创建目录mkdir()Files.createDirectory(path)返回 false / 抛异常创建多级目录mkdirs()Files.createDirectories(path)返回 false / 抛异常判断存在exists()Files.exists(path)返回 boolean / 返回 boolean1.3 资源释放数组免操心流必须关File.listFiles()返回的是File[]数组拿到数组后不需要额外释放什么资源。这看上去很省事但代价是“一次性全量加载”。如果目录里有十万个文件数组会一次性把全部条目加载进内存。NIO 的Files.newDirectoryStream()返回一个DirectoryStreamPath可以边遍历边处理但注意它是AutoCloseable的必须关闭否则会泄漏文件句柄。try (DirectoryStreamPath stream Files.newDirectoryStream(dir)) { for (Path entry : stream) { // 处理 entry } } // 自动 closeFiles.list()和Files.walk()返回的是StreamPath同样需要关闭。很多人会忽略这一点因为Stream平时用起来不像“资源”。事实上Files.list()底层就包了一个DirectoryStream如果不用try-with-resources包住流不关闭句柄就会一直占着。Windows 上尤其明显文件被句柄占着后面想删除或移动都会失败。2. 路径处理File 的历史包袱与 Path 的现代化2.1 分隔符别再自己拼字符串了旧的FileAPI 里最常见的路径拼接写法是这样String path data File.separator 2025 File.separator report.txt;如果不小心用了File.separatorWindows 和 Linux 还能自适应但很多人图省事直接写死/或者\\。其实File内部能处理两种分隔符只是拼接出来的字符串在跨平台场景容易被其他组件误解。Path从根本上消灭了这类问题。使用Paths.get()传入多个片段NIO 会自动用当前文件系统的分隔符拼接Path path Paths.get(data, 2025, report.txt);在 Windows 上它会得到data\2025\report.txt在 Linux 上得到data/2025/report.txt。无论后面是直接交给Files操作还是传给其他接口都不会出现分隔符不一致的问题。实际开发里我几乎不再手动拼接路径字符串全部用这种可变参数构造方式。2.2 绝对路径与规范路径一个“不碰磁盘”一个“必须碰”File提供了两个容易混淆的方法getAbsolutePath()和getCanonicalPath()。getAbsolutePath()纯粹从字面上补全路径不会解析..和.也不会访问文件系统。getCanonicalPath()会解析..、.、符号链接返回“规范路径”。因为它要访问文件系统所以方法签名上直接声明了throws IOException。Path对应的是toAbsolutePath()和toRealPath()。其中toRealPath()等价于getCanonicalPath()的增强版默认会解析符号链接也可以通过参数LinkOption.NOFOLLOW_LINKS不跟随链接Path path Paths.get(/tmp/data/../data/report.txt); System.out.println(path.toAbsolutePath()); // /tmp/data/../data/report.txt带着 .. System.out.println(path.normalize()); // /tmp/data/report.txt词法规约不碰磁盘 System.out.println(path.toRealPath()); // 解析符号链接要求文件必须存在否则抛异常三者各有用途。配置文件加载时我通常用toRealPath()因为它会顺带校验文件是否存在如果只是想规整一下路径格式但文件还不一定存在就用normalize()。搞清楚这三者的区别比记住一堆 API 名字有用得多。2.3 路径段操作subpath 这类高阶能力 File 完全没有File里跟路径段有关的只有getName()、getParent()、getPath()想取完整路径中的某一段需要自己在字符串上切。Path提供了更结构化的能力Path path Paths.get(/projects/order-service/src/main/java/OrderService.java); System.out.println(path.getNameCount()); // 7 System.out.println(path.getName(0)); // projects System.out.println(path.subpath(0, 4)); // projects/order-service/src/main System.out.println(path.getFileName()); // OrderService.java System.out.println(path.getRoot()); // /subpath(0, 4)这种“截取中段路径”的能力在按目录结构扫描代码、按约定解析模块路径时特别好用。老代码要实现相同的逻辑基本只能split(/)然后自己拼数组还得分隔符在不同平台的差异。路径段的操作能力是 NIO 对旧 API 一次实打实的降维打击。3. 文件元数据从多次 stat 到一次属性视图3.1 存在性与类型判断老代码里最常见的判断逻辑是这样File f new File(/tmp/conf/application.yml); if (f.exists() f.isFile()) { // 读配置 }这段代码在大多数场景没问题但如果/tmp/conf/application.yml是一个符号链接就得小心了。java.io.File的isDirectory()和isFile()默认会跟随符号链接——也就是说如果符号链接指向的是一个目录isFile()会返回falseisDirectory()会返回true但有时候你恰恰想知道“这个链接本身指向什么类型”。NIO 的Files系列方法提供了LinkOption参数Path path Paths.get(/tmp/conf/application.yml); // 判断是否是目标类型跟随链接 boolean isFile Files.isRegularFile(path); // 判断链接本身的属性不跟随链接 boolean isFileNoFollow Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS); // 直接判断是不是符号链接 boolean isSymlink Files.isSymbolicLink(path);实际项目里处理配置文件路径时我一般先用Files.isSymbolicLink()判断一下再决定是否读取链接目标。如果只是简单判断目录是否存在用Files.isDirectory(path)就够但涉及符号链接的部署场景不加上NOFOLLOW_LINKS很容易误判。3.2 属性视图一次调用拿全套元数据File查询文件大小和修改时间需要分别调用File f new File(/tmp/data.bin); long size f.length(); long lastModified f.lastModified(); boolean isDir f.isDirectory(); boolean isHidden f.isHidden();每次调用都可能触发一次文件系统操作也就是一次 stat。虽然单次 stat 开销不大但在批量处理成千上万个文件时反复查询多个属性会让性能明显变差而且代码也啰嗦。NIO 的Files.readAttributes()可以一次读取整套属性BasicFileAttributes attrs Files.readAttributes(path, BasicFileAttributes.class); long size attrs.size(); long lastModified attrs.lastModifiedTime().toMillis(); long creationTime attrs.creationTime().toMillis(); boolean isDirectory attrs.isDirectory(); boolean isRegularFile attrs.isRegularFile(); boolean isSymbolicLink attrs.isSymbolicLink();一次系统调用拿到全部信息。需要区分文件类型时attrs.isRegularFile()和attrs.isDirectory()已经帮你分好了不用像File那样先exists()再isFile()做两次判断。如果需要更精细的属性还可以换用视图类视图类适用平台扩展属性BasicFileAttributes所有大小、时间、文件类型DosFileAttributesWindows隐藏、只读、归档、系统文件PosixFileAttributesLinux/macOS权限、属主、属组PosixFileAttributes posix Files.readAttributes(path, PosixFileAttributes.class); SetPosixFilePermission perms posix.permissions();这套属性视图机制在File时代是完全缺失的。拿File查 Linux 文件的读、写、执行权限只能通过canRead()、canWrite()、canExecute()三个方法分别判断拿不到具体的权限组合更拿不到属主属组。3.3 修改属性从粒度过粗到精细可控File提供的修改能力很有限翻来覆去就那几个file.setReadOnly(); // 只读 file.setWritable(true, false); // 当前用户可写ownerOnlyfalse file.setExecutable(true); // 当前用户可执行 file.setLastModified(timestamp); // 修改时间粒度非常粗。想要“给所有用户加执行权限”这种操作File根本做不了只能借助外部命令。NIO 则可以通过 PosixFilePermissions 精确控制权限位SetPosixFilePermission perms PosixFilePermissions.fromString(rwxr-x---); Files.setPosixFilePermissions(path, perms);fromString(rwxr-x---)这种写法非常直观一眼就能看出属主是rwx、属组是r-x、其他用户是---。生产环境里我经常用来给脚本文件加执行权限部署完直接一条命令生效。这个能力在File时代只能靠Runtime.exec(chmod 750 xxx)绕过去麻烦还容易踩转义坑。4. 目录遍历与递归删除三种写法的演进4.1 遍历一个目录null 的坑和必须关闭的流File.listFiles()最大的坑是返回值。如果目录里面没有条目它返回空数组但如果发生了 IO 错误比如目录不存在、权限不足它返回null。如果代码拿到null不去判空直接for遍历瞬间NullPointerException。File[] files dir.listFiles(); if (files ! null) { // 必须判空否则可能 NPE for (File f : files) { // ... } }NIO 的Files.newDirectoryStream()遇到目录不存在时直接抛NoSuchFileException不会返回null也没有必要判空。更轻量的是Files.list()返回StreamPath配合现代 Java 的函数式风格非常自然try (StreamPath stream Files.list(Paths.get(/tmp/data))) { stream.filter(Files::isRegularFile) .filter(p - p.toString().endsWith(.log)) .forEach(System.out::println); }这三个方法的取舍我实际使用下来是只是列目录、无需过滤和自定义属性查询用Files.list()需要过滤条件比较复杂的比如只挑大于某个大小的文件用Files.newDirectoryStream()配合自定义过滤器更清晰如果目录很大优先newDirectoryStream()边读边处理避免一次性加载全部条目到内存。4.2 深度遍历walk 与 walkFileTree 怎么选按目录树递归遍历是文件操作里最高频的需求之一。File时代只能手写递归大概长这样void listAll(File dir) { File[] files dir.listFiles(); if (files null) return; for (File f : files) { if (f.isDirectory()) { listAll(f); } else { System.out.println(f.getPath()); } } }NIO 提供了两种现成的深度遍历方案。第一种Files.walk()惰性遍历并返回StreamPath适合过滤、收集类的场景try (StreamPath stream Files.walk(Paths.get(/tmp/data))) { stream.filter(Files::isRegularFile) .forEach(System.out::println); }第二种Files.walkFileTree()基于访问者模式需要写一个SimpleFileVisitor。它最大的优势是能够在“进入目录前”“离开目录后”“访问文件时”“访问失败时”四个时机分别插入逻辑删除目录树时尤其好用。Files.walkFileTree(Paths.get(/tmp/data), new SimpleFileVisitorPath() { Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } });FileVisitResult除了CONTINUE还有SKIP_SUBTREE跳过当前目录、TERMINATE终止遍历。比如备份时需要跳过.git目录在preVisitDirectory里判断目录名直接返回SKIP_SUBTREE即可这种控制力是Stream方案给不了的。4.3 递归删除的三种推荐写法删除一个非空目录File.delete()直接失败因为目录非空。File时代最原始的递归删除长这样void deleteRecursively(File f) { if (f.isDirectory()) { File[] children f.listFiles(); if (children ! null) { for (File child : children) { deleteRecursively(child); } } } f.delete(); }NIO 时代常见三种写法。前面提到的walkFileTree是最稳的。第二种是利用Files.walk()配合反序删除——因为Files.walk()默认深度优先先列出的路径在树的上层删除前需要把流排序成“子路径在前、父路径在后”try (StreamPath stream Files.walk(Paths.get(/tmp/data))) { stream.sorted(Comparator.reverseOrder()) .forEach(p - { try { Files.deleteIfExists(p); } catch (IOException e) { throw new UncheckedIOException(e); } }); }第三种是用递归加Files.deleteIfExists()简洁但对深层目录会造成较深的调用栈void deleteRecursive(Path dir) throws IOException { try (StreamPath stream Files.list(dir)) { for (Path p : stream) { if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { deleteRecursive(p); } else { Files.deleteIfExists(p); } } } Files.delete(dir); }我个人的选择是老代码重构时用walkFileTree因为语义清晰、可控性高写一次性脚本或临时清理逻辑时用sorted(reverseOrder())那一行流式写法简洁。搜索“java 文件相关的操作”这个主题时递归删除永远是最热门的场景之一所以这里特意把三种姿势都列出看你们项目风格自取。5. 复制、移动与删除原子性是最容易被忽略的点5.1 删除语义的差异File.delete()和Files.delete()的差别前面已经提到这里再补一个实操场景清理日志文件时我们经常遇到“目标可能不存在”的情况。// 旧写法 File logFile new File(/tmp/app.log); if (logFile.exists()) { // 有些人会先 exists 再 delete logFile.delete(); } // NIO 写法 Files.deleteIfExists(Paths.get(/tmp/app.log));Files.deleteIfExists()把“存在才删”这个语义封装好了不用再手动判存在也省掉了“exists 判断后文件被并发删除导致 delete 返回 false”的竞态问题。需要注意deleteIfExists()在目录非空时依然会抛DirectoryNotEmptyException所以删除目录还是要走递归方案。5.2 复制文件保留属性是个细节活java.io.File没有自己的复制能力老代码通常用两个流手动搬运try (InputStream in new FileInputStream(src); OutputStream out new FileOutputStream(dst)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } }能复制内容但源文件的修改时间、权限这些元数据全部丢失。而Files.copy()可以通过CopyOption控制复制行为Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);REPLACE_EXISTING表示目标存在时覆盖COPY_ATTRIBUTES表示尽量保留源文件的属性修改时间等。如果不加COPY_ATTRIBUTES复制出来的文件时间戳就是“当前时间”在发布构建产物的场景里会造成缓存判断错误。这里有个细节Files.copy()底层在不同文件系统上的实现有差异如果源和目标在同一个文件系统里某些平台会走更高效的路径但你不必关心这些差异API 层面一致即可。另外Files.copy()复制目录时是浅复制只复制目录本身不会递归复制子目录和文件。需要完整复制目录树还得配合walkFileTree逐个创建和复制。5.3 移动文件renameTo 靠不住ATOMIC_MOVE 有讲究File.renameTo()是旧 API 里我踩过最多坑的方法。它的行为高度依赖平台和文件系统在 Windows 上如果目标文件已经存在renameTo()很可能失败如果跨文件系统比如从 C 盘挪到 D 盘renameTo()大概率直接返回 false如果目标文件的父目录不存在也会失败。最难受的是它失败返回false你完全不知道是哪种原因。Files.move()则干净得多Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);这在同一个文件系统内基本是原子的。如果需要把原子性作为硬性要求可以加上AtomicMoveNotSupportedException兜底try { Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, AtomicMoveOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { // 文件系统不支持原子移动降级为普通 move Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); }ATOMIC_MOVE保证移动操作要么完成、要么完全没发生。文件替换、日志轮转这类场景特别看重这个语义——如果你在进程正在写文件时做替换非原子移动可能出现目标文件短暂不存在或内容不完整的情况。这一点在生产环境里尤其重要比如部署新版本 jar 包如果不用原子替换的方式服务重启瞬间可能读到半截文件。5.4 临时文件与退出清理File.createTempFile()和File.deleteOnExit()是老代码里常见的组合File temp File.createTempFile(data, .tmp); temp.deleteOnExit();deleteOnExit()会在 JVM 退出时删除文件逻辑本身还好但有几个坑它只删除注册的这个文件不清理目录而且如果文件已经被删除它不会报错。更麻烦的是如果 JVM 被kill -9强杀deleteOnExit()根本不会执行临时文件会残留。NIO 的createTempFile()只负责创建没有自带退出清理Path temp Files.createTempFile(null, .tmp);清理需要自己做。如果是边写边用的临时文件最简单的是用完立刻删try { Files.write(temp, data); // 使用 temp } finally { Files.deleteIfExists(temp); }如果临时文件要跨越多个方法使用直到 JVM 退出可以注册ShutdownHook做兜底。deleteOnExit()本身也是 JVM 内部维护了一个待删队列所以存在一个隐藏问题如果短时间创建大量临时文件都会进入队列等待 JVM 退出时逐个清理如果程序崩得早列队里的文件就全留下变成垃圾。实际项目里我倾向于尽快删除临时文件而不是依赖退出钩子。6. 符号链接与文件监听NIO 独有的两个高价值能力6.1 符号链接判断与读取目标java.io.File完全没有符号链接的概念遇到符号链接会直接当普通文件处理。NIO 在这块补齐了关键能力。Path link Paths.get(/usr/bin/java); System.out.println(Files.isSymbolicLink(link)); // true Path target Files.readSymbolicLink(link); System.out.println(target); // 实际指向的路径注意readSymbolicLink()读取的是链接自己保存的目标路径不会递归解析最终目标。如果你需要不断解析直到找到真实文件可以结合toRealPath()Path realPath link.toRealPath(); // 默认跟随所有符号链接返回最终真实路径在包含软链的部署环境比如/usr/bin/java普遍是软链下判断 JDK 版本时用link.toRealPath()能拿到真正安装的 JDK 路径比File.getCanonicalPath()稳定得多。6.2 WatchService监控目录变化替代无头轮询java.io.File没有文件监听能力。老代码想实现“目录里多了新文件就处理”一般只能靠轮询lastModified或者listFiles()比对前后差异又慢又容易漏。NIO 的WatchService是原生的目录监听机制try (WatchService watchService FileSystems.getDefault().newWatchService()) { Path dir Paths.get(/tmp/incoming); dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); while (true) { WatchKey key watchService.take(); // 阻塞等待事件 for (WatchEvent? event : key.pollEvents()) { Path changed (Path) event.context(); System.out.println(event.kind() : dir.resolve(changed)); } key.reset(); // 重置后继续监听 } }这个机制的几个注意事项很关键WatchService只能监听目录本身不会递归监听子目录。想监听整棵目录树需要手动遍历子目录逐个register并在新目录创建时动态注册。事件类型里ENTRY_MODIFY可能会触发多次文件写入过程中可能产生多次修改事件做业务处理时最好加一个短延迟去重。平台延迟差异明显Windows 上事件可能稍有延迟Linux 上则依赖 inotify 机制不同文件系统对事件粒度的支持也有区别。但在大多数场景下用WatchService替代“每 5 秒扫一遍目录”的轮询实现无论是实时性、准确率还是系统开销都有质的提升。部署配置热更新、文件导入落地的自动触发我都是用这个方案。7. 迁移清单与实操建议从 File 到 NIO 的平滑过渡7.1 方法替换映射表老项目改造时最实用的就是一张映射表。下面这份是我自己整理过的覆盖日常 90% 的文件操作场景java.io.File操作java.nio.file替代new File(path)Paths.get(path)file.exists()Files.exists(path)file.isFile()Files.isRegularFile(path)file.isDirectory()Files.isDirectory(path)file.length()Files.size(path)file.lastModified()Files.getLastModifiedTime(path).toMillis()file.isHidden()Files.isHidden(path)file.mkdir()Files.createDirectory(path)file.mkdirs()Files.createDirectories(path)file.listFiles()Files.list(path)或Files.newDirectoryStream(path)file.renameTo(dest)Files.move(src, dest)file.delete()Files.deleteIfExists(path)file.getAbsolutePath()path.toAbsolutePath().toString()file.getCanonicalPath()path.toRealPath().toString()file.deleteOnExit()手动立即清理或ShutdownHookfile.setReadOnly()Files.setPosixFilePermissions()或DosFileAttributeView这里特别强调一下file.length()到Files.size(path)的差异。File.length()对不存在的文件返回 0可能被误读为“文件存在但内容为空”Files.size(path)对不存在的文件会抛NoSuchFileException语义更准确。我见过不少生产 bug 就是“文件不存在时 length 返回 0然后被当成空文件处理”。7.2 常见问题速查表实际迁移过程中问得最多的几个问题我整理成一张表问题场景推荐方案关键坑点删除目录树失败walkFileTree配合SimpleFileVisitor目录非空时delete()直接失败遍历大目录内存暴涨Files.newDirectoryStream()listFiles()一次性加载全部判断符号链接Files.isSymbolicLink(path)File.isDirectory()默认跟随链接移动文件要求原子Files.moveATOMIC_MOVE文件系统不支持时抛AtomicMoveNotSupportedException目录不存在时静默创建Files.createDirectories(path)mkdirs()失败返回 false无异常信息流忘关导致句柄泄漏try-with-resources包住Stream/DirectoryStreamFiles.list()底层持有DirectoryStream需要监控文件变化WatchService不递归子目录需手动注册复制文件保留时间戳Files.copyCOPY_ATTRIBUTES不指定则时间戳是当前时间7.3 迁移节奏建议老项目如果代码量巨大不推荐一次性把所有File全换成Path/Files。我见过不少同事一上来就全局替换最后在File.separator、deleteOnExit这些边缘语义上翻了车。稳妥的做法是只改新代码老代码按模块逐步替换。一个实用的中间策略是写一个薄封装工具类把高频操作包一层。比如统一提供deleteQuietly(Path)、moveAtomic(Path, Path)、listFilesStream(Path)这类方法底层用 NIO 实现然后逐步把老代码的调用点迁移到工具类上。这样既能享受新 API 的能力又不至于一次性改动太大。比较难迁移的是这几类依赖FilenameFilter或FileFilter的老代码可以改成DirectoryStream.FilterPath依赖file.deleteOnExit()清理临时文件的要改成显式清理依赖file.renameTo()实现移动的务必改成Files.move()否则在 Windows 上的行为极不稳定依赖file.getCanonicalPath()解析软链路径的改成path.toRealPath()语义更明确。新代码只要运行环境是 Java 8 以上我可以直接建议默认java.nio.file没有理由再用java.io.File做新的文件操作。唯一需要保留File的场景是调用第三方库的旧接口——有些库方法签名还是File参数这时候用path.toFile()转换即可。最后说点我自己的体会。NIO 真正让人觉得舒服的地方不是某个 API 名字更好看而是它把“路径”和“对路径做什么”拆开了。File之所以难用根本原因是它把状态判断、属性读取、修改操作全堆在一个类里失败时只给你一个boolean false你根本不知道发生了什么。从File迁到Path/Files不是把 API 名字换掉就完了而是把每一次操作脑子里过一遍它的语义删除是不存在就报错还是不存在就跳过移动失败后是抛异常终止还是静默降级目录遍历是需要全部加载还是边读边处理把这些问题想清楚了代码的健壮性自然会上去这也是我写这篇对比最想传达的一个点。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →