首页 > 代码库 > 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.filejava.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);
}

 

newBufferedReadernewBufferedWriter方法返回的是带缓存的读入读出流,而newInputStreamnewOutputStream返回的是不带缓存的输入输出流

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中的文件操作的新特性