Windows上可靠的File.renameTo()替代方案?

101

Java的File.renameTo()在Windows系统下存在问题。

正如API文档所述,该方法的许多行为与平台有关。重命名操作可能无法将文件从一个文件系统移动到另一个文件系统,它可能不是原子性的,并且如果目标抽象路径名已经存在,则可能无法成功。应始终检查返回值以确保重命名操作成功。

在我的情况下,作为升级过程的一部分,我需要移动(重命名)一个可能包含吉比特数据的目录(具有不同大小的许多子目录和文件)。移动通常在同一分区/驱动器内完成,因此实际上没有必要物理移动磁盘上的所有文件。

希望被移动的目录中不会有任何文件锁定,但是很常见的情况是,renameTo()无法完成其工作并返回false。(我猜测在Windows上某些文件锁似乎会在任意时刻过期。)

目前我有一种备选方法,即使用复制和删除,但这种方法很慢,具体时间取决于文件夹的大小。我还考虑简单地记录用户手动移动文件夹的事实,以避免等待数小时。但是,正确的方法显然应该是自动且快速的。

因此,我的问题是,您是否知道在Windows上使用Java进行快速移动/重命名的可靠替代方法,无论是使用纯JDK还是某个外部库。或者,如果您知道检测和释放给定文件夹及其所有内容(可能包括数千个单独的文件)的任何文件锁的易于方法,这也可以。


编辑:在这种特定情况下,我们似乎通过考虑更多的因素,仅使用renameTo()就顺利完成了操作;请参见此答案


3
您可以等待/使用具有更好文件系统支持的JDK 7。 - akarnokd
@kd304,实际上我不能等待或使用早期版本,但很有趣知道这样的东西正在路上! - Jonik
15个回答

58

另请参阅JDK 7中的Files.move()方法。

示例:

String fileName = "MyFile.txt";

try {
    Files.move(new File(fileName).toPath(), new File(fileName).toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
    Logger.getLogger(SomeClass.class.getName()).log(Level.SEVERE, null, ex);
}

12
很遗憾,Java7并不总是答案(就像42一样)。 - wuppi
1
即使在Ubuntu上,使用JDK7,在EC2上运行代码时也会遇到这个问题,使用EBS存储。File.renameTo失败了,File.canWrite也是如此。 - saurabheights
请注意,这个方法和File#renameTo()一样不可靠。它只是在失败时提供了更有用的错误信息。我发现唯一相对可靠的方法是使用Files#copy将文件复制到新名称,然后使用Files#delete删除原始文件(由于同样的原因,Files#delete本身也可能失败,就像Files#move可能失败一样)。 - jwenting
这个例子应该包括两个文件名,还是只有一个用于源和目标?虽然这只是一个有趣的例子,但如果你字面上理解这个例子,目前看起来解决方案就是用要移动到或从中移动的文件覆盖自身。 - HopefullyHelpful

28

顺便提一下,还有一些相关概念:

  1. 在Windows上,如果目标目录存在(即使是空的),renameTo()似乎会失败。这让我很惊讶,因为我曾经在Linux上尝试过,在那里只要目标存在并且为空,renameTo()就会成功。

    (显然,我不应该假设跨平台的操作都一样;这正是Javadoc所警告的。)

  2. 如果你怀疑可能会有一些未释放的文件锁,移动/重命名之前等待一段时间可能会有帮助。(在我们的安装程序/升级程序中,我们添加了一个"sleep"动作和一个不确定的进度条,等了大约10秒钟,因为可能某个服务仍在持有某些文件)。甚至可以采用简单的重试机制,尝试 renameTo(),然后等待一段时间(可能逐渐增加),直到操作成功或达到某个超时。

在我的情况下,考虑到上述两点,大多数问题似乎已经解决了,所以我们不需要进行本地内核调用或类似的操作。


2
我现在接受自己的答案,因为它描述了在我们的情况下有所帮助的内容。不过,如果有人针对renameTo()更一般的问题提出了一个很好的答案,可以随时发表,我会乐意重新考虑接受的答案。 - Jonik
4
6.5年后,我认为是时候接受JDK 7答案了,尤其是因为很多人认为它很有帮助。 =) Translated: After 6.5 years, I think it's time to accept the JDK 7 answer, especially since many people consider it helpful. =) - Jonik

20

原始帖子要求“使用纯JDK或某些外部库以替代、可靠的方法来快速移动/重命名Windows上的文件夹或文件。”

这里还有另一种选择,即使用v1.3.2或更新版本的apache.commons.io库,其中包括FileUtils.moveFile()

它在出错时抛出IOException而不是返回boolean false。

请参见big lep此其他线程中的回答。


2
另外,看起来JDK 1.7将包括更好的文件系统I/O支持。请查看java.nio.file.Path.moveTo(): http://java.sun.com/javase/7/docs/api/java/nio/file/Path.html - MykennaC
2
JDK 1.7没有java.nio.file.Path.moveTo()方法。 - Malte Schwerhoff

5
在我的情况下,似乎是我自己的应用程序中存在一个无法访问该文件的死对象。所以这个解决方案对我有效:

for (int i = 0; i < 20; i++) {
    if (sourceFile.renameTo(backupFile))
        break;
    System.gc();
    Thread.yield();
}

优点:速度相对较快,因为没有硬编码时间的Thread.sleep()。

缺点:20是一些硬编码数字的限制。在我所有的测试中,i = 1就足够了。但为了确保,我将其保留在20。


1
我做了类似的事情,但在循环中加入了100毫秒的延迟。 - Lawrence Dol

5
在Windows上,我使用Runtime.getRuntime().exec("cmd \\c "),然后使用命令行重命名函数来实际重命名文件。它更加灵活,例如,如果您想将目录中所有txt文件的扩展名重命名为bak,只需将此写入输出流:

rename *.txt *.bak

我知道这不是一个好的解决方案,但显然对我总是有效的,比Java内联支持要好得多。


太好了,这样好多了!谢谢! :-) - gaffcz

4
以下代码片段并不是“替代方案”,但在Windows和Linux环境中都能可靠地工作:
```html

以下代码片段并不是“替代方案”,但在Windows和Linux环境中都能可靠地工作:

```
public static void renameFile(String oldName, String newName) throws IOException {
    File srcFile = new File(oldName);
    boolean bSucceeded = false;
    try {
        File destFile = new File(newName);
        if (destFile.exists()) {
            if (!destFile.delete()) {
                throw new IOException(oldName + " was not successfully renamed to " + newName); 
            }
        }
        if (!srcFile.renameTo(destFile))        {
            throw new IOException(oldName + " was not successfully renamed to " + newName);
        } else {
                bSucceeded = true;
        }
    } finally {
          if (bSucceeded) {
                srcFile.delete();
          }
    }
}

2
这段代码会在 renameTo(或 destFile.delete)失败并且方法抛出 IOException 时删除 srcFile,我不确定这是否是一个好主意。 - Jonik
1
@Jonik,谢谢,修复了代码,如果重命名失败就不会删除源文件。 - crazy horse
感谢分享,这解决了我在Windows上的重命名问题。 - BillMan

4

我知道这看起来有点不正规,但就我所需的内容而言,似乎缓冲读取器和缓冲写入器在创建文件时没有任何问题。

void renameFiles(String oldName, String newName)
{
    String sCurrentLine = "";

    try
    {
        BufferedReader br = new BufferedReader(new FileReader(oldName));
        BufferedWriter bw = new BufferedWriter(new FileWriter(newName));

        while ((sCurrentLine = br.readLine()) != null)
        {
            bw.write(sCurrentLine);
            bw.newLine();
        }

        br.close();
        bw.close();

        File org = new File(oldName);
        org.delete();

    }
    catch (FileNotFoundException e)
    {
        e.printStackTrace();
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }

}

对于作为解析器的一部分的小型文本文件,它可以很好地工作,只需确保oldName和newName是文件位置的完整路径。

祝好 Kactus


2

Why not....

import com.sun.jna.Native;
import com.sun.jna.Library;

public class RenamerByJna {
    /* Requires jna.jar to be in your path */

    public interface Kernel32 extends Library {
        public boolean MoveFileA(String existingFileName, String newFileName);
    }

    public static void main(String[] args) {
        String path = "C:/yourchosenpath/";
        String existingFileName = path + "test.txt";
        String newFileName = path + "renamed.txt";

        Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
            kernel32.MoveFileA(existingFileName, newFileName);
        }
}

该程序可以在Windows 7上运行,如果现有文件不存在,则不执行任何操作,但显然可以更好地进行仪器化以修复此问题。


2

我遇到了类似的问题。在Windows上复制文件而不是移动文件,但在Linux上却可以正常工作。我通过在调用renameTo()之前关闭已打开的fileInputStream来解决了这个问题。在Windows XP上进行了测试。

fis = new FileInputStream(originalFile);
..
..
..
fis.close();// <<<---- Fixed by adding this
originalFile.renameTo(newDesitnationForOriginalFile);

1
在我的情况下,错误出现在父目录的路径上。可能是一个bug,我不得不使用substring来获取正确的路径。
        try {
            String n = f.getAbsolutePath();
            **n = n.substring(0, n.lastIndexOf("\\"));**
            File dest = new File(**n**, newName);
            f.renameTo(dest);
        } catch (Exception ex) {
           ...

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