Oracle-专业认证-JavaSE8-编程测验-五-

51 阅读36分钟

Oracle 专业认证 JavaSE8 编程测验(五)

协议:CC BY-NC-SA 4.0

九、Java I/O 基础知识

认证目标
从控制台读取和写入数据
在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter

在本章中,我们将向您介绍 Java I/O 编程的基础知识。我们将讨论两个主题:如何从控制台读写数据,然后如何使用(文件)流读写数据。

java.io 和 java.nio 包中提供了对文件操作的支持。在本章的开始部分,我们将只关注 java.io 包;稍后,我们将关注使用流读写数据,但不关注 java.io 包中提供的其他特性。java.nio 包为文件 I/O 提供了全面的支持,我们将在第十章中介绍。

从控制台读取和写入控制台

认证目标
从控制台读取和写入数据

对于控制台的读写,可以使用标准的输入、输出和错误流,或者使用Console类。现在让我们来讨论这两种方法。

了解标准流

java.lang.System类中的公共静态字段inouterr分别代表标准输入、输出和错误流。System.injava.io.InputStreamSystem.outSystem.errjava.io.PrintStream型。

下面是一个从控制台读取并打印一个整数的编程示例(清单 9-1 ):

Listing 9-1. Read.java

import java.io.IOException;

class Read {

public static void main(String []args) {

System.out.print("Type a character: ");

int val = 0;

try {

// the return type of read is int, but it returns a byte value!

val = System.in.read();

} catch(IOException ioe) {

System.err.println("Cannot read input " + ioe);

System.exit(-1);

}

System.out.println("You typed: " + val);

}

}

以下是该程序的运行示例:

D:\> $ java Read

Type a character: 5

You typed: 53

read方法的返回类型是int,但是它返回一个范围在 0 到 255 之间的byte值(是的,它是不直观的)。因此,对于输入 5,程序打印其 ASCII 值 53。read方法“阻塞”(即等待)用户输入;如果读取时发生 I/O 异常,该方法抛出一个IOException

这个程序演示了所有三个流的使用——System.in用于从控制台获取输入,System.out用于打印读取的整数值,System.err用于在发生 I/O 异常时发出错误。

重载的read方法本质上是低级的,以字节为单位工作。读取其他类型的输入,如Strings需要与ReaderScanner类一起使用,如:

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String str = br.readLine();

// or use java.util.Scanner, as in:

Scanner scanner = new Scanner(System.in);

String str = scanner.next();

我们将在本章后面讨论更多关于ReaderScanner类的内容。

重新分配标准流

标准流在 JVM 启动时初始化。有时,通过重新分配来重定向标准流是很有用的。方法System.setIn接受一个InputStream对象,方法System.setOutSystem.setError接受PrintStream对象作为参数。下面是一个代码片段,它通过将System.out流重新分配给一个输出文本文件,将标准输出捕获到一个文件中:

import java.io.*;

class StreamTest {

public static void main(String []args ){

try{

PrintStream ps = new PrintStream("log.txt");

System.setOut(ps);

System.out.println("Test output to System.out");

} catch(Exception ee){

ee.printStackTrace();

}

}

}

当您执行这段代码时,程序将创建一个名为“log.txt”的文件,并在该文件中打印字符串“Test output to System.out”。

重定向流在许多情况下都很有用。例如,为了进行测试,您可能希望从文本文件中读取输入,而不是从控制台中读取。您可以通过将标准输入流重定向到文本文件来实现这一点。同样,您可能希望将错误流重新分配给文本文件,以便将所有错误消息存储在日志文件中。您可以通过调用System.setErr方法来实现这一点。

了解控制台类

使用Console类有助于从控制台读取数据和将数据写入控制台。注意,这里的“控制台”一词指的是字符输入设备(通常是键盘)和字符显示设备(通常是屏幕显示器)。您可以使用System.console()方法获得对控制台的引用;如果 JVM 不与任何控制台相关联,这个方法将返回null

您的第一个练习是实现一个简单的Echo命令,当您运行这个程序时,它打印出作为输入的文本行(清单 9-2 )。

Listing 9-2. Echo.java

import java.io.Console;

// simple implementation of Echo command

class Echo {

public static void main(String []args) {

// get the System console object

Console console = System.console();

if(console == null) {

System.err.println("Cannot retrieve console object - are you running your application from an IDE? Exiting the application … ");

System.exit(-1); // terminate the application

}

// read a line and print it through printf

console.printf(console.readLine());

}

}

下面是程序对于不同输出的行为(在第一次运行中,我们键入“hello world”作为输入,在第二次运行中,我们终止程序):

D:\>java Echo

hello world

hello world

D:\>java Echo

^Z

Exception in thread "main" java.lang.NullPointerException

at java.util.regex.Matcher.getTextLength(Matcher.java:1234)

… [this part of the stack trace elided to save space]

at Echo.main(Echo.java:14)

对于普通的文本输入,这个程序工作得很好。如果您键入 no input 并尝试用^z 或^d (Ctrl+Z 或 Ctrl+D 组合键)终止程序,那么程序不会收到任何输入,因此readLine()方法返回 null 当printf接受一个空参数时,它抛出一个NullReferenceException

请注意,您是从命令行运行这个程序的。如果从命令行调用 JVM 而不重定向输入或输出流,方法System.console()将成功,因为 JVM 将与控制台(通常是键盘和显示屏)相关联。如果 JVM 是由 IDE 间接调用的,或者如果 JVM 是从后台进程调用的,那么方法调用System.console()将失败并返回null。例如,如果您从 IntelliJ IDEA 或 Eclipse IDEs 运行,System.console()将通过返回 null 而失败。

A978-1-4842-1836-5_9_Figaa_HTML.gif如果 JVM 是由 IDE 间接调用的,或者如果 JVM 是从后台进程调用的,那么方法调用System.console()将失败并返回 null。

表 9-1 中列出了Console类中可用的一些重要方法。

表 9-1。

Important Methods in the Console Class

方法简短描述
Reader reader()返回与此Console对象相关联的Reader对象;可以通过这个返回的引用执行读取操作。
PrintWriter writer()返回与此Console对象相关联的PrintWriter对象;可以通过这个返回的引用执行写操作。
String readLine()读取一行文本String(这个返回的 string 对象不包含任何行终止字符);如果失败,则返回 null(例如,用户在控制台中按下了 Ctrl+Z 或 Ctrl+D)
String readLine(String fmt, Object… args)readLine()方法相同,但是它首先打印字符串fmt
char[] readPassword()读取密码文本并以 char 数组的形式返回;此方法禁用回显,因此当用户键入密码时,控制台中不会显示任何内容。
char[] readPassword(String fmt, Object… args)readPassword()方法相同,但是在读取密码字符串之前,它首先打印作为格式字符串参数给出的字符串。
Console format(String fmt, Object… args)将格式化字符串(基于fmt字符串和传递的args的值创建)写入控制台。
Console printf(String fmt, Object… args)将格式化字符串(基于fmt字符串和传递的args的值创建)写入控制台。这个printf方法与format方法相同:这是一个“方便的方法”——方法printf和格式说明符为大多数 C/C++程序员所熟悉,因此除了 format 方法之外还提供了这个方法。
void flush()刷新控制台对象缓冲区中仍待打印的任何数据。

使用控制台类格式化输出

Console类支持方法printf()format()中的格式化 I/O,加上重载方法readPassword()readLine()。我们现在将讨论使用方法printf()(以及类似的format()方法)的格式化输出,稍后讨论readPassword()readLine()方法。

方法printf()使用字符串格式化标志来格式化字符串。它非常类似于 C 编程语言库中提供的printf()函数。printf()方法的第一个参数是一个格式字符串。格式字符串可以包含字符串文字和格式说明符。实际参数在格式字符串之后传递。如果传递的格式不正确,这个方法会抛出IllegalFormatException

格式说明符是字符串格式概念的关键。它们定义了特定数据类型及其格式(如对齐和宽度)的占位符。printf()方法的其余参数是变量(或文字),它们提供实际数据来填充格式说明符序列中的占位符。

让我们讨论一个何时以及为什么需要使用格式说明符的详细例子。假设您想打印一个足球运动员的表格,其中包含他们的姓名、比赛场次、得分和每场比赛的进球数信息。但是,有一些限制:

  • 你想把玩家的名字印在左边(左对齐)。
  • 您需要为玩家的名字指定至少 15 个字符。
  • 您希望在距制表位一定距离处打印每一列。
  • 您希望在每场比赛的进球数信息中只指定一个精确点。

清单 9-3 展示了如何实现这一点。

Listing 9-3. FormattedTable.java

// This program demonstrates the use of format specifiers in printf

import java.io.Console;

class FormattedTable {

void line(Console console) {

console.printf("------------------------------------------------------------\n");

}

void printHeader(Console console) {

console.printf("%-15s \t %s \t %s \t %s \n", "Player", "Matches", "Goals", "Goals per match");

}

void printRow(Console console, String player, int matches, int goals) {

console.printf("%-15s \t %5d \t\t %d \t\t %.1f \n", player, matches, goals,

((float)goals/(float)matches));

}

public static void main(String[] str) {

FormattedTable formattedTable = new FormattedTable();

Console console = System.console();

if(console != null) {

formattedTable.line(console);

formattedTable.printHeader(console);

formattedTable.line(console);

formattedTable.printRow(console, "Demando", 100, 122);

formattedTable.printRow(console, "Mushi", 80, 100);

formattedTable.printRow(console, "Peale", 150, 180);

formattedTable.line(console);

}

}

}

该程序产生以下输出:

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

Player                   Matches         Goals      Goals per match

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

Demando                    100           122             1.2

Mushi                       80           100             1.3

Peale                      150           180             1.2

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

让我们分析一下在printRow()方法- "%-15s \t %5d \t\t %d \t\t %.1f \n"中指定的格式字符串

  • 格式字符串的第一部分是"%-15s"。这里,表达式以%开始,这表示格式说明符字符串的开始。
  • 下一个符号是'-',用来使字符串左对齐。数字"15"指定字符串的宽度,最后"s"的数据类型说明符指示输入数据类型为String
  • 下一个格式说明符字符串是"%5d",表示它需要一个最少显示 5 位数的整数。
  • 最后一个格式说明符字符串是"%.1f",它需要一个浮点数,显示一个精确数字。
  • 所有格式说明符字符串都用一个或多个"\t"s(制表位)分隔,以便在列之间留出空间。

现在让我们讨论一下printf()方法中格式说明符的模板:

%[argument_index][flags][width][.precision]datatype_specifier
  • 正如您所看到的,每个格式说明符都以%符号开始,后跟参数索引、标志、宽度和精度信息,并以数据类型说明符结束。在这个字符串中,参数索引、标志、宽度和精度信息是可选的,而%符号和数据类型说明符是必需的。

  • 参数索引是指参数在参数列表中的位置;它是一个后跟的整数,如1的整数,如 1和 2$分别代表第一个和第二个参数。

  • 标志是指定对齐和填充字符等特征的单字符符号。例如,标志"-"指定左对齐,而"0"用前导零填充数字。

  • 宽度说明符指示最终格式化字符串中将跨越的最小字符数。如果输入数据短于指定的宽度,则默认情况下会用空格填充。如果输入数据大于指定的宽度,则完整的数据会显示在输出中,而不会被修剪。

  • 精度字段指定输出中的精度位数。这个可选字段对于浮点数特别有用。

  • Finally, the data type specifier indicates the type of expected input data. The field is a placeholder for the specified input data. Table 9-2 provides a list of commonly used data type specifiers.

    表 9-2。

    Commonly Used Data Type Specifiers

    标志描述
    %b布尔代数学体系的
    %c性格;角色;字母
    %d十进制整数(带符号)
    %e科学格式的浮点数
    %f十进制格式的浮点数
    %g十进制或科学格式的浮点数(取决于作为参数传递的值)
    %h传递的参数的 Hashcode
    %n行分隔符(换行符)
    %o格式化为八进制值的整数
    %s线
    %t日期/时间
    %x格式化为十六进制值的整数

注意,关于printf()的讨论适用于Console类中的format()方法。实际上,printf只是在内部调用了format方法:

// code from java.io.Console.java

public Console printf(String format, Object … args) {

return format(format, args);

}

要记住的要点

以下几点可能对你的 OCPJP 八级考试有用:

  • 如果没有指定任何字符串格式说明符,printf()方法将不会打印给定参数中的任何内容!
  • 只有在使用格式说明符字符串指定宽度时,"-"0"这样的标志才有意义。
  • 也可以打印格式字符串中的%字符;然而,你需要为它使用一个转义序列。在格式说明符字符串中,%是一个转义字符,这意味着您需要使用%%来打印单个%
  • 您可以使用参数索引功能(一个整数值后跟一个符号)通过索引位置显式引用参数。例如,以下打印“worldhello”,因为参数的顺序颠倒了:console.printf("符号)通过索引位置显式引用参数。例如,以下打印“world hello”,因为参数的顺序颠倒了:`console.printf("%2s %1ss %n", "hello", "world");` `// 2 refers to the second argument ("world") and // $1 refers to the first argument ("hello")`
  • 格式字符串中的 console 是有效的Console对象,下面的代码段打印“10 a 12”:console.printf("%d %<x %<o", 10);``// 10 – the decimal value, a – the hexadecimal value of 10, and``// 12 – the octal value of 10
  • 如果您没有提供格式字符串所期望的输入数据类型,那么您可以得到一个IllegalFormatConversionException。例如,如果您在您的printRow()方法实现中提供一个字符串而不是一个期望的整数,您将得到下面的异常:Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.String

使用控制台类获取输入

您可以使用Console类中提供的重载方法readPassword()readLine()从控制台获取输入。在这些方法中,第一个参数是格式说明符字符串,后面的参数是将传递给格式说明符字符串的值。这两个方法返回从控制台读取的字符数据。readLine()readPassword()方法有什么区别?主要区别在于readPassword()不在控制台中显示输入的字符串(明显的原因是不显示密码),而readLine()显示您在控制台中输入的内容。另一个微小的区别是readLine()方法返回一个String,而readPassword()返回一个char数组(参见清单 9-4 )。

Listing 9-4. Login.java

import java.io.Console;

import java.util.Arrays;

// code to illustrate the use of readPassword method

class Login {

public static void main(String []args) {

Console console = System.console();

if(console != null) {

String userName = null;

char[] password = null;

userName = console.readLine("Enter your username: ");

// typed characters for password will not be displayed in the screen

password = console.readPassword("Enter password: ");

// password is a char[]: convert it to a String first

// before comparing contents

if(userName.equals("scrat") && new String(password).equals("nuts")) {

// we're hardcoding username and password here for

// illustration, don't do such hardcoding in pratice!

console.printf("login successful!");

}

else {

console.printf("wrong user name or password");

}

// "empty" the password since its use is over

Arrays.fill(password, ' ');

}

}

}

下面是运行该程序的一个实例,键入正确的用户名和密码:

D:\>java Login

Enter your username: scrat

Enter password:

login successful!

请注意,键入密码时,控制台中没有显示任何内容。为什么这个程序中提供了语句Arrays.fill(password, ' ');?建议在读取密码字符串使用完毕后将其“清空”;这里你使用Arrayfill()方法来达到这个目的。这是一种安全的编程实践,可以避免恶意读取程序数据来发现密码字符串。事实上,与返回一个StringreadLine()方法不同,readPassword()方法返回一个 char 数组。使用 char 数组,一旦验证了密码,就可以清空它并从内存中删除密码文本的痕迹;对于垃圾回收的String对象,这不像使用 char 数组那么简单。

使用流读写文件

认证目标
在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter

什么是流?流是有序的数据序列。Java 根据流来处理输入和输出。例如,当你从一个二进制文件中读取一个字节序列时,你是从一个输入流中读取;类似地,当您将一个字节序列写入一个二进制文件时,您正在写入一个输出流。请注意我们是如何从二进制文件中读取或写入字节的,但是从文本文件中读取或写入字符呢?与其他语言和操作系统类似,Java 在处理文本和二进制数据方面有所不同。在深入研究流和从文件中读取或写入数据之前,您必须首先理解字符流和字节流之间的区别,这对于理解本章的其余部分是必不可少的。

Java 8 中引入的 streams API(在第六章中介绍)不同于我们在本章中讨论的 I/O 流。

字符流和字节流

考虑一下 Java 源文件和编译器生成的类文件之间的区别。Java 源文件的扩展名为“”。java ”,旨在供人类以及编译器等编程工具阅读。但是,Java 类文件的扩展名为“”。类”并且不打算被人类阅读;它们应该由低级工具处理,比如 JVM(Windows 中的可执行 java.exe)和 Java 反汇编程序(Windows 中的可执行 javap.exe)。

A text file is a human-readable file containing text (or characters); Binary files are machine-readable or low-level data storage.

自然,你如何解释文本文件和二进制文件是不同的。例如,在文本文件中,您可以解释从文件中读取的数据,并区分制表符、空白字符、换行符等。然而,你不能像那样处理来自二进制文件的数据;都是低级的价值观。再举一个例子,考虑一个用文本编辑器(如 Windows 中的记事本)创建的. txt 文件;它包含人类可读的文本。现在,考虑把你的照片存储在. bmp 或者。jpeg 文件;这些文件肯定不是人类可读的。它们旨在通过照片编辑或图像处理软件进行处理,文件包含一些预先确定的低级格式的数据。

java.io包有支持字符流和字节流的类。您可以将字符流用于基于文本的 I/O。字节流用于基于数据的 I/O。用于读取和写入的字符流分别称为读取器和写入器(由抽象类 Reader 和 Writer 表示)。用于读写的字节流分别称为输入流和输出流(由 InputStream 和 OutputStream 的抽象类表示)。表 9-3 总结了字符流和字节流的区别,供你快速参考。

表 9-3。

Differences Between Character Streams and Byte Streams

字符流字节流
用于读写基于字符或文本的 I/O,如文本文件、文本文档、XML 和 HTML 文件。用于读取或写入二进制数据 I/O,如可执行文件、映像文件和低级文件格式的文件,如.zip.class.obj.exe
处理的数据是 16 位 Unicode 字符。处理的数据是字节(即 8 位数据单位)。
输入和输出字符流分别称为读取器和写入器。输入和输出字节流分别简称为输入流和输出流。
java.io包中的ReaderWriter的抽象类及其派生类提供了对字符流的支持。InputStreamOutputStream的抽象类以及它们在java.io包中的派生类提供了对字节流的支持。

如果你试图在需要字符流的时候使用字节流,反之亦然,你会在你的程序中得到一个讨厌的惊喜。例如,位图(.bmp)图像文件必须使用字节流进行处理;如果你尝试使用字符流,你的程序将无法工作。所以不要混淆溪流!

字符流

在本节中,您将探索字符流的 I/O。您将学习如何读写文本文件,以及一些可选特性,如缓冲以加速 I/O。对于读写文本文件,您可以分别使用从ReaderWriter抽象类派生的类。对于角色流,图 9-1 显示了重要的Reader类,表 9-4 提供了这些类的简短描述。图 9-2 显示了重要的Writer类,表 9-5 提供了这些类的简短描述。请注意,在这一章中,我们将只涉及这个类层次中的几个重要的类。

表 9-5。

Important Classes Deriving from the Writer Class

类别名简短描述
StringWriter在字符串缓冲区中收集输出的字符流,可用于创建string
OutputStreamWriter这个类是字符流和字节流之间的桥梁。
FileWriter为编写字符文件提供支持的OutputStreamWriter的派生类。
PipedWriterPipedReaderPipedWriter类形成一对,用于“管道化”字符流中字符的读/写。
FilterWriter流的抽象基类,支持将数据作为字符写入字符流时对数据应用的过滤操作。
PrintWriter支持将字符格式化打印到输出字符流。
BufferedWriter向基础字符流添加缓冲,这样就不需要为每个读写操作访问基础文件系统。

A978-1-4842-1836-5_9_Fig2_HTML.jpg

图 9-2。

Important classes deriving from the Writer class

表 9-4。

Important Classes Deriving from the Reader Class

类别名简短描述
StringReaderstring上操作的字符流。
InputStreamReader这个类是字符流和字节流之间的桥梁。
FileReader为读取字符文件提供支持的InputStreamReader的派生类。
PipedReaderPipedReaderPipedWriter类形成一对“管道化”的字符读/写。
FilterReader流的抽象基类,支持在从流中读取字符时对数据应用过滤操作。
PushbackReaderFilterReader的派生类,允许将读取的字符推回到流中。
BufferedReader向基础字符流添加缓冲,这样就不需要为每个读写操作访问基础文件系统。
LineNumberReader从底层字符流中读取字符时跟踪行号的BufferedReader的派生类。

A978-1-4842-1836-5_9_Fig1_HTML.jpg

图 9-1。

Important classes deriving from the Reader class

读取文本文件

类读取流中的内容,并尝试将它们解释为字符,如制表符、文件尾和换行符。清单 9-5 实现了 Windows 中type命令的简化版本(Linux/Unix/Mac 中类似的命令是cat命令)。type命令显示作为命令行参数传递的文件内容。

Listing 9-5. Type.java

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.IOException;

// implements a simplified version of "type" command provided in Windows given

// a text file name(s) as argument, it prints the content of the text file(s) on console

class Type {

public static void main(String []files) {

if(files.length == 0) {

System.err.println("pass the name of the file(s) as argument");

System.exit(-1);

}

// process each file passed as argument

for(String file : files) {

// try opening the file with FileReader

try (FileReader inputFile = new FileReader(file)) {

int ch = 0;

// while there are characters to fetch, read, and print the

// characters when EOF is reached, read() will return -1,

// terminating the loop

while( (ch = inputFile.read()) != -1) {

// ch is of type int - convert it back to char

// before printing

System.out.print( (char)ch );

}

} catch (FileNotFoundException fnfe) {

// the passed file is not found …

System.err.printf("Cannot open the given file %s ", file);

}

catch(IOException ioe) {

// some IO error occurred when reading the file …

System.err.printf("Error when processing file %s… skipping it", file);

}

// try-with-resources will automatically release FileReader object

}

}

}

对于一个示例文本文件,下面是 Windows 中的type命令和我们的Type程序的输出:

D:\> type SaturnMoons.txt

Saturn has numerous icy moons in its rings. Few large moons of Saturn are - Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, and Hyperion.

D:\> java Type SaturnMoons.txt

Saturn has numerous icy moons in its rings. Few large moons of Saturn are - Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, and Hyperion.

它像预期的那样工作。在这个程序中,您正在实例化FileReader类,并传递要打开的文件的名称。如果没有找到文件,FileReader 构造函数将抛出一个FileNotFoundException

文件打开后,使用read()方法获取底层文件中的字符。你在一个字符一个字符地阅读。或者,可以使用readLine()等方法逐行读取。

注意,read()方法返回一个 int 而不是一个char——这是因为当read()到达文件尾(EOF)时,它返回-1,这在 char 的范围之外。因此,read()方法返回一个 int 来表示已经到达了文件的末尾,您应该停止尝试从底层流中读取更多的字符。

在这个程序中,你只读取一个文本文件;现在,您将尝试读取和写入一个文本文件。

读取和写入文本文件

在前面读取文本文件的例子(清单 9-5 )中,您创建了如下的字符流:

FileReader inputFile = new FileReader(file);

这使用非缓冲 I/O,与缓冲 I/O 相比效率较低。换句话说,直接传递读取字符,而不是使用临时(内部)缓冲区,这将加速 I/O。要以编程方式使用缓冲 I/O,您可以将FileReader引用传递给BufferedReader对象,如下所示:

BufferedReader inputFile = new BufferedReader(new FileReader(file));

同理,也可以使用BufferedWriter进行缓冲输出。(在字节流的情况下,你可以使用BufferedInputStreamBufferedOutputStream,我们将在本章后面讨论)。

现在,您将使用缓冲 I/O 来读取和写入文本文件。清单 9-6 包含了 Windows 中copy命令的简化版本。

Listing 9-6. Copy.java

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.FileWriter;

import java.io.IOException;

// implements a simplified version of "copy" command provided in Windows

// syntax: java Copy SrcFile DstFile

// copies ScrFile to DstFile; over-writes the DstFile if it already exits

class Copy {

public static void main(String []files) {

if(files.length != 2) {

System.err.println("Incorrect syntax. Correct syntax: Copy SrcFile DstFile");

System.exit(-1);

}

String srcFile = files[0];

String dstFile = files[1];

// try opening the source and destination file

// with FileReader and FileWriter

try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));

BufferedWriter outputFile = new BufferedWriter(new FileWriter(dstFile))) {

int ch = 0;

// while there are characters to fetch, read the characters from

// source stream and write them to the destination stream

while( (ch = inputFile.read()) != -1) {

// ch is of type int - convert it back to char before

// writing it

outputFile.write( (char)ch );

}

// no need to call flush explicitly for outputFile - the close()

// method will first call flush before closing the outputFile stream

} catch (FileNotFoundException fnfe) {

// the passed file is not found …

System.err.println("Cannot open the file " + fnfe.getMessage());

}

catch(IOException ioe) {

// some IO error occurred when reading the file …

System.err.printf("Error when processing file; exiting … ");

}

// try-with-resources will automatically release FileReader object

}

}

我们先检查一下这个程序是否有效。将这个 Java 源程序本身(Copy.java)复制到另一个文件(DuplicateCopy.java)。您可以使用 Windows 中提供的fc(文件比较)命令(或 Linux/Unix/Mac 中的diff命令)来确保原始文件和复制文件的内容是相同的,以确保程序正确运行。

D:\> java Copy Copy.java DuplicateCopy.java

D:\> fc Copy.java DuplicateCopyjava

Comparing files Copy.java and DuplicateCopy.java

FC: no differences encountered

是的,它工作正常。如果给它一个不存在的源文件名呢?

D:\> java Copy Cpy.java DuplicateCopyjava

Cannot open the file Cpy.java (The system cannot find the file specified)

您键入了Cpy.java而不是Copy.java,程序终止,并显示一条可读的错误消息,这是意料之中的。

这个程序是这样工作的。在 try-with-resources 语句中,您打开了srcFile进行读取,打开了dstFile进行写入。您想使用缓冲的 I/O,所以您将FileReaderFileWriter分别传递给了BufferedReaderBufferedWriter

try (BufferedReader inputFile = new BufferedReader(new FileReader(srcFile));

BufferedWriter outputFile = new BufferedWriter(new FileWriter(dstFile)))

您正在使用 try-with-resources 语句(在第七章的中讨论过),在关闭流之前,BufferedWriterclose()方法将首先调用flush()方法。

A978-1-4842-1836-5_9_Figaa_HTML.gif当你在程序中使用缓冲的 I/O 时,在你想确保所有未决的字符或数据都被刷新(即写入底层文件)的地方显式调用flush()方法是个好主意。

“标记化”文本

在最后两个例子中(清单 9-5 和 9-6 ,您只是读取或写入文本文件。但是,在现实世界的程序中,您可能希望在读取或写入文件时执行一些处理。例如,您可能希望寻找某些模式,搜索某些特定的字符串,用一个字符序列替换另一个字符序列,过滤掉特定的单词,或者以某种方式格式化输出。为此,您可以使用现有的 API,比如正则表达式和Scanner

举例来说,假设您想要列出给定文本文件中的所有单词,并删除所有不必要的空格、标点符号等。此外,您需要按字母顺序打印结果单词。要解决这个问题,您可以使用一个Scanner并传递您想要匹配或定界的正则表达式(参见清单 9-7 )。

Listing 9-7. Tokenize.java

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.util.Scanner;

import java.util.Set;

import java.util.TreeSet;

// read the input file and convert it into "tokens" of words;

// convert the words to same case (lower case), remove duplicates, and print the words

class Tokenize {

public static void main(String []args) {

// read the input file

if(args.length != 1) {

System.err.println("pass the name of the file to be read as an argument");

System.exit(-1);

}

String fileName = args[0];

// use a TreeSet<String> which will automatically sort the words

// in alphabetical order

Set<String> words = new TreeSet<>();

try ( Scanner tokenizingScanner = new Scanner(new FileReader(fileName)) ) {

// set the delimiter for text as non-words (special characters,

// white-spaces, etc), meaning that all words other than punctuation

// characters, and white-spaces will be returned

tokenizingScanner.useDelimiter("\\W");

while(tokenizingScanner.hasNext()) {

String word = tokenizingScanner.next();

if(!word.equals("")) { // process only non-empty strings

// convert to lowercase and then add to the set

words.add(word.toLowerCase());

}

}

// now words are in alphabetical order without duplicates,

// print the words separating them with tabs

for(String word : words) {

System.out.print(word + '\t');

}

} catch (FileNotFoundException fnfe) {

System.err.println("Cannot read the input file - pass a valid file name");

}

}

}

让我们看看它是否有效:

D:\> type limerick.txt

There was a young lady of Niger

Who smiled as she rode on a tiger.

They returned from the ride

With the lady inside

And a smile on the face of the tiger.

D:\> java Tokenize limerick.txt

a      and    as      face    from    inside  lady   niger   of     on     returned  ride

rode   she    smile   smiled  the     there   they   tiger   was    who    with      young

是的,它确实工作正常。现在让我们看看这个程序做什么。程序首先使用一个FileReader打开文件,并将其传递给Scanner对象。程序用useDelimiter("\\W");Scanner设置分隔符,非单词的“\W”匹配,所以任何非单词字符都将成为分隔符。(注意,您正在设置分隔符,而不是您想要匹配的模式)。该程序利用一个TreeSet<String>来存储读取的字符串。程序从底层流中读取单词,检查它是否为非空字符串,并将字符串的小写版本添加到TreeSet。由于数据结构是一个TreeSet,所以它删除了重复项(记住一个TreeSet是-a Set,它不允许重复)。此外,它还是一个有序的数据结构,这意味着它维护了插入值的“排序”,在本例中是按字母顺序排列的Strings。因此,程序正确地打印出包含一首打油诗的给定文本文件中的单词。

字节流

在本节中,您将探索字节流的 I/O。您将首先学习如何读写数据文件,以及如何对对象进行流式处理,将它们存储在文件中,然后读回它们。OutputStream的类及其派生类如图 9-3 所示;InputStream及其派生类如图 9-4 所示。

A978-1-4842-1836-5_9_Fig4_HTML.jpg

图 9-4。

Important classes deriving from the InputStream abstract class

A978-1-4842-1836-5_9_Fig3_HTML.jpg

图 9-3。

Important classes deriving from the OutputStream abstract class

表 9-6 总结了InputStreamOutputStream的重要类别。

表 9-6。

Important Classes Deriving from the InputStream and OutputStream Classes

类别名简短描述
PipedInputStream, PipedOutputStreamPipedInputStreamPipedOutputStream创建可发送和接收数据的通信通道。PipedOutputStream发送数据,PipedInputStream接收信道上发送的数据。
FileInputStream, FileOutputStreamFileInputStream从文件中接收字节流,FileOutputStream将字节流写入文件。
FilterInputStream, FilterOutputStream这些过滤后的流用于向普通流添加功能。使用FilterInputStream可以过滤InputStream的输出。使用FilterOutputStream可以过滤OutputStream的输出。
BufferedInputStream, BufferedOutputStream向输入流添加缓冲能力。BufferedOutputStream给输出流增加缓冲能力。
PushbackInputStream作为FilterInputStream的子类,它为输入流增加了“推回”功能。
DataInputStream, DataOutputStreamDataInputStream可用于从输入流中读取 java 原始数据类型。DataOutputStream可用于将 Java 原始数据类型写入输出流。
读取字节流

字节流用于处理不包含人类可读文本的文件。例如,一个 Java 源文件有人类可读的内容,但是一个".class"文件没有。一个".class"文件意味着由 JVM 处理,因此您必须使用字节流来处理".class"文件。

一个".class"文件的内容以特定的文件格式编写,在 Java 虚拟机(JVM)的规范中有描述。不用担心;您不会理解这种复杂的文件格式,但您只需检查它的“幻数”每种文件格式都有一个神奇的数字,用于快速检查文件格式。比如”。MZ”是 Windows 中.exe文件的幻数(或者更确切地说,是幻串)。类似地,".class"文件有一个幻数"0xCAFEBABE",写为十六进制值。这些幻数通常被写成可变长度文件格式的前几个字节。

为了理解字节流是如何工作的,您只需检查给定的文件是否以幻数“0xCAFEBABE”开头(清单 9-8 )。如果是这样,它可能是一个有效的".class"文件;如果不是,那肯定不是".class"文件。

Listing 9-8. ClassFileMagicNumberChecker.java

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.util.Arrays;

// check if the passed file is a valid .class file or not.

// note that this is an elementary version of a checker that checks if the given file

// is a valid file that is written according to the JVM specification

// it checks only the magic number

class ClassFileMagicNumberChecker {

public static void main(String []args) {

if(args.length != 1) {

System.err.println("Pass a valid file name as argument");

System.exit(-1);

}

String fileName = args[0];

// create a magicNumber byte array with values for four bytes in 0xCAFEBABE

// you need to have an explicit down cast to byte since

// the hex values like 0xCA are of type int

byte []magicNumber = {(byte) 0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE};

try (FileInputStream fis = new FileInputStream(fileName)) {

// magic number is of 4 bytes –

// use a temporary buffer to read first four bytes

byte[] u4buffer = new byte[4];

// read a buffer full (4 bytes here) of data from the file

if(fis.read(u4buffer) != -1) { // if read was successful

// the overloaded method equals for two byte arrays

// checks for equality of contents

if(Arrays.equals(magicNumber, u4buffer)) {

System.out.printf("The magic number for passed file %s matches that of

a .class file", fileName);

}

else {

System.out.printf("The magic number for passed file %s does not match

that of a .class file", fileName);

}

}

} catch(FileNotFoundException fnfe) {

System.err.println("file does not exist with the given file name ");

} catch(IOException ioe) {

System.err.println("an I/O error occurred while processing the file");

}

}

}

我们先来看看通过传递源码(。java)文件和".class"文件为同一程序。

D:> java ClassFileMagicNumberChecker ClassFileMagicNumberChecker.java

The magic number for passed file ClassFileMagicNumberChecker.java does not match that of a .class file

D:\> java ClassFileMagicNumberChecker ClassFileMagicNumberChecker.class

The magic number for passed file ClassFileMagicNumberChecker.class matches that of a .class file

是的,它有效。类InputStreamOutputStream构成了字节流层次结构的基础。您执行文件 I/O,因此以一个FileInputStream打开给定的文件。您需要检查前四个字节,所以在临时缓冲区中读取四个字节。您需要将该缓冲区的内容与字节序列 0xCA、0xFE、0xBA 和 0xBE 进行比较。如果这两个数组的内容不相等,那么传递的文件就不是一个".class"文件。

在这个程序中,您使用一个FileInputStream直接操纵底层字节流。如果在读取大量字节时需要加速程序,可以使用缓冲输出流,如

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));

与这些输入流类似,您可以使用输出流将一个字节序列写入数据文件。你可以使用FileOutputStreamBufferedOutputStream来实现。

看了这个程序,你不觉得读取一个四字节的数组,比较字节数组的内容很别扭(而不是直接比较一个整数的内容)吗?换句话说,0xCAFEBABE是一个整数值,您可以将这个值作为一个整数值直接读取,并与读取的整数值进行比较。为此,您需要使用数据流,它提供了像readInt()这样的方法,我们现在将讨论这些方法。

数据流

为了理解如何用字节流写或读,让我们写一个简单的程序,该程序将常量值写到数据文件中,然后从数据文件中读取常量值(参见清单 9-9 )。为了使问题简单,您将只以下列原始类型值的形式编写 0 到 9 的值:byteshortintlongfloatdouble

Listing 9-9. DataStreamExample.java

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

// A simple class to illustrate data streams; write constants 0 and 1 in different

// data type values into a file and read the results back and print them

class DataStreamExample {

public static void main(String []args) {

// write some data into a data file with hard-coded name "temp.data"

try (DataOutputStream dos =

new DataOutputStream(new FileOutputStream("temp.data"))) {

// write values 1 to 10 as byte, short, int, long, float and double

// omitting boolean type because an int value cannot

// be converted to boolean

for(int i = 0; i < 10; i++) {

dos.writeByte(i);

dos.writeShort(i);

dos.writeInt(i);

dos.writeLong(i);

dos.writeFloat(i);

dos.writeDouble(i);

}

} catch(FileNotFoundException fnfe) {

System.err.println("cannot create a file with the given file name ");

System.exit(-1); // don't proceed – exit the program

} catch(IOException ioe) {

System.err.println("an I/O error occurred while processing the file");

System.exit(-1); // don't proceed – exit the program

}

// the DataOutputStream will auto-close, so don't have to worry about it

// now, read the written data and print it to console

try (DataInputStream dis = new DataInputStream(new FileInputStream("temp.data"))) {

// the order of values to read is byte, short, int, long, float and

// double since we've written from 0 to 10,

// the for loop has to run 10 times

for(int i = 0; i < 10; i++) {

// %d is for printing byte, short, int or long

// %f, %g, or %e is for printing float or double

// %n is for printing newline

System.out.printf("%d %d %d %d %g %g %n",

dis.readByte(),

dis.readShort(),

dis.readInt(),

dis.readLong(),

dis.readFloat(),

dis.readDouble());

}

} catch(FileNotFoundException fnfe) {

System.err.println("cannot create a file with the given file name ");

} catch(IOException ioe) {

System.err.println("an I/O error occurred while processing the file");

} // the DataOutputStream will auto-close, so don't have to worry about it

}

}

首先,让我们通过执行程序来看看它是否有效。

D:> java DataStreamExample

0 0 0 0 0.000000 0.000000

1 1 1 1 1.000000 1.000000

2 2 2 2 2.000000 2.000000

3 3 3 3 3.000000 3.000000

4 4 4 4 4.000000 4.000000

5 5 5 5 5.000000 5.000000

6 6 6 6 6.000000 6.000000

7 7 7 7 7.000000 7.000000

8 8 8 8 8.000000 8.000000

9 9 9 9 9.000000 9.000000

是的,它有效。如前所述,数据文件的内容是不可读的。在本例中,您将值 0 到 9 作为各种原始类型值写入名为temp.data的临时文件 write 中。如果您尝试打开此数据文件并查看其内容,您将无法识别或理解其中包含的内容。以下是其内容的一个示例:

D:>type temp.data

☺ ☺   ☺       ☺?Ç  ?       ☻ ☻  ☻       ☻@   @

♥ ♥   ♥       ♥@@         ♦  ♦    ♦        ♦        ♣ ♣   ♣        ♣  @¶

@L         A   @α@L         A   @

A  @"

文件temp.data的类型化内容看起来像垃圾值,因为像整数值 0 或 9 这样的原始类型值是以字节存储的。然而,Windows 中的type命令(或者 Linux/Unix/Mac 中的cat命令)试图将这些字节转换成人类可读的字符,因此输出没有任何意义。只有当我们知道存储在文件中的数据的格式并根据该格式读取它时,数据才有意义。

现在让我们回到程序上来,看看它是如何工作的。程序在运行程序的当前目录中使用名为temp.data的硬编码文件写入数据文件。这个程序首先写入数据,因此它将文件作为输出流打开。第一个 try 块中的以下语句是什么意思?

DataOutputStream dos = new DataOutputStream(new FileOutputStream("temp.data"))

可以用OutputStream和它的派生类FileOutputStream直接执行二进制 I/O,但是要处理原始类型值之类的数据格式,就需要使用DataOutputStream,它充当底层FileOutputStream的包装器。所以,您在这里使用了DataOutputStream,它提供了writeBytewriteShort等方法。您可以使用这些方法将基本类型值 0 到 9 写入数据文件。请注意,您不必显式关闭流,因为您在 try-with-resources 语句中打开了DataOutputStream,因此将自动调用dos引用上的close()方法。close()方法也刷新底层流;这个close()方法也将关闭对FileOutputStream的底层引用。

一旦文件被写入,就可以用类似的方式读取数据文件。你打开一个FileInputStream,用一个DataInputStream把它包起来。您从流中读取数据,并在控制台中打印出来。您使用了格式说明符,如%d(这是一种用于打印整型值(如byteshortintlong)的通用格式说明符),以及用于打印类型为floatdouble的浮点值的%f%g%e说明符;%n用于打印换行符。

在这个程序中,您编写和读取了原始类型值。那么引用类型的对象呢,比如对象、地图等等?读写对象是通过对象流实现的,我们现在将讨论对象流。

写入和读取对象流

假设您正在创建一个在线电子商务网站。您可以选择将包含在对象中的数据(如客户和购买请求)写入 RDBMS(我们将在第十二章的中介绍 JDBC)。或者,您可以将对象直接存储在平面文件中,而不是将数据存储在 RDBMS 中:在这种情况下,您必须知道如何将对象读写到流中。类ObjectInputStreamObjectOutputStream支持读写程序中使用的 Java 对象。

清单 9-10 包含了一个简单的例子,将Map数据结构的内容写入一个文件,并读回它,以说明使用类ObjectInputStreamObjectOutputStream来读取或写入对象。你把最近三任美国总统的详细资料储存在这张地图里。

Listing 9-10. ObjectStreamExample.java

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.util.HashMap;

import java.util.Map;

// A simple class to illustrate object streams: fill a data structure, write it to a

// temporary file and read it back and print the read data structure

class ObjectStreamExample {

public static void main(String []args) {

Map<String, String> presidentsOfUS = new HashMap<>();

presidentsOfUS.put("Barack Obama", "2009 to --, Democratic Party, 56th term");

presidentsOfUS.put("George W. Bush", "2001 to 2009, Republican Party, 54th and 55th terms");

presidentsOfUS.put("Bill Clinton", "1993 to 2001, Democratic Party, 52nd and 53rd terms");

try (ObjectOutputStream oos =

new ObjectOutputStream(new FileOutputStream("object.data"))) {

oos.writeObject(presidentsOfUS);

} catch(FileNotFoundException fnfe) {

System.err.println("cannot create a file with the given file name ");

} catch(IOException ioe) {

System.err.println("an I/O error occurred while processing the file");

} // the ObjectOutputStream will auto-close, so don't have to worry about it

try (ObjectInputStream ois =

new ObjectInputStream(new FileInputStream("object.data"))) {

Object obj = ois.readObject();

// first check if obj is of type Map

if(obj != null && obj instanceof Map) {

Map<?, ?> presidents = (Map<?, ?>) obj;

System.out.println("President name \t Description");

for(Map.Entry<?, ?> president : presidents.entrySet()) {

System.out.printf("%s \t %s %n", president.getKey(),

president.getValue());

}

}

} catch(FileNotFoundException fnfe) {

System.err.println("cannot create a file with the given file name ");

} catch(IOException ioe) {

System.err.println("an I/O error occurred while processing the file");

} catch(ClassNotFoundException cnfe) {

System.err.println("cannot recognize the class of the object - is the file

corrupted?");

}

}

}

在讨论程序如何工作之前,我们先检查一下它是否工作。

D:\> java ObjectStreamExample

President name   Description

Barack Obama     2009 to --, Democratic Party, 56th term

Bill Clinton     1993 to 2001, Democratic Party, 52nd and 53rd terms

George W. Bush   2001 to 2009, Republican Party, 54th and 55th terms

序列化过程使用内容描述(称为元数据)转换内存中对象的内容。当对象引用其他对象时,序列化机制也会将它们作为序列化字节的一部分。如果您尝试打开保存对象的文件,您将无法读取这些先序列化后保存的对象。例如,如果你试图读取object.data文件,你会看到许多不可读的字符。

现在,让我们回到程序,看看它是如何工作的。在这个程序中,你用最近三任美国总统的详细信息填充HashMap容器。然后,按如下方式打开输出流:

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.data"))

FileOutputStream在当前目录下打开一个名为object.data的临时文件。ObjectOutputStream是这个底层FileOutputStream的包装器。在这个 try-with-resources 块中,只有一条语句oos.writeObject(presidentsOfUS),它将对象写入object.data文件。

读取对象比编写对象需要更多的工作。ObjectInputStream中的readObject()方法返回一个Object类型。你需要把它转换回Map<String, String>。在将其向下转换为这个特定类型之前,您要检查obj是否属于类型Map。注意,您不必检查它是否是Map<String, String>,因为这些泛型类型在称为type erasure的过程中丢失了。因此,我们对泛型类型参数使用通配符,如:Map<?, ?> presidents = (Map<?, ?>) obj。一旦向下转换成功,就可以读取该对象中内容的值。参见第四章泛型和集合中关于类型擦除和通配符的讨论。

要记住的要点

以下是值得注意的几点,可能对你参加 OCPJP 八级考试有所帮助:

  • 当你使用缓冲流时,你应该在完成数据传输后调用flush()。内部缓冲区可能保存了一些数据,一旦您调用flush(),这些数据将被清除并发送到目的地。但是,流上的方法close()会自动调用flush()
  • 您可能已经注意到您可以组合流对象。您可以创建一个接受FileInputStream对象的BufferedInputStream对象。这样,一个流的输出被链接到过滤的流。这是以期望的方式定制流的重要、有用和漂亮的方式。
  • 如果想自定义序列化的流程,可以实现readObject()writeObject()。请注意,这两个方法都是私有方法,这意味着您没有重写或重载这些方法。JVM 检查这些方法的实现,并调用它们而不是通常的方法。这听起来很奇怪,但这就是在 JVM 中实现序列化过程定制的方式。

摘要

让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。

从控制台读取和写入数据

  • java.lang.System类中的公共静态字段inouterr分别代表标准输入、输出和错误流。System.injava.io.InputStreamSystem.outSystem.errjava.io.PrintStream型。
  • 您可以通过调用方法System.setInSystem.setOutSystem.setError来重定向标准流。
  • 您可以使用System.console()方法获得对控制台的引用;如果 JVM 不与任何控制台相关联,此方法将失败并返回 null。
  • Console-支持格式化 I/O 中提供了许多方法。您可以使用Console类中的printf()format()方法来打印格式化文本;重载的readLine()readPassword()方法将格式字符串作为参数。
  • 格式说明符的模板是:%[flags][width][.precision]datatype_specifier每个格式说明符以%符号开始,后面是标志、宽度和精度信息,以数据类型说明符结束。在格式字符串中,标志、宽度和精度信息是可选的,但%符号和数据类型说明符是必需的。
  • 使用readPassword()方法读取安全字符串,如密码。建议使用Arrayfill()方法将读入字符数组的密码“清空”(避免恶意访问键入的密码)。

在 java.io 包中使用 BufferedReader、BufferedWriter、File、FileReader、FileWriter、FileInputStream、FileOutputStream、ObjectOutputStream、ObjectInputStream 和 PrintWriter

  • java.io 包中有支持字符流和字节流的类。
  • 您可以将字符流用于基于文本的 I/O。字节流用于基于数据的 I/O。
  • 用于读取和写入的字符流分别称为读取器和写入器(由抽象类 Reader 和 Writer 表示)。
  • 用于读写的字节流分别称为输入流和输出流(由 InputStream 和 OutputStream 的抽象类表示)。
  • 您应该只使用字符流来处理文本文件(或人类可读的文件),使用字节流来处理数据文件。如果你尝试使用一种类型的流而不是另一种,你的程序将不会像你预期的那样工作;即使它偶然工作,你也会得到讨厌的错误。所以不要混淆不同的流,为手头的特定任务使用正确的流。
  • 对于字节流和字符流,都可以使用缓冲。缓冲区类是作为底层流的包装类提供的。在执行批量 I/O 操作时,使用缓冲可以加快 I/O 的速度。
  • 对于处理具有原始数据类型和字符串的数据,可以使用数据流。
  • 您可以使用对象流(ObjectInputStream 和 ObjectOutputStream 类)从内存中读取对象并将其写入文件,反之亦然。

Question TimeConsider the following code segment: OutputStream os = new FileOutputStream("log.txt"); System.setErr(new PrintStream(os)); // SET SYSTEM.ERR System.err.println("Error"); Which one of the following statements is true regarding this code segment? The line with comment SET SYSTEM.ERR will not compile and will result in a compiler error.   The line with comment SET SYSTEM.ERR will result in throwing a runtime exception since System.err cannot be programmatically redirected.   The program will print the text “Error” in console since System.err by default sends the output to console.   This code segment redirects the System.err to the log.txt file and will write the text “Error” to that file.     Which one of the following options correctly reads a line of string from the console? BufferedReader br = new BufferedReader(System.in); String str = br.readLine();   BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String str = br.readLine();   InputStreamReader isr = new InputStreamReader (new BufferedReader(System.in)); String str = isr.readLine();   String str = System.in.readLine(); String str; System.in.scanf(str);     Consider the following code snippet: console.printf("%d %1$x %1$o", 16); Assuming that console is a valid Console object, what will it print? This program crashes after throwing an IllegalFormatException   This program crashes after throwing ImproperFormatStringException   This program prints: 16 16 16   This program prints: 16 10 20     There are two kinds of streams in the java.io package: character streams (i.e., those deriving from Reader and Writer interfaces) and byte streams (i.e., those deriving from InputStream and OutputStream). Which of the following statements is true regarding the differences between these two kinds of streams? In character streams, data is handled in terms of bytes; in byte streams, data is handled in terms of Unicode characters.   Character streams are suitable for reading or writing to files such as executable files, image files, and files in low-level file formats such as .zip, .class, and .jag.   Byte streams are suitable for reading or writing to text-based I/O such as documents and text, XML, and HTML files.   Byte streams are meant for handling binary data that is not human-readable; character streams are meant for human-readable characters.     Consider the following code snippet: USPresident usPresident = new USPresident("Barack Obama", "2009 to --", 56); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("USPresident.data"))) {      oos.writeObject(usPresident);      usPresident.setTerm(57);      oos.writeObject(usPresident); } If you deserialize the object and print the field term (term is declared as int and is not a transient), what will it print? 56   57   null   Compiler error   Runtime exception     Consider the following code segment: FileInputStream findings = new FileInputStream("log.txt"); DataInputStream dataStream = new DataInputStream(findings); BufferedReader br = new BufferedReader(new InputStreamReader(dataStream)); String line; while ((line = br.readLine()) != null) {         System.out.println(line); } br.close(); Which TWO options are true regarding this code segment? br.close() statement will close only the BufferedReader object, and findings and dataStream will remain unclosed.   The br.close() statement will close the BufferedReader object and the underlying stream objects referred by findings and dataStream.   The readLine() method invoked in the statement br.readLine() can throw an IOException; if this exception is thrown, br.close() will not be called, resulting in a resource leak.   The readLine() method invoked in the statement br.readLine() can throw an IOException; however, there will not be any resource leaks since Garbage Collector collects all resources.   In this code segment, no exceptions can be thrown calling br.close(), so there is no possibility of resource leaks.    

答案:

D. This code segment redirects the System.err to the log.txt file and will write the text “Error” to that file. Note that you can redirect the System.err programmatically using the setErr() method. System.err is of type PrintStream, and the System.setErr() method takes a PrintStream as an argument. Once the error stream is set, all writes to System.err will be redirected to it. Hence, this program will create log.txt with the text “Error” in it.   B. BufferedReader br =         new BufferedReader(new InputStreamReader(System.in)); String str = br.readLine(); This is the right way to read a line of a string from the console where you pass a System.in reference to InputStreamReader and pass the returning reference to BufferedReader. From the BufferedReader reference, you can call the readLine() method to read the string from the console.   D. This program prints: 16 10 20 In the format specifier, “1referstofirstargument,whichis16inthisprintfstatement.Hence” refers to first argument, which is 16 in this printf statement. Hence “%1x” prints the hexadecimal value of 16, which is 10. Further, “%1$o” prints the octal value of 16, which is 20. Hence the output “16 10 20” from this program.   D. Byte streams are meant for handling binary data that is not human readable; character streams are for human-readable characters. In character streams, data is handled in terms of Unicode characters, whereas in byte streams, data is handled in terms of bytes. Byte streams are suitable for reading or writing to files such as executable files, image files, and files in low-level file formats such as .zip, .class, and .jar. Character streams are suitable for reading or writing to text-based I/O such as documents and text, XML, and HTML files.   A. 56 Yes, it will print 56 even though you changed the term using its setter to 57 and serialized again. At the time of serialization, JVM checks for the duplicate object; if an object is already serialized then JVM do not serialize the object again; instead, JVM stores a reference to the serialized object.   Options B and C. The br.close() statement will close the BufferedReader object and the underlying stream objects referred to by findings and dataStream. The readLine() method invoked in the statement br.readLine() can throw an IOException; if this exception is thrown, br.close() will not be called, resulting in a resource leak. Note that Garbage Collector will only collect unreferenced memory resources; it is the programmer’s responsibility to ensure that all other resources such as stream objects are released.

十、Java 文件 NIO.2

认证目标
使用路径接口操作文件和目录路径
使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据
将流 API 与 NIO.2 一起使用

Java 提供了一组丰富的 API,可以用来操作文件和目录。Java 7 引入了一组称为 NIO.2 的 I/O API,提供了执行与文件系统相关的操作的便捷方式。在 Java 8 中,你可以在 NIO.2 中使用 Stream API(在第六章的中讨论过)

前一章介绍了 I/O 基础知识;您学习了如何从控制台读取和写入,以及如何使用流读取和写入文件。在本章中,您将学习如何使用Path界面对文件和目录路径进行操作。您还将学习使用Files类执行各种文件操作,比如创建、移动、复制和删除。最后,您将看到如何在 NIO.2 中使用流 API。本章使用了java.util.function包中的函数式接口和java.util.stream包中的流 API,我们假设您在阅读本章之前已经阅读了第三章、第四章、第五章和第六章。

我们给出了文件和目录路径,假设您使用的是 Windows 机器。如果您在 Linux、Mac OS 或任何其他平台上,您可能需要对路径名进行一些小的更改,以便程序能够在您的机器上运行。

使用路径接口

| 认证目标 | 使用路径接口操作文件和目录路径 |

文件系统通常形成一棵树。文件系统从包含文件和目录(目录在 Windows 中也称为文件夹)的根目录开始。每个目录又可以有子目录或保存文件。要定位一个文件,您只需要把从根目录到包含该文件的直接目录的目录放在一起,加上一个文件分隔符,后跟文件名。例如,如果myfile.txt文件驻留在mydocs目录中,而后者驻留在根目录C:\中,那么文件的路径就是C:\mydocs\myfile.txt。每个文件都有一个唯一的路径来定位它(除了符号链接)。

路径可以是从根元素开始的绝对路径(比如C:\mydocs\myfile.txt)。另一方面,路径可以被指定为相对路径。当你试图编译一个 Java 程序时,你会写出类似于javac programFileName.java;此示例指定了相对于当前选定目录的 Java 源文件路径,因此这是一个相对路径。您需要一个引用路径(比如本例中的当前目录路径)来解释相对路径。

在继续之前,我们先来谈谈符号链接。符号链接就像指向实际文件的指针或引用。一般来说,符号链接对应用是透明的,这意味着操作是直接在文件上执行的,而不是在链接上执行的(当然,特定于符号链接的操作除外)。

接口是路径的编程抽象。path 对象包含构成由Path对象表示的文件/目录的完整路径的目录和文件的名称;Path抽象提供了提取路径元素、操作它们和添加它们的方法。稍后您会看到,几乎所有访问文件/目录以获取有关它们的信息或操纵它们的方法都使用了Path对象。表 10-1 总结了该接口中的重要方法。

表 10-1。

Important Methods in the Path Interface

方法描述
Path getRoot()返回一个代表给定路径根的Path对象,如果路径没有根,则返回 null。
Path getFileName()返回给定路径的文件名或目录名。请注意,文件/目录名是给定路径中的最后一个元素或名称。
Path getParent()返回代表给定路径的父对象的Path对象,如果该路径不存在父组件,则返回 null。
int getNameCount()返回给定路径中文件/目录名的数量;如果给定路径代表根,则返回 0。
Path getName(int index)返回第 I 个文件/目录名;索引 0 从离根最近的名称开始。
Path subpath(int beginIndex, int endIndex)返回属于此Path对象的一部分的Path对象;返回的Path对象的名字从beginIndex开始,到索引endIndex - 1的元素结束。换句话说,beginIndex包含该索引中的名称,不包含endIndex中的名称。如果beginIndex是> =元素个数,或者endIndex <= beginIndex,或者endIndex >元素个数,这个方法可能会抛出IllegalArgumentException
Path normalize()删除路径中的冗余元素,如.(表示当前目录的点符号)和..(表示父目录的双点符号)。
Path resolve(Path other) Path resolve(String other)根据给定路径解析路径。例如,此方法可以将给定路径与另一个路径组合起来,并返回结果路径。
Boolean isAbsolute()如果给定路径是绝对路径,则返回 true 否则返回 false(例如,当给定路径是相对路径时)。
Path startsWith(String path) Path startsWith(Path path)如果这个Path对象以给定的path开始,则返回 true,否则返回 false。
Path toAbsolutePath()返回绝对路径。

获取路径信息

让我们创建一个Path对象,并检索与该对象相关的基本信息。清单 10-1 展示了如何创建一个Path对象并获取关于它的信息。

Listing 10-1. PathInfo1.java

import java.nio.file.Path;

import java.nio.file.Paths;

// Class to illustrate how to use Path interface and its methods

public class PathInfo1 {

public static void main(String[] args) {

// create a Path object by calling static method get() in Paths class

Path testFilePath = Paths.get("D:\\test\\testfile.txt");

// retrieve basic information about path

System.out.println("Printing file information: ");

System.out.println("\t file name: " + testFilePath.getFileName());

System.out.println("\t root of the path: " + testFilePath.getRoot());

System.out.println("\t parent of the target: " + testFilePath.getParent());

// print path elements

System.out.println("Printing elements of the path: ");

for(Path element : testFilePath) {

System.out.println("\t path element: " + element);

}

}

}

该程序打印以下内容:

Printing file information:

file name: testfile.txt

root of the path: D:\

parent of the target: D:\test

Printing elements of the path:

path element: test

path element: testfile.txt

输出是不言自明的。让我们来看看这个程序:

  • 首先,使用Paths类的get()方法创建一个Path实例。get()方法期望一个代表路径的string作为输入。这是创建一个Path对象最简单的方法。
  • 请注意,您在Paths.get("D:\\test\\testfile.txt")中使用了转义字符(\)。在路径中,如果你给了D:\test,那么\t就意味着一个制表符,当你运行程序时你会得到一个java.nio.file.InvalidPathException。确保在路径字符串中提供必要的转义字符。
  • 使用Path对象的getFilename()方法提取由这个Path对象表示的文件名。
  • 您还可以使用getRoot()来获取Path对象的根元素,使用getParent()来获取目标文件的父目录。
  • 使用一个for循环迭代路径中的元素。或者,您可以使用getNameCount()来获得路径中元素或名称的数量,使用getName(index)来逐个迭代和访问元素/名称。

让我们试试另一个例子。它探索了一个Path对象的一些有趣的方面,比如如何从一个相对路径获得一个绝对路径,以及如何规范化一个路径。在看这个例子之前,您需要首先理解它使用的方法:

  • The toUri()方法从路径中返回 URI(可以从浏览器打开的路径)。
  • The toAbsolutePath()方法从给定的相对路径中返回绝对路径。如果输入路径已经是绝对路径,则该方法返回相同的对象。
  • normalize()方法在输入路径上执行标准化。换句话说,它从Path对象中删除了不必要的符号(如...)。
  • toRealPath()是一个有趣的方法。它从输入路径对象返回一个绝对路径(如toAbsolutepath())。它也使路径正常化(如在normalize())。此外,如果链接选项选择得当,它可以解析符号链接。但是,目标文件/目录必须存在于文件系统中,这不是其他Path方法的先决条件。

清单 10-2 展示了这个例子。假设文件名Test在您的文件系统中不存在。

Listing 10-2. PathInfo2.java

import java.io.IOException;

import java.nio.file.LinkOption;

import java.nio.file.Path;

import java.nio.file.Paths;

// To illustrate important methods such as normalize(), toAbsolutePath(), and toRealPath()

class PathInfo2 {

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

// get a path object with relative path

Path testFilePath = Paths.get(".\\Test");

System.out.println("The file name is: " + testFilePath.getFileName());

System.out.println("Its URI is: " + testFilePath.toUri());

System.out.println("Its absolute path is: " + testFilePath.toAbsolutePath());

System.out.println("Its normalized path is: " + testFilePath.normalize());

// get another path object with normalized relative path

Path testPathNormalized = Paths.get(testFilePath.normalize().toString());

System.out.println("Its normalized absolute path is: " +

testPathNormalized.toAbsolutePath());

System.out.println("Its normalized real path is: " +

testFilePath.toRealPath (LinkOption.NOFOLLOW_LINKS));

}

}

在我们的机器上,该代码打印了以下内容:

The file name is: Test

Its URI is: file:///D:/OCPJP/programs/NIO2/./Test

Its absolute path is: D:\OCPJP\programs\NIO2\.\Test

Its normalized path is: Test

Its normalized absolute path is: D:\OCPJP\programs\NIO2\Test

Exception in thread "main" java.nio.file.NoSuchFileException: D:\OCPJP\programs\NIO2\Test

at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:79)

[... stack trace elided ...]

at PathInfo2.main(PathInfo2.java:16)

根据运行该程序的目录,目录路径会有所不同。这个程序使用相对路径实例化一个Path对象。方法getFileName()返回目标文件名,正如您在上一个例子中看到的。getUri()方法返回可用于浏览器的 URI,toAbsolutePath()方法返回给定相对路径的绝对路径。(注意,我们正在执行来自D:/OCPJP/programs/NIO2/文件夹的程序;因此,它成为当前的工作目录,并出现在绝对路径和 URI。)

您调用normalize()方法来删除路径中的冗余符号,因此它删除了前导点。(在很多操作系统中,.【单点符号】代表当前目录,..【双点】代表父目录。)然后使用规范化输出实例化另一个Path对象,并再次打印绝对路径。最后,您尝试调用toRealpath();,但是,您得到一个异常(NoSuchFileException)。为什么呢?因为,你还没有在当前工作目录下创建Test目录。

现在,让我们在D:/OCPJP/programs/NIO2/目录中创建一个Test目录,并再次运行这个例子。我们得到了以下输出:

The file name is: Test

Its URI is: file:///D:/OCPJP/programs/NIO2/./Test/

Its absolute path is: D:\OCPJP\programs\NIO2\.\Test

Its normalized path is: Test

Its normalized absolute path is: D:\OCPJP\programs\NIO2\Test

Its normalized real path is: D:\OCPJP\programs\NIO2\Test

现在对toRealPath()的最后一次调用工作正常,并返回绝对规范化路径。

Path提供了许多其他有用的方法,包括之前在表 10-1 中列出的那些方法。例如,下面是如何使用resolve()方法:

Path dirName = Paths.get("D:\\OCPJP\\programs\\NIO2\\");

Path resolvedPath = dirName.resolve("Test");

System.out.println(resolvedPath);

这段代码显示了以下内容:

D:\OCPJP\programs\NIO2\Test

这个resolve()方法认为给定的路径是一个目录,并将传递的路径与其连接(解析),如下所示。

A978-1-4842-1836-5_10_Figa_HTML.jpgjava.io.File类中的toPath()方法返回Path对象;这个方法是在 Java 7 中添加的。同样,您可以使用Path接口中的toFile()方法来获得一个File对象。

比较两条路径

Path接口提供了两种方法来比较两个Path对象:equals()compareTo()equals()方法检查two Path对象的相等性并返回一个Boolean值,而compareTo()逐字符比较两个Path对象并返回一个整数:0,如果两个Path对象相等;如果此路径在字典上小于参数路径,则为负整数;如果该路径在字典上大于参数路径,则为正整数。清单 10-3 包含了一个演示这些方法的小程序。

Listing 10-3. PathCompare1.java

import java.nio.file.Path;

import java.nio.file.Paths;

// illustrates how to use compareTo and equals and also shows

// the difference between the two methods

class PathCompare1 {

public static void main(String[] args) {

Path path1 = Paths.get("Test");

Path path2 = Paths.get("D:\\OCPJP\\programs\\NIO2\\Test");

// comparing two paths using compareTo() method

System.out.println("(path1.compareTo(path2) == 0) is: "

+ (path1.compareTo(path2) == 0));

// comparing two paths using equals() method

System.out.println("path1.equals(path2) is: " + path1.equals(path2));

// comparing two paths using equals() method with absolute path

System.out.println("path2.equals(path1.toAbsolutePath()) is "

+ path2.equals(path1.toAbsolutePath()));

}

}

有意地,一个路径是相对路径,另一个是绝对路径。假设您执行这个程序的当前目录是D:\\OCPJP\\programs\\NIO2\\Test。你能猜出程序的输出吗?

内容如下:

(path1.compareTo(path2) == 0) is: false

path1.equals(path2) is: false

path2.equals(path1.toAbsolutePath()) is true

让我们一步一步地检查这个程序:

  • 它首先使用compareTo()方法比较两个路径,逐字符比较路径并返回一个整数。在这种情况下,因为一个路径是相对路径,另一个是绝对路径,所以您首先会得到一条消息,指出这两个路径不相等。
  • 然后使用equals()比较两条路径。结果是一样的,这意味着即使两个Path对象指向同一个文件/目录,equals()也有可能返回 false。您需要确保两个路径都是绝对路径。
  • 在下一步中,您将相对路径转换为绝对路径,然后使用equals()比较它们。这一次两条路径都匹配。

即使两个Path对象指向同一个文件/目录,也不能保证equals()方法会返回 true。您需要确保两者都是绝对的和规范化的路径,以便路径的相等比较成功。

使用文件类

| 认证目标 | 使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据 |

上一节讨论了如何创建一个Path实例并从中提取有用的信息。在本节中,您将使用Path对象来操作文件/目录。Java 7 提供了一个Files类(在java.nio.file包中),可以用来对文件或目录执行各种与文件相关的操作。注意Files是一个实用类,这意味着它是一个带有私有构造函数的最终类,并且只包含静态方法。所以你可以通过调用它提供的静态方法来使用Files类,比如copy()来复制文件。这个类提供了广泛的功能。您可以创建目录、文件或符号链接。创建流,如目录流、字节通道和输入/输出流;检查文件的属性;遍历文件树;并执行文件操作,如读、写、复制和删除。表 10-2 提供了Files类中重要方法的示例。

表 10-2。

Some Important Methods in the Files Class

方法描述
Path createDirectory(Path dirPath, FileAttribute<?>... dirAttrs) Path createDirectories(Path dir, FileAttribute<?>... attrs)创建一个由dirPath给出的文件,并设置由dirAttributes给出的属性。可能会抛出一个异常,比如FileAlreadyExistsException或者UnsupportedOperationException(比如文件属性不能按照dirAttrs给的那样设置)。createDirectorycreateDirectories的区别在于createDirectories会创建由dirPath给出的中间目录,如果它们还不存在的话。
Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs)在由dir给出的目录中用给定的前缀、后缀和属性创建一个临时文件。
Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs)在由dir指定的路径中用给定的前缀和目录属性创建一个临时目录。
Path copy(Path source, Path target, CopyOption... options)将文件从源复制到目标。CopyOption可以是 REPLACE_EXISTING,COPY_ATTRIBUTES,或者 NOFOLLOW_LINKS。可以抛出FileAlreadyExistsException之类的异常。
Path move(Path source, Path target, CopyOption... options)类似于copy操作,但是源文件被删除。如果源和目标在同一个目录中,这是一个文件重命名操作。
boolean isSameFile(Path path, Path path2)检查两个Path对象是否定位同一个文件。
boolean exists(Path path, LinkOption... options)检查给定路径中是否存在文件/目录;可以指定LinkOption.NOFOLLOW_LINKS不跟随符号链接。
Boolean isRegularFile(Path path, LinkOption...)如果由path表示的文件是常规文件,则返回 true。
Boolean isSymbolicLink(Path path)如果由path表示的文件是一个符号链接,则返回 true。
Boolean isHidden(Path path)如果由path表示的文件是隐藏文件,则返回 true。
long size(Path path)返回由path表示的文件的大小,以字节为单位。
UserPrincipal getOwner(Path path, LinkOption...), Path setOwner(Path path, UserPrincipal owner)获取/设置文件的所有者。
FileTime getLastModifiedTime(Path path, LinkOption...), Path setLastModifiedTime(Path path, FileTime time)获取/设置指定文件的上次修改时间。
Object getAttribute(Path path, String attribute, LinkOption...), Path setAttribute(Path path, String attribute, Object value, LinkOption...)获取/设置指定文件的指定属性。

检查文件属性和元数据

在前面关于Path接口的部分中,您试图判断两个路径是否指向同一个文件(参见清单 10-3 )。有另一种方法可以发现同样的事情:您可以使用来自Files类的isSameFile()方法。清单 10-4 展示了如何做到这一点。

Listing 10-4. PathCompare2.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

// illustrates how to use Files class to compare two paths

class PathCompare2 {

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

Path path1 = Paths.get("Test");

Path path2 = Paths.get("D:\\OCPJP\\programs\\NIO2\\Test");

System.out.println("Files.isSameFile(path1, path2) is: "

+ Files.isSameFile(path1, path2));

}

}

假设您的计算机上存在目录 D:\ \ OCPJP \ \程序\ \ NIO2 \ \测试。该程序打印以下内容:

Files.isSameFile(path1, path2) is: true

在这种情况下,路径D:\OCPJP\programs\NIO2\中有Test目录,所以代码工作正常。

如果给定的路径中不存在Test文件/目录,就会得到一个NoSuchFileException。但是,如何判断给定路径上是否存在文件/目录呢?Files类提供了exists()方法来做到这一点。您还可以使用来自Files类的isDirectory()方法来区分文件和目录。清单 10-5 使用了这些方法。

Listing 10-5. PathExists.java

import java.nio.file.Files;

import java.nio.file.LinkOption;

import java.nio.file.Path;

import java.nio.file.Paths;

class PathExists {

public static void main(String[] args) {

Path path = Paths.get(args[0]);

if(Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {

System.out.println("The file/directory " + path.getFileName() + " exists");

// check whether it is a file or a directory

if(Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {

System.out.println(path.getFileName() + " is a directory");

}

else {

System.out.println(path.getFileName() + " is a file");

}

}

else {

System.out.println("The file/directory " + path.getFileName()

+ " does not exist");

}

}

}

这个程序从命令行接受一个文件/目录名,并创建一个Path对象。然后,使用来自Files类的exists()方法来找出文件/目录是否存在。exists()方法的第二个参数是 link option,用于指定是否跟随符号链接;在这种情况下,您没有跟踪符号链接。如果与输入路径相关联的文件/目录存在,那么使用Files类的isDirectory()方法检查输入路径指示的是文件还是目录。

我们用两个不同的命令行参数运行这个程序,得到了下面的输出(假设PathExists.java存储在目录D:\OCPJP\programs\NIO2\src中):

D:\OCPJP\programs\NIO2\src>java PathExists PathExists.java

The file/directory PathExists.java exists

PathExists.java is a file

D:\OCPJP\programs\NIO2\src>java PathExists D:\OCPJP\

The file/directory OCPJP exists

OCPJP is a directory

D:\OCPJP\programs\NIO2\src>java PathExists D:\

The file/directory null exists

null is a directory

在这个输出中,您可能已经注意到了根目录名(在本例中是 Windows 中的驱动器名)作为参数给出时的行为。根目录名是一个目录,但是如果路径是根目录名的话,path.getFileName()返回 null 这就是结果。

基于您的凭据,现有文件可能不允许您读取、写入或执行。您可以检查程序以编程方式读取、写入或执行的能力。Files类提供了方法isReadable()isWritable()isExecutable()来实现这一点。清单 10-6 使用了这些方法:为这个程序创建一个名为readonly.txt的文件,其权限是可读可执行但不可写。

Listing 10-6. FilePermissions.java

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

class FilePermissions {

public static void main(String[] args) {

Path path = Paths.get(args[0]);

System.out.printf( "Readable: %b, Writable: %b, Executable: %b ",

Files.isReadable(path), Files.isWritable(path), Files.isExecutable(path));

}

}

让我们用两个不同的输入来执行这个程序。以下是输出:

D:\OCPJP\programs\NIO2\src>java FilePermissions readonly.txt

Readable: true, Writable: false, Executable: true

D:\OCPJP\programs\NIO2\src>java FilePermissions FilePermissions.java

Readable: true, Writable: true, Executable: true

对于readonly.txt文件,权限是可读和可执行的,但不可写。文件FilePermissions.java拥有所有三种权限:可读、可写和可执行。

您可以使用许多其他方法来获取文件属性。让我们使用getAttribute()方法来获取文件的一些属性。该方法接受可变数量的参数:一个Path对象、一个属性名和链接选项(参见清单 10-7 )。

Listing 10-7. FileAttributes.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.LinkOption;

import java.nio.file.Path;

import java.nio.file.Paths;

class FileAttributes {

public static void main(String[] args) {

Path path = Paths.get(args[0]);

try {

Object object = Files.getAttribute(path, "creationTime",

LinkOption.NOFOLLOW_LINKS);

System.out.println("Creation time: " + object);

object = Files.getAttribute(path, "lastModifiedTime", LinkOption.NOFOLLOW_LINKS);

System.out.println("Last modified time: " + object);

object = Files.getAttribute(path, "size", LinkOption.NOFOLLOW_LINKS);

System.out.println("Size: " + object);

object = Files.getAttribute(path, "dos:hidden", LinkOption.NOFOLLOW_LINKS);

System.out.println("isHidden: " + object);

object = Files.getAttribute(path, "isDirectory", LinkOption.NOFOLLOW_LINKS);

System.out.println("isDirectory: " + object);

} catch (IOException e) {

e.printStackTrace();

}

}

}

让我们首先通过给出这个程序的名字来执行这个程序,然后看看会发生什么:

D:\> java FileAttributes FileAttributes.java

Creation time: 2012-10-06T10:20:10.34375Z

Last modified time: 2012-10-06T10:21:54.859375Z

Size: 914

isHidden: false

isDirectory: false

这个例子中棘手的部分是getAttribute()方法的第二个参数。您需要提供正确的属性名来提取相关的值。应该以view:attribute格式指定期望的字符串,其中viewFileAttributeView的类型,attributeview支持的属性的名称。如果没有指定view,则假定为basic。在这种情况下,您指定属于一个基本视图的所有属性,除了来自dos视图的一个属性。如果没有指定正确的视图名,就会得到一个UnsupportedOperationException;如果你弄乱了属性名,你会得到一个IllegalArgumentException

例如,如果您键入 size 而不是 size,您将得到以下异常:

Exception in thread "main" java.lang.IllegalArgumentException: 'sized' not recognized

[...stack trace elided...]

现在,您知道了如何使用getAttribute()方法读取与文件相关的元数据。然而,如果您想要读取许多属性,为每个属性调用getAttribute()可能不是一个好主意(从性能的角度来看)。在这种情况下,Java 7 提供了一个解决方案:一个 API——readAttributes()——一次性读取属性。API 有两种风格:

Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options)

<A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)

第一个方法返回一个属性-值对的Map,并接受可变长度的参数。attributes参数是关键参数,您可以在其中指定想要检索的内容。该参数类似于您在getAttribute()方法中使用的参数;但是,这里可以指定一个属性列表,也可以使用星号(*)来指定所有属性。例如,使用*表示默认FileAttributeView的所有属性,如BasicFileAttributes(指定为基本档案属性)。再比如dos:*,指 dos 文件属性的所有属性。

第二种方法使用通用语法(第四章)。这里的第二个参数从BasicFileAttributes层次结构中获取一个类,稍后将对此进行讨论。该方法从BasicFileAttributes层次结构中返回一个实例。

文件属性层次结构如图 10-1 所示。BasicFileAttributes是派生DosFileAttributesPosixFileAttributes的基础接口。注意这些属性接口是在java.nio.file.attribute包中提供的。

A978-1-4842-1836-5_10_Fig1_HTML.jpg

图 10-1。

The hierarchy of BasicFileAttributes

如你所见,BasicFileAttributes接口定义了所有通用平台支持的基本属性。但是,特定的平台定义了自己的文件属性,这些属性由DosFileAttributesPosixFileAttributes捕获。您可以指定这些接口中的任何一个来检索相关的文件属性。清单 10-8 包含一个使用BasicFileAttributes获取文件所有属性的程序。

Listing 10-8. FileAttributes2.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.nio.file.attribute.BasicFileAttributes;

class FileAttributes2 {

public static void main(String[] args) {

Path path = Paths.get(args[0]);

try {

BasicFileAttributes fileAttributes =

Files.readAttributes(path, BasicFileAttributes.class);

System.out.println("File size: " + fileAttributes.size());

System.out.println("isDirectory: " + fileAttributes.isDirectory());

System.out.println("isRegularFile: " + fileAttributes.isRegularFile());

System.out.println("isSymbolicLink: " + fileAttributes.isSymbolicLink());

System.out.println("File last accessed time: " + fileAttributes.lastAccessTime());

System.out.println("File last modified time: " +

fileAttributes.lastModifiedTime());

System.out.println("File creation time: " + fileAttributes.creationTime());

} catch (IOException e) {

e.printStackTrace();

}

}

}

以下是该程序的一些示例输出:

D:\>java FileAttributes2 FileAttributes2.java

File size: 904

isDirectory: false

isRegularFile: true

isSymbolicLink: false

File last accessed time: 2012-10-06T10:28:29.0625Z

File last modified time: 2012-10-06T10:28:22.4375Z

File creation time: 2012-10-06T10:26:39.1875Z

您使用readAttribute()方法和BasicFileAttributes来检索基本的文件属性。类似地,您可以分别使用DosFileAttributesPosixFileAttributes在 DOS 或 Unix 环境中检索与文件相关的属性。

复制文件

现在让我们试着将一个文件/目录从一个位置复制到另一个位置。这个任务很容易完成:只需调用Files.copy()将文件从源复制到目标。下面是这个方法的签名:

Path copy(Path source, Path target, CopyOption... options)

清单 10-9 使用这种方法编写了一个简单的文件复制程序。

Listing 10-9. FileCopy.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

public class FileCopy {

public static void main(String[] args) {

if(args.length != 2){

System.out.println("usage: FileCopy <source-path> <destination-path>");

System.exit(1);

}

Path pathSource = Paths.get(args[0]);

Path pathDestination = Paths.get(args[1]);

try {

Files.copy(pathSource, pathDestination);

System.out.println("Source file copied successfully");

} catch (IOException e) {

e.printStackTrace();

}

}

}

让我们执行它,看看它是否有效。

D:\> java FileCopy FileCopy.java Backup.java

Source file copied successfully

是的,起作用了。尝试使用相同的参数再次运行它:

D:\OCPJP\programs\NIO2\src>java FileCopy FileCopy.java Backup.java

java.nio.file.FileAlreadyExistsException: Backup.java

at sun.nio.fs.WindowsFileCopy.copy(Unknown Source)

[...stack trace elided...]

哎呀!发生了什么事?当您第二次尝试复制文件时,您会得到一个FileAlreadyExistsException,因为目标文件已经存在。如果你想覆盖现有的文件呢?解决方案:您需要告诉copy()方法您想要覆盖一个现有的文件。在清单 10-9 中,将copy()修改如下:

Files.copy(pathSource, pathDestination, StandardCopyOption.REPLACE_EXISTING);

您指定一个额外的参数(因为copy()方法支持可变参数)来告诉该方法您想要覆盖一个已经存在的文件。运行这个程序,看看它是否工作:

D:\>java FileCopy FileCopy.java Backup.java

Source file copied successfully

D:\>java FileCopy FileCopy.java Backup.java

Source file copied successfully

是的,它有效。现在,尝试将文件复制到新目录:

D:\OCPJP\programs\NIO2\src>java FileCopy FileCopy.java bak\Backup.java

java.nio.file.NoSuchFileException: FileCopy.java -> bak\Backup.java

[...stack trace elided ...]

这里,您试图将一个文件复制回一个不存在的目录。所以,你得到了NoSuchFileException。不仅仅是给定的目录,路径上的所有中间目录都必须存在,这样copy()方法才能成功。

A978-1-4842-1836-5_10_Figa_HTML.jpg指定路径上的所有目录(除了最后一个,如果你正在复制一个目录)必须存在,以避免NoSuchFileException

如果你试着复制一个目录呢?它可以工作,但是记住它只会复制顶层目录,而不会复制该目录中包含的文件/目录。

A978-1-4842-1836-5_10_Figa_HTML.jpg如果使用copy()方法复制一个目录,它不会复制源目录中包含的文件/目录;您需要显式地将它们复制到目标文件夹。

移动文件

移动文件类似于复制文件;为此,您可以使用Files.move()方法。此方法的签名如下:

Path move(Path source, Path target, CopyOption... options)

清单 10-10 包含了一个使用这种方法的小程序。注意,一旦move()方法成功执行,源文件就不再存在。

Listing 10-10. FileMove.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.nio.file.StandardCopyOption;

public class FileMove {

public static void main(String[] args) {

if(args.length != 2){

System.out.println("usage: FileMove <source-path> <destination-path>");

System.exit(-1);

}

Path pathSource = Paths.get(args[0]);

Path pathDestination = Paths.get(args[1]);

try {

Files.move(pathSource, pathDestination, StandardCopyOption.REPLACE_EXISTING);

System.out.println("Source file moved successfully");

} catch (IOException e) {

e.printStackTrace();

}

}

}

这个程序是这样执行的(假设当前目录中存在一个名为text.txt的文件):

D:\OCPJP\programs\NIO2\src> java FileMove text.txt newtext.txt

Source file moved successfully

以下是对move()方法的一些观察:

  • copy()方法一样,move()方法不会覆盖现有的目标文件,除非您使用REPLACE_EXISTING指定它应该这样做。
  • 如果移动符号链接,则移动的是链接本身,而不是链接的目标文件。需要注意的是,在copy()的情况下,如果你指定一个符号链接,链接的目标被复制,而不是链接本身。
  • 如果移动目录不需要移动包含的文件/目录,则可以移动非空目录。例如,将一个目录从一个物理驱动器移动到另一个可能不成功(将抛出一个IOException)。如果移动目录成功,那么所有包含的文件/目录也会被移动。
  • 您可以使用ATOMIC_MOVE复制选项将move()指定为原子操作。当您指定原子移动时,可以确保移动成功完成或源继续存在。如果move()作为非原子操作执行,并且在执行过程中失败,那么两个文件的状态都是未知和未定义的。

删除文件

Files类提供了一个delete()方法来删除一个文件/目录/符号链接。清单 10-11 包含一个删除指定文件的简单程序。

Listing 10-11. FileDelete.java

import java.io.IOException;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

public class FileDelete {

public static void main(String[] args) {

if(args.length != 1){

System.out.println("usage: FileDelete <source-path>");

System.exit(1);

}

Path pathSource = Paths.get(args[0]);

try {

Files.delete(pathSource);

System.out.println("File deleted successfully");

} catch (IOException e) {

e.printStackTrace();

}

}

}

它在执行时打印以下内容:

D:\> java FileDelete log.txt

File deleted successfully

使用Files.delete()方法时有几点需要记住。在目录的情况下,应该在空目录上调用delete()方法;否则,该方法将失败。在符号链接的情况下,删除的是链接,而不是链接的目标文件。您要删除的文件必须存在;否则你会得到一个NoSuchFileException。如果您静默删除一个文件,并且不想被这个异常所困扰,那么您可以使用deleteIfExists()方法,如果文件不存在,它不会报错,如果文件存在,它会删除文件。此外,如果文件是只读的,某些平台可能会阻止您删除该文件。

要记住的要点

记住这些要点,帮助你通过 OCPJP 八级考试。

  • 不要混淆FileFiles,以及Paths的路径:它们是不同的。File是一个旧类(Java 4 ),表示文件/目录路径名,而Files是 Java 7 中引入的一个实用类,对 I/O API 有全面的支持。Path接口表示一个文件/目录路径,并定义了一个有用的方法列表。然而,Paths类是一个只提供两种方法的实用程序类(都是为了获取Path对象)。
  • Path对象表示的文件或目录可能不存在。除了像toRealPath()这样的方法之外,Path中的方法不需要为Path对象提供底层文件或目录。
  • 您学习了如何对文件/目录执行复制。但是,没有必要只对两个文件/目录执行复制。您可以从一个InputStream获取输入并写入一个文件,或者您可以从一个文件获取输入并复制到一个OutputStream。这里可以使用copy(InputStream, Path, CopyOptions...)copy(Path, OutputStream, CopyOptions...)两种方法。

在 NIO.2 中使用流 API

| 认证目标 | 将流 API 与 NIO.2 一起使用 |

Java 8 中对 JDK 的大量增强简化了使用 NIO.2 的编程。本节讨论 Java 8 中对java.nio包的一些重要增强。

在 Files 类中使用 list()方法

让我们首先使用 Java 8 中添加的Files.list()方法来列出当前目录中的所有文件(参见清单 10-12 )。在底层,它使用了一个DirectoryStream,因此必须调用close()方法来释放 I/O 资源。这个程序使用带有自动关闭流的try-with-resources语句的流。

Listing 10-12. ListFiles.java

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.io.IOException;

import java.util.stream.Stream;

class ListFiles {

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

try(Stream<Path> entries = Files.``list``(Paths.``get

entries.forEach(System.``out

}

}

}

它打印了当前目录中的文件:

./ListFiles.class

./ListFiles.java

... (rest of the output elided)

list()方法声明如下:

static Stream<Path> list(Path dir) throws IOException

因为list()方法返回一个Stream,所以您可以使用Stream接口中提供的众多方法中的任何一种,包括map()filter()findFirst()findAny()distinct()sorted()allMatch()noneMatch()anyMatch()

这段代码是清单 10-12 的修改版本,它打印文件的绝对路径:

Files.list(Paths.get("."))

.map(path -> path.toAbsolutePath())

.forEach(System.out::println);

结果如下:

D:\OCPJP\NIO2\src\ListFiles.class

D:\OCPJP\NIO2\src\ListFiles.java

... (rest of the output elided)

注意,list()方法不会递归地遍历给定的Path中的条目。要递归地遍历目录,可以使用Files.walk()方法:

Files.walk(Paths.get(".")).forEach(System.out::println);

Files.walk()方法是一个重载方法:

static Stream<Path> walk(Path path, FileVisitOption... options) throws IOException

static Stream<Path> walk(Path path, int maxDepth, FileVisitOption... options) throws IOException

FileVisitOption有一个枚举值:FileVisitOption.FOLLOW_LINKS。你可以把它传递给walk()方法。您还可以指定maxDepth:递归遍历目录条目的嵌套层次的限制(参见清单 10-13 )。

Listing 10-13. CountEntriesRecur.java

import java.nio.file.FileVisitOption;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.io.IOException;

import java.util.stream.Stream;

class CountEntriesRecur {

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

try(Stream<Path> entries =

Files.walk(Paths.get("."), 4, FileVisitOption.FOLLOW_LINKS)) {

long numOfEntries = entries.count();

System.out.printf("Found %d entries in the current path", numOfEntries);

}

}

}

在我们的机器上,这个程序打印了以下内容:

Found 179 entries in the current path

这段代码将嵌套深度限制为 4,作为Files.walk()方法的第二个参数。

最后,让我们使用Files. find()方法列出符合给定条件的文件(列出 10-14 )。

Listing 10-14. FindFiles.java

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.io.IOException;

import java.nio.file.attribute.BasicFileAttributes;

import java.util.function.BiPredicate;

import java.util.stream.Stream;

class FindFiles {

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

BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs)

-> attrs.isRegularFile() && path.toString().endsWith("class");

try(Stream<Path> entries = Files.find(Paths.get("."), 4, predicate)) {

entries.limit(100).forEach(System.out::println);

}

}

}

这个程序打印长输出,所以这里不给出。

这个例子在Stream<Path>对象上使用了limit()方法来限制从Files.find()方法返回时处理的条目数量。find()方法将开始搜索的路径、要搜索的最大深度、BiPredicate和可选的FileVisitOption作为参数:

static Stream<Path> find(Path path, int maxDepth, BiPredicate<Path,BasicFileAttributes>

matcher, FileVisitOption... options) throws IOException

在这个例子中,您正在寻找以扩展名class结尾的文件,并且您将条目的数量限制为 100。

使用 Files 类中的 lines()方法

Files.lines()是一种非常方便的读取文件内容的方法:

static Stream<String> lines(Path path)

在内部,它使用一个Reader,因此在使用后必须关闭。您使用清单 10-15 中的try-with-resources来打印文件名作为参数传递的文件的内容。

Listing 10-15. Type.java

import java.io.IOException;

import java.nio.file.Paths;

import java.nio.file.Files;

import java.util.Arrays;

import java.util.stream.Stream;

// implements a simplified version of "type" command provided in Windows;

// given a text file name(s) as argument, it prints the content of the file(s)

class Type {

private static void processFile(String file) {

try(Stream<String> lines = Files.``lines``(Paths.``get

lines.forEach(System.``out

} catch (IOException ioe) {

System.``err

System.``exit

}

}

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

if (files.length == 0) {

System.``err

System.``exit

}

// process each file passed as argument

Arrays.``stream``(files).forEach(Type::``processFile

}

}

这段代码比你在 IO 基础章节中看到的版本要简洁得多(在第九章中列出了 9-5 )。

摘要

让我们简要回顾一下本章中每个认证目标的要点。请在参加考试前阅读这一部分。

使用路径接口操作文件和目录路径

  • 一个Path对象是一个表示文件/目录路径的编程抽象。
  • 您可以使用Paths类的get()方法获得Path的实例。
  • Path提供了两种比较Path对象的方法:equals()compareTo()。即使两个Path对象指向同一个文件/目录,也不能保证equals()方法返回 true。

使用 Files 类来检查、读取、删除、复制、移动和管理文件或目录的元数据

  • 您可以使用Files类的exists()方法来检查文件是否存在。
  • Files类提供了方法isReadable()isWritable()isExecutable()来分别检查程序以编程方式读取、写入和执行的能力。
  • 您可以使用getAttributes()方法检索文件的属性。
  • 您可以使用Files类的readAttributes()方法来批量读取文件属性。
  • copy()方法可用于将文件从一个位置复制到另一个位置。类似地,move()方法将文件从一个位置移动到另一个位置。
  • 复制时,指定路径上的所有目录(除了最后一个,如果您正在复制一个目录)必须存在,以避免出现NoSuchFileException
  • 使用delete()方法删除文件;使用deleteIfExists()方法删除一个存在的文件。

将流 API 与 NIO.2 一起使用

  • Files.list()方法返回一个Stream<Path>。它不会递归地遍历给定的Path中的目录。
  • Files.walk()方法通过从给定的Path;的一个重载版本中递归遍历条目来返回一个Stream<Path>,您也可以传递这种遍历的最大深度,并提供FileVisitOption.FOLLOW_LINKS作为第三个选项。
  • Files.find()方法通过递归遍历来自给定Path;的条目返回一个Stream<Path>,它还将最大搜索深度、一个BiPredicate和一个可选的FileVisitOption作为参数。
  • Files.lines()是一种非常方便的读取文件内容的方法。它返回一个Stream<String>

Question TimeConsider the following program: import java.nio.file.*; public class PathInfo {         public static void main(String[] args) {                 Path aFilePath = Paths.get("D:\\directory\\file.txt");                // FILEPATH                 while(aFilePath.iterator().hasNext()) {                         System.out.println("path element: " + aFilePath.iterator().next());                 }         } } Assume that the file D:\directory\file.txt exists in the underlying file system. Which one of the following options correctly describes the behavior of this program? The program gives a compiler error in the line marked with the comment FILEPATH because the checked exception FileNotFoundException is not handled.   The program gives a compiler error in the line marked with the comment FILEPATH because the checked exception InvalidPathException is not handled.   The program gets into an infinite loop, printing “path element: directory” forever.   The program prints the following: path element: directory path element: file.txt     Consider the following program: import java.nio.file.*; class SubPath {         public static void main(String []args) {                 Path aPath = Paths.get("D:\\OCPJP\\programs\\..\\NIO2\\src\\.\\SubPath.java");                 aPath = aPath.normalize();                 System.out.println(aPath.subpath(2, 3));         } } This program prints the following: ..   src   NIO2   NIO2\src   ..\NIO2     Consider the following program: import java.nio.file.*; import java.io.IOException; class PathExists {         public static void main(String []args) throws IOException {                 Path aFilePath = Paths.get("D:\\directory\\file.txt");                 System.out.println(aFilePath.isAbsolute());         } } Assuming that the file D:\directory\file.txt does not exist, what will be the behavior of this program? This program prints false.   This program prints true.   This program crashes by throwing a java.io.IOException.   This program crashes by throwing a java.nio.file.NoSuchFileException.     Given this code segment (assume that necessary import statements are provided in the program that contains this code segment) Stream<String> lines = Files. lines (Paths. get ("./text.txt")) // line n1 If a file named text.txt exists in the current directory in which you are running this code segment, which one of the following statements will result in printing the first line of the file’s contents? lines.limit(1).forEach(System.out::println);   lines.forEach(System.out::println);   lines.println();   lines.limit(1).println();   lines.forEach(1);     Consider the following code segment: try(Stream<Path> entries = Files.find(Paths.get("."), 4, predicate)) {      entries.forEach(System.out::println); } Which one of the following is a valid definition of the variable predicate that can be used in this code segment? BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs) -> true;   Predicate<Path> predicate = (path) -> true   Predicate<BasicFileAttributes> predicate = (attrs) -> attrs.isRegularFile();   Predicate predicate = FileVisitOption.FOLLOW_LINKS;    

答案:

C. The program gets into an infinite loop, printing “path element: directory” forever. In the while loop, you use iterator() to get a temporary iterator object. So, the call to next() on the temporary variable is lost, and the while loop gets into an infinite loop. In other words, the following loop terminates after printing the directory and file.txt parts of the path: Iterator<Path> paths = aFilePath.iterator(); while(paths.hasNext()) {     System.out.println("path element: " + paths.next()); } Option A is wrong because the Paths.get method does not throw FileNotFoundException. Option B is wrong because InvalidPathException is a RuntimeException. Also, even if the file path does not exist in the underlying file system, this exception will not be thrown when the program is executed. Option D is wrong because the program gets into an infinite loop.   B. src The normalize() method removes redundant name elements in the given path, so after the call to the normalize() method, the aPath value is D:\OCPJP\NIO2\src\SubPath.java. The subpath(int beginIndex, int endIndex) method returns a path based on the values of beginIndex and endIndex. The name that is closest to the root has index 0; note that the root itself (in this case, D:\) is not considered an element in the path. Hence, the name elements “OCPJP”, “NIO2”, “src”, and “SubPath.java” are in index positions 0, 1, 2, and 3, respectively. Note that beginIndex is the index of the first element, inclusive of that element; endIndex is the index of the last element, exclusive of that element. Hence, the subpath is src, which is at index position 2 in this path.   B. This program prints: true To use methods such as isAbsolute(), the actual file need not exist. Because the path represents an absolute path (and not a relative path), this program prints true.   A. lines.limit(1).forEach(System.out::println); The limit(1) method truncates the result to one line; and the forEach() method, when passed with the System.out::println method reference, prints that line to the console. Option B prints all the lines in the given file and thus is the wrong answer. The code segments given in the other three options will result in compiler errors.   A. BiPredicate<Path, BasicFileAttributes> predicate = (path, attrs) -> true; The find() method takes the path to start searching from, the maximum depth to search, a BiPredicate, and an optional FileVisitOption as arguments: static Stream<Path> find(Path path, int maxDepth, BiPredicate<Path,BasicFileAttributes> matcher, FileVisitOption... options) throws IOException Option A provides a definition of BiPredicate and hence it is the correct answer. Using the other options will result in a compiler error.