java官网教程(基础篇)—— 基础的Java类 —— 基础 I / O

基本 Java 类

关于异常、基本输入/输出、并发、正则表达式和平台环境的课程。

基础 I/O

涵盖了用于基本输入和输出的 Java 平台类。它主要关注I/O 流,这是一个强大的概念,可以极大地简化 I/O 操作。本课程还介绍了序列化,它允许程序将整个对象写入流并再次读取它们。然后本课将介绍一些文件系统操作,包括随机访问文件。最后,简要介绍了 New I/O API 的高级特性。

I/O流

I/O流表示输入源或输出目标。一个流可以表示许多不同类型的源和目的地,包括磁盘文件、设备、其他程序和内存数组。

流支持多种不同类型的数据,包括简单字节、原始数据类型、本地化字符和对象。有些流只是简单地传递数据;其他的在有用的方面操作和转换数据。

不管它们内部如何工作,所有的流都向使用它们的程序呈现了相同的简单模型:流是数据序列。程序使用输入流从源读取数据,每次读取一项:
在这里插入图片描述
程序使用输出流将数据写入目标,每次写入一项:
在这里插入图片描述
在这一课中,我们将看到流可以处理各种类型的数据,从原始值到高级对象。

上图所示的数据源和数据目的地可以是保存、生成或使用数据的任何东西。显然,这包括磁盘文件,但源或目标也可以是另一个程序、外围设备、网络套接字或数组。

在下一节中,我们将使用最基本的流,即字节流,来演示流I/O的常见操作。对于示例输入,我们将使用示例文件xanadu.txt,它包含以下诗句:

In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.

字节流

程序使用字节流来执行8位字节的输入和输出。所有字节流类都是从InputStream和OutputStream派生而来的。

有许多字节流类。为了演示字节流是如何工作的,我们将重点介绍文件I/O字节流FileInputStream和FileOutputStream。其他类型的字节流的使用方式大致相同;它们的主要区别在于它们的构造方式。

使用字节流

我们将通过一个名为CopyBytes的示例程序来研究FileInputStream和FileOutputStream,该程序使用字节流一次一个字节地复制xanadu.txt。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("xanadu.txt");
            out = new FileOutputStream("outagain.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

CopyBytes的大部分时间都花在一个简单的循环中,读取输入流并写入输出流,每次一个字节,如下图所示。
在这里插入图片描述
总是关闭流

当不再需要流时关闭流是非常重要的——非常重要,CopyBytes使用finally块来保证即使发生错误,两个流也会被关闭。这种做法有助于避免严重的资源泄漏。

一个可能的错误是,CopyBytes无法打开一个或两个文件。当这种情况发生时,对应于文件的流变量从它的初始值空值永远不会改变。这就是为什么CopyBytes确保每个流变量在调用close之前包含一个对象引用。

什么时候不使用字节流

CopyBytes看起来像一个普通的程序,但它实际上代表了一种应该避免的低级I/O。由于xanadu.txt包含字符数据,因此最好的方法是使用字符流,这将在下一节中讨论。还有用于更复杂数据类型的流。字节流应该只用于最基本的I/O。

那么为什么要讨论字节流呢?因为所有其他流类型都建立在字节流的基础上。

字符流

Java平台使用Unicode存储字符值。字符流I/O自动地将这种内部格式转换为本地字符集。在西方语言环境中,本地字符集通常是ASCII的8位超集。

对于大多数应用程序来说,字符流的I/O并不比字节流的I/O复杂。使用流类完成的输入和输出会自动与本地字符集相互转换。一个使用字符流代替字节流的程序可以自动适应本地字符集并准备国际化——这一切都无需程序员额外的努力。

如果国际化不是优先级,您可以简单地使用字符流类,而无需过多关注字符集问题。之后,如果国际化成为一个优先事项,您的程序可以进行调整,而不需要大量的重新编码。有关更多信息,请参阅国际化部分。

使用字符流

所有字符流类都是从Reader和Writer派生而来的。与字节流一样,也有专门用于文件I/O的字符流类:FileReader和FileWriter。CopyCharacters示例演示了这些类。

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyCharacters {
    public static void main(String[] args) throws IOException {

        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("xanadu.txt");
            outputStream = new FileWriter("characteroutput.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

CopyCharacters与CopyBytes非常相似。最重要的区别是,CopyCharacters使用FileReader和FileWriter来输入和输出,而不是FileInputStream和FileOutputStream。注意,CopyBytes和CopyCharacters都使用一个int变量来读写。然而,在CopyCharacters中,int变量在它的最后16位中保存一个字符值;在CopyBytes中,int变量在它的最后8位保存一个字节值。

使用字节流的字符流

字符流通常是字节流的“包装器”。字符流使用字结流来执行物理I/O,同时字符流处理字符和字节之间的转换。例如,FileReader使用FileInputStream,而FileWriter使用FileOutputStream。

有两种通用的字节到字符“桥”流:InputStreamReader和OutputStreamWriter。当没有满足需要的预先打包的字符流类时,使用它们来创建字符流。网络跟踪( networking trail )中的套接字课程(sockets lesson)展示了如何从套接字类提供的字节流创建字符流。

面向行的I / O

字符I/O通常以比单个字符更大的单位出现。一个常见的单位是行:以行结束符结尾的一串字符。行结束符可以是回车/换行序列(“r\n”)、单个回车(“r”)或单个换行(“n”)。支持所有可能的行终止符允许程序读取在任何广泛使用的操作系统上创建的文本文件。

让我们修改CopyCharacters示例,以使用面向行的I/O。为此,我们必须使用两个以前没有见过的类BufferedReader和PrintWriter。我们将在Buffered I/O和Formatting中更深入地探讨这些类。现在,我们只对它们对面向行I/O的支持感兴趣。

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopyLines {
    public static void main(String[] args) throws IOException {

        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("xanadu.txt"));
            outputStream = new PrintWriter(new FileWriter("characteroutput.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

调用readLine将返回一行文本。CopyLines使用println输出每一行,它追加当前操作系统的行结束符。这可能与输入文件中使用的行结束符不同。

除了字符和行之外,还有许多方法可以构造文本输入和输出。有关更多信息,请参见扫描和格式化(Scanning and Formatting)。

缓冲流

到目前为止,我们看到的大多数示例都使用无缓冲的I/O。这意味着每个读或写请求都是由底层操作系统直接处理的。这可能会使程序的效率大大降低,因为每个这样的请求通常会触发磁盘访问、网络活动或其他一些相对昂贵的操作。

为了减少这种开销,Java平台实现了缓冲的I/O流。缓冲的输入流从称为缓冲区的存储区读取数据;本机输入API只在缓冲区为空时被调用。类似地,缓冲的输出流将数据写入缓冲区,只有当缓冲区满时才调用本机输出API。

程序可以使用我们已经多次使用的包装器风格将未缓冲的流转换为缓冲的流,其中未缓冲的流对象被传递给缓冲流类的构造函数。下面是如何修改CopyCharacters示例中的构造函数调用,以使用缓冲I/O:

inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));

有四个缓冲流类用于包装未缓冲的流:BufferedInputStream和BufferedOutputStream创建缓冲的字节流,而BufferedReader和BufferedWriter创建缓冲的字符流。

冲洗缓冲流

在临界点处写入缓冲区通常是有意义的,而不必等待缓冲区被填满。这称为刷新缓冲区。

一些缓冲输出类支持自动刷新,由可选的构造函数参数指定。启用自动刷新时,某些关键事件会导致刷新缓冲区。例如,一个自动刷新的PrintWriter对象会在每次调用println或format时刷新缓冲区。有关这些方法的更多信息,请参见格式化。

若要手动刷新流,请调用其刷新方法。刷新方法对任何输出流都有效,但除非对流进行缓冲,否则无效。

扫描和格式化

编程I/O通常涉及到对人们喜欢使用的格式化整洁的数据进行转换。为了帮助您完成这些工作,Java平台提供了两个api。scanner API将输入分解为与数据位相关联的单个令牌。formatting API将数据组装成格式化良好的、人类可读的形式。

扫描

Scanner类型的对象用于将格式化的输入分解为令牌并根据标记的数据类型翻译单个标记。

将输入分解为令牌

默认情况下,扫描程序使用空白分隔标记。(空白字符包括空格、制表符和行终止符。有关完整列表,请参阅有关Character.isWhitespace的文档。)为了了解扫描是如何工作的,让我们看看ScanXan,这是一个程序,它读取xanadu.txt中的单个单词并将它们逐行打印出来。

import java.io.*;
import java.util.Scanner;

public class ScanXan {
    public static void main(String[] args) throws IOException {

        Scanner s = null;

        try {
            s = new Scanner(new BufferedReader(new FileReader("xanadu.txt")));

            while (s.hasNext()) {
                System.out.println(s.next());
            }
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
}

注意,当扫描程序对象完成时,ScanXan会调用Scanner的close方法。即使扫描程序不是流,您也需要关闭它,以表明您已经完成了它的底层流。

ScanXan的输出如下所示:

In
Xanadu
did
Kubla
Khan
A
stately
pleasure-dome
...

要使用不同的令牌分隔符,请调用useDelimiter(),并指定一个正则表达式。例如,假设您希望令牌分隔符是一个逗号,可以选择后面跟着空格。你会调用,

s.useDelimiter(",\\s*");

**翻译独特的符号 **

ScanXan示例将所有输入标记视为简单的String值。Scanner还支持所有Java语言的基本类型(除了char)的令牌,以及BigInteger和BigDecimal。此外,数值可以使用千位分隔符。因此,在美国区域设置中,Scanner将字符串“32,767”正确地读取为表示一个整数值。

我们必须提到区域设置,因为千位的分隔符和十进制符号都是区域设置特定的。因此,如果我们没有指定扫描仪应该使用美国地区,那么下面的示例将不能在所有地区正确工作。这不是您通常需要担心的问题,因为您的输入数据通常来自与您使用相同区域设置的源。但是这个例子是Java教程的一部分,并且在世界各地都有发布。

ScanSum示例读文件中的double值并将其相加。这是来源:

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Scanner;
import java.util.Locale;

public class ScanSum {
    public static void main(String[] args) throws IOException {

        Scanner s = null;
        double sum = 0;

        try {
            s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt")));
            s.useLocale(Locale.US);

            while (s.hasNext()) {
                if (s.hasNextDouble()) {
                    sum += s.nextDouble();
                } else {
                    s.next();
                }   
            }
        } finally {
            s.close();
        }

        System.out.println(sum);
    }
}

这是样本输入文件usnumbers.txt

8.5
32,767
3.14159
1,000,000.1

输出字符串为“1032778.74159”。在某些地区,句点将是不同的字符,因为System.out是一个PrintStream对象,并且该类不提供重写默认区域设置的方法。我们可以覆盖整个程序的语言环境——或者我们可以只使用格式,如下一个主题“格式化”所述。

格式化

实现格式化的流对象是PrintWriter(字符流类)或PrintStream(字节流类)的实例。


注意:您可能需要的唯一PrintStream对象是System.out和System.err。(有关这些对象的更多信息,请参见命令行中的I/O。)当你需要创建一个格式化的输出流时,实例化PrintWriter,而不是PrintStream。

像所有字节流和字符流对象一样,PrintStream和PrintWriter的实例实现了一组用于简单字节和字符输出的标准写入方法。此外,PrintStream和PrintWriter都实现了一组相同的方法来将内部数据转换为格式化输出。提供了两种级别的格式:

  • print和println以标准方式格式化单个值。
  • format基于格式字符串来格式化几乎任意数量的值,有许多选项用于精确格式化。

print和println方法

调用print或println在使用适当的toString方法转换值后输出单个值。我们可以在Root例子中看到:

public class Root {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

下面是Root的输出:

The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.

变量 i 和 r 被格式化了两次:第一次使用重载的打印代码,第二次使用Java编译器自动生成的转换代码,它也使用toString。您可以以这种方式格式化任何值,但对结果没有太多的控制权。

format方法

format方法基于格式字符串对多个参数进行格式化。格式字符串由嵌入格式说明符的静态文本组成;除了格式说明符外,格式字符串的输出是不变的。

格式字符串支持许多特性。在本教程中,我们只介绍一些基础知识。有关完整的描述,请参见API规范中的格式字符串语法

Root2示例用一个格式调用格式化两个值:

public class Root2 {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

输出如下:

The square root of 2 is 1.414214.

与本例中使用的三个格式说明符一样,所有格式说明符都以%开头,以一个或两个字符的转换结尾,该转换指定生成的格式化输出的类型。这里使用的三种转换是:

  • d将整数值格式化为十进制数值。
  • f将浮点值格式化为十进制值。
  • n输出特定于平台的行终止符。

以下是一些其他的转换:

  • x将整数格式化为十六进制值。
  • s将任何值格式化为字符串。
  • tB将整数格式化为特定于区域设置的月份名。

还有许多其他的转换。


注意:

除了%%和%n,所有格式说明符都必须匹配参数。如果没有,就会抛出异常。

在Java编程语言中,转义符\n总是生成换行符(\u000A)。不要使用\n,除非你特别想要换行符。要获得本地平台的正确行分隔符,请使用%n。


除了转换之外,格式说明符还可以包含进一步自定义格式化输出的几个其他元素。这里有一个例子,Format,它使用了所有可能的元素类型。

public class Format {
    public static void main(String[] args) {
        System.out.format("%f, %1$+020.10f %n", Math.PI);
    }
}

输出:

3.141593, +00000003.1415926536

其他元素都是可选的。下图显示了较长的说明符如何分解为元素。

在这里插入图片描述
元素必须按照显示的顺序出现。从右边开始,可选元素是:

  • 精度(Precision)。对于浮点值,这是格式化值的数学精度。对于s和其他一般的转换,这是格式化值的最大宽度;如果需要,该值将右截断。
  • 宽度(Width)。格式化值的最小宽度;如果需要,该值将被填充。默认情况下,该值在左边填充空格。
  • 标志(Flags )指定额外的格式化选项。在Format示例中,+标志指定数字应该始终使用符号进行格式化,而0标志指定0是填充字符。其他标志包括-(右边的填充符)和,(格式编号带有特定于地区的千位分隔符)。请注意,某些标志不能与某些其他标志或某些转换一起使用。
  • 参数索引(Argument Index)允许您显式匹配指定的参数。你也可以指定<来匹配前面的参数。因此,这个例子可以说:System.out.format("%f, %<+020.10f %n", Math.PI);
从命令行中进行IO操作

程序通常从命令行运行,并在命令行环境中与用户交互。Java平台以两种方式支持这种交互:通过标准流和控制台。

标准流

标准流是许多操作系统的一个特性。默认情况下,它们从键盘读取输入,并将输出写入显示器。它们还支持文件上和程序之间的I/O,但该特性是由命令行解释器控制的,而不是程序。

Java平台支持三种标准流:标准输入,通过system.in;标准输出,通过system.out;标准错误,通过System.err访问。这些对象是自动定义的,不需要打开。标准输出和标准误差都是输出;单独使用错误输出允许用户将常规输出转移到一个文件中,并且仍然能够读取错误消息。有关更多信息,请参阅命令行解释器的文档。

您可能期望标准流是字符流,但由于历史原因,它们是字节流。System.out和System.err被定义为PrintStream对象。虽然从技术上讲它是一个字节流,但PrintStream利用一个内部字符流对象来模拟字符流的许多特性。

相比之下,System.in 是没有字符流特性的字节流。要使用标准输入作为一个字符流,在InputStreamReader中包装System.in。

InputStreamReader cin = new InputStreamReader(System.in);

控制台

标准流的一个更高级的替代品是控制台。这是一个单独的、预定义的Console类型对象,它具有标准流提供的大部分特性,以及其他特性。控制台对于安全的密码输入特别有用。Console对象还通过其reader和writer方法提供了真正的字符流。

在程序可以使用Console之前,它必须尝试通过调用System.console()来检索Console对象。如果Console对象可用,此方法将返回它。如果System.console返回NULL,那么控制台操作是不允许的,要么因为操作系统不支持它们,要么因为程序是在非交互式环境中启动的。

Console对象通过其readPassword方法支持安全的密码输入。这种方法可以通过两种方式保护密码输入。首先,它抑制了回显,因此密码在用户屏幕上是不可见的。其次,readPassword返回一个字符数组,而不是字符串,因此可以重写密码,一旦不再需要它,就从内存中删除它。

Password示例是一个更改用户密码的原型程序。它演示了几个Console方法。

import java.io.Console;
import java.util.Arrays;
import java.io.IOException;

public class Password {
    
    public static void main (String args[]) throws IOException {

        Console c = System.console();
        if (c == null) {
            System.err.println("No console.");
            System.exit(1);
        }

        String login = c.readLine("Enter your login: ");
        char [] oldPassword = c.readPassword("Enter your old password: ");

        if (verify(login, oldPassword)) {
            boolean noMatch;
            do {
                char [] newPassword1 = c.readPassword("Enter your new password: ");
                char [] newPassword2 = c.readPassword("Enter new password again: ");
                noMatch = ! Arrays.equals(newPassword1, newPassword2);
                if (noMatch) {
                    c.format("Passwords don't match. Try again.%n");
                } else {
                    change(login, newPassword1);
                    c.format("Password for %s changed.%n", login);
                }
                Arrays.fill(newPassword1, ' ');
                Arrays.fill(newPassword2, ' ');
            } while (noMatch);
        }

        Arrays.fill(oldPassword, ' ');
    }
    
    // Dummy change method.
    static boolean verify(String login, char[] password) {
        // This method always returns
        // true in this example.
        // Modify this method to verify
        // password according to your rules.
        return true;
    }

    // Dummy change method.
    static void change(String login, char[] password) {
        // Modify this method to change
        // password according to your rules.
    }
}

Password类遵循以下步骤:

  1. 尝试检索Console对象。如果对象不可用,则中止。
  2. 调用Console.readLine提示并读取用户的登录名。
  3. 调用Console.readPassword提示并读取用户的现有密码。
  4. 调用verify来确认用户被授权修改密码。(在本例中,verify是一个总是返回true的虚拟方法。)
  5. 重复以下步骤,直到用户两次输入相同的密码。
    a. 调用Console.readPassword两次提示并读取新密码。
    b. 如果用户两次输入相同的密码,则调用change来更改它。(同样,change是一个虚拟的方法。)
    c. 用空格覆盖这两个密码。
  6. 使用空格覆盖旧密码。
数据流

数据流支持基本数据类型值(boolean、char、byte、short、int、long、float和double)的二进制I/O,以及String值。所有数据流实现DataInput接口或DataOutput接口。本节重点介绍这些接口最广泛使用的实现:DataInputStream和DataOutputStream。

DataStreams示例通过写出一组数据记录,然后再将它们读入来演示数据流。每条记录由三个与发票上的一个项目相关的值组成,如下表所示:
在这里插入图片描述
让我们研究一下数据流中的关键代码。首先,程序定义了一些常量,这些常量包含了数据文件的名称和将被写入其中的数据:

static final String dataFile = "invoicedata";

static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
static final int[] units = { 12, 8, 13, 29, 50 };
static final String[] descs = {
    "Java T-shirt",
    "Java Mug",
    "Duke Juggling Dolls",
    "Java Pin",
    "Java Key Chain"
};

然后DataStreams打开一个输出流。由于DataOutputStream只能作为现有字节流对象的包装器创建,因此DataStreams提供了一个缓冲的文件输出字节流。

out = new DataOutputStream(new BufferedOutputStream(
              new FileOutputStream(dataFile)));

DataStreams写出记录并关闭输出流。

for (int i = 0; i < prices.length; i ++) {
    out.writeDouble(prices[i]);
    out.writeInt(units[i]);
    out.writeUTF(descs[i]);
}

writeUTF方法以修改后的UTF-8形式写出String值。这是一种可变宽度的字符编码,普通的西文字符只需要一个字节。

现在DataStreams再次读取数据。首先,它必须提供一个输入流和保存输入数据的变量。像DataOutputStream一样,DataInputStream必须被构造为字节流的包装器。

in = new DataInputStream(new
            BufferedInputStream(new FileInputStream(dataFile)));

double price;
int unit;
String desc;
double total = 0.0;

现在DataStreams可以读取流中的每条记录,报告它遇到的数据。

try {
    while (true) {
        price = in.readDouble();
        unit = in.readInt();
        desc = in.readUTF();
        System.out.format("You ordered %d" + " units of %s at $%.2f%n",
            unit, desc, price);
        total += unit * price;
    }
} catch (EOFException e) {
}

注意,DataStreams通过捕获EOFException来检测文件结束条件,而不是测试一个无效的返回值。所有的DataInput方法的实现都使用EOFException而不是返回值。

还要注意,Datastream中的每个专门化write都与相应的专门化read完全匹配。程序员需要确保输出类型和输入类型以如下方式匹配:输入流由简单的二进制数据组成,没有指示单个值的类型或它们在流中的起始位置。

DataStreams使用了一种非常糟糕的编程技术:它使用浮点数来表示货币价值。一般来说,浮点数对于精确值来说是不好的。这对于十进制分数来说尤其糟糕,因为普通值(如0.1)没有二进制表示。

用于货币值的正确类型是java.math.BigDecimal。不幸的是,BigDecimal是一个对象类型,所以它不能处理数据流。但是,BigDecimal将处理对象流,这将在下一节中讨论。

对象流

就像数据流支持原始数据类型的I/O一样,对象流也支持对象的I/O。大多数(但不是全部)标准类支持其对象的序列化。那些都实现了标记接口Serializable。

对象流类是ObjectInputStream和ObjectOutputStream。这些类实现了ObjectInput和ObjectOutput,它们是DataInput和DataOutput的子接口。这意味着数据流中涉及的所有原始数据I/O方法也在对象流中被实现。因此,对象流可以包含原始值和对象值的混合。ObjectStreams示例说明了这一点。ObjectStreams创建了与DataStreams相同的应用程序,只是做了一些更改。首先,价格现在是BigDecimal对象,以更好地表示小数。其次,将Calendar对象写入数据文件,指示发票日期。

如果readObject()没有返回预期的对象类型,尝试将其转换为正确的类型可能会抛出ClassNotFoundException。在这个简单的示例中,这是不可能发生的,所以我们不尝试捕获异常。相反,我们通过在主方法的throws子句中添加ClassNotFoundException来通知编译器我们已经注意到了这个问题。

复杂对象的输出和输入

writeObject和readObject方法使用起来很简单,但它们包含一些非常复杂的对象管理逻辑。这对于像Calendar这样的类并不重要,因为它只是封装了原始值。但是许多对象包含对其他对象的引用。如果readObject要从流重构对象,它必须能够重构原始对象所引用的所有对象。这些额外的对象可能有它们自己的引用,等等。在这种情况下,writeObject将遍历整个对象引用网络,并将该网络中的所有对象写入流中。因此,一次writeObject调用可能导致大量对象被写入流。

下图演示了这一点,其中调用writeObject来写入名为a的单个对象。该对象包含对对象b和c的引用,而对象b包含对对象d和e的引用。调用writeobject(a)不仅仅写一个对象,而是所有需要重构一个对象的对象,所以这个web中的其他四个对象也会被写出来。当一个对象被readObject读回时,其他四个对象也被读回,并且所有原始对象引用都被保留。

在这里插入图片描述
您可能想知道,如果同一个流中的两个对象都包含对单个对象的引用,会发生什么。当它们被读取回来时,它们都指向一个对象吗?答案是肯定的。一个流只能包含一个对象的副本,尽管它可以包含任意数量的对它的引用。因此,如果显式地将一个对象写入流两次,实际上只写入引用两次。例如,如果下面的代码将对象ob两次写入一个流:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

每个writeObject必须与一个readObject相匹配,所以读取流返回的代码看起来像这样:

Object ob1 = in.readObject();
Object ob2 = in.readObject();

这将产生两个变量ob1和ob2,它们是对单个对象的引用。

然而,如果一个对象被写入两个不同的流,它就被有效地复制了——一个程序读取两个流将看到两个不同的对象。

文件 I/O(采用 NIO.2)


注意:本教程反映了JDK 7发行版中引入的文件I/O机制。

java.nio.file包及其相关包java.nio.file.attribute,为文件I/O和访问默认文件系统提供全面的支持。虽然API有很多类,但您只需要关注几个入口点。您将看到这个API非常直观且易于使用。

本教程首先问,什么是路径?然后,介绍包的主要入口点Path类。解释了Path类中与语法操作相关的方法。本教程接着介绍包中的另一个主类,即Files类,它包含处理文件操作的方法。首先,介绍了一些常见的文件操作的概念。本教程将介绍检查、删除、复制和移动文件的方法。

在学习文件I/O和目录I/O之前,本教程将介绍如何管理元数据。解释了随机访问文件,并检查了符号和硬链接的具体问题。

接下来,我们将介绍一些非常强大但更高级的主题。首先,演示递归遍历文件树的功能,然后介绍如何使用通配符搜索文件。接下来,将解释和演示如何监视目录的更改。然后,对不适合其他地方的方法给予一些关注。

最后,如果您在Java SE 7发布之前编写了文件I/O代码,则有一个从旧API到新API的映射,以及关于File.toPath方法的重要信息提供给希望在不重写现有代码的情况下利用新的API的开发人员。

什么是路径?

文件系统以一种很容易检索的方式在某种形式的媒体(通常是一个或多个硬盘驱动器)上存储和组织文件。目前使用的大多数文件系统以树(或分层)结构存储文件。在树的顶部是一个(或多个)根节点。在根节点下,有文件和目录(Microsoft Windows中的文件夹)。每个目录可以包含文件和子目录,而子目录又可以包含文件和子目录,等等,可能具有几乎无限的深度。

什么是路径?

下图显示了包含单个根节点的目录树。Microsoft Windows支持多个根节点。每个根节点映射到一个卷,例如 C:\ 或 D:\。Solaris操作系统支持单个根节点,由斜杠字符/表示。
在这里插入图片描述
文件由它在文件系统中的路径标识,从根节点开始。例如上图中的statusReport文件在Solaris操作系统中是这样描述的:

/home/user2/statusReport

在Microsoft Windows中,statusReport的描述方法如下:

C:\home\use2\statusReport

用于分隔目录名的字符(也称为分隔符)是特定于文件系统的:Solaris操作系统使用正斜杠(/),Microsoft Windows使用反斜杠()。

相对或绝对?

路径可以是相对的,也可以是绝对的。绝对路径总是包含根元素和定位文件所需的完整目录列表。例如:/home/user2/statusReport为绝对路径。定位文件所需的所有信息都包含在路径字符串中。

一个相对路径需要与另一个路径相结合才能访问一个文件。例如,joe/foo是一个相对路径。如果没有更多的信息,程序无法在文件系统中可靠地定位joe/foo目录。

符号连接

文件系统对象通常是目录或文件。这些对象大家都很熟悉。但是一些文件系统也支持符号链接的概念。symbolic link也被称为symlink或软链接。

符号链接是一个特殊的文件,它作为对另一个文件的引用。在大多数情况下,符号链接对应用程序是透明的,对符号链接的操作会自动重定向到链接的目标。(被指向的文件或目录称为链接的目标。)例外情况是符号链接被删除或重命名,在这种情况下是链接本身被删除或重命名而不是链接的目标。

在下面的图中,logFile对用户来说是一个普通文件,但它实际上是一个到dir/logs/HomeLogFile的符号链接。HomeLogFile是链接的目标。
在这里插入图片描述
符号链接通常对用户是透明的。对符号链接的读或写与对其他文件或目录的读或写相同。

短语解析链接的意思是用文件系统中的实际位置替换符号链接。在这个例子中,解析logFile会生成dir/logs/HomeLogFile。

在实际的场景中,大多数文件系统都自由地使用符号链接。偶尔,不小心创建的符号链接会导致循环引用。循环引用发生在链接的目标指向原始链接时。循环引用可以是间接的:目录a指向目录b,目录b指向目录c,目录c包含一个指向目录a的子目录。当程序递归地遍历目录结构时,循环引用可能会造成混乱。然而,这种情况已经被考虑,不会导致您的程序无限循环。

下一页将讨论Java编程语言中文件I/O支持的核心——Path类。

Path类

Path类是Java SE 7发行版中引入的,它是Java .nio.file包的主要入口点之一。如果您的应用程序使用文件I/O,您将希望了解这个类的强大特性。


注:如果你有使用java.io.File的jdk7之前的代码,你仍然可以通过使用File.toPath的方法来利用Path类的功能。有关更多信息,请参阅遗留文件I/O代码。

顾名思义,Path类是文件系统中路径的编程表示。Path对象包含用于构造路径的文件名和目录列表,并用于检查、定位和操作文件。

Path实例反映了底层平台。在Solaris操作系统中,路径使用Solaris语法(/home/joe/foo),而在Microsoft Windows中,路径使用Windows语法(C: home\joe\foo)。Path不是独立于系统的。您不能比较来自Solaris文件系统的Path并期望它与来自Windows文件系统的Path相匹配,即使目录结构相同且两个实例定位相同的相对文件。

Path对应的文件或目录可能不存在。你可以创建一个Path实例并以多种方式操作它:你可以添加它,提取它的片段,将它与另一个路径进行比较。在适当的时候,您可以使用Files类中的方法来检查与Path对应的文件是否存在,创建文件、打开文件、删除文件、更改其权限等。

下一节详细介绍Path类。

路径操作

Path类包括各种方法,可用于获取有关路径的信息、访问路径元素、将路径转换为其他形式或提取路径的部分内容。还有用于匹配路径字符串的方法和用于删除路径冗余的方法。本课讨论这些Path方法,有时称为语法操作,因为它们操作路径本身,而不访问文件系统。

创建路径

Path实例包含用于指定文件或目录位置的信息。在定义Path时,Path提供了一系列一个或多个名称。可以包含根元素或文件名,但这两者都不是必需的。Path可能只包含一个目录或文件名。

你可以很容易地创建一个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");

下面的例子创建/u/joe/logs/foo.log,假设你的主目录是/u/joe,或者C: joe\logs\foo.log(如果你在Windows上)。

Path p5 = Paths.get(System.getProperty("user.home"),"logs", "foo.log");

检索路径信息

您可以将Path看作是将这些名称元素存储为一个序列。目录结构中最高的元素位于索引0处。目录结构中最低的元素位于索引[n-1],其中n是Path中name元素的数量。有一些方法可以使用这些索引来检索Path的各个元素或子序列。

本课中的示例使用以下目录结构。
在这里插入图片描述
下面的代码片段定义了一个Path实例,然后调用几个方法来获取关于路径的信息:

// None of these methods requires that the file corresponding
// to the Path exists.
// Microsoft Windows syntax
Path path = Paths.get("C:\\home\\joe\\foo");

// Solaris syntax
Path path = Paths.get("/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());

下面是Windows和Solaris操作系统的输出:

Method InvokedReturns in the Solaris OSReturns in Microsoft WindowsComment
toString/home/joe/fooC:\home\joe\foo返回路径的字符串表示形式。如果路径是使用Filesystems.getDefault(). getpath (String)或Paths. get创建的(后者是getPath的一个方便方法),该方法执行少量的语法清理。例如,在UNIX操作系统中,它会将输入的字符串//home/joe/foo更正为/home/joe/foo。
getFileNamefoofoo返回文件名或name元素序列的最后一个元素。
getName(0)homehome返回与指定索引对应的路径元素。第0个元素是最接近根的路径元素。
getNameCount33返回路径中的元素个数。
subpath(0,2)home/joehome\joe返回由开始索引和结束索引指定的Path的子序列(不包括根元素)。
getParent/home/joe\home\joe返回父目录的路径。
getRoot/C:\返回路径的根目录。

前面的示例显示了绝对路径的输出。在以下示例中,指定了一个相对路径:

// Solaris syntax
Path path = Paths.get("sally/bar");
or
// Microsoft Windows syntax
Path path = Paths.get("sally\\bar");

下面是Windows和Solaris操作系统的输出:
在这里插入图片描述

移除路径冗余

许多文件系统使用“.”符号表示当前目录,“…”符号表示父目录。您可能会遇到这样的情况:Path包含冗余的目录信息。也许服务器被配置为将其日志文件保存在“/dir/logs/.”目录下,您想要从路径中删除末尾的“/.”符号。

下面的例子都包含了冗余:

/home/./joe/foo
/home/sally/../joe/foo

normalize方法删除任何冗余元素,包括任何"."或"directory/…"出现的情况。前面的两个例子都归一化为/home/joe/foo。

需要注意的是,normalize在清理路径时不会检查文件系统。这是一个纯粹的语法操作。在第二个例子中,如果sally是一个符号链接,则删除sally/…可能导致路径不再定位预期的文件。

要清理路径,同时确保结果定位到正确的文件,可以使用toRealPath方法。此方法将在下一节转换路径中进行描述。

转换路径

您可以使用三种方法来转换Path。如果需要将路径转换为可以从浏览器打开的字符串,可以使用toUri。例如:

Path p1 = Paths.get("/home/logfile");
// Result is file:///home/logfile
System.out.format("%s%n", p1.toUri());

toAbsolutePath方法将路径转换为绝对路径。如果传入的路径已经是绝对的,它将返回相同的path对象。在处理用户输入的文件名时,toAbsolutePath方法非常有用。例如:

public class FileTest {
    public static void main(String[] args) {

        if (args.length < 1) {
            System.out.println("usage: FileTest file");
            System.exit(-1);
        }

        // 将输入字符串转换为Path对象。
        Path inputPath = Paths.get(args[0]);

        // 将输入的路径转换为绝对路径。 
        // 通常,这意味着预先添加当前工作目录。  
        // 如果这个例子是这样调用的: 
        // java FileTest foo的getRoot和getParent方法会在原始的"inputPath"实例上返回null。
        // 在“fullPath”实例上调用getRoot和getParent将返回预期值。
        Path fullPath = inputPath.toAbsolutePath();
    }
}

toAbsolutePath方法转换用户输入,并返回一个Path,在查询时返回有用的值。这个文件不需要存在,这个方法就可以工作。

toRealPath方法返回现有文件的实际路径。这个方法同时执行几个操作:

  • 如果将true传递给该方法,并且文件系统支持符号链接,则该方法解析路径中的任何符号链接。
  • 如果Path是相对的,则返回绝对路径。
  • 如果Path包含任何冗余元素,它将返回一个删除了这些元素的路径。

如果文件不存在或无法访问,此方法将抛出异常。当您想要处理这些情况中的任何一种时,可以捕获异常。例如:

try {
    Path fp = path.toRealPath();
} catch (NoSuchFileException x) {
    System.err.format("%s: no such" + " file or directory%n", path);
    // Logic for case when file doesn't exist.
} catch (IOException x) {
    System.err.format("%s%n", x);
    // Logic for other sort of file error.
}

加入两条路径

您可以使用resolve方法组合路径。传入一个不包含根元素的部分路径,该部分路径被追加到原始路径。

例如,考虑以下代码片段:

// Solaris
Path p1 = Paths.get("/home/joe/foo");
// Result is /home/joe/foo/bar
System.out.format("%s%n", p1.resolve("bar"));

or

// Microsoft Windows
Path p1 = Paths.get("C:\\home\\joe\\foo");
// Result is C:\home\joe\foo\bar
System.out.format("%s%n", p1.resolve("bar"));

将绝对路径传递给resolve方法将返回传入的路径:

// Result is /home/joe
Paths.get("foo").resolve("/home/joe");

在两条路径之间创建路径

在编写文件I/O代码时,一个常见的需求是能够构建从文件系统中的一个位置到另一个位置的路径。你可以用relativize方法来解决这个问题。此方法构造一个从原始路径开始并在传入路径指定的位置结束的路径。新路径是相对于原路径。

例如,考虑两个定义为joe和sally的相对路径:

Path p1 = Paths.get("joe");
Path p2 = Paths.get("sally");

在没有任何其他信息的情况下,假设joe和sally是兄弟节点,这意味着节点位于树结构的同一层。要从joe导航到sally,你需要首先导航一级到父节点,然后向下到sally:

// Result is ../sally
Path p1_to_p2 = p1.relativize(p2);
// Result is ../joe
Path p2_to_p1 = p2.relativize(p1);

考虑一个稍微复杂一点的例子:

Path p1 = Paths.get("home");
Path p3 = Paths.get("home/sally/bar");
// Result is sally/bar
Path p1_to_p3 = p1.relativize(p3);
// Result is ../..
Path p3_to_p1 = p3.relativize(p1);

在本例中,这两条路径共享同一个节点home。要从home导航到bar,首先要向下导航到sally,然后再向下导航到bar。从bar到home需要向上移动两层。

如果只有一条路径包含根元素,则不能构造相对路径。如果两条路径都包含根元素,则构造相对路径的能力取决于系统。

递归Copy示例使用relativize和resolve方法。

比较两个路径

Path类支持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类实现了Iterable接口。iterator方法返回一个对象,该对象允许您迭代路径中的name元素。返回的第一个元素是目录树中最接近根的元素。下面的代码片段遍历一个路径,打印每个name元素:

Path path = ...;
for (Path name: path) {
    System.out.println(name);
}

Path类还实现了Comparable接口。您可以使用compareTo来比较Path对象,这对于排序非常有用。

您还可以将Path对象放入Collection中。有关这个功能强大的特性的更多信息,请参阅Collections。

当您想要验证两个Path对象定位相同的文件时,可以使用isSameFile方法,请参见检查文件或目录一节中检查两条路径是否找到相同的文件一段。

文件操作

Files类是java.nio.file包的另一个主要入口点。这个类提供了一组丰富的静态方法,用于读取、写入和操作文件和目录。Files方法作用于Path对象的实例。在继续阅读其余部分之前,你应该先熟悉以下常见概念:

  • 释放系统资源
  • 捕获异常
  • 可变参数
  • 原子操作
  • 方法链
  • 什么是Glob?
  • 链接意识

释放系统资源

这个API中使用的许多资源,比如流或通道,实现或扩展了java.io.Closeable接口。Closeable资源的一个要求是,当不再需要时,必须调用close方法来释放资源。忽略关闭资源可能会对应用程序的性能产生负面影响。下一节中描述的try-with-resources语句将为您处理这个步骤。

捕获异常

对于文件I/O,意外的情况是不可避免的:当一个文件存在(或不存在)时,程序不能访问该文件系统,默认的文件系统实现不支持特定的功能,等等。可能会遇到许多错误。

所有访问文件系统的方法都可以抛出一个IOException。捕捉这些异常的最佳实践是将这些方法嵌入到Java SE 7发行版中引入的try-with-resources语句中。try-with-resources语句的优点是,当不再需要资源时,编译器会自动生成关闭资源的代码。下面的代码显示了这可能看起来是怎样的:

Charset charset = Charset.forName("US-ASCII");
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);
}

有关更多信息,请参见try-with-resources章节部分。

或者,您可以将文件I/O方法嵌入到try块中,然后在catch块中捕获任何异常。如果你的代码打开了任何流或通道,你应该在finally块中关闭它们。使用try-catch-finally方法,前面的示例如下所示:

Charset charset = Charset.forName("US-ASCII");
String s = ...;
BufferedWriter writer = null;
try {
    writer = Files.newBufferedWriter(file, charset);
    writer.write(s, 0, s.length());
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
} finally {
    if (writer != null) writer.close();
}

有关更多信息,请参见捕获和处理异常章节。

除了IOException,许多特定的异常扩展了FileSystemException。这个类有一些有用的方法,它们返回所涉及的文件(getFile)、详细的消息字符串(getMessage)、文件系统操作失败的原因(getReason)以及所涉及的“其他”文件(如果有的话)(getOtherFile)。

下面的代码片段展示了如何使用getFile方法:

try (...) {
    ...    
} catch (NoSuchFileException x) {
    System.err.format("%s does not exist\n", x.getFile());
}

为了清晰起见,本课中的文件I/O示例可能不会显示异常处理,但您的代码应该始终包含它。

可变参数

当指定标志时,几个Files方法接受任意数量的参数。例如,在下面的方法签名中,CopyOption参数后的省略号表示该方法接受可变数量的参数,或称作varargs,正如它们通常被调用的那样:

Path Files.move(Path, Path, CopyOption...)

当一个方法接受一个varargs参数时,你可以传递给它一个逗号分隔的值列表或者一个值数组(CopyOption[])。

在move例子中,这个方法可以被如下调用:

import static java.nio.file.StandardCopyOption.*;

Path source = ...;
Path target = ...;
Files.move(source,
           target,
           REPLACE_EXISTING,
           ATOMIC_MOVE);

有关varargs语法的更多信息,请参阅任意数量的参数章节。

原子操作

一些Files方法,如move,可以在某些文件系统中自动执行某些操作。原子文件操作是一种不能中断或“部分”执行的操作。要么执行整个操作,要么操作失败。当多个进程在文件系统的同一个区域上运行时,您需要保证每个进程访问一个完整的文件这一点非常重要。

方法链

许多文件I/O方法支持方法链的概念。

首先调用一个返回对象的方法。然后立即调用该对象的方法,该方法将返回另一个对象,以此类推。许多I/O示例使用以下技术:

String value = Charset.defaultCharset().decode(buf).toString();
UserPrincipal group = file.getFileSystem().getUserPrincipalLookupService().lookupPrincipalByName("me");

这种技术生成紧凑的代码,并使您能够避免声明不需要的临时变量。

什么是Glob?

Files类中的两个方法接受glob参数,但是什么是glob呢?

您可以使用glob语法来指定模式匹配行为。

glob模式被指定为一个字符串,并与其他字符串(如目录或文件名)进行匹配。Glob语法遵循以下几个简单的规则:

  • 星号*可以匹配任意数量的字符(包括不匹配)。
  • 两个星号**与*类似,但跨越了目录边界。此语法通常用于匹配完整路径。
  • 问号?只匹配一个字符。
  • 大括号指定子模式集合。例如:
    1){sun,moon,stars}匹配“sun”,“moon”或“stars”。
    2){temp*,tmp*}匹配所有以"temp"或"tmp"开头的字符串。
  • 方括号表示一组单个字符,或者,当使用连字符(-)时,表示一个字符范围。例如:
    1)[aeiou]匹配任何小写元音。
    2)[0-9]匹配任意数字。
    3)[A-Z]匹配任何大写字母。
    4)[a-z, a-z]匹配任何大写或小写字母。
    在方括号内,*、?和\匹配自己。
  • 所有其他字符都与自己匹配。
  • 要匹配*、?或其他特殊字符,可以使用反斜杠字符\对它们进行转义。例如:\匹配单个反斜杠,?匹配问号。

下面是一些glob语法的例子:

  • *.html —— 匹配所有以.html结尾的字符串
  • ??? —— 匹配所有三个字母或数字的字符串
  • *[0-9]* —— 匹配所有包含数字值的字符串
  • *.{htm,html,pdf} —— 匹配任何以.htm, .html或.pdf结尾的字符串
  • a?*.java —— 匹配任何以a开头,后面至少一个字母或数字,以.java结尾的字符串
  • {foo*,*[0-9]*} —— 匹配任何以foo开头的字符串或任何包含数字值的字符串

注意:如果你在键盘上输入glob模式,并且它包含一个特殊字符,你必须将该模式放在引号(“*”)中,使用反斜杠(\*),或者使用命令行支持的任何转义机制。

glob语法功能强大,易于使用。但是,如果它还不能满足您的需求,您还可以使用正则表达式。有关更多信息,请参见正则表达式课。

有关glob语法的更多信息,请参阅FileSystem类中getPathMatcher方法的API规范。

链接意识

Files类是“链接感知的”。每个Files方法要么检测遇到符号链接时该做什么,要么提供一个选项,使您能够配置遇到符号链接时的行为。

检查文件或目录

您有一个表示文件或目录的Path实例,但是该文件是否存在于文件系统中?它是可读的吗?可写?可执行?

验证文件或目录是否存在

Path类中的方法是语法性的,这意味着它们在Path实例上操作。但是最终您必须访问文件系统来验证特定的Path是否存在。你可以使用exists(Path, LinkOption…)和notExists(Path, LinkOption…)方法来做到这一点。请注意!Files.exists(path)并不等同于Files.notExists(path)。当你测试一个文件是否存在时,可能有三个结果:

  • 验证文件存在。
  • 验证该文件不存在。
  • 文件状态未知。当程序无法访问该文件时,可能会出现此结果。

如果exists和notExists都返回false,则不能验证文件是否存在。

检查文件的可访问性

要验证程序是否可以根据需要访问文件,可以使用isReadable(Path)、isWritable(Path)和isExecutable(Path)方法。

下面的代码片段验证特定文件是否存在,以及程序是否有能力执行该文件。

Path file = ...;
boolean isRegularExecutableFile = Files.isRegularFile(file) &
         Files.isReadable(file) & Files.isExecutable(file);

注意:一旦这些方法完成,不能保证该文件可以被访问。在许多应用程序中,一个常见的安全缺陷是执行检查,然后访问文件。要了解更多信息,请使用您最喜欢的搜索引擎查找TOCTTOU(发音为TOCK-too)。

检查两条路径是否找到相同的文件

当您有一个使用符号链接的文件系统时,可以有两个不同的路径来定位同一个文件。isSameFile(Path, Path)方法比较两个路径,以确定它们是否在文件系统中定位相同的文件。例如:

Path p1 = ...;
Path p2 = ...;

if (Files.isSameFile(p1, p2)) {
    // Logic when the paths locate the same file
}
删除文件或目录

您可以删除文件、目录或链接。对于符号链接,删除的是链接而不是链接的目标。对于目录,目录必须为空,否则删除将失败。

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 not empty%n", path);
} catch (IOException x) {
    // File permission problems are caught here.
    System.err.println(x);
}

deleteIfExists(Path)方法也删除该文件,但是如果该文件不存在,则不会抛出异常。当您有多个线程在删除文件,并且您不想仅仅因为一个线程先删除文件而抛出异常时,静默失败是很有用的。

复制文件或目录

你可以使用copy(Path, Path, CopyOption…)方法来复制一个文件或目录。如果目标文件存在,复制就会失败,除非指定了REPLACE_EXISTING选项。

可以复制目录。但是,目录中的文件不会被复制,因此即使原始目录包含文件,新目录也是空的。

复制符号链接时,会复制链接的目标。如果您希望复制链接本身,而不是链接的内容,请指定NOFOLLOW_LINKS或REPLACE_EXISTING选项。

这个方法接受一个可变参数。支持以下StandardCopyOption和LinkOption枚举:

  • REPLACE_EXISTING —— 即使目标文件已经存在,仍然执行复制。如果目标是一个符号链接,则链接本身会被复制(而不是链接的目标)。如果目标是非空目录,则复制失败,并出现DirectoryNotEmptyException异常。
  • COPY_ATTRIBUTES —— 将与文件相关的文件属性复制到目标文件。所支持的确切文件属性是与文件系统和平台相关的,但是跨平台支持最后修改时间,并将其复制到目标文件中。
  • NOFOLLOW_LINKS —— 表示符号链接不应该被跟随。如果要复制的文件是一个符号链接,则复制链接(而不是链接的目标)。

如果您不熟悉枚举,请参阅Enum类型章节。

复制方法的使用方法如下:

import static java.nio.file.StandardCopyOption.*;
...
Files.copy(source, target, REPLACE_EXISTING);

除了文件复制之外,Files类还定义了可以用于在文件和流之间复制的方法。copy(InputStream, Path, CopyOptions…)方法可以用来将输入流中的所有字节复制到文件中。copy(Path, OutputStream)方法可用于将文件中的所有字节复制到输出流中。

Copy示例使用copy和Files.walkFileTree方法支持递归复制。有关更多信息,请参见遍历文件树章节。

移动文件或目录

你可以使用move(Path, Path, CopyOption…)方法移动文件或目录。如果目标文件存在,则移动失败,除非指定了REPLACE_EXISTING选项。

空目录可以被移动。如果目录不为空,则允许在不移动目录内容的情况下移动目录。在UNIX系统中,将目录移动到同一个分区中通常需要重命名该目录。在这种情况下,该方法即使在目录包含文件时也能工作。

该方法接受一个varargs参数 —— 支持以下StandardCopyOption枚举:

  • REPLACE_EXISTING —— 即使目标文件已经存在,也会执行移动操作。如果目标是符号链接,则符号链接被替换,但它指向的对象不受影响。
  • ATOMIC_MOVE —— 作为原子文件操作执行移动操作。如果文件系统不支持原子移动,则抛出异常。使用ATOMIC_MOVE,您可以将一个文件移动到一个目录中,并保证监视该目录的任何进程访问一个完整的文件。

下面展示了如何使用move方法:

import static java.nio.file.StandardCopyOption.*;
...
Files.move(source, target, REPLACE_EXISTING);

尽管您可以在单个目录上实现move方法,如上所示,但该方法最常与文件树递归机制一起使用。有关更多信息,请参见遍历文件树章节。

管理元数据(文件和文件存储属性)

元数据的定义是“关于其他数据的数据”。在文件系统中,数据包含在文件和目录中,元数据跟踪这些对象的信息:它是一个普通文件、一个目录还是一个链接?它的大小、创建日期、最后修改日期、文件所有者、组所有者和访问权限是什么?

文件系统的元数据通常称为其文件属性。Files类包含可用于获取文件的单个属性或设置属性的方法。

MethodsComment
size(Path)返回指定文件的大小(以字节为单位)。
isDirectory(Path, LinkOption)如果指定的Path定位到一个属于目录的文件,则返回true。
isRegularFile(Path, LinkOption…)如果指定的Path定位到一个普通文件,则返回true。
isSymbolicLink(Path)如果指定的Path定位到一个作为符号链接的文件,则返回true。
isHidden(Path)如果指定的Path定位到文件系统认为隐藏的文件,则返回true。
getLastModifiedTime(Path, LinkOption…) ,setLastModifiedTime(Path, FileTime)返回或设置指定文件的最后修改时间。
getOwner(Path, LinkOption…) ,setOwner(Path, UserPrincipal)返回或设置文件的所有者。
getPosixFilePermissions(Path, LinkOption…),setPosixFilePermissions(Path, Set< PosixFilePermission >)返回或设置文件的POSIX文件权限。
getAttribute(Path, String, LinkOption…),setAttribute(Path, String, Object, LinkOption…)返回或设置文件属性的值。

如果一个程序在同一时间需要多个文件属性,那么使用检索单个属性的方法可能会效率低下。重复访问文件系统以检索单个属性可能会对性能产生不利影响。由于这个原因,Files类提供了两个readAttributes方法来在一个批量操作中获取文件的属性。

MethodComment
readAttributes(Path, String, LinkOption…)以批量操作的形式读取文件的属性。String参数标识要读取的属性。
readAttributes(Path, Class< A>, LinkOption…)以批量操作的形式读取文件的属性。Class参数是请求的属性的类型,该方法返回该类的一个对象。

在展示readAttributes方法的示例之前,应该提到不同的文件系统对于应该跟踪哪些属性有不同的概念。由于这个原因,相关的文件属性被分组到视图中。视图映射到特定的文件系统实现,如POSIX或DOS,或映射到公共功能,如文件所有权。

支持的视图如下:

  • BasicFileAttributeView——提供所有文件系统实现都需要支持的基本属性的视图。
  • DosFileAttributeView——使用支持 DOS 属性的文件系统支持的标准四位扩展基本属性视图。
  • PosixFileAttributeView——使用支持POSIX标准系列(如UNIX)的文件系统上支持的属性来扩展基本属性视图。这些属性包括文件所有者、组所有者和9个相关的访问权限。
  • FileOwnerAttributeView——任何支持文件所有者概念的文件系统实现都支持。
  • AclFileAttributeView——支持读取或更新文件的访问控制列表(ACL)。支持NFSv4 ACL模型。任何ACL模型,如Windows ACL模型,都可以支持具有良好定义的NFSv4模型映射的ACL模型。
  • UserDefinedFileAttributeView——支持用户定义的元数据。这个视图可以映射到系统支持的任何扩展机制。例如,在Solaris操作系统中,您可以使用这个视图来存储文件的MIME类型。

一个特定的文件系统实现可能只支持基本的文件属性视图,或者它可能支持这些文件属性视图中的几个。文件系统实现可能支持此API中未包含的其他属性视图。

在大多数情况下,你不应该直接处理任何FileAttributeView接口。(如果你需要直接使用FileAttributeView,你可以通过getFileAttributeView(Path, Class, LinkOption…)方法访问它。)

readAttributes方法使用泛型,可以用来读取任何文件属性视图的属性。本页面其余部分中的示例使用了readAttributes方法。

基本的文件属性

如前所述,要读取文件的基本属性,可以使用其中一个Files.readAttributes方法,它在一个批量操作中读取所有的基本属性。这比单独访问文件系统来读取每个单独的属性要高效得多。varargs参数目前支持LinkOption枚举NOFOLLOW_LINKS。当您不希望跟随符号链接时,请使用此选项。


关于时间戳:基本属性集包括三个时间戳:reatetime、lastModifiedTime和lastAccessTime。在特定的实现中,这些时间戳可能都不受支持,在这种情况下,相应的访问器方法将返回特定于实现的值。如果支持,时间戳将作为FileTime对象返回。

下面的代码片段读取和打印给定文件的基本文件属性,并使用BasicFileAttributes类中的方法。

Path file = ...;
BasicFileAttributes attr = 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());

除了本例中显示的访问器方法外,还有一个fileKey方法,它要么返回唯一标识文件的对象,要么返回空(如果没有可用的文件键)。

设置时间戳

下面的代码片段设置了最后一次修改时间(以毫秒为单位):

Path file = ...;
BasicFileAttributes attr =
    Files.readAttributes(file, BasicFileAttributes.class);
long currentTime = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTime);
Files.setLastModifiedTime(file, ft);
}

DOS文件属性

在DOS以外的文件系统(如Samba)上也支持DOS文件属性。下面的代码片段使用了DosFileAttributes类的方法。

Path file = ...;
try {
    DosFileAttributes attr =
        Files.readAttributes(file, DosFileAttributes.class);
    System.out.println("isReadOnly is " + attr.isReadOnly());
    System.out.println("isHidden is " + attr.isHidden());
    System.out.println("isArchive is " + attr.isArchive());
    System.out.println("isSystem is " + attr.isSystem());
} catch (UnsupportedOperationException x) {
    System.err.println("DOS file" +
        " attributes not supported:" + x);
}

但是,你可以使用setAttribute(Path, String, Object, LinkOption…)方法设置DOS属性,如下所示:

Path file = ...;
Files.setAttribute(file, "dos:hidden", true);

POSIX文件权限

POSIX是UNIX可移植操作系统接口(Portable Operating System Interface for UNIX)的首字母缩写,是一组IEEE和ISO标准,旨在确保不同类型UNIX之间的互操作性。如果一个程序符合这些POSIX标准,那么它应该很容易移植到其他POSIX兼容的操作系统。

除了文件所有者和组所有者外,POSIX还支持9种文件权限:文件所有者、同一组成员和“其他人”的读、写和执行权限。

下面的代码片段读取给定文件的POSIX文件属性,并将它们打印到标准输出。代码使用PosixFileAttributes类中的方法。

Path file = ...;
PosixFileAttributes attr =
    Files.readAttributes(file, PosixFileAttributes.class);
System.out.format("%s %s %s%n",
    attr.owner().getName(),
    attr.group().getName(),
    PosixFilePermissions.toString(attr.permissions()));

PosixFilePermissions helper类提供了几个有用的方法,如下所示:

  • 在前面的代码片段中使用的toString方法将文件权限转换为一个字符串(例如,rw-r–r--)。
  • fromString方法接受一个表示文件权限的字符串,并构造一个文件权限集。
  • asFileAttribute方法接受一组文件权限并构造一个文件属性,可以传递给Path.createFile或Path.createDirectory方法。

下面的代码片段从一个文件中读取属性并创建一个新文件,将原始文件的属性赋值给新文件:

Path sourceFile = ...;
Path newFile = ...;
PosixFileAttributes attrs =
    Files.readAttributes(sourceFile, PosixFileAttributes.class);
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(attrs.permissions());
Files.createFile(file, attr);

asFileAttribute方法将权限包装为FileAttribute。然后,代码尝试使用这些权限创建一个新文件。注意umask也适用,因此新文件可能比请求的权限更安全。

要将文件的权限设置为表示为硬编码字符串的值,可以使用以下代码:

Path file = ...;
Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.setPosixFilePermissions(file, perms);

Chmod示例以类似于Chmod实用程序的方式递归地更改文件的权限。

设置文件或组的所有者

要将名称转换为可以存储为文件所有者或组所有者的对象,可以使用UserPrincipalLookupService服务。该服务将名称或组名称作为字符串查找,并返回表示该字符串的UserPrincipal对象。你可以通过使用FileSystem.getUserPrincipalLookupService方法获取默认文件系统的用户主体查找服务。

下面的代码片段展示了如何使用setOwner方法设置文件所有者:

Path file = ...;
UserPrincipal owner = file.GetFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByName("sally");
Files.setOwner(file, owner);

在Files类中没有用于设置组所有者的特殊用途的方法。但是,一个安全的方法是通过POSIX文件属性视图,如下所示:

Path file = ...;
GroupPrincipal group =
    file.getFileSystem().getUserPrincipalLookupService()
        .lookupPrincipalByGroupName("green");
Files.getFileAttributeView(file, PosixFileAttributeView.class)
     .setGroup(group);

用户定义的文件属性

如果您的文件系统实现支持的文件属性不足以满足您的需求,您可以使用UserDefinedAttributeView来创建和跟踪您自己的文件属性。

一些实现将这个概念映射到像NTFS可选数据流这样的特性和文件系统(如ext3和ZFS)上的扩展属性。大多数实现都对值的大小进行了限制,例如,ext3将该值的大小限制为4kb。

文件的MIME类型可以通过以下代码片段存储为用户定义的属性:

Path file = ...;
UserDefinedFileAttributeView view = Files
    .getFileAttributeView(file, UserDefinedFileAttributeView.class);
view.write("user.mimetype",
           Charset.defaultCharset().encode("text/html");

要读取MIME类型属性,你可以使用下面的代码段:

Path file = ...;
UserDefinedFileAttributeView view = Files.getFileAttributeView(file,UserDefinedFileAttributeView.class);
String name = "user.mimetype";
ByteBuffer buf = ByteBuffer.allocate(view.size(name));
view.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();

Xdd示例展示了如何获取、设置和删除用户定义的属性。


注意:在Linux中,您可能需要为用户定义的属性启用扩展属性。如果您在尝试访问用户定义的属性视图时收到一个UnsupportedOperationException,您需要重新挂载文件系统。下面的命令使用ext3文件系统的扩展属性重新挂载根分区。如果这个命令不适合您的Linux风格,请参阅文档。
$ sudo mount -o remount,user_xattr /

如果您想使更改永久生效,请在/etc/fstab中添加一个条目。


文件存储属性

您可以使用FileStore类来了解有关文件存储的信息,比如可用空间的大小。getFileStore(Path)方法获取指定文件的文件存储。

下面的代码片段打印了特定文件所在的文件存储空间的使用情况:

Path file = ...;
FileStore store = Files.getFileStore(file);

long total = store.getTotalSpace() / 1024;
long used = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;
long avail = store.getUsableSpace() / 1024;

DiskUsage示例使用这个API打印默认文件系统中所有存储的磁盘空间信息。这个示例使用FileSystem类中的getFileStores方法来获取文件系统的所有文件存储。

读取、写入和创建文件

这个页面讨论了读取、写入、创建和打开文件的细节。有大量的文件I/O方法可供选择。为了更好地理解这个API,下图按照复杂性排列了文件I/O方法。
在这里插入图片描述
在图的最左边是实用程序方法readAllBytes、readAllLines和write方法,它们是为简单的、常见的情况设计的。在它们的右边是用于迭代文本流或文本行的方法,例如newBufferedReader、newBufferedWriter,然后是newInputStream和newOutputStream。这些方法可以与java.io包互操作。在它们的右边是处理ByteChannels、SeekableByteChannels和ByteBuffers的方法,例如newByteChannel方法。最后,在最右边的是将FileChannel用于需要文件锁定或内存映射I/O的高级应用程序的方法。


注意:创建新文件的方法允许您为文件指定一组可选的初始属性。例如,在支持POSIX标准集(如UNIX)的文件系统上,您可以在创建文件时指定文件所有者、组所有者或文件权限。管理元数据页面解释了文件属性,以及如何访问和设置它们。

OpenOptions参数

本节中的几个方法采用一个可选的OpenOptions参数。这个参数是可选的,API告诉您当没有指定时,该方法的默认行为是什么。

支持以下StandardOpenOptions枚举:

  • WRITE —— 打开文件以进行写访问。
  • APPEND —— 将新数据追加到文件的末尾。此选项与WRITE或CREATE选项一起使用。
  • TRUNCATE_EXISTING —— 将文件截断为零字节。这个选项与WRITE选项一起使用。
  • CREATE_NEW —— 创建一个新文件,如果该文件已经存在,则抛出异常。
  • CREATE —— 如果文件存在,打开它;如果不存在,创建一个新文件。
  • DELETE_ON_CLOSE —— 当流关闭时删除文件。这个选项对于临时文件很有用。
  • SPARSE —— 提示一个新创建的文件是稀疏的。在一些文件系统(如NTFS)上可以使用这种高级选项,在这些系统中,具有数据“间隙”的大文件可以以更有效的方式存储,而这些间隙不会占用磁盘空间。
  • SYNC —— 保持文件(内容和元数据)与底层存储设备同步。
  • DSYNC —— 保持文件内容与底层存储设备同步。

小文件的常用方法

读取文件中的所有字节或行

如果您有一个较小的文件,并且希望一次性读取其全部内容,那么可以使用readAllBytes(Path)或readAllLines(Path, Charset)方法。这些方法为您处理了大部分工作,例如打开和关闭流,但并不打算处理大型文件。下面的代码展示了如何使用readAllBytes方法:

Path file = ...;
byte[] fileArray;
fileArray = Files.readAllBytes(file);

将所有字节或行写入文件

可以使用其中一种写方法将字节或行写入文件。

  • write(Path, byte[], OpenOption…)
  • write(Path, Iterable< extends CharSequence>, Charset, OpenOption…)

下面的代码片段展示了如何使用写方法。

Path file = ...;
byte[] buf = ...;
Files.write(file, buf);

文本文件的缓冲I/O方法

java.nio.file包支持通道I/O,它可以在缓冲区中移动数据,绕过一些可能导致I/O流瓶颈的层。

使用缓冲流I/O读取文件

newBufferedReader(Path, Charset)方法打开一个用于读取的文件,返回一个BufferedReader,该方法可用于以一种有效的方式从文件中读取文本。

下面的代码片段展示了如何使用newBufferedReader方法从文件中读取数据。该文件以“US-ASCII”编码。

Charset charset = Charset.forName("US-ASCII");
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);
}

使用缓冲流I/O写入文件

你可以使用newBufferedWriter(Path, Charset, OpenOption…)方法来使用BufferedWriter写入文件。

下面的代码片段展示了如何使用这个方法创建一个以"US-ASCII"编码的文件:

Charset charset = Charset.forName("US-ASCII");
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);
}

无缓冲流和与java.ioAPI互操作的方法

使用流I/O读取文件

要打开一个文件进行读取,可以使用newInputStream(Path, OpenOption…)方法。此方法返回一个未缓冲的输入流,用于从文件中读取字节。

Path file = ...;
try (InputStream in = Files.newInputStream(file);
    BufferedReader reader =
      new BufferedReader(new InputStreamReader(in))) {
    String line = null;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException x) {
    System.err.println(x);
}

使用流I/O创建和写入文件

您可以使用newOutputStream(Path, OpenOption…)方法创建文件、向文件追加或写入文件。此方法打开或创建用于写入字节的文件,并返回未缓冲的输出流。

该方法接受一个可选的OpenOption参数。如果没有指定打开选项,且该文件不存在,则创建一个新文件。如果文件存在,它将被截断。这个选项相当于用CREATE和TRUNCATE_EXISTING选项调用方法。

下面的示例打开一个日志文件。如果该文件不存在,则创建它。如果文件存在,则打开以追加。

import static java.nio.file.StandardOpenOption.*;
import java.nio.file.*;
import java.io.*;

public class LogFileTest {

  public static void main(String[] args) {

    // Convert the string to a
    // byte array.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    Path p = Paths.get("./logfile.txt");

    try (OutputStream out = new BufferedOutputStream(
      Files.newOutputStream(p, CREATE, APPEND))) {
      out.write(data, 0, data.length);
    } catch (IOException x) {
      System.err.println(x);
    }
  }
}

通道和ByteBuffers的方法

使用通道I/O读写文件

当流I/O一次读取一个字符时,通道I/O一次读取一个缓冲区。ByteChannel接口提供基本的读写功能。SeekableByteChannel是一个ByteChannel,它有能力维持信道中的一个位置并改变该位置。SeekableByteChannel还支持截断与通道关联的文件,并查询文件的大小。

可以移动到文件中的不同位置,然后对该位置进行读写,这使得对文件的随机访问成为可能。有关更多信息,请参阅随机访问文件章节。

读写通道I/O有两种方法。

  • newByteChannel(Path, OpenOption…)
  • newByteChannel(Path, Set<? extends OpenOption>, FileAttribute<?>…)

注意:newByteChannel方法返回一个SeekableByteChannel的实例。使用默认的文件系统,你可以将这个可查找的字节通道转换为FileChannel,从而提供更高级的功能,比如将文件的一个区域直接映射到内存中,以实现更快的访问,锁定文件的一个区域,这样其他进程就不能访问它,或者从绝对位置读取和写入字节而不影响通道的当前位置。

这两个newByteChannel方法都允许您指定一个OpenOption选项列表。除了另外一个选项外,还支持newOutputStream方法使用的相同的打开选项:READ是必需的,因为seekablebytecnel支持读和写。

指定READ打开读取通道。指定WRITE或APPEND将打开写入通道。如果没有指定这些选项,则打开通道供读取。

下面的代码片段读取一个文件并将其打印到标准输出:

public static void readFile(Path path) throws IOException {

    // Files.newByteChannel() defaults to StandardOpenOption.READ
    try (SeekableByteChannel sbc = Files.newByteChannel(path)) {
        final int BUFFER_CAPACITY = 10;
        ByteBuffer buf = ByteBuffer.allocate(BUFFER_CAPACITY);

        // Read the bytes with the proper encoding for this platform. If
        // you skip this step, you might see foreign or illegible
        // characters.
        String encoding = System.getProperty("file.encoding");
        while (sbc.read(buf) > 0) {
            buf.flip();
            System.out.print(Charset.forName(encoding).decode(buf));
            buf.clear();
        }
    }    
}

下面的示例是为UNIX和其他POSIX文件系统编写的,它使用一组特定的文件权限创建了一个日志文件。这段代码创建一个日志文件,如果日志文件已经存在,则将其追加到日志文件中。创建日志文件时,所有者具有读写权限,组具有只读权限。

import static java.nio.file.StandardOpenOption.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
import java.util.*;

public class LogFilePermissionsTest {

  public static void main(String[] args) {
  
    // Create the set of options for appending to the file.
    Set<OpenOption> options = new HashSet<OpenOption>();
    options.add(APPEND);
    options.add(CREATE);

    // Create the custom permissions attribute.
    Set<PosixFilePermission> perms =
      PosixFilePermissions.fromString("rw-r-----");
    FileAttribute<Set<PosixFilePermission>> attr =
      PosixFilePermissions.asFileAttribute(perms);

    // Convert the string to a ByteBuffer.
    String s = "Hello World! ";
    byte data[] = s.getBytes();
    ByteBuffer bb = ByteBuffer.wrap(data);
    
    Path file = Paths.get("./permissions.log");

    try (SeekableByteChannel sbc =
      Files.newByteChannel(file, options, attr)) {
      sbc.write(bb);
    } catch (IOException x) {
      System.out.println("Exception thrown: " + x);
    }
  }
}

创建常规文件和临时文件的方法

创建文件

您可以使用createFile(Path, FileAttribute<?>)方法创建一个带有初始属性集的空文件。例如,如果在创建时,您希望一个文件具有一组特定的文件权限,那么可以使用createFile方法来做到这一点。如果不指定任何属性,则使用默认属性创建文件。如果文件已经存在,createFile抛出一个异常。

在单个原子操作中,createFile方法检查文件是否存在,并使用指定的属性创建该文件,这使得该进程更安全,不会受到恶意代码的攻击。

下面的代码片段创建了一个具有默认属性的文件:

Path file = ...;
try {
    // Create the empty file with default permissions, etc.
    Files.createFile(file);
} catch (FileAlreadyExistsException x) {
    System.err.format("file named %s" +
        " already exists%n", file);
} catch (IOException x) {
    // Some other sort of failure, such as permissions.
    System.err.format("createFile error: %s%n", x);
}

POSIX文件权限有一个例子,使用createFile(Path, FileAttribute<?>)创建一个预先设置权限的文件。

您还可以使用newOutputStream方法创建一个新文件,如使用流I/O创建和写入文件中所述。如果您打开一个新的输出流并立即关闭它,则会创建一个空文件。

创建临时文件

你可以使用以下createTempFile方法创建一个临时文件:

  • createTempFile(Path, String, String, FileAttribute<?>)
  • createTempFile(String, String, FileAttribute<?>)

第一个方法允许代码为临时文件指定一个目录,第二个方法在默认的临时文件目录中创建一个新文件。这两个方法都允许您为文件名指定后缀,第一个方法还允许您指定前缀。下面的代码片段给出了第二个方法的示例:

try {
    Path tempFile = Files.createTempFile(null, ".myapp");
    System.out.format("The temporary file" +
        " has been created: %s%n", tempFile)
;
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}

运行这个文件的结果如下所示:

The temporary file has been created: /tmp/509668702974537184.myapp

临时文件名的特定格式是平台特定的。

随机存取文件

随机访问文件允许对文件内容进行无序或随机的访问。要随机访问一个文件,您需要打开该文件,寻找特定的位置,并对该文件进行读写。

这个功能可以通过SeekableByteChannel接口实现。SeekableByteChannel接口用当前位置的概念扩展了通道I/O。方法使您能够设置或查询位置,然后可以从该位置读取数据或向该位置写入数据。该API由一些易于使用的方法组成:

  • position —— 返回通道的当前位置
  • position(long) —— 设置通道的位置
  • read(ByteBuffer) —— 从通道读取字节到缓冲区
  • write(ByteBuffer) —— 将字节从缓冲区写入通道
  • truncate(long) —— 截断连接到该通道的文件(或其他实体)

使用I/O通道读写文件时,显示Path.newByteChannel方法返回一个SeekableByteChannel的实例。在默认的文件系统上,你可以直接使用该通道,或者你可以将其转换为FileChannel,让你访问更高级的特性,比如将文件的一个区域直接映射到内存中以获得更快的访问,锁定文件的一个区域,或者从绝对位置读取和写入字节而不影响通道的当前位置。

下面的代码片段通过使用newbytecnel方法之一打开一个文件进行读写。返回的seekablebytecnel被转换为FileChannel。然后,从文件的开头读取12个字节,并在该位置写入字符串“I was here!”。将文件中的当前位置移动到末尾,并追加从开头开始的12个字节。最后,字符串“I was here!”被追加,并且文件中的通道被关闭。

String s = "I was here!\n";
byte data[] = s.getBytes();
ByteBuffer out = ByteBuffer.wrap(data);

ByteBuffer copy = ByteBuffer.allocate(12);

try (FileChannel fc = (FileChannel.open(file, READ, WRITE))) {
    // Read the first 12
    // bytes of the file.
    int nread;
    do {
        nread = fc.read(copy);
    } while (nread != -1 && copy.hasRemaining());

    // Write "I was here!" at the beginning of the file.
    fc.position(0);
    while (out.hasRemaining())
        fc.write(out);
    out.rewind();

    // Move to the end of the file.  Copy the first 12 bytes to
    // the end of the file.  Then write "I was here!" again.
    long length = fc.size();
    fc.position(length-1);
    copy.flip();
    while (copy.hasRemaining())
        fc.write(copy);
    while (out.hasRemaining())
        fc.write(out);
} catch (IOException x) {
    System.out.println("I/O Exception: " + x);
}
创建和读取目录

前面讨论的一些方法,如删除,处理文件、链接和目录。但是如何在文件系统的顶部列出所有的目录呢?如何列出目录的内容或创建目录?

列出文件系统的根目录

通过FileSystem.getRootDirectories方法可以列出文件系统的所有根目录。此方法返回一个Iterable,使您能够使用增强的for语句遍历所有根目录。

下面的代码片段打印了默认文件系统的根目录:

Iterable<Path> dirs = FileSystems.getDefault().getRootDirectories();
for (Path name: dirs) {
    System.err.println(name);
}

创建目录

您可以使用createDirectory(Path, FileAttribute<?>)方法创建一个新目录。如果不指定任何FileAttributes,则新目录将具有默认属性。例如:

Path dir = ...;
Files.createDirectory(path);

下面的代码片段在POSIX文件系统上创建了一个具有特定权限的新目录:

Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rwxr-x---");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.createDirectory(file, attr);

当一个或多个父目录可能还不存在时,要创建多级目录,可以使用方便的方法createDirectories(Path, FileAttribute<?>)。与createDirectory(Path, FileAttribute<?>)方法一样,您可以指定一组可选的初始文件属性。下面的代码片段使用了默认属性:

Files.createDirectories(Paths.get("foo/bar/test"));

根据需要,从上到下创建目录。在foo/bar/test的例子中,如果foo目录不存在,就会创建它。接下来,创建bar目录(如果需要的话),最后,创建test目录。

在创建一些(但不是全部)父目录之后,此方法可能会失败。

创建临时目录

你可以使用createTempDirectory方法创建一个临时目录:

  • createTempDirectory(Path, String, FileAttribute<?>…)
  • createTempDirectory(String, FileAttribute<?>…)

第一个方法允许代码为临时目录指定一个位置,第二个方法在默认的临时文件目录中创建一个新目录。

列出目录的内容

您可以使用newDirectoryStream(Path)方法列出目录的所有内容。此方法返回一个实现DirectoryStream接口的对象。实现了DirectoryStream接口的类也实现了Iterable,因此您可以遍历目录流,读取所有对象。这种方法可以很好地扩展到非常大的目录。


记住:返回的DirectoryStream是一个流。如果你没有使用try-with-resources语句,不要忘记在finally块中关闭流。try with-resources语句会为您处理这个问题。

下面的代码片段展示了如何打印目录的内容:

Path dir = ...;
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 the iteration.
    // In this snippet, it can only be thrown by newDirectoryStream.
    System.err.println(x);
}

迭代器返回的Path对象是根据该目录解析的条目的名称。因此,如果您列出/tmp目录的内容,条目将以/tmp/a、/tmp/b等形式返回。

这个方法返回一个目录的全部内容:文件、链接、子目录和隐藏文件。如果您想对检索的内容进行更有选择性的选择,您可以使用其他的newDirectoryStream方法之一,如本页面后面所述。

注意,如果在目录迭代过程中出现异常,则会抛出DirectoryIteratorException异常,IOException作为原因。迭代器方法不能抛出异常异常。

使用Globbing过滤目录列表

如果您只想获取每个名称都与特定模式匹配的文件和子目录,可以使用newDirectoryStream(Path, String)方法来实现,该方法提供了一个内置的glob过滤器。如果您不熟悉glob语法,请参阅什么是glob?

例如,下面的代码片段列出了与Java相关的文件:.class、. Java和.jar文件:

Path dir = ...;
try (DirectoryStream<Path> stream =
     Files.newDirectoryStream(dir, "*.{java,class,jar}")) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    // IOException can never be thrown by the iteration.
    // In this snippet, it can // only be thrown by newDirectoryStream.
    System.err.println(x);
}

编写您自己的目录过滤器

也许您希望根据模式匹配之外的某些条件筛选目录的内容。您可以创建自己的过滤器,通过实现DirectoryStream.Filter< T> 接口。这个接口由一个方法accept组成,它决定一个文件是否满足搜索需求。

例如,下面的代码片段实现了一个只检索目录的过滤器:

DirectoryStream.Filter<Path> filter =
    newDirectoryStream.Filter<Path>() {
    public boolean accept(Path file) throws IOException {
        try {
            return (Files.isDirectory(path));
        } catch (IOException x) {
            // Failed to determine if it's a directory.
            System.err.println(x);
            return false;
        }
    }
};

一旦创建了过滤器,就可以通过使用newDirectoryStream(Path, DirectoryStream.Filter< ? super Path>)方法。下面的代码片段使用了isDirectory过滤器,只将目录的子目录输出到标准输出:

Path dir = ...;
try (DirectoryStream<Path>
                       stream = Files.newDirectoryStream(dir, filter)) {
    for (Path entry: stream) {
        System.out.println(entry.getFileName());
    }
} catch (IOException x) {
    System.err.println(x);
}

此方法仅用于过滤单个目录。但是,如果您希望找到文件树中的所有子目录,您可以使用遍历文件树的机制。

链接,符号或其他

如前所述,java.nio.file包,特别是Path类,是“链接感知的”。每个Path方法要么检测遇到符号链接时该做什么,要么提供一个选项,使您能够配置遇到符号链接时的行为。

到目前为止,讨论的都是符号链接或软链接,但是有些文件系统也支持硬链接。硬链接比符号链接更具限制性,如下所示:

  • 链路的目标必须存在。
  • 目录上通常不允许有硬链接。
  • 硬链接不允许跨分区或卷。:因此,它们不能跨文件系统存在。
  • 硬链接的外观和行为与常规文件类似,因此很难找到它们。
  • 无论出于何种目的,硬链接都是与原始文件相同的实体。它们具有相同的文件权限、时间戳等。所有属性都是相同的。

由于这些限制,硬链接不像符号链接那样经常使用,但是Path方法与硬链接无缝地工作。

有几种方法专门处理链接,并将在以下部分中介绍:

创建符号链接

如果您的文件系统支持它,您可以使用createSymbolicLink(Path, Path, FileAttribute< ?>)方法创建一个符号链接。第二个Path参数表示目标文件或目录,它可能存在,也可能不存在。下面的代码片段创建了一个具有默认权限的符号链接:

Path newLink = ...;
Path target = ...;
try {
    Files.createSymbolicLink(newLink, target);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not support symbolic links.
    System.err.println(x);
}

FileAttributes可变参数允许您指定在创建链接时自动设置的初始文件属性。但是,这个参数是供将来使用的,目前还没有实现。

创建硬链接

您可以使用createLink(Path, Path)方法创建到现有文件的硬链接(或常规链接)。第二个Path参数定位现有文件,它必须存在,否则抛出NoSuchFileException。下面的代码片段展示了如何创建链接:

Path newLink = ...;
Path existingFile = ...;
try {
    Files.createLink(newLink, existingFile);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not
    // support adding an existing
    // file to a directory.
    System.err.println(x);
}

检测符号链路

要确定一个Path实例是否是一个符号链接,可以使用isSymbolicLink(Path)方法。下面的代码片段展示了如何操作:

Path file = ...;
boolean isSymbolicLink = Files.isSymbolicLink(file);

有关更多信息,请参见管理元数据。

查找链路的目标

你可以通过readSymbolicLink(Path)方法获取符号链接的目标,如下所示:

Path link = ...;
try {
    System.out.format("Target of link" +
        " '%s' is '%s'%n", link,
        Files.readSymbolicLink(link));
} catch (IOException x) {
    System.err.println(x);
}

如果Path不是一个符号链接,这个方法抛出一个NotLinkException。

遍历文件树

您是否需要创建一个将递归地访问文件树中的所有文件的应用程序?也许您需要删除树中的每个.class文件,或者查找去年没有被访问的每个文件。您可以使用FileVisitor接口来做到这一点。

FileVisitor接口

要遍历文件树,首先需要实现一个FileVisitor。FileVisitor指定遍历过程中关键点的必要行为:当文件被访问时、目录被访问前、目录被访问后、目录被访问失败时。该接口有四个方法对应于这些情况:

  • preVisitDirectory —— 在访问目录条目之前调用。
  • postVisitDirectory —— 在访问目录中的所有条目之后调用。如果遇到任何错误,则将特定的异常传递给该方法。
  • visitFile —— 在被访问的文件上调用。文件的BasicFileAttributes被传递给该方法,或者您可以使用文件属性包来读取一组特定的属性。例如,你可以选择读取文件的DosFileAttributeView来确定文件是否设置了“隐藏”位。
  • visitFileFailed —— 当文件无法被访问时调用。特定的异常被传递给该方法。您可以选择是否抛出异常、将其打印到控制台或日志文件,等等。

如果您不需要实现所有四个FileVisitor方法,那么您可以扩展SimpleFileVisitor类,而不是实现FileVisitor接口。这个类实现了FileVisitor接口,它访问树中的所有文件,并在遇到错误时抛出一个IOError。您可以扩展这个类并只重写您需要的方法。

下面是一个扩展SimpleFileVisitor以打印文件树中的所有条目的示例。无论条目是普通文件、符号链接、目录还是其他“未指定”类型的文件,它都会打印该条目。它还打印每个文件的大小(以字节为单位)。遇到的任何异常都会打印到控制台。

FileVisitor方法以粗体显示:

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("Symbolic link: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("Regular file: %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(Path file,
                                       IOException exc) {
        System.err.println(exc);
        return CONTINUE;
    }
}

启动过程

一旦实现了FileVisitor,如何启动文件遍历呢?在Files类中有两个walkFileTree方法。

  • walkFileTree(Path, FileVisitor)
  • walkFileTree(Path, Set< FileVisitOption>, int, FileVisitor)

第一个方法只需要一个起点和一个FileVisitor实例。你可以如下方式调用PrintFiles文件访问器:

Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);

第二个walkFileTree方法允许您另外指定访问级别的数量限制和一组FileVisitOption枚举。如果希望确保此方法遍历整个文件树,可以指定Integer。MAX_VALUE为最大深度参数。

你可以指定FileVisitOption enum, FOLLOW_LINKS,它表示符号链接应该在后面。

下面的代码片段展示了如何调用四个参数的方法:

import static java.nio.file.FileVisitResult.*;

Path startingDir = ...;

EnumSet<FileVisitOption> opts = EnumSet.of(FOLLOW_LINKS);

Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);

创建FileVisitor时的注意事项

先遍历文件树的深度,但不能对访问子目录的迭代顺序做任何假设。

如果您的程序将更改文件系统,则需要仔细考虑如何实现FileVisitor。

例如,如果要写递归删除,首先要删除目录中的文件,然后再删除目录本身。在本例中,删除postVisitDirectory中的目录。

如果您正在编写递归副本,那么在尝试将文件复制到它(在visitFiles中)之前,您需要在preVisitDirectory中创建新目录。如果您希望保留源目录的属性(类似于UNIX cp -p命令),您需要在文件被复制后,在postVisitDirectory中这样做。Copy示例展示了如何做到这一点。

如果您正在编写一个文件搜索,您可以在visitFile方法中执行比较。此方法查找符合您的条件的所有文件,但不查找目录。如果您想同时找到文件和目录,您还必须在preVisitDirectory或postVisitDirectory方法中执行比较。Find示例演示了如何做到这一点。

您需要决定是否希望遵循符号链接。例如,如果您正在删除文件,可能不建议使用符号链接。如果您正在复制文件树,您可能希望允许这样做。默认情况下,walkFileTree不遵循符号链接。

对文件调用visitFile方法。如果您已经指定了FOLLOW_LINKS选项,并且您的文件树有一个到父目录的循环链接,那么在使用FileSystemLoopException的visitFileFailed方法中会报告这个循环目录。下面的代码片段展示了如何捕捉循环链接,它来自Copy的例子:

@Override
public FileVisitResult
    visitFileFailed(Path file,
        IOException exc) {
    if (exc instanceof FileSystemLoopException) {
        System.err.println("cycle detected: " + file);
    } else {
        System.err.format("Unable to copy:" + " %s: %s%n", file, exc);
    }
    return CONTINUE;
}

这种情况只有在程序遵循符号链接时才会发生。

控制流量

也许您希望遍历文件树以查找特定的目录,找到后希望终止进程。也许您想要跳过特定的目录。

FileVisitor方法返回一个FileVisitResult值。你可以中止文件遍历过程,或者通过你在FileVisitor方法中返回的值来控制一个目录是否被访问:

  • CONTINUE —— 表示文件遍历应该继续。如果preVisitDirectory方法返回CONTINUE,则访问该目录。
  • TERMINATE —— 立即终止文件遍历。在返回这个值之后,不会再调用任何文件遍历方法。
  • SKIP_SUBTREE —— 当preVisitDirectory返回此值时,将跳过指定的目录及其子目录。这根树枝被从树上“剪掉”了。
  • SKIP_SIBLINGS —— 当preVisitDirectory返回这个值时,指定的目录不会被访问,postVisitDirectory不会被调用,并且不会访问其他未访问的同级目录。如果从postVisitDirectory方法返回,则不会再访问其他兄弟目录。从本质上说,在指定的目录中不会发生任何进一步的事情。

在这段代码中,任何名为SCCS的目录都会被跳过:

import static java.nio.file.FileVisitResult.*;

public FileVisitResult
     preVisitDirectory(Path dir,
         BasicFileAttributes attrs) {
    (if (dir.getFileName().toString().equals("SCCS")) {
         return SKIP_SUBTREE;
    }
    return CONTINUE;
}

在这段代码中,只要找到一个特定的文件,该文件的名称就会被打印到标准输出,并且对文件的遍历结束:

import static java.nio.file.FileVisitResult.*;

// The file we are 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;
}

例子

下面的例子演示了文件遍历机制:

  • Find —— 递归文件树查找与特定glob模式匹配的文件和目录。这个例子将在查找文件中讨论。
  • Chmod —— 递归地更改文件树的权限(仅适用于POSIX系统)。
  • Copy —— 递归复制文件树。
  • WatchDir —— 演示监视目录中已创建、删除或修改的文件的机制。用-r选项调用这个程序可以观察整个树的变化。有关文件通知服务的详细信息,请参见监视目录的更改章节。
查找文件

如果您曾经使用过shell脚本,那么您很可能使用过模式匹配来定位文件。事实上,您可能已经广泛地使用了它。如果您还没有使用过它,模式匹配将使用特殊字符创建一个模式,然后可以将文件名与该模式进行比较。例如,在大多数shell脚本中,星号*匹配任意数量的字符。例如,下面的命令列出了当前目录中所有以.html结尾的文件:

% ls *.html

java.nio.file包为这个有用的特性提供了编程支持。每个文件系统实现都提供一个PathMatcher。你可以使用FileSystem类中的getPathMatcher(String)方法来检索文件系统的PathMatcher。下面的代码片段为默认文件系统获取路径匹配器:

String pattern = ...;
PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:" + pattern);

传递给getPathMatcher的字符串参数指定要匹配的语法风格和模式。这个例子指定了glob语法。如果您不熟悉glob语法,请参阅什么是glob章节。

Glob语法易于使用且灵活,但如果您愿意,还可以使用正则表达式或regex语法。有关正则表达式的更多信息,请参见正则表达式章节。一些文件系统实现可能支持其他语法。

如果你想使用其他形式的基于字符串的模式匹配,你可以创建你自己的PathMatcher类。本页面中的示例使用glob语法。

一旦创建了PathMatcher实例,就可以根据它来匹配文件了。PathMatcher接口有一个单独的方法matches,它接受一个Path参数并返回一个布尔值:它要么匹配模式,要么不匹配。下面的代码片段查找以.java或.class结尾的文件,并将这些文件打印到标准输出:

PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:*.{java,class}");

Path filename = ...;
if (matcher.matches(filename)) {
    System.out.println(filename);
}

递归模式匹配

搜索匹配特定模式的文件与遍历文件树密切相关。有多少次您知道文件在文件系统的某个地方,但是在哪里呢?或者,您可能需要在文件树中找到具有特定文件扩展名的所有文件。

Find示例正是这样做的。Find类似于UNIX Find实用程序,但在功能上有所缩减。您可以扩展此示例以包含其他功能。例如,find实用程序支持-prune标志来从搜索中排除整个子树。您可以通过在preVisitDirectory方法中返回SKIP_SUBTREE来实现该功能。要实现符号链接后面的-L选项,可以使用4个参数的walkFileTree方法并传入FOLLOW_LINKS枚举(但是要确保在visitFile方法中测试循环链接)。

运行查找应用程序,使用以下格式:

% java Find <path> -name "<glob_pattern>"

模式放置在引号内,因此任何通配符都不会被shell解释。例如:

% java Find . -name "*.html"

下面是查找示例的源代码:

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import static java.nio.file.FileVisitResult.*;
import static java.nio.file.FileVisitOption.*;
import java.util.*;


public class Find {

    public static class Finder
        extends SimpleFileVisitor<Path> {

        private final PathMatcher matcher;
        private int numMatches = 0;

        Finder(String pattern) {
            matcher = FileSystems.getDefault()
                    .getPathMatcher("glob:" + pattern);
        }

        // Compares the glob pattern against
        // the file or directory name.
        void find(Path file) {
            Path name = file.getFileName();
            if (name != null && matcher.matches(name)) {
                numMatches++;
                System.out.println(file);
            }
        }

        // Prints the total number of
        // matches to standard out.
        void done() {
            System.out.println("Matched: "
                + numMatches);
        }

        // Invoke the pattern matching
        // method on each file.
        @Override
        public FileVisitResult visitFile(Path file,
                BasicFileAttributes attrs) {
            find(file);
            return CONTINUE;
        }

        // Invoke the pattern matching
        // method on each directory.
        @Override
        public FileVisitResult preVisitDirectory(Path dir,
                BasicFileAttributes attrs) {
            find(dir);
            return CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file,
                IOException exc) {
            System.err.println(exc);
            return CONTINUE;
        }
    }

    static void usage() {
        System.err.println("java Find <path>" +
            " -name \"<glob_pattern>\"");
        System.exit(-1);
    }

    public static void main(String[] args)
        throws IOException {

        if (args.length < 3 || !args[1].equals("-name"))
            usage();

        Path startingDir = Paths.get(args[0]);
        String pattern = args[2];

        Finder finder = new Finder(pattern);
        Files.walkFileTree(startingDir, finder);
        finder.done();
    }
}

递归遍历文件树将在遍历文件树中介绍。

观察目录的变化

您是否曾经发现自己在使用IDE或其他编辑器编辑文件时,出现了一个对话框,通知您打开的某个文件在文件系统上发生了更改,需要重新加载?或者,像NetBeans IDE一样,应用程序只是悄悄地更新文件而不通知您。下面的示例对话框显示了这个通知在免费编辑器jEdit下的样子:
在这里插入图片描述
要实现这个称为文件更改通知的功能,程序必须能够检测到文件系统上相关目录发生了什么。一种方法是轮询文件系统以查找更改,但是这种方法效率很低。它不能扩展到有数百个打开的文件或目录要监视的应用程序。

java.nio.file包提供了一个文件更改通知API,称为Watch Service API。这个API允许您向监视服务注册一个(或多个)目录。注册时,你告诉服务你感兴趣的事件类型:文件创建、删除或修改。当服务检测到感兴趣的事件时,它被转发到注册的进程。已注册进程有一个线程(或线程池),用于监视它已注册的任何事件。当事件传入时,将根据需要对其进行处理。

看服务概述

WatchService API是相当低级的,允许您自定义它。您可以按原样使用它,也可以选择在此机制之上创建一个高级API,以便它适合您的特定需求。以下是实现手表服务所需的基本步骤:

  • 为文件系统创建一个WatchService“监视器”。
  • 对于希望监视的每个目录,将其注册到监视程序中。注册目录时,可以指定要通知的事件类型。您将为您注册的每个目录收到一个WatchKey实例。
  • 实现一个无限循环来等待传入的事件。当一个事件发生时,这个键就会发出信号并被放置到监视者的队列中。
  • 从监视者的队列中检索密钥。可以从密钥中获取文件名。
  • 检索键的每个挂起事件(可能有多个事件)并根据需要处理。
  • 重置键,并恢复等待事件。
  • 关闭服务:监视服务在线程退出或线程关闭时退出(通过调用其关闭的方法)。

WatchKeys是线程安全的,可以与java.nio.concurrent包一起使用。您可以为此工作指定一个线程池。

试用它

因为这个API更高级,所以在继续之前先试用一下。将WatchDir示例保存到您的计算机中,并编译它。创建将传递给示例的测试目录。WatchDir使用一个线程来处理所有事件,因此它在等待事件时阻塞键盘输入。可以在单独的窗口中运行程序,也可以在后台运行,如下所示:

java WatchDir test &

在测试目录中创建、删除和编辑文件。当这些事件发生时,一条消息将打印到控制台。完成后,删除test目录,并退出WatchDir。或者,如果您愿意,也可以手动终止该进程。

您还可以通过指定-r选项来监视整个文件树。当指定-r时,WatchDir遍历文件树,向监视服务注册每个目录。

创建监视服务并注册事件

第一步是通过使用FileSystem类中的newWatchService方法创建一个新的WatchService,如下所示:

WatchService watcher = FileSystems.getDefault().newWatchService();

接下来,向监视服务注册一个或多个对象。任何实现Watchable接口的对象都可以注册。ath类实现了Watchable接口,因此要监视的每个目录都注册为Path对象。

与任何Watchable一样,Path类实现了两个寄存器方法。这个页面使用双参数版本,register(WatchService, WatchEvent.Kind<?>…)。(由三个参数组成的版本接受一个WatchEvent.Modifier,目前还没有实现。)

在向监视服务注册对象时,您可以指定要监视的事件类型。支持的standardwatcheventtypes事件类型如下:

  • ENTRY_CREATE —— 创建一个目录条目。
  • ENTRY_DELETE —— 删除目录条目。
  • ENTRY_MODIFY —— 修改目录条目。
  • OVERFLOW —— 指示事件可能已经丢失或丢弃。您不必注册OVERFLOW事件来接收它。

下面的代码片段展示了如何为这三种事件类型注册一个Path实例:

import static java.nio.file.StandardWatchEventKinds.*;

Path dir = ...;
try {
    WatchKey key = dir.register(watcher,
                           ENTRY_CREATE,
                           ENTRY_DELETE,
                           ENTRY_MODIFY);
} catch (IOException x) {
    System.err.println(x);
}

处理事件

事件处理循环中的事件顺序如下:

  1. 拿到手表钥匙。提供了三种方法:
    1)poll —— 如果可用,返回一个排队的键。如果不可用,则立即返回空值。
    2)poll(long, TimeUnit) —— 如果有可用的键,返回一个排队的键。如果一个排队的键没有立即可用,程序将等待到指定的时间。TimeUnit参数确定指定的时间是纳秒、毫秒还是其他时间单位。
    3)take —— 返回一个排队的键。如果没有可用的队列键,此方法将等待。
  2. 处理键的未决事件。:从pollEvents方法获取watchevents列表。
  3. 使用kind方法检索事件的类型。无论该键注册了什么事件,都可能收到OVERFLOW事件。您可以选择处理溢出或忽略它,但您应该对其进行测试。
  4. 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用上下文方法来检索它。
  5. 在处理了键的事件之后,您需要通过调用reset将键放回就绪状态。如果此方法返回false,则该键不再有效,循环可以退出。这一步非常重要。如果调用reset失败,此键将不会接收任何进一步的事件。

手表钥匙有一个状态。在任何给定的时间,它的状态可能是以下状态之一:

  • Ready表示该键已准备好接受事件。当第一次创建时,键处于就绪状态。
  • Signaled表示一个或多个事件已排队。一旦键被发出信号,它就不再处于就绪状态,直到调用reset方法。
  • Invalid表示该密钥不再处于活动状态。当下列事件之一发生时,该状态就会发生:
    1)进程通过使用cancel方法显式地取消键。
    2)目录变得不可访问。
    3)手表服务关闭。

下面是一个事件处理循环的例子。它取自Email示例,该示例监视一个目录,等待新文件出现。当一个新文件可用时,会使用probeContentType(Path)方法检查它,以确定它是否是一个text/plain文件。这样做的目的是将text/plain文件通过电子邮件发送到一个别名,但实现细节留给读者。

特定于watch服务API的方法以粗体显示:

for (;;) {

    // 等待钥匙发出信号
    WatchKey key;
    try {
        key = watcher.take();
    } catch (InterruptedException x) {
        return;
    }

    for (WatchEvent<?> event: key.pollEvents()) {
        WatchEvent.Kind<?> kind = event.kind();

        // 这个键只在ENTRY_CREATE事件中注册,
        // 但是,不管事件丢失或丢弃,都可以发生OVERFLOW事件。
        if (kind == OVERFLOW) {
            continue;
        }

        // 文件名是事件的上下文。
        WatchEvent<Path> ev = (WatchEvent<Path>)event;
        Path filename = ev.context();

        // 验证新文件是否是文本文件。
        try {
            // 根据目录解析文件名。
            // 如果文件名是"test",目录是"foo",
            // 解析的名称是“test/foo”。
            Path child = dir.resolve(filename);
            if (!Files.probeContentType(child).equals("text/plain")) {
                System.err.format("New file '%s'" +
                    " is not a plain text file.%n", filename);
                continue;
            }
        } catch (IOException x) {
            System.err.println(x);
            continue;
        }

        // 将文件发送到指定的电子邮件别名。
        System.out.format("Emailing file %s%n", filename);
        //Details left to reader....
    }

    // 重置键——如果你想接收更多的观察事件,这个步骤很关键。
    // 如果该键不再有效,则该目录是不可访问的,因此退出循环。
    boolean valid = key.reset();
    if (!valid) {
        break;
    }
}

检索文件名

从事件上下文中检索文件名。Email例子用下面的代码检索文件名:

WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();

当你编译Email示例时,它会生成以下错误:

Note: Email.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

此错误是由将WatchEvent< T>转换为WatchEvent< Path>的代码行导致的。WatchDir示例通过创建一个实用程序强制转换方法来避免这个错误,该方法抑制未检查的警告,如下所示:

@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
    return (WatchEvent<Path>)event;
}

如果您不熟悉@SuppressWarnings语法,请参阅注释章节。

何时使用和不使用此 API

Watch Service API是为需要得到文件更改事件通知的应用程序设计的。它非常适合任何应用程序,比如编辑器或IDE,这些应用程序可能有许多打开的文件,并且需要确保文件与文件系统同步。它也非常适合于监视目录的应用服务器,可能会等待.jsp或.jar文件被删除,以便部署它们。

这个API不是为硬盘驱动索引而设计的。大多数文件系统实现都支持文件更改通知。监视服务API在可用的情况下利用了这种支持。但是,当文件系统不支持这种机制时,Watch Service将轮询文件系统,等待事件。

其他有用的方法

一些有用的方法不适合本课的其他地方,在这里介绍。

确定 MIME 类型

要确定文件的MIME类型,您可能会发现probeContentType(Path)方法很有用。例如:

try {
    String type = Files.probeContentType(filename);
    if (type == null) {
        System.err.format("'%s' has an" + " unknown filetype.%n", filename);
    } else if (!type.equals("text/plain") {
        System.err.format("'%s' is not" + " a plain text file.%n", filename);
        continue;
    }
} catch (IOException x) {
    System.err.println(x);
}

注意,如果内容类型无法确定,probeContentType返回null。

这种方法的实现是高度平台特定的,并不是绝对可靠的。内容类型由平台的默认文件类型检测器决定。例如,如果检测器基于.class扩展名确定文件的内容类型为application/x-java,那么它可能上当了。

如果默认值不足以满足您的需求,您可以提供一个自定义的FileTypeDetector。

Email示例使用了probeContentType方法。

默认文件系统

要检索默认文件系统,请使用getDefault方法。通常,这个FileSystems方法(注意复数)链接到FileSystem方法之一(注意单数),如下所示:

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.*");

路径字符串分隔符

POSIX文件系统的路径分隔符是正斜杠,/,而对于Microsoft Windows是反斜杠,\。其他文件系统可能使用其他分隔符。要检索默认文件系统的Path分隔符,可以使用以下方法之一:

String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();

getSeparator方法还用于检索任何可用文件系统的路径分隔符。

文件系统的文件存储

一个文件系统有一个或多个文件存储区来保存它的文件和目录。文件存储代表底层存储设备。在UNIX操作系统中,每个挂载的文件系统都由一个文件存储区表示。在Microsoft Windows中,每个卷都由一个文件存储区表示:C:、D:,等等。

要检索文件系统的所有文件存储的列表,可以使用getFileStores方法。此方法返回一个Iterable,它允许您使用增强的for语句来遍历所有根目录。

for (FileStore store: FileSystems.getDefault().getFileStores()) {
   ...
}

如果你想要检索特定文件所在的文件存储,使用Files类中的getFileStore方法,如下所示:

Path file = ...;
FileStore store= Files.getFileStore(file);

DiskUsage的例子使用了getFileStores方法。

遗留文件I/O代码

与遗留代码的互操作性

在Java SE 7发行版之前,Java .io. file类是用于文件I/O的机制,但它有几个缺点。

  • 许多方法失败时不会抛出异常,因此不可能获得有用的错误消息。例如,如果文件删除失败,程序将收到一个“删除失败”,但不知道这是因为文件不存在,用户没有权限,或有其他一些问题。
  • rename方法在不同的平台上不能一致地工作。
  • 对符号链接没有真正的支持。
  • 需要更多的元数据支持,比如文件权限、文件所有者和其他安全属性。
  • 访问文件元数据的效率很低。
  • 许多File方法不能伸缩。在服务器上请求大型目录列表可能会导致挂起。大目录还可能导致内存资源问题,从而导致拒绝服务。
  • 如果存在循环符号链接,则不可能编写可靠的代码来递归地遍历文件树并适当地响应。

也许您有使用java.io.File的遗留代码,并希望利用java.nio.file.Path功能,而对代码的影响最小。

java.io.File类提供了toPath方法,它将旧样式的File实例转换为java.nio.file.Path实例,如下所示:

Path input = file.toPath();

然后,您可以利用Path类可用的丰富特性集。

例如,假设你有一些代码删除了一个文件:

file.delete();

你可以修改这段代码来使用Files.delete方法,如下所示:

Path fp = file.toPath();
Files.delete(fp);

相反,Path.toFile方法为Path对象构造一个java.io.File对象。

将java.io.File功能映射到java.nio.file

因为文件I/O的Java实现在Java SE 7发行版中已经完全重新构建了体系结构,所以不能将一种方法替换为另一种方法。如果您想使用java.nio.file包提供的丰富功能,最简单的解决方案是使用File.toPath方法,如前一节所建议的。但是,如果您不想使用这种方法,或者它不能满足您的需要,则必须重写文件I/O代码。

这两个API之间没有一对一的对应关系,但是下表让您大致了解java.io.File API中映射到java.nio.file API中的哪些功能,并告诉您可以从哪里获得更多信息。

java.io.File功能java.nio.file功能教程覆盖
java.io.Filejava.nio.file.PathPath类
java.io.RandomAccessFileThe SeekableByteChannel functionality.随机存取文件
File.canRead, canWrite, canExecuteFiles.isReadable, Files.isWritable, and Files.isExecutable. 在 UNIX 文件系统上,管理元数据(文件和文件存储属性)包用于检查九个文件权限。检查文件或目录,管理元数据
File.isDirectory(), File.isFile(), 和 File.length()Files.isDirectory(Path, LinkOption…), Files.isRegularFile(Path, LinkOption…), and Files.size(Path)管理元数据
File.lastModified() 和File.setLastModified(long)Files.getLastModifiedTime(Path, LinkOption…) 和Files.setLastMOdifiedTime(Path, FileTime)管理元数据
设置各种属性的File方法:setExecutable, setReadable, setReadOnly, setWritable这些方法被Files方法替换setAttribute(Path, String, Object, LinkOption…).管理元数据
new File(parent, “newfile”)parent.resolve(“newfile”)路径操作
File.renameToFiles.move移动文件或目录
File.deleteFiles.delete删除文件或目录
File.createNewFileFiles.createFile创建文件
File.deleteOnExit由createFile方法中指定的DELETE_ON_CLOSE选项替换。创建文件
File.createTempFileFiles.createTempFile(Path, String, FileAttributes<?>), Files.createTempFile(Path, String, String, FileAttributes<?>)创建文件,使用流 I/O 创建和写入文件,使用通道I/O读写文件
File.existsFiles.exists and Files.notExists验证文件或目录是否存在
File.compareTo and equalsPath.compareTo and equals比较两条路径
File.getAbsolutePath和getAbsoluteFilePath.toAbsolutePath转换路径
File.getCanonicalPath和getCanonicalFilePath.toRealPath or normalize转换路径 ( toRealPath),从路径中删除冗余 ( normalize)
File.toURIPath.toURI转换路径
File.isHiddenFiles.isHidden检索有关路径的信息
File.list和listFilesPath.newDirectoryStream列出目录的内容
File.mkdir和mkdirsFiles.createDirectory创建目录
File.listRootsFileSystem.getRootDirectories列出文件系统的根目录
File.getTotalSpace, File.getFreeSpace, File.getUsableSpaceFileStore.getTotalSpace, FileStore.getUnallocatedSpace, FileStore.getUsableSpace, FileStore.getTotalSpace文件存储属性

总结

java.io包包含许多类,您的程序可以使用这些类来读写数据。大多数类实现顺序访问流。顺序访问流可分为两类:读和写字节和读和写Unicode字符。每个顺序访问流都有自己的特性,比如从文件中读取或写入数据,在读取或写入数据时过滤数据,或序列化对象。

java.nio.file包为文件和文件系统I/O提供了广泛的支持。这是一个非常全面的API,但其关键入口如下:

  • Path类有操作路径的方法。
  • Files类有用于文件操作的方法,比如移动、复制、删除,还有用于检索和设置文件属性的方法。
  • FileSystem类有各种各样的方法来获取关于文件系统的信息。

更多关于NIO.2的信息可以在OpenJDK: NIO项目的网站上找到。这个站点提供了NIO.2提供的超出本教程范围的特性的资源,例如多播、异步I/O和创建自己的文件系统实现。

问题和练习:基本 I/O

问题

  1. 您将使用什么类和方法来读取位于大文件末尾的已知位置的几段数据?
  2. 当调用format时,什么是指示新行的最佳方式?
  3. 如何确定文件的MIME类型?
  4. 你会使用什么方法来确定一个文件是否是一个符号链接?

练习

  1. 写一个例子,计算一个特定字符(如e)在文件中出现的次数。可以在命令行中指定该字符。您可以使用xanadu.txt作为输入文件。
  2. 文件datafile以一个long开头,它告诉你同一文件中单个int类型数据块的偏移量。写一个程序来获取int类型的数据。什么是int数据?

Check your answers.

并发

说明如何编写同时执行多个任务的应用程序。Java平台从头开始设计,以支持并发编程,并在Java编程语言和Java类库中提供基本的并发支持。从5.0版本开始,Java平台还包含了高级并发性api。本课介绍了平台的基本并发支持,并总结了java.util.concurrent包中的一些高级api。

具体详情在java官网教程(服务器篇)中的并发章节

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值