Java NIO 中 File.deleteOnExit() 的替代方案是什么?

64

Java IO提供了File.deleteOnExit()方法,该方法会在JVM正常结束时删除调用它的文件。我发现这对于清理临时文件特别是在单元测试期间非常有用。

然而,在Java NIO的Files类中并没有同名方法。我知道可以使用path.toFile().deleteOnExit()来实现相同的功能,但我想知道是否有使用NIO的另一种方法。

有替代方法吗?如果没有,为什么没有呢?


1
NIO 版本会有什么不同或更好的功能?(除了从调用链中删除 .toFile() 之外。) - Peter Lawrey
1
@Thunderforge DataInputStream.readLine()在1998年就已经被@Deprecated了,但它仍然存在。“完全生活在NIO中”的好处是什么? - Peter Lawrey
5
我知道Java倾向于不删除他们弃用的内容(这对于习惯Python的人来说似乎很奇怪),但是他们有一天可能会这样做。至于"完全使用NIO",如果代码完全使用NIO而不是同时使用两者,那么代码会更容易理解(特别是对于可能从未使用过IO的年轻开发者)。无论您是否同意这种理由,我仍想知道是否有替代方案。 - Thunderforge
3
我理解你的意思是,在Java中,除非有充分的理由需要添加某些东西,否则他们会避免这样做。他们会非常仔细地考虑每一个要添加的方法,如果它与现有的方法几乎没有区别,我猜它就不会被添加。 - Peter Lawrey
3
“完全生活在NIO中”并不是一个好处,它只是一个任意的自我限制。 - user207421
显示剩余6条评论
3个回答

44

简短回答

在Java NIO中,您无法删除任意文件,但是可以在打开新流时使用StandardOpenOption.DELETE_ON_CLOSE参数,这将在流关闭时(包括从try-with-resources语句中调用.close()或JVM终止)立即删除文件。例如:

Files.newOutputStream(Paths.get("Foo.tmp"), StandardOpenOption.DELETE_ON_CLOSE);

详细回答

经过大量探索,我发现Java NIO确实有一种删除文件的方法,但它采用了与Java I/O不同的方式。

首先,Files.createTempFile() 的Javadoc描述了三种删除文件的方式:

如果作为工作文件,可以使用DELETE_ON_CLOSE选项打开生成的文件,以便在调用适当的关闭方法时删除该文件。或者,可以使用shutdown hookFile.deleteOnExit()机制自动删除文件。

最后一个选择File.deleteOnExit()显然是Java I/O方法,我们要避免使用它。当您调用上述方法时,后台发生的是Shutdown-hook。但DELETE_ON_CLOSE选项是纯Java NIO。

Java NIO假设您只想删除实际打开的文件,而不是任意文件。因此,创建新流的方法(例如Files.newOutputStream())可以可选地采用多个OpenOptions,在其中您可以输入StandardOpenOption.DELETE_ON_CLOSE。那样会在流关闭时(通过调用.close()或JVM退出)立即删除该文件。

例如:

Files.newOutputStream(Paths.get("Foo.tmp"), StandardOpenOption.DELETE_ON_CLOSE);

当流被关闭时,无论是由于显式调用.close()、在try-with-resources语句中作为一部分关闭,还是由JVM终止,都将删除与该流关联的文件。

更新:在某些操作系统(例如Linux)上,StandardOpenOption.DELETE_ON_CLOSE会在创建OutputStream时立即删除。如果您只需要一个OutputStream,那可能仍然可以使用。有关详细信息,请参见DELETE_ON_CLOSE deletes files before close on Linux

因此,Java NIO比Java I/O增加了新功能,您可以在关闭流时删除文件。如果这足以替代在JVM退出期间删除文件,则可以使用纯Java NIO实现。否则,您将不得不依赖于Java I/O的File.deleteOnExit()或shutdown-hook来删除文件。


7
这是一个危险的答案;DELETE_ON_CLOSE不能完全取代File.deleteOnExit(),因为它要求你必须保持输出流打开状态才能与文件交互。而且,在一些平台上,文件实际上会在关闭前立即被删除。 - daiscog
@megaflop 我在我的回答中指出,DELETE_ON_CLOSE 的工作方式不同,它会在“流关闭时立即删除文件”,因此我认为我已经清楚地表明它的工作方式与 File.deleteOnExit() 不同。如果你可以接受它立即删除(在我的用例中,我是可以接受的),那么这是一个很好的替代方案。 - Thunderforge
2
如果不相同,则问题未得到回答。 - Simon Jenkins

25

在幕后,File.deleteOnExit()只会通过Runtime.addShutdownHook()创建一个shutdown hook

然后,您可以使用NIO执行相同的操作:

Runtime.getRuntime().addShutdownHook(new Thread() {
  public void run() {
    Path path = ...;

    Files.delete(path);
  }
});

2
+1 在被接受的答案中,关闭挂钩与“OutputStream”耦合,因此,如果您打算从创建临时文件的方法中返回“Path”,则会发现您的临时文件已被删除。 - dutoitns

11
我不建议将StandardOpenOption.DELETE_ON_CLOSE硬塞到File.deleteOnExit()的替代方案中。正如文档所述,它既不是通用的,也不太可能在非琐碎情况下正常工作。
正如名称所示,DELETE_ON_CLOSE旨在在文件关闭后立即清理不再需要的资源,因此适用于删除文件Files.createTempFile()的文档也明确指出,DELETE_ON_CLOSE仅适用于文件打开时才需要的“工作文件”。

Files.createTempFile()文档建议直接编写自己的关闭挂钩程序,或者继续使用File.deleteOnExit()。如果你只是在使用本地文件系统,那么使用File.deleteOnExit()并没有任何本质上的问题,尽管你希望使用NIO。如果你没有使用(或者不确定是否使用)本地文件系统,因此无法使用File.deleteOnExit(),那么编写自己的关闭挂钩程序就足够简单了,就像File一样

public final class DeletePathsAtShutdown {
  private static LinkedHashSet<Path> files = new LinkedHashSet<>();

  static {
    Runtime.getRuntime().addShutdownHook(
        new Thread(DeletePathsAtShutdown::shutdownHook));
  }

  private static void shutdownHook() {
    LinkedHashSet<Path> local;
    synchronized {
      local = paths;
      paths = null;
    }

    ArrayList<Path> toBeDeleted = new ArrayList<>(theFiles);
    Collections.reverse(toBeDeleted);
    for (Path p : toBeDeleted) {
      try {
        Files.delete(p);
      } catch (IOException | RuntimeException e) {
        // do nothing - best-effort
      }
    }
  }

  public static synchronized void register(Path p) {
    if (paths == null) {
      throw new IllegalStateException("ShutdownHook already in progress.");
    }
    paths.add(p);
  }
}

当然,如果NIO可以默认提供类似的关闭钩子会很好,但是它的缺失并不是使用错误工具的理由。你也可以给DeletePathsAtShutdown添加更多功能,例如remove()函数或删除目录的支持。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接