请慎用java的File#renameTo(File)方法

以前我一直以为File#renameTo(File)方法与OS下面的 move/mv 命令是相同的,可以达到改名、移动文件的目的。不过后来经常发现问题:File#renameTo(File)方法会返回失败(false),文件没有移动,又查不出原因,再后来干脆弃用该方法,自己实现一个copy方法,问题倒是再也没有出现过。

昨天老板同学又遇到这个问题,File#renameTo(File)方法在windows下面工作的好好的,在linux下偶尔又失灵了。回到家我扫了一遍JDK中File#renameTo(File)方法的源代码,发现它调用的是一个本地的方法(native method),无法再跟踪下去。网上有人说该方法在window下是正常的,在linux下面是不正常的。这个很难说通,SUN不可能搞出这种平台不一致的代码出来啊。

后面在SUN的官方论坛上看到有人提到这个问题“works on windows, don't work on linux”,后面有人回复说是“file systems”不一样。究竟怎么不一样呢?还是没有想出来...

后面在一个论坛里面发现了某人关于这个问题的阐述:

引用

In the Unix'esque O/S's you cannot renameTo() across file systems. This behavior is different than the Unix "mv" command. 
When crossing file systems mv does a copy and delete which is what you'll have to do if this is the case. 

The same thing would happen on Windows if you tried to renameTo a different drive, i.e. C: -> D:

终于明白咯。做个实验:

File sourceFile = new File("c:/test.txt");  
File targetFile1 = new File("e:/test.txt");  
File targetFile2 = new File("d:/test.txt");  
System.out.println("source file is exist? " + sourceFile.exists() + ", source file => " + sourceFile);  
System.out.println(targetFile1 + " is exist? " + targetFile1.exists());  
System.out.println("rename to " + targetFile1 + " => " + sourceFile.renameTo(targetFile1));  
System.out.println("source file is exist? " + sourceFile.exists() + ", source file => " + sourceFile);  
System.out.println(targetFile2 + " is exist? " + targetFile2.exists());  
System.out.println("rename to " + targetFile2 + " => " + sourceFile.renameTo(targetFile2));

结果:

source file is exist? true, source file => c:\test.txt  
e:\test.txt is exist? false  
rename to e:\test.txt => false  
source file is exist? true, source file => c:\test.txt  
d:\test.txt is exist? false  
rename to d:\test.txt => true 

注意:,从C盘到E盘失败了,从C盘到D盘成功了。因为我的电脑C、D两个盘是NTFS格式的,而E盘是FAT32格式的。所以从C到E就是上面文章所说的"file systems"不一样。从C到D由于同是NTFS分区,所以不存在这个问题,当然就成功了。

果然是不能把File#renameTo(File)当作move方法使用。

可以考虑使用apache组织的commons-io包里面的FileUtils#copyFile(File,File)和FileUtils#copyFileToDirectory(File,File)方法实现copy的效果。至于删除嘛,我想如果要求不是那么精确,可以调用File#deleteOnExit()方法,在虚拟机终止的时候,删除掉这个目录或文件。

OK,到此为止都是引用原作者的内容,http://xiaoych.iteye.com/blog/149328,一下是我自己做的实验。

首先在windows上作实验。

import java.io.File;

public class Test {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("Test");
		File sourceFile = new File("c:/test.txt");
		File targetFile = new File("d:/test.txt");
		try {
			boolean result = targetFile.renameTo(sourceFile);
			System.out.println("result = " + result);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

结构为true,但是我的C盘的文件系统为FAT32,D盘为NTFS,并不能重现原作者的问题。此时看了原文章的一些评论内容,说可能与系统有关,也有人说可能与JDK版本有关,但是现在我也不能确定具体与什么有关。

接着实验,我又换了一个环境,在Ubuntu上做实验。

package com.darren.test;

import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;

public class Test {
	public static void main(String[] args) {
		File source = new File("/test.txt");
		File target = new File("/dev/test.txt");
		Test.testCopy(source, target);

	}

	public static void testRenameTo(File source, File target) {
		try {
			boolean result = source.renameTo(target);
			System.out.println(result);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public static void testCopy(File source, File target) {
		try {
			FileUtils.copyFile(source, target);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

测试renameTo方法的时候失败了,而测试copy方法的时候成功了,我测试的两个目录的文件系统也不一样。

再做一个测试:

最近遇到一个BUG,说mount一个forder之后,使用renameTo方法失败,然后我就在Ubuntu上做了这个测试,发现确实失败,因为mount之后得到文件系统有变化,导致使用renameTo前后两个目录的文件系统不一致,导致无法移动,所以在此说明renameTo方法不等同于操作系统的move方法,当然可以使用apache的commons-io中的FileUtils.moveFile(source, target)去替换renameTo方法。那么还有没有官方的证据证明renameTo方法存在缺陷呢? 答案是当然有,让我门看一下JDK的API文档:

public boolean renameTo(File dest)
Renames the file denoted by this abstract pathname.
Many aspects of the behavior of this method are inherently platform-dependent: The rename operation might not be able to 
move a file from one filesystem to another, it might not be atomic, and it might not succeed if a file with the destination 
abstract pathname already exists. The return value should always be checked to make sure that the rename operation 
was successful.

Note that the Files class defines the move method to move or rename a file in a platform independent manner.

Parameters:
dest - The new abstract pathname for the named file
Returns:
true if and only if the renaming succeeded; false otherwise
Throws:
SecurityException - If a security manager exists and its SecurityManager.checkWrite(java.lang.String) method denies write 
access to either the old or new pathnames
NullPointerException - If parameter dest is null

所以说,当遇到多文件系统的时候,请尽量避免使用File#renameTo(File)方法,可是使用apache的commons-io包去替换,避免一些潜在BUG。



你可能感兴趣的:(Java)