首页 > 代码库 > JDK 7中的文件操作的新特性
JDK 7中的文件操作的新特性
文件系统综述
一个文件系统在某种媒介(通常是一个或多个硬盘)上存储和组织文件。如今的大多数文件系统都是以树状结构来存储文件。在树的顶端是一个或多个根节点,在根节点一下,是文件和目录(在Windows系统里时文件夹)。每个目录可以包含文件或者子目录。
下面这张图表示一个仅包含一个根节点的树形结构。Windows系统支持多个根节点。
文件系统以根节点起始的路径来识别一个文件,例如,上图中的statusReport在Solaris操作系统中的路径为:
/home/sally/statusReport
在Windows操作系统中的路径为:
C:\home\sally\statusReport
路径的分割符根据具体的文件系统而变化
包含了根节点以及完整目录的路径为绝对路径。
相对路径则指一个文件相对于另一个文件的路径,程序需要借助于更多的相关信息才能根据相对路径定位该文件。
符号链接(symboliclink)也称为软链接(soft link),是指对一个文件的引用。在大多数情况下,符号链接对应用是透明的,对符号链接的操作会自动重定向至它所指向的真实文件
上图中,logFile就是一种符号链接,实际上它代表的真实路径是dir/logs/HomeLogFile。但是,对于用户来说,操作符号链接就跟操作它指向的真是路径是一样的
路径解析(resolving a link)的意思就是将符号链接替换为真实的路径
注意:以下类及操作在JDK 7以前的版本是不存在的
java.nio.file和java.nio.file.attribute是JDK 7新增的两个包,提供了对文件I/O操作的综合支持。尽管API有很多类,但是我们只需要重点关注一些重点条目就能满足一般的需求。
Path类
Path类是java.nio.file包中最基本的一项,它实际上是一个接口,它代表层次化文件系统的一个路径。路径可以只包含一个根元素,可以是根元素和一系列文件名称的组合,也可以只是一个或多个文件。访问一个空路径相当于访问该文件系统的默认目录。
Path类定义了getFileName,getParent, getRoot, subpath等一系列方法来获取该路径的相关属性
此外,Path类还定义了resolve 方法和resolveSibling方法来联接路径。路径是可以被比较的,也可以使用startsWith 和 endWith方法来匹配该路径的开始与结尾元素。
Path类可以利用Files类来对文件、目录或其他类型的文件进行操作。例如,我们需要一个BufferedReader类来读取一个名为“access.log”的文件,这个文件位于相对于工作路径的名为“logs”的路径下,且用UTF-8编码
Path path = FileSystems.getDefault().getPath("logs","access.log");
BufferReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
由默认提供者(即FileSystemProvider)创建的Path类可以与java.io.File类互用,非默认提供者创建的Path类则不能。File类的toPath方法可以获取File对象所代表的抽象路径名,而Path类的toFile方法可以根据一个抽象路径来创建一个File类实例
Path可能会包含根元素或者文件名,但两者不是必需的。一个Path可能只包含单个目录或者文件名。
可以通过Paths类的静态方法来创建Path类:
Path p1 =Paths.get("/tmp/foo"); Path p2 =Paths.get(args[0]); Path p3 =Paths.get(URI.create("file:///Users/joe/FileTest.java"));Paths.get方法是是下面这行代码的缩写形式:
Path p4 =FileSystems.getDefault().getPath("/users/sally");
下面这个Path指向用户目录下的“logs”目录下的“foo.log”文件Path p5 =Paths.get(System.getProperty("user.home"),"logs","foo.log");Path存储着元素序列,在层次结构中最高元素的索引为0,最低元素的索引为n-1,n为Path中的元素总数
下面常用方法,并不会要求Path必须对应着某个文件或目录,就是说,Path有可能指向一个不存在的文件或目录
Path path =Paths.get("C:\\home\\joe\\foo"); System.out.format("toString:%s%n", path.toString()); System.out.format("getFileName:%s%n", path.getFileName()); System.out.format("getName(0):%s%n", path.getName(0)); System.out.format("getNameCount:%d%n", path.getNameCount()); System.out.format("subpath(0,2):%s%n", path.subpath(0,2)); System.out.format("getParent:%s%n", path.getParent()); System.out.format("getRoot:%s%n", path.getRoot());输出:
C:\home\joe\foo
foo
home
3
home\joe
\home\joe
C:
上面的例子是对绝对路径的输出,下面来看一下相对路径的输出
Path path =Paths.get("sally\\bar"); //其余代码不变输出:
sally\bar
bar
sally
2
sally
sally
null
很多文件系统使用“.”来代表当前目录,使用“..”来代表父目录。有时候我们需要去除路径中的冗余字符。
/home/./joe/foo
/home/sally/../joe/foo
上面的两个Path都包含冗余字符。normalize方法可以去除这些冗余字符,上面两个Path经过normalize方法处理后,都会变成
/home/joe/foo
应该注意,normalize方法在清理冗余字符时并不会关联具体的文件系统,就是说它只是根据字面的语义来操作的。在第二个Path中,如果sally是一个字符链接,去除掉“sally/..”之后,该Path将不能正确的定位到预期的文件或目录。如果想要去除冗余字符而又保证能正确地定位,可以使用toRealPath方法
有三种方法可以对Path进行转换
toUri方法把Path转换成一个浏览器可以打开的字符串
Path p1 =Paths.get("/home/logfile"); System.out.format("%s%n",p1.toUri());输出:file:///home/logfile
toAbsolutePath方法将Path转换成一个绝对路径。该方法在处理用户输入的文件名时非常有用,一般情况下,该方法会将当前的工作目录加到文件名的前面
toRealPath方法返回存在文件的真实路径。如果将true作为参数传入该方法则代表将会解析Path中的符号链接,默认为false;如果Path是相对路径则返回绝对路径;如果Path包含冗余字符,则返回去除后的路径
resolve方法用来连接连个路径。传入一个部分路径(即不包含根元素的路径),该部分路径会被追加到原始路径的后面
Path p1 =Paths.get("C:\\home\\joe\\foo"); System.out.format("%s%n",p1.resolve("bar"));输出:/home/joe/foo/bar
如果传入的是一个绝地路径,则返回该绝对路径
relativize方法用以返回两个路径之间的相对路径。
来看下面这两个相对路径:
Path p1 =Paths.get("joe"); Path p2 =Paths.get("sally");如果没有提供额外的信息,系统会假设这两个路径为兄弟节点,即两者处于树形结构的同一层级上。想要得出p1到p2相对路径,需要先从p1回溯之上一个层级,然后在从该层级向下寻找p2
Path p1_to_p2 =p1.relativize(p2);输出:../sally
Path p2_to_p1 =p2.relativize(p1);输出:../joe
再看一个稍微复杂一点的例子
Path p1 =Paths.get("home"); Path p3 =Paths.get("home/sally/bar");// 结果为:sally/bar
Path p1_to_p3 =p1.relativize(p3);// 结果为:../..
Path p3_to_p1 =p3.relativize(p1);在该方法中,如果只有一个路径包含根元素,不能得出相对路径的;如果两个路径都包含根元素,能否得到相对路径取决于具体的系统
equals方法用于比较两个路径是否相同,而startsWith方法和endsWith方法用于测试路径是否以某个元素开始或结尾
Path path = ...; Path otherPath =...; Path beginning =Paths.get("/home"); Path ending = Paths.get("foo"); if(path.equals(otherPath)) { // equality logic here } else if(path.startsWith(beginning)) { // path begins with "/home" } else if(path.endsWith(ending)) { // path ends with "foo" }Path实现了Iterate接口,迭代时,第一个被返回的是离根元素最近的元素
Path path =Paths.get("D:\\doc\\logs\\log.txt"); for(Pathname : path){ System.out.println(name); }输出:
doc
logs
log.txt
Path也实现了Comparable接口。
Files类
Files类是java.nio.file包中另一个基本类,它提供了丰富的方法来读、写、操作文件和目录。Files类中的方法是基于Path对象来实现的
我们可以用Path实例来代表一个文件或者目录,但是该对象是否存在,是否可读写都是未知的,这就需要结合Files类来实现
exists(Path,LikeOption...)方法和notExists(Path,LikeOption...)方法用于检测Path是否存在,
注意:!Files.exists(path) 与 Files.notExists(path)并不等价
检测文件的存在性是,有三种可能的结果:该文件存在;该文件不存在;该文件的存在性未知,如果程序对文件没有访问权限就会出现这种情况。
如果exists方法和notExists方法都返回false,则文件的存在性未知
isReadable(Path),isWritable(Path)和isExecutable(Path)用于检测文件的可访问性
boolean isRegularExecutableFile = Files.isRegularFile(file) & Files.isReadable(file) &Files.isExecutable(file);
在使用符号链接的文件系统中,两个不同的路径有可能定位到同一个文件,isSameFile(Path,Path)用于检测两个路径是否定位到同一个文件
删除操作对于符号链接而言,只会在形式上删除此链接,而不会删除链接指向的真实目标;对于目录而言,该目录必须为空,否则删除失败。Files类提供了两种删除操作
delete(Path)方法在删除失败时会抛出异常,如果文件不存在则抛出NoSuchFileException异常
try { Files.delete(path); } catch(NoSuchFileException x) { System.err.format("%s: no such" +" file or directory%n", path); } catch(DirectoryNotEmptyException x) { System.err.format("%s notempty%n", path); } catch(IOException x) { // File permission problems are caughthere. System.err.println(x); }deleteIfExists(Path)方法同样是实现删除操作的,不同的是当文件不存在时不会抛出异常
copy(Path, Path, CopyOption...)用于复制文件或目录,如果目标路径已经存在,复制会失败,除非指定了REPLACE_EXISTING参数。
目录也是可以复制的,但是目录内的文件不会被复制,所以即便原来的目录下有文件,复制后的目录也是空的。
复制一个符号链接时,目标文件也会被复制。如果只想要复制链接本身,需要指定NOFOLOW_LINKS参数或者REPLACE_EXISTING参数
copy方法使用了可变参数,支持下面的三个可选参数
(java.nio.file.StandardCopyOption)
REPLACE_EXISTING :如果目标路径已存在,则覆盖掉。如果复制的是符号链接,只复制链接本身。
COPY_ATTRIBUTES :同时复制文件的相关属性,如最后修改时间、文件大小等。
NOFOLLOW_LINKS :如果是符号链接,只复制链接本身,不复制链接指向的目标文件。
此外,Files类还提供了文件和流之间的复制操作,copy(InputStream, Path, CopyOptions...)方法用于将指定文件中的全部字节复制到InputStream中,copy(Path, OutputStream)方法用于将OutputStream流中的所有字节复制到指定文件之中
move(Path, Path,CopyOption...)方法用于文件的移动,如果目标路径已存在,操作会失败,除非指定了REPLACE_EXISTING参数,移动目录时不会移动目录中的内容
可选参数:
REPLACE_EXISTING:与上面复制操作的参数一样
ATOMIC_MOVE:原子文件操作,即该操作不能被打断或者部分地执行,该操作被完全执行或者执行失败。
Files类提供了丰富的方法来获取文件的属性,如size(Path)用于获取文件的字节数大小;isDirectory(Path,LinkOption)用于判断该路径是否为目录;isHidden(Path)用于判断是否为隐藏文件等等,大多数方法都能做到望文知义。
此外,还可以使用readAttributes来对文件属性进行整体的读取
BasicFileAttributesattr = Files.readAttributes(file, BasicFileAttributes.class); System.out.println("creationTime:" + attr.creationTime()); System.out.println("lastAccessTime:" + attr.lastAccessTime()); System.out.println("lastModifiedTime:" + attr.lastModifiedTime()); System.out.println("isDirectory:" + attr.isDirectory()); System.out.println("isOther:" + attr.isOther()); System.out.println("isRegularFile:" + attr.isRegularFile()); System.out.println("isSymbolicLink:" + attr.isSymbolicLink()); System.out.println("size:" + attr.size());这样比一个一个地读取属性效率要高很多,readAttributes方法接受的参数BasicFileAttributes.class代表,读取的是文件的基本属性,适于平台无关的一些共有属性。也可以通过传入参数DosFileAttributes.class来读取与DOS平台下的更丰富的属性,以及传入PosixFileAttributes.class来读取与UNIX平台相关的属性等
Files类提供了一套读写文件、创建文件的方法
readAllBytes(Path)适用于读取较小的文件,一次性的将文件内容读入字节数组
byte[] fileArray; fileArray =Files.readAllBytes(file);readAllLines(Path path, Charset cs)一行一行的读取整个文件,并以指定字符编码为字符串集合
example.txt中的内容为:
a
bb
ccc
dddd
Path path = Paths.get("D:\\example.txt"); List<String> list =Files.readAllLines(path, StandardCharsets.UTF_8); for(String str : list){ System.out.println(str); }输出:
a
bb
ccc
dddd
相应地,write(Path, byte[], OpenOption...)方法则是用来写入文件的
Files也提供了带缓存的输入处处流
Charset charset= Charset.forName("UTF-8"); try(BufferedReader reader = Files.newBufferedReader(file, charset)) { String line = null; while ((line = reader.readLine()) != null){ System.out.println(line); } } catch(IOException x) { System.err.format("IOException:%s%n", x); } Charset charset= Charset.forName("UTF-8"); String s = ...; try(BufferedWriter writer = Files.newBufferedWriter(file, charset)) { writer.write(s, 0, s.length()); } catch(IOException x) { System.err.format("IOException:%s%n", x); }
newBufferedReader与newBufferedWriter方法返回的是带缓存的读入读出流,而newInputStream与newOutputStream返回的是不带缓存的输入输出流
String s = ...; byte data[] =s.getBytes(); try(OutputStream out = new BufferedOutputStream( Files.newOutputStream(CREATE,APPEND))) { ... out.write(data, 0, data.length); } catch(IOException x) { System.err.println(x); }Files.newOutputStream(CREATE,APPEND)中的参数是java.nio.file. StandardOpenOptions类中定义的常量。CREATE参数代表如果文件存在则打开,如不存在则创建;APPEND参数代表将新数据追加到文件的末尾,此参数与CREAT和WRITE参数一块使用。该类中还有其他参数,请参照JDK 7的API
createFile(Path, FileAttribute<?>)方法用于创建空文件,可以指定文件的各种属性,如不指定,按文件系统默认的属性写入。createTempFile方法可以用来创建临时文件
以上我们讨论的是对文件、链接以及目录的操作,如果我们想要列出目录中的文件、创建一个目录或者列出文件系统的根目录,JDK提供了相应的方法吗?答案是肯定的
使用FileSystem.getRootDirectories可以列出该文件系统所有的根目录,此方法返回一个Iterable接口实例
Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories(); for (Path name:dirs) { System.err.println(name); }createDirectory(Path, FileAttribute<?>)用来创建目录,用法与createFile类似
newDirectoryStream(Path)方法可以列出一个目录中的所有内容,返回一个实现了DirectoryStream接口和Iterable接口的对象
try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { for (Path file: stream) { System.out.println(file.getFileName()); } } catch(IOException | DirectoryIteratorException x) { // IOException can never be thrown by theiteration. // In this snippet, it can only be thrownby newDirectoryStream. System.err.println(x); }该方法返回的是相对于该目录的文件的路径名,比如,要列出的是“/tmp”下的所有内容,返回的结果将是:/tmp/a /tmp/b ……
使用该方法时,目录内的文件、子目录、链接以及隐藏文件都会被列出,如果想要对内容进行更精确的检索,可以使用其他的newDirectoryStream方法
比如,我们只想找出目录下的.class文件、.java文件和.jar文件,用下面这条语句就可以实现:
Files.newDirectoryStream(dir,"*.{java,class,jar}")如果上面这种过滤还不能满足我们的要求,我还可以通过实现DirectoryStream.Filter<T>接口来定制过滤器
DirectoryStream.Filter<Path>filter = newDirectoryStream.Filter<Path>() { public boolean accept(Path file) throwsIOException { try { return (Files.isDirectory(path)); } catch (IOException x) { // Failed to determine if it's adirectory. System.err.println(x); return false; } } }自定义的过滤器可以调用newDirectoryStream(Path, DirectoryStream.Filter<?super Path>)来实现
try(DirectoryStream<Path> stream =Files.newDirectoryStream(dir, filter)) { for (Path entry: stream) { System.out.println(entry.getFileName()); } } catch(IOException x) { System.err.println(x); }FileVisitor接口
FileVisitor接口是为遍历树形结构而设计的,该接口有四个方法:
preVisitDirectory –在访问目录之前触发
postVisitDirectory – 在目录中的所有条目被访问之后触发
visitFile – 在目录中的文件被访问时触发。该文件的BasicFileAttributes 会被传递到该方法中。
visitFileFailed –当文件无法被访问时触发
如果不需要实现上面的所有方法,还可以继承实现了FileVisitor接口的SimpleFileVisitor类,只需要覆写父类中的需要用到的方法即可
import static java.nio.file.FileVisitResult.*; public static class PrintFiles extends SimpleFileVisitor<Path> { // Print information about // each type of file. @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { if (attr.isSymbolicLink()) { System.out.format("Symboliclink: %s ", file); } else if (attr.isRegularFile()) { System.out.format("Regularfile: %s ", file); } else { System.out.format("Other: %s", file); } System.out.println("(" +attr.size() + "bytes)"); return CONTINUE; } // Print each directory visited. @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) { System.out.format("Directory:%s%n", dir); return CONTINUE; } // If there is some error accessing // the file, let the user know. // If you don't override this method // and an error occurs, an IOException // is thrown. @Override public FileVisitResult visitFileFailed(Pathfile, IOException exc) { System.err.println(exc); return CONTINUE; } }实现了FileVisitor接口,怎么利用它来遍历呢?在Files类中有两个方法:
walkFileTree(Path, FileVisitor)
walkFileTree(Path, Set<FileVisitOption>, int,FileVisitor)
第一个方法很简单,传入欲遍历的路径和FileVisitor即可
第二个方法可以控制访问的层次数并利用FileVisitOption对遍历做进一步的限制
在遍历的时候,可能我们希望找到某个文件后就结束遍历,或者跳过某些文件,这时,我们可以在preVisitDirectory方法中返回特定的值来控制流程
import static java.nio.file.FileVisitResult.*; public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { (if(dir.getFileName().toString().equals("SCCS")) { return SKIP_SUBTREE; } return CONTINUE; }上例中,返回的是CONTINUE,代表该目录会被访问。还有其他参数,如TERMINATE代表终止遍历,SKIP_SUBTREE代表调过该子目录,SKIP_SIBLINGS代表调过所有兄弟目录,具体用法请参考API
import static java.nio.file.FileVisitResult.*; // The file weare looking for. Path lookingFor= ...; public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { if (file.getFileName().equals(lookingFor)){ System.out.println("Located file:" + file); return TERMINATE; } return CONTINUE; }上例表示,如果遇到要找的文件则打印出该文件的路径并终止遍历,否则继续遍历
其他常用方法
probeContentType(Path)
该方法可以用来探测文件类型
try { String type =Files.probeContentType(filename); if (type == null) { System.err.format("'%s' hasan" + " unknown filetype.%n", filename); } else if (!type.equals("text/plain"){ System.err.format("'%s' isnot" + " a plain text file.%n", filename); continue; } } catch(IOException x) { System.err.println(x); }如果文件的类型不能识别,会返回null
文件类型是与平台相关的,是由平台默认的类型探测器决定的
如果默认的机制不能满足需求,还可以定制一个FileTypeDetector来满足更丰富的需求
FileSystems.getDefault()
该方法返回默认的文件系统,一般情况下,该方法后面跟着另一个方法来获取具体的属性,如
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.*");获取一个模式匹配器
FileSystems.getDefault().getSeparator();该方法返回一个由平台决定的分隔符
在java 7 以前,java.io.File类是用来处理文件的,但是它有很多缺点,java 7 推荐使用java.nio.file.Path来替换java.io.File。但是,有很多程序使用以前的代码下的,如何进行转换呢?
File类中提供了toPath方法,将File实例转换为Path 实例
File file =newFile(“D:\\example.txt”); file.delete();就可以改为Path path =file.toPath(); Files.delete(path);同样地,Path类也提供了toFile 方法来进行逆转换
JDK 7中的文件操作的新特性