Java编程的逻辑 (59) - 文件和目录操作

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


前面两节我们介绍了如何通过流的方式读写文件内容,本节我们介绍文件元数据和目录的一些操作。

文件和目录操作最终是与操作系统和文件系统相关的,不同系统的实现是不一样的,但Java中的java.io.File类提供了统一的接口,底层它会通过本地方法调用操作系统和文件系统的具体实现,本节,我们就来介绍File类。

File类中的操作大概可以分为三类:

  • 文件元数据
  • 文件操作
  • 目录操作 

在介绍这些操作之前,我们先来看下File的构造方法。

构造方法

File既可以表示文件,也可以表示目录,它的主要构造方法有:

public File(String pathname)
public File(String parent, String child)
public File(File parent, String child) 

可以是一个参数pathname,表示完整路径,该路径可以是相对路径,也可以是绝对路径。还可以是两个参数,表示父目录的parent和表示孩子的child。

File中的路径可以是已经存在的,也可以是不存在的。

通过new新建一个File对象,不会实际创建一个文件,只是创建一个表示文件或目录的对象,new之后,File对象中的路径是不可变的。

文件元数据

文件名与文件路径

有了File对象后,就可以获取它的文件名和路径信息,相关方法有:

public String getName()
public boolean isAbsolute()
public String getPath()
public String getAbsolutePath()
public String getCanonicalPath() throws IOException
public String getParent()
public File getParentFile()
public File getAbsoluteFile()
public File getCanonicalFile() throws IOException

getName()返回的就是文件或目录名称,不含路径名。isAbsolute()判断File中的路径是否是绝对路径。

getPath()返回构造File对象时的完整路径名,包括路径和文件名称。getAbsolutePath()返回完整的绝对路径名。getCanonicalPath()返回标准的完整路径名,它会去掉路径中的冗余名称如".","..",跟踪软连接(Unix系统概念)等。这三个路径容易混淆,我们看一个例子来说明:

File f = new File("../io/test/students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("path: " + f.getPath());
System.out.println("absolutePath: " + f.getAbsolutePath());
System.out.println("canonicalPath: " + f.getCanonicalPath());

这里,使用相对路径来构造File对象,..表示上一级目录,输出为:

/Users/majunchang/io
path: ../io/test/students.txt
absolutePath: /Users/majunchang/io/../io/test/students.txt
canonicalPath: /Users/majunchang/io/test/students.txt

当前目录为/Users/majunchang/io,getPath()返回的就是构造File对象时使用的相对路径,而getAbsolutePath()返回的是完整路径,但是包含冗余路径"../io/",而getCanonicalPath()则去除了该冗余路径。

getParent()返回父目录路径,getParentFile()返回父目录的File对象,需要注意的是,如果File对象是相对路径,则这些方法可能得不到父目录,比如:

File f = new File("students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("parent: " + f.getParent());
System.out.println("parentFile: " + f.getParentFile());

输出为:

/Users/majunchang/io
parent: null
parentFile: null

即使是有父目录的,getParent()的返回值也是null。那如何解决这个问题呢?可以先使用getAbsoluteFile()或getCanonicalFile()方法,它们都返回一个新的File对象,新的File对象分别使用getAbsolutePath()和getCanonicalPath()的返回值作为参数构造。比如,修改上面的代码为:

File f = new File("students.txt");
System.out.println(System.getProperty("user.dir"));
System.out.println("parent: " + f.getCanonicalFile().getParent());
System.out.println("parentFile: " + f.getCanonicalFile().getParentFile());

这次,就能得到父目录了,输出为:

/Users/majunchang/io
parent: /Users/majunchang/io
parentFile: /Users/majunchang/io

File类中有四个静态变量,表示路径分隔符,它们是:

public static final String separator
public static final char separatorChar
public static final String pathSeparator
public static final char pathSeparatorChar

separator和separatorChar表示文件路径分隔符,在Windows系统中,一般为"\",Linux系统中一般为"/"。

pathSeparator和pathSeparatorChar表示多个文件路径中的分隔符,比如环境变量PATH中的分隔符,Java类路径变量classpath中的分隔符,在执行命令时,操作系统会从PATH指定的目录中寻找命令,Java运行时加载class文件时,会从classpath指定的路径中寻找类文件。在Windows系统中,这个分隔符一般为';',在Linux系统中,这个分隔符一般为':'。

文件基本信息

除了文件名和路径,File对象还有如下方法,以获取文件或目录的基本信息:

//文件或目录是否存在
public boolean exists()
//是否为目录
public boolean isDirectory()
//是否为文件
public boolean isFile()
//文件长度,字节数
public long length()
//最后修改时间,从纪元时开始的毫秒数
public long lastModified()
//设置最后修改时间,设置成功返回true,否则返回false
public boolean setLastModified(long time)

对于目录,length()方法的返回值是没有意义的。

需要说明的是,File对象没有返回创建时间的方法,因为创建时间不是一个公共概念,Linux/Unix就没有创建时间的概念。

安全和权限信息

File类中与安全和权限相关的方法有:

//是否为隐藏文件
public boolean isHidden()
//是否可执行
public boolean canExecute()
//是否可读
public boolean canRead()
//是否可写
public boolean canWrite()
//设置文件为只读文件
public boolean setReadOnly()
//修改文件读权限
public boolean setReadable(boolean readable, boolean ownerOnly)
public boolean setReadable(boolean readable)
//修改文件写权限
public boolean setWritable(boolean writable, boolean ownerOnly)
public boolean setWritable(boolean writable)
//修改文件可执行权限
public boolean setExecutable(boolean executable, boolean ownerOnly)
public boolean setExecutable(boolean executable)

在修改方法中,如果修改成功,返回true,否则返回false。在设置权限方法中,ownerOnly为true表示只针对owner,为false表示针对所有用户,没有指定ownerOnly的方法中,ownerOnly相当于是true。

文件操作

文件操作主要有创建、删除、重命名。

创建

新建一个File对象不会实际创建文件,但如下方法可以:

public boolean createNewFile() throws IOException 

创建成功返回true,否则返回false,新创建的文件内容为空。如果文件已存在,不会创建。

File对象还有两个静态方法,可以创建临时文件:

public static File createTempFile(String prefix, String suffix) throws IOException
public static File createTempFile(String prefix, String suffix, File directory) throws IOException

临时文件的完整路径名是系统指定的、唯一的,但可以通过参数指定前缀(prefix)、后缀(suffix)和目录(directory),prefix是必须的,且至少要三个字符,suffix如果为null,则默认为".tmp", directory如果不指定或指定为null,则使用系统默认目录。我们看个例子:

File file = File.createTempFile("upload_", ".jpg");
System.out.println(file.getAbsolutePath());

在我的电脑上的一些运行的输出为:

/var/folders/fs/8s4jdbj51jvcm7vc6lm_144r0000gn/T/upload_8850973909847443784.jpg

删除

File类如下删除方法:

public boolean delete()
public void deleteOnExit()

delete删除文件或目录,删除成功返回true,否则返回false。如果File是目录且不为空,则delete不会成功,返回false,换句话说,要删除目录,先要删除目录下的所有子目录和文件。

deleteOnExit将File对象加入到待删列表,在Java虚拟机正常退出的时候进行实际删除。

重命名

方法为:

public boolean renameTo(File dest) 

参数dest代表重命名后的文件,重命名能否成功与系统有关,如果成功返回true,否则返回false。

目录操作

当File对象代表目录时,可以执行目录相关的操作,如创建、遍历。

创建

有两个方法用于创建目录:

public boolean mkdir()
public boolean mkdirs()

它们都是创建目录,创建成功返回true,失败返回false。需要注意的是,如果目录已存在,返回值是false。这两个方法的区别在于,如果某一个中间父目录不存在,则mkdir会失败,返回false,而mkdirs则会创建必需的中间父目录。

遍历

有如下方法访问一个目录下的子目录和文件:

public String[] list()
public String[] list(FilenameFilter filter)
public File[] listFiles()
public File[] listFiles(FileFilter filter)
public File[] listFiles(FilenameFilter filter)

它们返回的都是直接子目录或文件,不会返回子目录下的文件。list返回的是文件名数组,而listFiles返回的是File对象数组。FilenameFilter和FileFilter都是接口,用于过滤,FileFilter的定义为:

public interface FileFilter {
    boolean accept(File pathname);
}

FilenameFilter的定义为:

public interface FilenameFilter {
    boolean accept(File dir, String name);
}

在遍历子目录和文件时,针对每个文件,会调用FilenameFilter或FileFilter的accept方法,只有accept方法返回true时,才将该子目录或文件包含到返回结果中。

FilenameFilter和FileFilter的区别在于,FileFilter的accept方法参数只有一个File对象,而FilenameFilter的accept方法参数有两个,dir表示父目录,name表示子目录或文件名。

我们来看个例子,列出当前目录下的所有后缀为.txt的文件,代码可以为:

File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Override
    public boolean accept(File dir, String name) {
        if(name.endsWith(".txt")){
            return true;
        }
        return false;
    }
});
for(File file : files){
    System.out.println(file.getCanonicalPath());
}

我们创建了个FilenameFilter的匿名内部类对象并传递给了listFiles。

使用遍历方法,我们可以方便的进行递归遍历,完成一些更为高级的功能。

比如,计算一个目录下的所有文件的大小(包括子目录),代码可以为:

public static long sizeOfDirectory(final File directory) {
    long size = 0;
    if (directory.isFile()) {
        return directory.length();
    } else {
        for (File file : directory.listFiles()) {
            if (file.isFile()) {
                size += file.length();
            } else {
                size += sizeOfDirectory(file);
            }
        }
    }
    return size;
}

再比如,在一个目录下,查找所有给定文件名的文件,代码可以为:

public static Collection<File> findFile(final File directory,
        final String fileName) {
    List<File> files = new ArrayList<>();
    for (File f : directory.listFiles()) {
        if (f.isFile() && f.getName().equals(fileName)) {
            files.add(f);
        } else if (f.isDirectory()) {
            files.addAll(findFile(f, fileName));
        }
    }
    return files;
}

前面介绍了File类的delete方法,我们提到,如果要删除目录而目录不为空,需要先清空目录,利用遍历方法,我们可以写一个删除非空目录的方法,代码可以为:

public static void deleteRecursively(final File file) throws IOException {
    if (file.isFile()) {
        if (!file.delete()) {
            throw new IOException("Failed to delete "
                    + file.getCanonicalPath());
        }
    } else if (file.isDirectory()) {
        for (File child : file.listFiles()) {
            deleteRecursively(child);
        }
        if (!file.delete()) {
            throw new IOException("Failed to delete "
                    + file.getCanonicalPath());
        }
    }
}

小结

本节介绍了如何在Java中利用File类进行文件和目录操作,File类封装了操作系统和文件系统的差异,提供了统一的API。

理解了这些操作,我们回过头来,再看下文件内容的操作,前面我们介绍的都是流,除了流,还有其他操作方式,如随机访问和内存映射文件,为什么还需要这些方式?它们有什么特点?适用于什么场合?让我们接下来继续探索。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

posted @ 2016-12-28 09:16  老马说编程  阅读(2177)  评论(2编辑  收藏  举报