Java7-新特性秘籍-全-

Java7 新特性秘籍(全)

原文:zh.annas-archive.org/md5/5FB42CDAFBC18FB5D8DD681ECE2B0206

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着 Java 7 的发布,许多新功能已被添加,显著提高了开发人员创建和维护 Java 应用程序的能力。这些包括语言改进,如更好的异常处理技术,以及 Java 核心库的添加,如新的线程机制。

本书使用一系列配方涵盖了这些新功能。每个配方都涉及一个或多个新功能,并提供了使用这些功能的模板。这应该使读者更容易理解这些功能以及何时以及如何使用它们。提供了逐步说明,以指导读者完成配方,然后解释生成的代码。

本书以讨论新语言增强开始,然后是一系列章节,每个章节都涉及特定领域,如文件和目录管理。假定读者熟悉 Java 6 的功能。本书不需要按顺序阅读,这使读者可以选择感兴趣的章节和配方。但建议读者阅读第一章,因为后续配方中会使用那里的许多功能。如果在配方中使用了其他新的 Java 7 功能,则提供了相关配方的交叉引用。

本书涵盖的内容

第一章, Java 语言改进: 本章讨论了作为 Coin 项目的一部分引入的各种语言改进。这些功能包括简单的改进,如在文字中使用下划线和在 switch 语句中使用字符串。还有更重要的改进,如 try-with-resources 块和引入的菱形操作符。

第二章, 使用路径定位文件和目录: 本章介绍了 Path 类。它在本章和其他章节中被使用,并且是 Java 7 中许多新的与文件相关的添加的基础。

第三章, 获取文件和目录信息: 许多应用程序需要访问特定的文件和目录信息。本章介绍了如何访问文件信息,包括访问基本文件属性、Posix 属性和文件的访问控制列表等信息。

第四章, 管理文件和目录: 本章涵盖了管理文件和目录的基本机制,包括创建和删除文件等操作。还讨论了临时文件的使用和符号链接的管理。

第五章, 管理文件系统: 这里介绍了许多有趣的主题,如如何获取文件系统和文件存储信息、用于遍历文件结构的类、如何监视文件和目录事件以及如何使用 ZIP 文件系统。

第六章, Java 7 中的流 IO: 引入了 NIO2。详细介绍了执行异步 IO 的新技术,以及执行随机访问 IO 和使用安全目录流的新方法。

第七章, 图形用户界面改进: Java 7 中增加了几项功能,以解决创建 GUI 界面的问题。现在可以创建不同形状的窗口和透明窗口。此外,还解释了许多增强功能,如使用 JLayer 装饰器,它改善了在窗口上叠加图形的能力。

第八章,事件处理: 在本章中,我们将研究处理各种应用程序事件的新方法。Java 7 现在支持额外的鼠标按钮和精确的鼠标滚轮。改进了控制窗口焦点的能力,并引入了辅助循环来模拟模态对话框的行为。

第九章,数据库、安全和系统增强: 说明了各种数据库改进,例如引入新的 RowSetFactory 类以及如何利用新的 SSL 支持。此外,还演示了其他系统改进,例如对 MXBeans 的额外支持。

第十章,并发处理: 添加了几个新类来支持线程的使用,包括支持 fork/join 范式、phaser 模型、改进的 dequeue 类和 transfer queue 类的类。解释了用于生成随机数的新 ThreadLocalRandom 类。

第十一章,杂项: 本章演示了许多其他 Java 7 改进,例如对周、年和货币的新支持。本章还包括了对处理空引用的改进支持。

您需要为这本书做什么

本书所需的软件包括 Java 开发工具包(JDK)1.7 或更高版本。任何支持 Java 7 的集成开发环境都可以用于创建和执行示例。本书中的示例是使用 NetBeans 7.0.1 开发的。

这本书适合谁

本书旨在让熟悉 Java 的人了解 Java 7 中的新功能。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码单词显示如下:“我们可以通过使用include指令包含其他上下文。”

代码块设置如下:

private void gameEngine(List<Entity> entities)
{
final Phaser phaser = new Phaser(1);
for (final Entity entity : entities)
{
final String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread()
{
@Override
public void run()
{
System.out.println(member +
" waiting for the remaining participants");
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
System.out.println(member + " starting run");
entity.run();
}
}.start();
}
phaser.arriveAndDeregister(); //Deregister and continue
System.out.println("Phaser continuing");
}

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目会以粗体显示:

private void gameEngine(List<Entity> entities)
{
final Phaser phaser = new Phaser(1);
for (final Entity entity : entities)
{
final String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread()
{
@Override
public void run()
{
System.out.println(member +
" waiting for the remaining participants");
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
System.out.println(member + " starting run");
entity.run();
}
}.start();
}
phaser.arriveAndDeregister(); //Deregister and continue
System.out.println("Phaser continuing");
}

任何命令行输入或输出都是这样写的:

Paths.get(new URI("file:///C:/home/docs/users.txt")), Charset.defaultCharset()))

新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中以这种方式出现:“单击下一步按钮将您移动到下一个屏幕”。

注意

警告或重要说明会以这样的方式出现在框中。

提示

提示和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们开发您真正受益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在消息主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您是 Packt 图书的自豪所有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误是难免的。如果您在我们的书籍中发现错误,无论是文字还是代码方面的错误,我们将不胜感激地接受您的报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/support,选择您的书籍,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站,或者添加到该书籍的勘误列表中的“勘误”部分。

盗版

互联网上的侵犯版权行为是各种媒体持续存在的问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您帮助保护我们的作者,以及我们为您提供有价值内容的能力。

问题

如果您在阅读本书的过程中遇到任何问题,请通过<questions@packtpub.com>与我们联系,我们将尽力解决。

第一章:Java 语言改进

在本章中,我们将涵盖以下内容:

  • 在 switch 语句中使用字符串字面量

  • 使用下划线改进代码可读性的字面量

  • 使用 try-with-resources 块改进异常处理代码

  • 创建可与 try-with-resources 技术一起使用的资源

  • 捕获多个异常类型以改进类型检查

  • 在 Java 7 中重新抛出异常

  • 使用diamond操作符进行构造类型推断

  • 使用@SafeVarargs 注解

介绍

Java 7于 2011 年 7 月发布,引入了许多新功能。在 Java SDK 文档中,您可能会看到它被称为Java 1.7。本章将重点介绍作为 Coin 项目的一部分分组的功能(openjdk.java.net/projects/coin/)。Coin 项目指的是 Java 7 中设计为尽可能删除多余文本以使程序更易读的小语言更改。语言的更改不涉及修改Java 虚拟机JVM)。这些新功能包括:

  • 在 switch 语句中使用字符串

  • 添加二进制字面量和在数字字面量中插入下划线的能力

  • 多重捕获块的使用

  • try-with-resources 块

  • 使用diamond操作符改进类型推断

  • 改进了具有可变数量参数的方法的使用

自 Java 问世以来,只能使用整数值来控制 switch 语句。现在可以使用字符串,并且可以提供一种更方便的技术来控制基于字符串的执行流程。在 switch 语句中使用字符串字面量配方说明了这一特性。

现在可以在字面量中使用下划线,如使用下划线改进代码可读性的字面量配方中所述。这些可以使程序更易读和易维护。此外,现在可以使用二进制字面量。例如,可以使用字面位模式,而不是使用十六进制字面量。

在 Java 7 中新增了改进的 try-catch 块机制。这包括从单个 catch 块中捕获多个异常的能力,以及如何抛出异常的改进。捕获多个异常类型以改进类型检查配方探讨了这些增强功能。

异常处理的另一个改进涉及自动关闭资源。在早期版本的 Java 中,当在 try 块中打开多个资源时,当发生异常时有效关闭资源可能会很困难。Java 7 提供了一种新技术,如使用 try-with-resources 块改进异常处理代码配方中所讨论的。

要利用这种技术,表示资源的类必须实现新的java.lang.AutoCloseable接口。该接口由一个名为close的方法组成,当实现时,应根据需要释放资源。许多核心 Java 类已经增强了这一点。配方:创建可与 try-with-resources 技术一起使用的资源说明了如何为非核心类执行此操作。

Java 7 提供了以灵活的方式重新抛出异常的能力。它提供了一种更精确的抛出异常的方式,并在 try/catch 块中处理它们的灵活性更大。在 Java 7 中重新抛出异常配方说明了这一能力。

Java 1.5引入泛型时,编写代码来解决许多类似问题变得更容易。然而,有时它的使用可能变得有些冗长。引入了diamond操作符减轻了这一负担,并在使用diamond操作符进行构造类型推断配方中进行了说明。

当一个方法使用变量数量的泛型参数时,有时会生成无效的警告。@SafeVarargs注解已被引入以标记方法为安全。这个问题与堆污染有关,并在使用@SafeVarargs 注解中进行了讨论。

注意

在本章和其他章节中,大多数代码示例将被编写为从主方法中执行。虽然不需要特定的集成开发环境IDE)来使用 Java 7 的新功能,但本书中的示例是使用NetBeans 7.0.1Windows 7开发的,除非另有说明。至少需要Java 开发工具包JDK1.7或更高版本。

另外,请注意提供的代码示例不包括import语句。这里不显示这些内容是为了减少代码行数。大多数 IDE 都可以很容易地插入这些导入,但您需要小心使用正确的导入。

switch语句中使用字符串文字

在 Java 7 中,使用字符串文字在switch语句中是新的。以前,switch语句中只有整数值是有效的参数。根据字符串值做出决定并使用switch语句执行此任务可以简化原本需要的一系列if语句。这可以导致更易读和更高效的代码。

准备工作

应用程序可能会基于字符串值进行选择。一旦识别出这种情况,执行以下操作:

  1. 创建一个String变量,通过switch语句进行处理。

  2. 创建switch块,使用字符串文字作为 case 子句。

  3. 使用String变量来控制switch语句。

如何做...

这里演示的例子将使用switch语句来处理应用程序的命令行参数。创建一个新的控制台应用程序。在main方法中,我们将使用args参数来处理应用程序的命令行参数。许多应用程序允许使用命令行参数来自定义或以其他方式影响应用程序的操作。在这个例子中,我们的应用程序将支持详细模式、日志记录,并提供有关应用程序的有效命令行参数的帮助消息。

  1. 在这个例子中,创建一个名为StringSwitchExample的类,该类具有三个实例变量,可以通过命令行参数设置,如下所示:
public class StringSwitchExample {
private static boolean verbose = false;
private static boolean logging = false;
private static boolean displayHelp = false;
}

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接发送到您的邮箱。

  1. 接下来,添加以下main方法,它将根据提供的命令行参数设置这些变量:
public static void main(String[] args) {
for (String argument : args) {
switch (argument) {
case "-verbose":
case "-v":
verbose = true;
switch statementsstring literals, usingbreak;
case "-log":
logging = true;
break;
case "-help":
displayHelp = true;
break;
default:
System.out.println("Illegal command line argument");
}
}
displayApplicationSettings();
}

  1. 添加以下辅助方法来显示应用程序设置:
private static void displayApplicationSettings() {
System.out.println("Application Settings");
System.out.println("Verbose: " + verbose);
System.out.println("Logging: " + logging);
System.out.println("Help: " + displayHelp);
}

  1. 使用以下命令行执行应用程序:
java StringSwitchExample -verbose -log

  1. 如果您使用的是集成开发环境(IDE),通常有一种方法可以设置命令行参数。例如,在 NetBeans 中,右键单击项目窗口中的项目名称,然后选择属性菜单将打开项目属性对话框。在运行类别中,参数文本框允许您设置命令行参数,如下截图所示:如何做...

  2. 当应用程序被执行时,您的输出应该如下所示:

应用程序设置

详细信息:true

日志记录:true

帮助:false

工作原理...

应用程序设置变量都初始化为false。使用 for-each 循环遍历每个命令行参数。switch语句使用特定的命令行参数来打开应用程序设置。switch语句的行为类似于早期的 Javaswitch语句。

注意

有趣的是,Java 虚拟机(JVM)目前不直接支持使用字符串进行切换。Java 编译器负责将switch语句中的字符串转换为适当的字节码。

for循环完成时,将调用displayApplicationSettings方法。这将显示当前的应用程序设置,反映了由命令行参数指定的配置。

然而,需要注意的是,虽然String变量可以传递给switch语句,但与switch语句中使用的其他数据类型一样,用于案例子句的字符串必须是字符串文字。在使用字符串文字时,适用于switch语句的一般规则。switch块中的每个语句必须具有有效的非空标签,不能有两个相同的标签,并且每个switch块只能关联一个默认标签。

还有更多...

在使用字符串时,您需要注意以下两个问题:

  • 字符串的空值

  • 字符串的情况

使用一个被赋予空值的字符串引用变量将导致java.lang.NullPointerException。有关如何处理NullPointerException的更多信息,请参阅第十一章中的处理空引用配方,杂项。当与switch语句一起使用时,这也是真实的。此外,在switch语句中,对案例表达式的评估是区分大小写的。在前面的例子中,如果命令行参数与案例表达式中显示的内容不同,那么该案例将被跳过。如果我们使用以下命令行,其中我们将单词 verbose 大写:

java StringSwitchExample -Verbose -log

然后,冗长模式将不再使用,如下面的输出所示:

应用程序设置

冗长:假

日志记录:真

帮助:假

使用下划线来改善代码的可读性

在 Java 7 中,数字文字可以包含下划线字符(_)。这旨在通过将文字的数字分成显著的组,在几乎任意满足开发人员需求的地方,来提高代码的可读性。下划线可以应用于任何支持的基数(二进制、八进制、十六进制或十进制)的原始数据类型,以及整数和浮点文字。

准备就绪

第一步是识别开发人员以这种方式格式化文字将有益的实例。通常,您会希望识别更长的数字或在其外部形式中具有显著部分的数字,例如借记卡号。基本步骤包括:

  1. 识别要与下划线一起使用的文字。

  2. 在文字中适当的位置插入下划线,使文字更易读。

如何做...

此示例说明了使用下划线来澄清大多数借记卡号中存在的固有间隙,并演示了它们在浮点数中的使用。

  1. 创建一个新的控制台应用程序,并添加以下main方法:
public static void main(String[] args) {
long debitCard = 1234_5678_9876_5432L;
System.out.println("The card number is: " + debitCard);
System.out.print("The formatted card number is:");
printFormatted(debitCard);
float minAmount = 5_000F;
float currentAmount = 5_250F;
float withdrawalAmount = 500F;
if ((currentAmount - withdrawalAmount) < minAmount) {
System.out.println("Minimum amount limit exceeded " + minAmount);
}
}

  1. 添加一个方法来正确格式化输出的信用卡号,如下所示:
private static void printFormatted(long cardNumber) {
String formattedNumber = Long.toString(cardNumber);
for (int i = 0; i < formattedNumber.length(); i++) {
if (i % 4 == 0) {
System.out.print(" ");
}
System.out.print(formattedNumber.charAt(i));
}
System.out.println();
}

  1. 执行应用程序。输出将如下所示:

卡号是:1234567898765432

格式化后的卡号是:1234 5678 9876 5432

最低金额限额超过 5000.0

请注意,在第一行输出中,显示的数字不包含下划线,但我们的第二行格式化为在下划线的位置使用空格。这是为了说明数字在内部的外观与需要为外部显示格式化的方式之间的差异。

它是如何工作的...

借记卡示例将数字分成四个部分,使其更易读。由于借记卡号的长度,需要一个long变量。

接下来,在银行账户中设置了最低限额。类型为float的变量minAmount被设置为 5,000.00,使用下划线表示逗号的位置。另外两个名为currentAmountwithdrawalAmountfloat被声明并分别设置为 5,250.00 和 500.00。然后代码确定了是否可以从currentAmount中减去withdrawalAmount并仍然保持余额高于minAmount。如果不行,将显示相应的消息。

注意

在大多数涉及货币的应用中,java.util.Currency类将是更合适的选择。前面的例子只使用浮点文字来解释下划线的用法。

下划线的唯一目的是使代码对开发人员更易读。编译器在代码生成期间和任何后续变量操作期间都会忽略下划线。连续的下划线被视为一个,并且也被编译器忽略。如果变量的输出格式很重要,它将需要单独处理。

还有更多...

下划线不仅可以用于十进制文字。此外,下划线也可能被误用。在这里,我们将讨论以下内容:

  • 简单的下划线使用错误

  • 使用下划线与十六进制文字

  • 使用下划线与二进制文字

简单的下划线使用错误

下划线通常可以随意放置在文字中,但有限制它们的使用。在数字的开头或结尾、在使用floatdouble时与小数点相邻、在 D、F 或 L 后缀之前,或者在需要一串数字的地方放置下划线都是无效的。

以下是无效下划线使用的例子:

long productKey = _12345_67890_09876_54321L;
float pi = 3._14_15F;
long licenseNumber = 123_456_789_L;

这将生成语法错误,错误:非法下划线

使用下划线与十六进制文字

下划线在处理用十六进制或二进制表示的二进制数据时特别有用。在下面的例子中,表示要发送到数据端口的命令的整数值被表示为十六进制和二进制文字:

int commandInHex = 0xE_23D5_8C_7;
int commandInBinary = 0b1110_0010001111010101_10001100_0111;

这两个数字是相同的。它们只是用不同的进制表示。在这里,我们使用了 2 进制和 16 进制。在这个例子中,16 进制表示可能更易读。2 进制文字将在下一节中更深入地讨论。

下划线用于更清晰地识别命令的各个部分。假设命令的前四位表示运算符,接下来的 16 位是操作数。接下来的 8 位和 4 位可能表示命令的其他方面。

使用下划线与二进制文字

我们还可以在二进制文字中使用下划线。例如,为了初始化设备,我们可能需要向数据端口发送一个特定的 8 位序列。这个序列可以被组织成这样,前两位指定操作(读、写等),接下来的三位可以指定设备资源,最后三位可以表示操作数。我们可以使用带有下划线的二进制文字来编码这个序列,如下所示:

byte initializationSequence = 0b10_110_010;

使用下划线清楚地标识了每个字段。虽然不必使用变量initializationSequence,但它允许我们在程序中的多个地方使用该序列。另一个例子定义了一个掩码,在这种情况下,第一个三位在AND操作中被消除,如下所示:

result = inputValue & 0b000_11111;

在按位 AND 操作中,操作数的每一位都与对方进行 AND 运算。这些例子如下所示:

byte initializationSequence = (byte) 0b01_110_010;
byte inputValue = (byte) 0b101_11011;
byte result = (byte) (inputValue & (byte) 0b000_11111);
System.out.println("initializationSequence: " +
Integer.toBinaryString(initializationSequence));
System.out.println("result: " + Integer.toBinaryString(result));

执行此序列时,我们得到以下输出:

初始化序列:1110010

结果:11011

需要使用字节转换运算符,因为二进制文字默认为int类型。另外,请注意toBinaryString方法不显示前导零。

使用 try-with-resources 块来改进异常处理代码

在 Java 7 之前,为了正确打开和关闭资源(如java.io.InputStreamjava.nio.Channel),所需的代码非常冗长且容易出错。尝试与资源块已添加,以简化错误处理并使代码更简洁。使用 try-with-resources 语句会导致在 try 块退出时自动关闭所有资源。使用 try-with-resources 块声明的资源必须实现接口java.lang.AutoCloseable

这种方法可以更好地避免嵌套和过多的 try-catch 块,确保准确的资源管理,文献中可能称之为自动资源管理ARM)。

准备就绪

在处理需要打开和关闭的资源时,通过以下方式实现try-with-resource块:

  1. 创建 try 块并声明要管理的资源。

  2. 在 try 块内使用资源。

如何做...

  1. 创建一个控制台应用程序,并向其添加以下main方法。在工作目录中创建一个名为users.txt的文本文件,并向文件中添加一系列名称。此示例打开该文件并创建一个备份,同时演示了使用try-with-resources技术,其中使用 try 块创建了一个java.io.BufferedReaderjava.io.BufferedWriter对象:
public static void main(String[] args) {
try (BufferedReader inputReader = Files.newBufferedReader(
Paths.get(new URI ("file:///C:/home/docs/users.txt")),
Charset.defaultCharset());
BufferedWriter outputWriter = Files.newBufferedWriter(
Paths.get(new URI("file:///C:/home/docs/users.bak")),
Charset.defaultCharset())) {
String inputLine;
while ((inputLine = inputReader.readLine()) != null) {
outputWriter.write(inputLine);
outputWriter.newLine();
}
System.out.println("Copy complete!");
}
catch (URISyntaxException | IOException ex) {
ex.printStackTrace();
}
}

  1. 执行应用程序。输出应该如下:

复制完成!

工作原理...

要管理的资源在try关键字和 try 块的左花括号之间的一组括号内声明和初始化。在这种情况下,创建了两个资源。第一个是与users.txt文件关联的BufferedReader对象,第二个是与users.bak文件关联的BufferedWriter对象。使用java.nio.file.Path接口的新 IO 技术在第六章中进行了讨论,Java 7 中的流 IO

然后逐行读取第一个文件,并将其写入第二个文件。当 try 块退出时,两个 IO 流会自动关闭。然后显示一条消息,显示复制操作已完成。

请注意在 catch 块中使用垂直线。这是 Java 7 中的新功能,允许我们在单个 catch 块中捕获多个异常。这个操作符的使用在捕获多个异常类型以改进类型检查中进行了讨论。

请记住,使用 try-with-resources 块声明的资源之间用分号分隔。否则将导致编译时错误。此外,无论 try 块是否正常完成,都将尝试关闭资源。如果资源无法关闭,通常会抛出异常。

无论资源是否关闭,catch 和 finally 块始终被执行。但是,异常仍然可以从这些块中抛出。这在创建可与 try-with-resources 技术一起使用的资源中有更详细的讨论。

还有更多...

为了完全理解try-with-resources技术,我们需要解决另外两个主题,如下所示:

  • 理解抑制异常

  • 使用try-with-resources技术时的结构问题

理解抑制异常

为了支持这种方法,java.lang.Exception类添加了一个新的构造函数以及两个方法:addSuppressedgetSuppressed。抑制的异常是那些没有明确报告的异常。在 try-with-resources try 块的情况下,可能会从 try 块本身抛出异常,或者在 try 块创建的资源关闭时抛出异常。当抛出多个异常时,可能会抑制异常。

在 try-with-resources 块的情况下,与关闭操作相关的任何异常在从块本身抛出异常时都会被抑制。这在Creating a resource that can be used with the try-with-resources technique中有所示。

可以使用getSuppressed方法检索抑制的异常。程序员创建的异常可以使用addSuppressed方法将异常标记为被抑制。

在使用 try-with-resources 技术时的结构问题

当使用单个资源时,可能不希望使用这种技术。我们将展示三种不同的代码序列实现来显示users.txt文件的内容。首先,如下所示的代码使用了 try-with-resources 块。但是,需要在此块之前加上一个 try 块来捕获java.net.URISyntaxException:

Path path = null;
try {
path = Paths.get(new URI("file:///C:/home/docs/users.txt"));
}
catch (URISyntaxException e) {
System.out.println("Bad URI");
}
try (BufferedReader inputReader = Files.newBufferedReader(path, Charset.defaultCharset())) {
String inputLine;
while ((inputLine = inputReader.readLine()) != null) {
System.out.println(inputLine);
}
}
catch (IOException ex) {
ex.printStackTrace();
}

这个例子是基于需要捕获URISyntaxException。可以通过在get方法中创建java.net.URI对象来避免这种情况。然而,这会使代码更难阅读:

try (BufferedReader inputReader = Files.newBufferedReader(
Paths.get(new URI("file:///C:/home/docs/users.txt")), Charset.defaultCharset())) {
String inputLine;
while ((inputLine = inputReader.readLine()) != null) {
System.out.println(inputLine);
}
}
catch (IOException | URISyntaxException ex) {
ex.printStackTrace();
}

注意使用多个 catch 块,如Catching multiple exception types to improve type checking中所讨论的。另一种方法是通过使用带有String参数的get方法来避免URI对象:

try {
Path path = Paths.get("users.txt");
BufferedReader inputReader =
Files.newBufferedReader(path, Charset.defaultCharset());
String inputLine;
while ((inputLine = inputReader.readLine()) != null) {
System.out.println(inputLine);
}
}
catch (IOException ex) {
ex.printStackTrace();
}

使用的方法和代码结构会影响代码的可读性和可维护性。在代码序列中可能有可能消除URI对象或类似对象的使用,也可能不可行。然而,仔细考虑替代方法可以大大改善应用程序。

另请参阅

Catching multiple exception types to improve type checkingCreating a resource that can be used with the try-with-resources technique提供了 Java 7 中异常处理的更多覆盖范围。

创建一个可以与 try-with-resources 技术一起使用的资源

Java 库中有许多资源,可以作为try-with-resource技术的一部分使用。然而,有时您可能希望创建自己的资源,以便与这种技术一起使用。本示例演示了如何做到这一点。

准备工作

要创建一个可以与try-with-resources技术一起使用的资源:

  1. 创建一个实现java.lang.AutoCloseable接口的类。

  2. 重写close方法。

  3. 实现特定于资源的方法。

任何使用 try-with-resources 块创建的对象都必须实现AutoCloseable接口。这个接口有一个方法,即close

如何做...

在这里,我们将通过创建三个类来说明这种方法:

  • 包含main方法的一个类

  • 实现AutoCloseable接口的两个类

  1. 创建两个名为FirstAutoCloseableResourceSecondAutoCloseableResource的类。在这些类中,实现manipulateResourceclose方法,如下所示:
public class FirstAutoCloseableResource implements AutoCloseable {
@Override
public void close() throws Exception {
// Close the resource as appropriate
System.out.println("FirstAutoCloseableResource close method executed");
throw new UnsupportedOperationException(
"A problem has occurred in FirstAutoCloseableResource");
}
public void manipulateResource() {
// Perform some resource specific operation
System.out.println("FirstAutoCloseableResource manipulateResource method executed");
try-with-resource blockresource, creating}
}
public class SecondAutoCloseableResource implements AutoCloseable {
@Override
public void close() throws Exception {
// Close the resource as appropriate
System.out.println("SecondAutoCloseableResource close method executed");
throw new UnsupportedOperationException(
"A problem has occurred in SecondAutoCloseableResource");
}
public void manipulateResource() {
// Perform some resource specific operation
System.out.println("SecondAutoCloseableResource manipulateResource method executed");
}
}

  1. 接下来,将以下代码添加到main方法中。我们使用try-with-resources技术与两个资源,然后调用它们的manipulateResource方法:
try (FirstAutoCloseableResource resource1 = new FirstAutoCloseableResource();
SecondAutoCloseableResource resource2 = new SecondAutoCloseableResource()) {
resource1.manipulateResource();
resource2.manipulateResource();
}
catch (Exception e) {
e.printStackTrace();
for(Throwable throwable : e.getSuppressed()) {
System.out.println(throwable);
}
}

  1. 当代码执行时,close方法会抛出UnsupportedOperationException,如下所示:

FirstAutoCloseableResource manipulateResource 方法执行

SecondAutoCloseableResource manipulateResource 方法执行

SecondAutoCloseableResource close 方法执行

FirstAutoCloseableResource close 方法执行

java.lang.UnsupportedOperationException: SecondAutoCloseableResource 中发生了问题

在 packt.SecondAutoCloseableResource.close(SecondAutoCloseableResource.java:9)

在 packt.TryWithResourcesExample.displayAutoCloseableExample(TryWithResourcesExample.java:30)

在 packt.TryWithResourcesExample.main(TryWithResourcesExample.java:22)

被抑制:java.lang.UnsupportedOperationException: 在 FirstAutoCloseableResource 中发生了问题

在 packt.FirstAutoCloseableResource.close(FirstAutoCloseableResource.java:9)

... 2 个更多

java.lang.UnsupportedOperationException: 在 FirstAutoCloseableResource 中发生了问题

它是如何工作的...

在资源类中,创建了manipulateResource方法来执行一些特定于资源的操作。资源类被声明为 try 块的一部分,并调用了manipulateResource方法。这在输出的第一部分中有所说明。输出已经被突出显示以澄清这个过程。

当 try 块终止时,close方法被执行。它们的执行顺序与预期相反。这是应用程序堆栈工作原理的结果。

在 catch 块中,堆栈被转储。此外,我们使用getSuppressed方法返回并显示被抑制的方法。在 Java 7 中引入了对被抑制异常的支持。这些类型的异常在使用 try-with-resource 块改进异常处理代码配方中讨论,并在本配方后面讨论。

还有更多...

close方法中,可能有以下三种操作之一:

  • 如果没有要关闭的内容或资源将始终关闭

  • 关闭资源并返回而不出错

  • 尝试关闭资源,但在失败时抛出异常

前两个条件很容易处理。在最后一个条件中,有一些事情需要记住。

始终实现close方法并提供特定的异常。这将为用户提供有关潜在问题更有意义的反馈。此外,不要抛出InterruptedException。如果InterruptedException被抑制,可能会出现运行时问题。

close方法不需要是幂等的。幂等方法是指重复执行该方法不会引起问题。例如,两次从同一文件中读取数据不一定会引起问题。而将相同的数据两次写入文件可能会引起问题。close方法不必是幂等的,但建议应该是。

另请参阅

使用 try-with-resources 块改进异常处理代码配方涵盖了这种类型的 try 块的使用。

捕获多个异常类型以改进类型检查

在 try 块内,可能会生成和抛出多个异常。一系列对应的 catch 块用于捕获并处理这些异常。经常情况下,处理一个异常所需的操作对其他异常也是相同的。一个例子是当执行异常的日志记录时。

在 Java 7 中,现在可以在单个 catch 块中处理多个异常。这种能力可以减少代码的重复。在 Java 的早期版本中,通常会有诱惑去通过捕获更高级别的异常类并从该块中处理多个异常来解决这个问题。现在这种方法的需求较少。

准备工作

通过使用单个捕获块捕获多个异常来实现:

  1. 添加一个捕获块

  2. 在捕获块的括号内包括多个异常,用竖线分隔

如何做...

在这个例子中,我们希望通过记录异常来处理用户的无效输入。这是一个简单的方法,足以解释如何处理多个异常。

  1. 创建一个包含两个类MultipleExceptionsInvalidParameter的应用程序。InvalidParameter类用于处理无效的用户输入,而MultipleExceptions类包含main方法和示例代码。

  2. 创建InvalidParameter类如下:

public class InvalidParameter extends java.lang.Exception {
public InvalidParameter() {
super("Invalid Parameter");
}
}

  1. 接下来,创建MultipleExceptions类,并添加一个java.util.logging.Logger对象,如下所示:
public class MultipleExceptions {
private static final Logger logger = Logger.getLogger("log.
txt");
public static void main(String[] args) {
System.out.print("Enter a number: ");
try {
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
if (number < 0) {
throw new InvalidParameter();
}
System.out.println("The number is: " + number);
}
catch (InputMismatchException | InvalidParameter e) {
logger.log(Level.INFO, "Invalid input, try again");
}
}

  1. 使用各种输入执行程序。使用有效数字,比如 12,会产生以下输出:

输入一个数字:12

数字是:12

  1. 使用无效输入,比如非数字值,比如 cat,或者负数,比如-5,会产生以下输出:

输入一个数字:cat

无效输入,请重试

2011 年 8 月 28 日下午 1:48:59 packt.MultipleExceptions main

信息:无效输入,请重试

输入一个数字:-5

无效输入,请重试

2011 年 8 月 28 日下午 1:49:20 packt.MultipleExceptions main

信息:无效输入,请重试

它是如何工作的...

记录器已创建,当发生异常时,记录器文件中会有一条记录。使用 NetBeans 创建的输出也会显示这些日志消息。

当抛出异常时,进入 catch 块。请注意,这里感兴趣的两个异常,java.util.InputMismatchExceptionInvalidParameter出现在同一个 catch 语句中,并用竖线分隔。还要注意,只有一个变量e用于表示异常。

当需要处理几个特定的异常并以相同的方式处理时,这种方法是有用的。当一个 catch 块处理多个异常时,catch 块参数是隐式 final 的。这意味着无法给参数赋新值。以下是非法的,使用它会导致语法错误:

}
catch (InputMismatchException | InvalidParameter e) {
e = new Exception(); // multi-catch parameter e may not be assigned
logger.log(Level.INFO, "Invalid input, try again");
}

除了比使用多个 catch 块更可读和更简洁之外,生成的字节码也更小,不会产生重复的代码。

还有更多...

一组异常的基类影响何时使用 catch 块捕获多个异常。此外,断言在创建健壮的应用程序时是有用的。这些问题如下所述:

  • 使用一个共同的异常基类和java.lang.ReflectiveOperationException

  • 在 Java 7 中使用java.lang.AssertionError

使用一个共同的异常基类和 ReflectiveOperationException

当不同的异常需要以相同的方式处理时,在同一个 catch 块中捕获多个异常是有用的。但是,如果多个异常共享一个公共基础异常类,那么捕获基类异常可能更简单。这是许多IOException派生类的情况。

例如,Files类的delete方法可能会抛出以下四种不同的异常之一:

  • java.nio.file.NoSuchFileException

  • java.nio.file.DirectoryNotEmptyException

  • java.io.IOException

  • java.lang.SecurityException

其中,NoSuchFileExceptionDirectoryNotEmptyException最终都是从IOException派生出来的。因此,捕获IOException可能足够,就像下面的代码所示:

public class ReflectiveOperationExceptionExample {
public static void main(String[] args) {
try {
Files.delete(Paths.get(new URI("file:///tmp.txt")));
}
catch (URISyntaxException ex) {
ex.printStackTrace();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}

在这个例子中,注意URI构造函数可能抛出URISyntaxException异常。在第四章的食谱删除文件或目录中,详细介绍了delete方法的使用。

在 Java 7 中,ReflectiveOperationExceptionjava.lang包中新增的一个异常。它是以下异常的基类:

  • ClassNotFoundException

  • IllegalAccessException

  • InstantiationException

  • InvocationTargetException

  • NoSuchFieldException

  • NoSuchMethodException

这个异常类可以简化反射类型异常的处理。多异常捕获机制更适用于那些没有共同基类的异常集合。

注意

一般来说,最好捕获尽可能特定于问题的异常。例如,处理缺少文件时,最好捕获NoSuchFileException而不是更广泛的Exception,这提供了更多关于异常的细节。

在 Java 7 中使用 AssertionError 类

断言在构建更健壮的应用程序中很有用。关于这个主题的很好介绍可以在download.oracle.com/javase/1.4.2/docs/guide/lang/assert.html找到。在 Java 7 中,添加了一个新的构造函数,允许将消息附加到用户生成的断言错误。此构造函数有两个参数。第一个是与AssertionError关联的消息,第二个是Throwable子句。

在此配方中早期开发的MultipleExceptions类中,我们测试了数字是否小于零,如果是,则抛出异常。在这里,我们将通过抛出AssertionError来说明使用AssertionError构造函数,如果数字大于 10。

将以下代码添加到main方法中,靠近原始数字的测试:

if(number>10) {
throw new AssertionError("Number was too big",new Throwable("Throwable assertion message"));
}

再次执行程序并输入12。您的结果应该类似于以下内容:

输入一个数字:12

线程"main"中的异常 java.lang.AssertionError:数字太大

在 packt.MultipleExceptions.main(MultipleExceptions.java:28)

Caused by: java.lang.Throwable: Throwable assertion message

... 1 more

Java 结果:1

在 Java 7 之前,不可能将消息与用户生成的AssertionError关联起来。

另请参阅

Files类的使用详细介绍在第四章中,管理文件和目录

在 Java 7 中重新抛出异常

当在 catch 块中捕获异常时,有时希望重新抛出异常。这允许当前方法和调用当前方法的方法处理异常。

然而,在 Java 7 之前,只能重新抛出基类异常。当需要重新抛出多个异常时,您被限制在方法声明中声明一个公共基类。现在,可以对可以为方法抛出的异常更加严格。

做好准备

为了在 Java 中重新抛出异常,必须首先捕获它们。在 catch 块内部,使用throw关键字和要抛出的异常。Java 7 中的新的重新抛出技术要求您:

  • 在 catch 块中使用基类异常类

  • 使用throw关键字从 catch 块抛出派生类异常

  • 修改方法的签名以抛出派生异常

如何做...

  1. 我们将修改在Catching multiple exception types to improve type checking配方中开发的ReflectiveOperationExceptionExample类。修改main方法,以在 try 块中调用deleteFile方法,如下面的代码所示:
public class ReflectiveOperationExceptionExample {
public static void main(String[] args) {
try {
deleteFile(Paths.get(new URI("file:///tmp.txt")));
}
catch (URISyntaxException ex) {
ex.printStackTrace();
}
catch (IOException ex) {
ex.printStackTrace();
}
}

  1. 添加deleteFile方法,如下所示:
private static void deleteFile(Path path) throws NoSuchFileException, DirectoryNotEmptyException {
Java 7exceptions, rethrowingtry {
Files.delete(path);
}
catch (IOException ex) {
if(path.toFile().isDirectory()) {
throw new DirectoryNotEmptyException(null);
}
else {
throw new NoSuchFileException(null);
}
}
}
}

  1. 使用不存在的文件执行应用程序。输出应该如下:

java.nio.file.NoSuchFileException

在 packt.ReflectiveOperationExceptionExample.deleteFile(ReflectiveOperationExceptionExample.java:33)

在 packt.ReflectiveOperationExceptionExample.main(ReflectiveOperationExceptionExample.java:16)

它是如何工作的...

main方法调用并处理了deleteFile调用生成的异常。该方法声明可以抛出NoSuchFileExceptionDirectoryNotEmptyException。请注意,基类IOException用于捕获异常。在 catch 块内部,使用File类的isDirectory方法进行测试,以确定异常的原因。确定异常的根本原因后,抛出适当的异常。Files类的使用详细介绍在第四章中,管理文件和目录

通过明确指定方法可能抛出的异常,我们可以清楚地了解方法的调用者可以期望什么。 此外,它可以防止方法意外抛出其他IOException派生的异常。 此示例的缺点是,如果另一个异常,例如FileSystemException,是根本原因,那么我们将错过它。 它将在deleteFile方法中捕获,因为它是从IOException派生的。 但是,我们未能在方法中处理它或将其传递给调用方法。

另请参阅

前三个配方提供了 Java 7 中异常处理的其他覆盖范围。

在构造函数类型推断中使用钻石操作符

使用钻石操作符简化了创建对象时的泛型使用。 它避免了程序中的未经检查的警告,并通过不需要显式重复指定参数类型来减少泛型冗长。 相反,编译器推断类型。 动态类型语言一直这样做。 虽然 Java 是静态类型的,但是钻石操作符的使用允许比以前更多的推断。 编译后的代码没有区别。

编译器将推断构造函数的参数类型。 这是约定大于配置的一个例子(en.wikipedia.org/wiki/Convention_over_configuration)。 通过让编译器推断参数类型(约定),我们避免了对象的显式规范(配置)。 Java 还在许多领域使用注释来影响这种方法。 类型推断现在可用,而以前只能用于方法。

准备就绪

使用钻石操作符:

  1. 创建对象的通用声明。

  2. 使用钻石操作符<>来指定要使用的类型推断。

如何做...

  1. 创建一个简单的 Java 应用程序,其中包含一个main方法。 将以下代码示例添加到main方法中,以查看它们的工作原理。 例如,要声明字符串的java.util.List,我们可以使用以下内容:
List<String> list = new ArrayList<>();

  1. 标识符list声明为字符串列表。 钻石操作符<>用于推断List类型为String。 对于此代码不会生成警告。

它是如何工作的...

当创建对象时没有指定数据类型时,称为原始类型。 例如,在实例化标识符list时,以下使用了原始类型:

List<String> list = new ArrayList(); // Uses raw type

编译代码时,将生成以下警告:

注意:packt\Bin.java 使用未经检查或不安全的操作

注意:重新编译时使用-Xlint:unchecked 以获取详细信息

将生成未经检查的警告。 通常希望在应用程序中消除未经检查的警告。 使用—Xlint:unchecked时,我们会得到以下结果:

packt\Bin.java:26: 警告:[unchecked]未经检查的转换

List arrayList = new ArrayList();

^

需要:List

找到:ArrayList

1 个警告

在 Java 7 之前,我们可以通过显式使用参数类型来解决此警告,如下所示:

List<String> list = new ArrayList<String>();

使用 Java 7,钻石操作符使这更短,更简单。 此操作符在处理更复杂的数据类型时变得更加有用,例如,ListMap对象如下所示:

List<Map<String, List<String>> stringList = new ArrayList<>();

还有更多...

还有几个类型推断的方面需要讨论:

  • 在类型不明显时使用钻石操作符

  • 抑制未经检查的警告

  • 了解擦除

在类型不明显时使用钻石操作符

在 Java 7 及更高版本中支持类型推断,只有构造函数的参数类型是明显的情况下才支持。 例如,如果我们在不指定类型的情况下使用钻石操作符,如下所示,将会收到一系列警告:

List arrayList = new ArrayList<>();
arrayList.add("First");
arrayList.add("Second");

使用—Xlint:unchecked编译程序,将得到以下警告:

... packt\Bin.java:29: 警告:[unchecked]未经检查的调用 add(E)作为原始类型 ArrayList 的成员

arrayList.add("First");

“其中 E 是类型变量:”

E 扩展 Object 在 ArrayList 类中声明

“... \packt\Bin.java:30:警告:[unchecked]未经检查的调用 add(E)作为原始类型 ArrayList 的成员”

arrayList.add("Second");

“其中 E 是类型变量:”

E 扩展 Object 在 ArrayList 类中声明

2 个警告

如果指定数据类型,则这些警告将消失:

List<String> arrayList = new ArrayList<>();

抑制未经检查的警告

虽然不一定是理想的,但可以使用@SuppressWarnings注解来抑制由于未使用菱形操作符而生成的未经检查的异常。以下是一个示例:

@SuppressWarnings("unchecked")
List<String> arrayList = new ArrayList();

理解擦除

当使用泛型时会发生擦除。声明中使用的数据类型在运行时不可用。这是在 Java 1.5 引入泛型时做出的语言设计决定,以使代码向后兼容。

考虑以下三种方法。它们只在arrayList变量的声明中有所不同:

private static void useRawType() {
List<String> arrayList = new ArrayList();
arrayList.add("First");
arrayList.add("Second");
System.out.println(arrayList.get(0));
}
private static void useExplicitType() {
List<String> arrayList = new ArrayList<String>();
arrayList.add("First");
arrayList.add("Second");
System.out.println(arrayList.get(0));
}
private static void useImplicitType() {
List<String> arrayList = new ArrayList<>();
arrayList.add("First");
arrayList.add("Second");
System.out.println(arrayList.get(0));
}

当这些方法被编译时,编译时可用的类型信息将丢失。如果我们检查这三种方法的编译后字节码,我们会发现它们之间没有区别。

使用以下命令将显示程序的字节码:

javap -v -p packt/Bin

这三种方法生成的代码是相同的。useImplicitType 的代码如下所示。它与其他两种方法相同;

private static void useImplicitType();
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: new #5 // class java/util/ArrayList
3: dup
4: invokespecial #6 // Method java/util/ArrayList."<in
it>":()V
7: astore_0
8: aload_0
9: ldc #7 // String First
11: invokevirtual #8 // Method java/util/ArrayList.add:
(Ljava/lang/Object;)Z
14: pop
15: aload_0
16: ldc #9 // String Second
18: invokevirtual #8 // Method java/util/ArrayList.add:
(Ljava/lang/Object;)Z
21: pop
22: getstatic #10 // Field java/lang/System.out:Ljav
a/io/PrintStream;
25: aload_0
26: iconst_0
27: invokevirtual #11 // Method java/util/ArrayList.get:
(I)Ljava/lang/Object;
30: checkcast #12 // class java/lang/String
33: invokevirtual #13 // Method java/io/PrintStream.prin
tln:(Ljava/lang/String;)V
36: return

使用@SafeVarargs 注解

@SafeVarargs@SuppressWarnings注解可用于处理通常是无害的各种警告。@SuppressWarnings注解,顾名思义,将抑制特定类型的警告。

@SafeVarargs注解是在 Java 7 中引入的,用于指定某些使用可变数量参数的方法和构造函数是安全的。方法可以传递可变数量的参数。这些参数可能是泛型。如果是,那么可能希望使用@SafeVarargs注解来抑制无害的警告。

准备就绪

@SafeVarargs注解用于构造函数和方法。要使用@SafeVarargs注解,需要按照以下步骤进行:

  1. 创建使用可变数量的泛型参数的方法或构造函数。

  2. 在方法声明之前添加@SafeVarargs注解。

在 Java 7 中,使用泛型可变参数方法或构造函数会生成强制性的编译器警告。使用@SafeVarargs注解可以抑制这些警告,当这些方法或构造函数被认为是无害的时候。

如何做…

  1. 为了演示@SafeVarargs注解,创建一个名为displayElements的应用程序,该方法显示有关每个参数及其值的信息:
package packt;
import java.util.ArrayList;
public class SafeVargExample {
public static void main(String[] args) {
}
@SafeVarargs
public static <T> void displayElements(T... array) {
for (T element : array) {
System.out.println(element.getClass().getName() + ": " + element);
}
}
}

该方法使用可变数量的泛型参数。Java 将可变数量的参数实现为对象数组,该数组仅包含可重用类型。可重用类型在“它是如何工作”的部分中讨论。

  1. main方法中添加以下代码以测试该方法:
ArrayList<Integer> a1 = new ArrayList<>();
a1.add(new Integer(1));
a1.add(2);
ArrayList<Float> a2 = new ArrayList<>();
a2.add(new Float(3.0));
a2.add(new Float(4.0));
displayElements(a1, a2, 12);

  1. 执行应用程序。输出应如下所示:

java.util.ArrayList: [1, 2]

java.util.ArrayList: [3.0, 4.0]

java.lang.Integer: 12

  1. 注意在声明java.util.ArrayList时使用了菱形操作符<>。这个操作符是 Java 7 中的新功能,在“使用菱形操作符进行构造函数类型推断”这个主题中进行了讨论。

它是如何工作的…

在 Java 中,使用..符号创建具有可变数量参数的方法或构造函数,就像在displayElements方法中使用的那样。在这种情况下,元素类型是泛型的。

基本问题是泛型和数组无法很好地配合。当泛型在 Java 语言中添加到 1.5 时,它们被实现为使它们与早期代码向后兼容。这意味着它们是使用擦除实现的。也就是说,编译时可用的任何类型信息在运行时被移除。这些数据被称为不可实体化

数组是实体化的。有关数组元素类型的信息被保留并可以在运行时使用。请注意,不可能声明一个泛型数组。可以按以下方式创建一个简单的字符串数组:

String arr[] = {"First", "Second"};

然而,我们不能创建一个泛型数组,比如下面的例子:

List<String> list1 = new ArrayList<String>();
list1.add("a");
List<String> list2 = new ArrayList<String>();
list2.add("b");
List<String> arr[] = {list1, list2}

这段代码将生成以下错误消息:

无法创建 List的泛型数组

使用可变数量的参数的方法被实现为对象数组。它只能处理可实体化的类型。当调用使用可变数量的参数的方法时,将创建一个数组来保存这些参数。

由于我们使用了具有可变数量的泛型参数的方法,可能会出现称为堆污染的运行时问题。当将参数化类型的变量分配给与其定义时使用的类型不同的类型时,将在运行时表现为未经检查的警告。在运行时,它将导致java.lang.ClassCastException。使用@SafeVarargs注解将一个方法指定为避免堆污染的方法。

使用可变数量的泛型参数的方法将导致编译时警告。然而,并非所有使用可变数量的泛型参数的方法都会导致运行时异常。@SafeVarargs用于标记安全方法为安全。如果可能发生运行时异常,则不应使用该注解。这在下一节中进一步探讨。

请注意,如果没有使用@SafeVarargs注解,将生成以下警告:

警告:[unchecked]为类型 ArrayList<? extends INT#1>[]的可变参数创建了未经检查的泛型数组

警告:[unchecked]可能会导致参数化可变参数类型 T 的堆污染

第一个警告适用于displayElements调用,第二个警告适用于实际方法。代码没有问题,因此可以完全接受这些警告的抑制。

我们可以使用@SuppressWarnings("unchecked")注解来抑制方法声明处的警告,但在使用时仍会生成警告。使用@SafeVarargs可以在两个地方抑制警告。

还有更多...

还有一个有趣的地方是:

  • 在 Java 核心库中使用@SafeVarargs注解

  • 堆污染的一个例子

在 Java 核心库中使用@SafeVarargs 注解

JDK 1.7 库已经包含了@SafeVarargs注解。其中包括以下内容:

  • public static <T> List<T> java.util.Arrays.asList(T... a)

  • public static <T> boolean java.util.Collections.addAll(Collection<? super T> c, T... elements)

  • public static <E extends Enum<E>> java.util.EnumSet<E> EnumSet.of(E first, E... rest)

  • protected final void javax.swing.SwingWorker.publish(V... chunks)

这些方法被标记为@SafeVarargs注解,表示它们不会导致堆污染。这些方法被认为是安全的。

堆污染的一个例子

一些方法不应标记为安全,如下面从@SafeVarargs注解的 javadoc 描述中的代码所示(download.oracle.com/javase/7/docs/api/index.htmljava.lang.SafeVarargs注解文档下)。

在您的代码中添加以下方法:

@SafeVarargs // Not actually safe!
static void merge(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // Semantically invalid, but compiles without warnings
String element = stringLists[0].get(0); // runtime ClassCastException
}

使用以下代码测试该方法:

List<String> list1 = new ArrayList<>();
list1.add("One");
list1.add("Two");
list1.add("Three");
List<String> list2 = new ArrayList<>();
list2.add("Four");
list2.add("Five");
list2.add("Six");
merge(list1,list2);

执行程序。您应该会收到以下错误消息:

异常线程"main"java.lang.ClassCastException:java.lang.Integer 无法转换为 java.lang.String

一个字符串列表被传递给方法,并分配给标识符stringList。接下来,声明了一个对象数组,并将其分配给了由stringList引用的相同对象。在这一点上,stringListarray引用了同一个对象,即java.util.List的字符串。以下说明了此时内存的配置:

堆污染的示例

通过以下分配:

array[0] = tmpList

数组的第一个元素被重新分配给了tmpList。这个重新分配在下图中有所说明:

堆污染的示例

在这一点上,我们已经有效地将一个Integer对象分配给了一个String引用变量。它已被分配给了stringListsarray引用的数组的第一个元素。虚线显示了旧的引用,它已被替换为该行。当在运行时尝试将这个Integer对象分配给String引用变量时,会发生ClassCastException

这种方法会导致堆污染,不应该用@SafeVarargs进行注释,因为它不安全。允许将tmpList分配给数组的第一个元素,因为我们只是将一个List<Integer>对象分配给了一个Object引用变量。这是 Java 中合法的向上转型的一个例子。

另请参阅

前面的配方*使用菱形操作符进行构造类型推断解释了泛型使用的改进。

第二章:使用路径定位文件和目录

在本章中,我们将涵盖以下内容:

  • 创建 Path 对象

  • java.io.File 和 java.nio.file.Files 之间的互操作性

  • 将相对路径转换为绝对路径

  • 通过规范化路径来消除冗余

  • 使用路径解析合并路径

  • 在两个位置之间创建路径

  • 在不同路径类型之间转换

  • 确定两个路径是否等价

  • 管理符号链接

介绍

文件系统是计算机上组织数据的一种方式。通常,它由一个或多个顶级目录组成,每个目录包含一系列文件。顶级目录通常被称为根。此外,文件系统存储在介质上,称为文件存储。

Java 7 引入了许多新的类和接口,使得与文件系统的工作更加简单和高效。这些类在很大程度上取代了java.io包中的旧类。

在本章和后续章节中,我们将演示如何使用目录结构管理文件系统,如下图所示:

Introduction

椭圆代表目录,而矩形代表文件。基于 Unix 的系统和 Windows 系统在对根节点的支持上有所不同。Unix 系统支持单个根节点,而 Windows 系统允许多个根节点。目录或文件的位置使用路径来描述。路径的元素、目录和文件之间用正斜杠或反斜杠分隔。在 Unix 中使用正斜杠,在 Windows 中使用反斜杠。

音乐文件来自freepd.com/70s%20Sci%20Fi/status.txt用于保存简单的状态信息,而users.txt则用于保存用户列表。音乐目录中的users.txt文件是指向docs目录中实际文件的符号链接,如红线所示。这些文件将在本章的各个示例中使用。当然,您可以使用任何您希望的文件或文件结构。

符号链接在基于 Unix 的平台上更常见。要为音乐目录中的users.txt文件创建符号链接,请在命令控制台中使用以下命令:mklink users.txt c:\home\docs\users.txt。这需要管理员权限才能执行。

本章涉及由java.nio.file.Path类表示的路径的管理。Path对象被java.nio包中的类广泛使用,由以下几个部分组成:

  • 作为路径基础的根目录,比如 C 盘

  • 用于分隔路径中组成目录和文件的名称的分隔符

  • 中间目录的名称

  • 终端元素,可以是文件或目录

这些内容在理解路径一节中进行了讨论和说明。以下是处理文件和目录的类:

  • java.nio.file.Paths包含用于创建Path对象的静态方法

  • java.nio.file.Path接口包含许多用于处理路径的方法

  • java.nio.file.FileSystems是用于访问文件系统的主要类

  • java.nio.file.FileSystem表示文件系统,比如 UNIX 系统上的/或 Windows 平台上的 C 盘

  • java.nio.file.FileStore表示实际存储设备并提供设备特定信息

  • java.nio.file.attribute.FileStoreAttributeView提供对文件信息的访问

最后两个类在后续章节中会更深入地讨论。为了访问文件或目录,我们通常会使用FileSystems类的getDefault方法来检索 JVM 可访问的文件系统的引用。要访问特定驱动器,我们可以使用getFileSystem方法,传入表示感兴趣的驱动器或目录的统一资源标识符URI)对象。

FileSystems类提供了创建或访问文件系统的技术。在本章中,我们对类如何支持创建Path对象感兴趣。一旦我们有了文件系统对象的引用,我们就可以使用几种方法之一获取Path对象:

  • getPath:这使用系统相关路径来获取Path对象。Path对象用于定位和访问文件。

  • getPathMatcher:这将创建一个PathMatcher。它执行文件的各种匹配类型操作,并在第五章的“获取文件系统信息”配方中进行了讨论。

  • getRootDirectories:用于获取根目录列表。这个方法在第五章的“获取文件系统信息”配方中进行了说明。

理解路径配方介绍了Path对象的创建和一般用法。这些知识在后续配方和其他章节中使用,因此请确保理解本配方中涵盖的基本过程。

您仍然可以使用较旧的java.io包元素。可以使用File类的toPath方法创建表示java.io.File对象的路径。这在java.io.File 和 java.nio.file.Files 之间的互操作性配方中进行了讨论,并且在维护较旧的代码时可能会有用。

路径可以是相对的,也可以是绝对的。这些类型的路径以及处理它们的技术在“使用相对和绝对路径”配方中进行了讨论。

路径可能包含冗余和多余的元素。去除这些元素称为规范化。通过“通过规范化路径来删除路径中的冗余”配方,我们可以检查简化这些类型路径的可用技术。

路径可以组合成一个新的复合路径。这称为解析路径,并在使用路径解析合并路径配方中进行了讨论。这种技术可以用于创建新的路径,其中路径的部分来自不同的来源。

当需要文件的引用时,该路径有时相对于当前位置或其他位置。在两个位置之间创建路径配方说明了创建这样一个路径的过程。这个过程称为相对化

不仅有相对和绝对路径,还有其他表示路径的方式,例如使用java.net.URI对象。创建Path对象时,并不一定需要实际路径存在。例如,可以创建Path以创建新的文件系统元素。在不同路径类型之间转换配方介绍了用于在这些不同类型路径之间转换的方法。

路径是依赖于系统的。也就是说,UNIX 系统上的路径与 Windows 系统上找到的路径不同。比较在同一平台上找到的两个路径可能相同,也可能不同。这在确定两个路径是否等效配方中进行了研究。

创建 Path 对象

需要路径来标识目录或文件。本配方的重点是如何为典型的文件和目录操作获取Path对象。路径在本章和许多后续章节中用于大多数配方,这些配方涉及文件和目录。

有几种方法可以创建或返回Path对象。在这里,我们将研究用于创建Path对象的方法以及如何使用其方法来进一步了解 Java 中使用的路径概念。

准备工作

为了创建Path对象,我们需要使用以下方法之一:

  • FileSystem类的getPath方法

  • Paths类的get方法

我们将首先使用getPath方法。get方法在本配方的更多部分中进行了解释。

如何做...

  1. 创建一个带有main方法的控制台应用程序。在main方法中,添加以下代码序列,为文件status.txt创建一个Path对象。我们将使用几种Path类的方法来检查创建的路径,如下所示:
Path path = FileSystems.getDefault().getPath("/home/docs/status.txt");
System.out.println();
System.out.printf("toString: %s\n", path.toString());
System.out.printf("getFileName: %s\n", path.getFileName());
System.out.printf("getRoot: %s\n", path.getRoot());
System.out.printf("getNameCount: %d\n", path.getNameCount());
for(int index=0; index<path.getNameCount(); index++) {
System.out.printf("getName(%d): %s\n", index, path.getName(index));
}
System.out.printf("subpath(0,2): %s\n", path.subpath(0, 2));
System.out.printf("getParent: %s\n", path.getParent());
System.out.println(path.isAbsolute());
}

  1. 注意在path字符串中使用正斜杠。这种方法在任何平台上都可以工作。但是,在 Windows 上,您还可以使用如下所示的反斜杠:
Path path = FileSystems.getDefault().getPath("\\home\\docs\\status.txt");

  1. 在 Windows 平台上,任何一种方法都可以工作,但使用正斜杠更具可移植性。

  2. 执行程序。您的输出应该如下所示:

toString: \home\docs\status.txt

getFileName: status.txt

getRoot: \

getNameCount: 3

getName(0): home

getName(1): docs

getName(2): status.txt

subpath(0,2): home\docs

getParent: \home\docs

false

它是如何工作的...

使用调用链接创建了Path对象,从FileSystems类的getDefault方法开始。这返回一个表示 JVM 可用文件系统的FileSystem对象。FileSystem对象通常指的是当前用户的工作目录。接下来,使用表示感兴趣文件的字符串执行了getPath方法。

代码的其余部分使用了各种方法来显示有关路径的信息。正如本章介绍中所详细介绍的那样,我们可以使用Path类的方法来显示有关路径部分的信息。toString方法针对路径执行,以说明默认情况下会得到什么。

getFileName返回了Path对象的文件名,getRoot返回了根目录。getNameCount方法返回了中间目录的数量加上一个文件名。for 循环列出了路径的元素。在这种情况下,有两个目录和一个文件,总共三个。这三个元素组成了路径。

虽然使用简单的 for 循环来显示这些名称,但我们也可以使用iterator方法来列出这些名称,如下面的代码所示:

Iterator iterator = path.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}

Path对象可能包括其他路径。可以使用subpath方法检索子路径。该方法具有两个参数。第一个表示初始索引,第二个参数指定排他性的最后索引。在此示例中,第一个参数设置为 0,表示要检索根级目录。最后一个索引设置为 2,这意味着只列出了顶部两个目录。

在这种情况下,getParent方法也返回相同的路径。但是,请注意它以反斜杠开头。这表示从每个元素的顶级元素开始,但最后一个元素除外的路径。

还有更多...

有几个问题需要进一步考虑:

  • 使用Paths类的get方法

  • 父路径的含义

使用 Paths 类的 get 方法

Paths类的get方法也可以用于创建Path对象。此方法使用可变数量的String参数来构造路径。在以下代码序列中,创建了一个从当前文件系统的根目录开始的path

try {
path = Paths.get("/home", "docs", "users.txt");
System.out.printf("Absolute path: %s", path.toAbsolutePath());
}
catch (InvalidPathException ex) {
System.out.printf("Bad path: [%s] at position %s",
ex.getInput(), ex.getIndex());
}

使用toAbsolutePath方法的输出显示了构建的路径。注意“E”元素。代码在 Windows 系统上执行,当前驱动器为“E”驱动器。toAbsolutePath方法在“使用相对路径和绝对路径”配方中进行了讨论。

绝对路径: E:\home\docs\users.txt

如果我们在路径的String中不使用斜杠,那么路径是基于当前工作目录创建的。删除斜杠并执行程序。您的输出应该类似于以下内容,其中“currentDirectory”被执行代码时使用的内容替换:

绝对路径: currentDirectory\home\docs\users.txt

使用“resolve”方法是一种更灵活的方法,如“使用路径解析合并路径”配方中所讨论的。

将输入参数转换为路径是依赖于系统的。如果用于创建路径的字符对于文件系统无效,则会抛出java.nio.file.InvalidPathException。例如,在大多数文件系统中,空值是一个非法字符。为了说明这一点,将反斜杠 0 添加到path字符串中,如下所示:

path = Paths.get("/home\0", "docs", "users.txt");

执行时,部分输出将如下所示:

错误路径:[/home \docs\users.txt] 位置在第 5 位

InvalidPathException类的getInput方法返回用于创建路径的连接字符串。getIndex方法返回有问题的字符的位置,在本例中是空字符。

父路径的含义

getParent方法返回父路径。但是,该方法不访问文件系统。这意味着对于给定的Path对象,可能有也可能没有父级。

考虑以下路径声明:

path = Paths.get("users.txt");

这是在当前工作目录中找到的users.txt文件。getNameCount将返回 1,getParent方法将返回 null。实际上,文件存在于目录结构中,并且有一个根和一个父级。因此,该方法的结果在某些情境下可能无用。

使用此方法大致相当于使用subpath方法:

path = path.subpath(0,path.getNameCount()-1));

另请参阅

toRealPath方法在使用相对路径和绝对路径通过规范化路径来消除冗余中有讨论。

java.io.File 和 java.nio.file.Files 之间的互操作性

在引入java.nio包之前,java.io包的类和接口是 Java 开发人员用于处理文件和目录的唯一可用选项。虽然较新的包已经补充了java.io包的大部分功能,但仍然可以使用旧类,特别是java.io.File类。本文介绍了如何实现这一点。

准备工作

要使用File类获取Path对象,需要按照以下步骤进行:

  1. 创建一个表示感兴趣文件的java.io.File对象

  2. 应用toPath方法以获得Path对象

如何做...

  1. 创建一个控制台应用程序。添加以下主要方法,我们在其中创建一个File对象和一个表示相同文件的Path对象。接下来,我们比较这两个对象,以确定它们是否表示相同的文件:
public static void main(String[] args) {
try {
Path path =
Paths.get(new URI("file:///C:/home/docs/users.txt"));
File file = new File("C:\\home\\docs\\users.txt");
Path toPath = file.toPath();
System.out.println(toPath.equals(path));
}
catch (URISyntaxException e) {
System.out.println("Bad URI");
}
}

  1. 当执行应用程序时,输出将为 true。

工作原理...

创建了两个Path对象。第一个Path对象是使用Paths类的get方法声明的。它使用java.net.URI对象为users.txt文件创建了一个Path对象。第二个Path对象toPath是从File对象使用toPath方法创建的。使用Pathequals方法来证明这些路径是等价的。

提示

注意使用正斜杠和反斜杠表示文件的字符串。URI字符串使用正斜杠,这是与操作系统无关的。而反斜杠用于 Windows 路径。

另请参阅

理解路径中演示了创建Path对象。此外,使用相对路径和绝对路径中讨论了创建URI对象。

将相对路径转换为绝对路径

路径可以表示为绝对路径或相对路径。两者都很常见,在不同情况下都很有用。Path类和相关类支持创建绝对路径和相对路径。

相对路径用于指定文件或目录的位置与当前目录位置的关系。通常,使用一个点或两个点来表示当前目录或下一个更高级目录。但是,在创建相对路径时,不需要使用点。

绝对路径从根级别开始,列出每个目录,用正斜杠或反斜杠分隔,取决于操作系统,直到达到所需的目录或文件。

在本示例中,我们将确定当前系统使用的路径分隔符,并学习如何将相对路径转换为绝对路径。在处理文件名的用户输入时,这是有用的。与绝对和相对路径相关的是路径的 URI 表示。我们将学习如何使用Path类的toUri方法来返回给定路径的这种表示。

准备工作

在处理绝对和相对路径时,经常使用以下方法:

  • getSeparator方法确定文件分隔符

  • subpath方法获取路径的一个部分或所有部分/元素

  • toAbsolutePath方法获取相对路径的绝对路径

  • toUri方法获取路径的 URI 表示

如何做...

  1. 我们将逐个解决前面的每个方法。首先,使用以下main方法创建一个控制台应用程序:
public static void main(String[] args) {
String separator = FileSystems.getDefault().getSeparator();
System.out.println("The separator is " + separator);
try {
Path path = Paths.get(new URI("file:///C:/home/docs/users.txt"));
System.out.println("subpath: " + path.subpath(0, 3));
path = Paths.get("/home", "docs", "users.txt");
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("URI: " + path.toUri());
}
catch (URISyntaxException ex) {
System.out.println("Bad URI");
}
catch (InvalidPathException ex) {
System.out.println("Bad path: [" + ex.getInput() + "] at position " + ex.getIndex());
}
}

  1. 执行程序。在 Windows 平台上,输出应如下所示:

*分隔符是*

子路径:home\docs\users.txt

绝对路径:E:\home\docs\users.txt

URI:file:///E:/home/docs/users.txt

工作原理...

getDefault方法返回一个表示 JVM 当前可访问的文件系统的FileSystem对象。对此对象执行getSeparator方法,返回一个反斜杠字符,表示代码在 Windows 机器上执行。

users.txt文件创建了一个Path对象,并对其执行了subpath方法。这个方法在理解路径中有更详细的讨论。subpath方法总是返回一个相对路径。

接下来,使用get方法创建了一个路径。由于第一个参数使用了正斜杠,路径从当前文件系统的根开始。在这个例子中,提供的路径是相对的。

路径的 URI 表示与绝对和相对路径相关。Path类的toUri方法返回给定路径的这种表示。URI对象用于表示互联网上的资源。在这种情况下,它返回了一个文件的 URI 方案形式的字符串。

绝对路径可以使用Path类的toAbsolutePath方法获得。绝对路径包含路径的根元素和所有中间元素。当用户被要求输入文件名时,这可能很有用。例如,如果用户被要求提供一个文件名来保存结果,文件名可以添加到表示工作目录的现有路径中。然后可以获取绝对路径并根据需要使用。

还有更多...

请记住,toAbsolutePath方法无论路径引用有效文件还是目录都可以工作。前面示例中使用的文件不需要存在。考虑使用如下代码中所示的虚假文件。假设文件bogusfile.txt不存在于指定目录中:

Path path = Paths.get(new URI("file:///C:/home/docs/bogusfile.txt"));
System.out.println("File exists: " + Files.exists(path));
path = Paths.get("/home", "docs", "bogusfile.txt");
System.out.println("File exists: " + Files.exists(path));

程序执行时,输出如下:

*分隔符是*

文件存在:false

子路径:home\docs\bogusfile.txt

文件存在:false

绝对路径:E:\home\docs\bogusfile.txt

URI:file:///E:/home/docs/bogusfile.txt

如果我们想知道这是否是一个真实的路径,我们可以使用toRealPath方法,如通过规范化路径来删除路径中的冗余中所讨论的那样。

另请参阅

可以使用normalize方法删除路径中的冗余,如通过规范化路径来删除路径中的冗余中所讨论的那样。

当符号链接用于文件时,路径可能不是文件的真实路径。Path类的toRealPath方法将返回文件的真实绝对路径。这在通过规范化路径来消除冗余示例中进行了演示。

通过规范化路径消除冗余

当在定义路径时使用“.”或“..”符号时,它们的使用可能会引入冗余。也就是说,所描述的路径可能通过删除或以其他方式更改路径来简化。本示例讨论了使用normalize方法来影响这种转换。通过简化路径,可以避免错误并提高应用程序的性能。toRealPath方法还执行规范化,并在本示例的还有更多...部分进行了解释。

准备就绪

消除路径中冗余的基本步骤包括以下内容:

  • 识别可能包含冗余的路径

  • 使用normalize方法消除冗余

如何做...

介绍中的目录结构在此处复制以方便起见:

如何做...

首先考虑以下路径:

/home/docs/../music/ Space Machine A.mp3
/home/./music/ Robot Brain A.mp3

这些包含冗余或多余的部分。在第一个示例中,路径从home开始,然后进入docs目录的一个目录级别。然后,.符号将路径返回到home目录。然后继续进入music目录并到mp3文件。docs/.元素是多余的。

在第二个示例中,路径从home开始,然后遇到一个句点。这代表当前目录,即home目录。接下来,路径进入music目录,然后遇到mp3文件。/是多余的,不需要。

  1. 创建一个新的控制台应用程序,并添加以下main方法:
public static void main(String[] args) {
Path path = Paths.get("/home/docs/../music/Space Machine A.mp3");
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("URI: " + path.toUri());
System.out.println("Normalized Path: " + path.normalize());
System.out.println("Normalized URI: " + path.normalize().toUri());
System.out.println();
path = Paths.get("/home/./music/ Robot Brain A.mp3");
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("URI: " + path.toUri());
System.out.println("Normalized Path: " + path.normalize());
System.out.println("Normalized URI: " + path.normalize().toUri());
}

  1. 执行应用程序。您应该获得以下输出,尽管根目录可能会根据系统配置而有所不同:

绝对路径:E:\home\docs..\music\Space Machine A.mp3

URI:file:///E:/home/docs/../music/Space%20Machine%20A.mp3

规范化路径:\home\music\Space Machine A.mp3

规范化的 URI:file:///E:/home/music/Space%20Machine%20A.mp3

绝对路径:E:\home.\music\ Robot Brain A.mp3

URI:file:///E:/home/./music/%20Robot%20Brain%20A.mp3

规范化路径:\home\music\ Robot Brain A.mp3

规范化的 URI:file:///E:/home/music/%20Robot%20Brain%20A.mp3

它是如何工作的...

使用Paths类的get方法使用先前讨论过的冗余多余路径创建了两个路径。get方法后面的代码显示了绝对路径和 URI 等效项,以说明创建的实际路径。接下来,使用了normalize方法,然后与toUri方法链接,以进一步说明规范化过程。请注意,冗余和多余的路径元素已经消失。toAbsolutePathtoUri方法在使用相对和绝对路径示例中进行了讨论。

normalize方法不会检查文件或路径是否有效。该方法只是针对路径执行语法操作。如果符号链接是原始路径的一部分,则规范化路径可能不再有效。符号链接在管理符号链接示例中讨论。

还有更多...

Path类的toRealPath将返回表示文件实际路径的路径。它会检查路径是否有效,如果文件不存在,则会返回java.nio.file.NoSuchFileException

修改先前的示例,使用toRealPath方法并显示不存在的文件,如下面的代码所示:

try
Path path = Paths.get("/home/docs/../music/NonExistentFile.mp3");
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("Real path: " + path.toRealPath());
}
catch (IOException ex) {
System.out.println("The file does not exist!");
}

执行应用程序。结果应包含以下输出:

绝对路径:\Richard-pc\e\home\docs..\music\NonExistentFile.mp3

文件不存在!

toRealPath方法规范化路径。它还解析任何符号链接,尽管在此示例中没有符号链接。

另请参阅

Path对象的创建在理解路径配方中有所讨论。符号链接在管理符号链接配方中有所讨论。

使用路径解析来组合路径

resolve方法用于组合两个路径,其中一个包含根元素,另一个是部分路径。这在创建可能变化的路径时非常有用,例如在应用程序的安装中使用的路径。例如,可能有一个默认目录用于安装应用程序。但是,用户可能能够选择不同的目录或驱动器。使用resolve方法创建路径允许应用程序独立于实际安装目录进行配置。

准备工作

使用resolve方法涉及两个基本步骤:

  • 创建一个使用根元素的Path对象

  • 对此路径执行resolve方法,使用第二个部分路径

部分路径是指仅提供完整路径的一部分,并且不包含根元素。

如何做...

  1. 创建一个新的应用程序。将以下main方法添加到其中:
public static void main(String[] args) {
Path rootPath = Paths.get("/home/docs");
Path partialPath = Paths.get("users.txt");
Path resolvedPath = rootPath.resolve(partialPath);
System.out.println("rootPath: " + rootPath);
System.out.println("partialPath: " + partialPath);
System.out.println("resolvedPath: " + resolvedPath);
System.out.println("Resolved absolute path: " + resolvedPath.toAbsolutePath());
}

  1. 执行代码。您应该得到以下输出:

rootPath: \home\docs

partialPath: users.txt

resolvedPath: \home\docs\users.txt

解析的绝对路径:E:\home\docs\users.txt

工作原理...

以下三条路径已创建:

  • \home\docs:这是根路径

  • users.txt:这是部分路径

  • \home\docs\users.txt:这是生成的解析路径

通过使用partialPath变量作为resolve方法的参数执行对rootPath变量的操作来创建解析路径。然后显示这些路径以及resolvedPath的绝对路径。绝对路径包括根目录,尽管这在您的系统上可能有所不同。

还有更多...

resolve方法是重载的,一个使用String参数,另一个使用Path参数。resolve方法也可能被误用。此外,还有一个overloadedresolveSibling方法,其工作方式类似于resolve方法,只是它会移除根路径的最后一个元素。这些问题在这里得到解决。

使用String参数与resolve方法

resolve方法是重载的,其中一个接受String参数。以下语句将实现与前面示例相同的结果:

Path resolvedPath = rootPath.resolve("users.txt");

路径分隔符也可以使用如下:

Path resolvedPath = rootPath.resolve("backup/users.txt");

使用这些语句与先前的代码会产生以下输出:

根路径:\home\docs

partialPath: users.txt

resolvedPath: \home\docs\backup\users.txt

解析的绝对路径:E:\home\docs\backup\users.txt

请注意,解析的路径不一定是有效路径,因为备份目录可能存在,也可能不存在。在通过规范化路径来消除路径中的冗余配方中,可以使用toRealPath方法来确定它是否有效。

错误使用resolve方法

resolve方法有三种用法,可能会导致意外行为:

  • 根路径和部分路径的顺序不正确

  • 使用部分路径两次

  • 使用根路径两次

如果我们颠倒resolve方法的使用顺序,也就是将根路径应用于部分路径,那么只会返回根路径。下面的代码演示了这一点:

Path resolvedPath = partialPath.resolve(rootPath);

当执行代码时,我们得到以下结果:

根路径:\home\docs

partialPath: users.txt

resolvedPath: \home\docs

解析的绝对路径:E:\home\docs

这里只返回根路径。部分路径不会附加到根路径上。如下面的代码所示,使用部分路径两次:

Path resolvedPath = partialPath.resolve(partialPath);

将产生以下输出:

rootPath: \home\docs

partialPath: users.txt

resolvedPath: users.txt\users.txt

解析的绝对路径:currentWorkingDIrectory\users.txt\users.txt

请注意,解析的路径是不正确的,绝对路径使用了当前工作目录。如下所示,使用根路径两次:

Path resolvedPath = rootPath.resolve(rootPath);

结果与以相反顺序使用路径时相同:

rootPath: \home\docs

partialPath: users.txt

resolvedPath: \home\docs

解析的绝对路径:E:\home\docs

每当绝对路径被用作resolve方法的参数时,该绝对路径将被返回。如果空路径被用作方法的参数,则根路径将被返回。

使用resolveSibling

resolveSibling方法是重载的,可以接受StringPath对象。使用resolve方法时,部分路径被附加到根路径的末尾。resolveSibling方法与resolve方法不同之处在于,在附加部分路径之前,根路径的最后一个元素被移除。考虑以下代码序列:

Path rootPath = Paths.get("/home/music/");
resolvedPath = rootPath.resolve("tmp/Robot Brain A.mp3");
System.out.println("rootPath: " + rootPath);
System.out.println("resolvedPath: " + resolvedPath);
System.out.println();
resolvedPath = rootPath.resolveSibling("tmp/Robot Brain A.mp3");
System.out.println("rootPath: " + rootPath);
System.out.println("resolvedPath: " + resolvedPath);

当执行时,我们得到以下输出:

rootPath: \home\music

resolvedPath: \home\music\tmp\Robot Brain A.mp3

rootPath: \home\music

resolvedPath: \home\tmp\Robot Brain A.mp3

请注意,解析路径在存在music目录时与使用resolveSibling方法时不同。当使用resolve方法时,目录存在。当使用resolveSibling方法时,目录不存在。如果没有父路径,或者方法的参数是绝对路径,则返回传递给方法的参数。如果参数为空,则返回父目录。

另请参阅

Path对象的创建在理解路径配方中有所讨论。此外,toRealPath方法在通过规范化路径来消除路径中的冗余配方中有所解释。

在两个位置之间创建路径

相对化路径意味着基于另外两个路径创建一个路径,使得新路径表示从原始路径中的一个导航到另一个的方式。这种技术找到了从一个位置到另一个位置的相对路径。例如,第一个路径可以表示一个应用程序默认目录。第二个路径可以表示一个目标目录。从这些目录创建的相对路径可以促进对目标的操作。

准备工作

要使用relativize方法从一个路径到另一个路径创建新路径,我们需要执行以下操作:

  1. 创建一个代表第一个路径的Path对象。

  2. 创建一个代表第二个路径的Path对象。

  3. 对第一个路径使用第二个路径作为参数应用relativize方法。

如何做...

  1. 创建一个新的控制台应用程序,并使用以下main方法。该方法创建两个Path对象,并显示它们之间的相对路径如下:
public static void main(String[] args) {
Path firstPath;
Path secondPath;
firstPath = Paths.get("music/Future Setting A.mp3");
secondPath = Paths.get("docs");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();
firstPath = Paths.get("music/Future Setting A.mp3");
secondPath = Paths.get("music");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();
firstPath = Paths.get("music/Future Setting A.mp3");
secondPath = Paths.get("docs/users.txt");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();
}

  1. 执行应用程序。您的结果应该类似于以下内容:

从 firstPath 到 secondPath: ....\docs

从 secondPath 到 firstPath: ..\music\Future Setting A.mp3

从 firstPath 到 secondPath: ..

从 secondPath 到 firstPath: Future Setting A.mp3

从 firstPath 到 secondPath: ....\docs\users.txt

从 secondPath 到 firstPath: ....\music\Future Setting A.mp3

工作原理...

在第一个例子中,从Future Setting A.mp3文件到docs目录创建了一个相对路径。假定musicdocs目录是兄弟目录。.符号表示向上移动一个目录。本章的介绍说明了这个例子的假定目录结构。

第二个例子演示了从同一目录中创建路径。从firstpathsecondPath的路径实际上是一个潜在的错误。取决于如何使用它,我们可能会最终进入music目录上面的目录,因为返回的路径是.,表示向上移动一个目录级别。第三个例子与第一个例子类似,只是两个路径都包含文件名。

该方法创建的相对路径可能不是有效的路径。通过使用可能不存在的tmp目录来说明,如下所示:

firstPath = Paths.get("music/Future Setting A.mp3");
secondPath = Paths.get("docs/tmp/users.txt");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));

输出应该如下所示:

从 firstPath 到 secondPath: ....\docs\tmp\users.txt

从 secondPath 到 firstPath:......\music\Future Setting A.mp3

还有更多...

还有三种情况需要考虑:

  • 两条路径相等

  • 一条路径包含根

  • 两条路径都包含根

两条路径相等

当两条路径相等时,relativize方法将返回一个空路径,如下面的代码序列所示:

firstPath = Paths.get("music/Future Setting A.mp3");
secondPath = Paths.get("music/Future Setting A.mp3");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();

输出如下:

从 firstPath 到 secondPath:

从 secondPath 到 firstPath:

虽然这不一定是错误,但请注意它不返回一个经常用来表示当前目录的单个点。

一条路径包含根

如果两条路径中只有一条包含根元素,则可能无法构造相对路径。是否可能取决于系统。在下面的例子中,第一条路径包含根元素c:

firstPath = Paths.get("c:/music/Future Setting A.mp3");
secondPath = Paths.get("docs/users.txt");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();

当在 Windows 7 上执行此代码序列时,我们得到以下输出:

线程"main"中的异常"java.lang.IllegalArgumentException: 'other'是不同类型的路径

从 firstPath 到 secondPath:.

从 secondPath 到 firstPath:Future Setting A.mp3

atsun.nio.fs.WindowsPath.relativize(WindowsPath.java:388)

atsun.nio.fs.WindowsPath.relativize(WindowsPath.java:44)

atpackt.RelativizePathExample.main(RelativizePathExample.java:25)

Java 结果:1

注意输出中对other的引用。这指的是relativize方法的参数。

两条路径都包含根

relativize方法在两条路径都包含根元素时创建相对路径的能力也取决于系统。这种情况在下面的例子中有所说明:

firstPath = Paths.get("c:/music/Future Setting A.mp3");
secondPath = Paths.get("c:/docs/users.txt");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println();

在 Windows 7 上执行时,我们得到以下输出:

从 firstPath 到 secondPath:....\docs\users.txt

从 secondPath 到 firstPath:....\music\Future Setting A.mp3

另请参阅

Path对象的创建在理解路径配方中讨论。符号链接的结果取决于系统,并在管理符号链接配方中进行了更深入的讨论。

在路径类型之间进行转换

Path接口表示文件系统中的路径。这个路径可能是有效的,也可能不是。有时我们可能想要使用路径的另一种表示。例如,可以使用文件的URI在大多数浏览器中加载文件。toUri方法提供了路径的这种表示。在这个示例中,我们还将看到如何获取Path对象的绝对路径和真实路径。

准备好了

有三种方法提供替代路径表示:

  • toUri方法返回URI表示

  • toAbsolutePath方法返回绝对路径

  • toRealPath方法返回真实路径

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,我们将使用之前的每种方法。将以下main方法添加到应用程序中:
public static void main(String[] args) {
try {
Path path;
path = Paths.get("users.txt");
System.out.println("URI path: " + path.toUri());
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("Real path: " + path.toRealPath(LinkOption.NOFOLLOW_LINKS));
}
catch (IOException ex) {
Logger.getLogger(ConvertingPathsExample.class.getName()).log(Level.SEVERE, null, ex);
}
}

  1. 如果尚未存在,请在应用程序的工作目录中添加一个users.txt文件。执行程序。您的输出应该类似于以下内容,除了此输出中的..应反映users.txt文件的位置:

URI 路径:file:///.../ConvertingPathsExample/users.txt

绝对路径...\ConvertingPathsExample\users.txt

真实路径:...\ConvertingPathsExample\users.txt

它是如何工作的...

一个users.txt文件被添加到 Java 应用程序的工作目录中。该文件应包含用户名列表。get方法返回表示此文件的Path对象。然后对该对象执行了三种方法。

toUritoAbsolutePath方法按预期返回路径。返回的路径取决于应用程序的工作目录。toRealPath方法应该返回与toAbsolutePath方法相同的输出。这是预期的,因为users.txt文件不是作为符号链接创建的。如果这是一个符号链接,那么将显示代表文件实际路径的不同路径。

还有更多...

由于Path对象可能实际上并不代表文件,如果文件不存在,使用toRealPath方法可能会抛出java.nio.file.NoSuchFileException。使用一个无效的文件名,如下所示:

path = Paths.get("invalidFileName.txt");

输出应该如下所示:

URI 路径:file:///.../ConvertingPathsExample/invalidFileName.txt

绝对路径:...\ConvertingPathsExample\invalidFileName.txt

Sep 11, 2011 6:40:40 PM packt.ConvertingPathsExample main

严重:null

java.nio.file.NoSuchFileException: ...\ConvertingPathsExample\invalidFileName.txt

请注意,toUritoAbsolutePath方法无论指定的文件是否存在都可以工作。在我们想要使用这些方法的情况下,我们可以使用Files类的exists方法来测试文件是否存在。前面的代码序列已经修改为使用exists方法,如下所示:

if(Files.exists(path)) {
System.out.println("Real path: " + path.toRealPath(LinkOption.NOFOLLOW_LINKS));
}
else {
System.out.println("The file does not exist");
}

java.nio.fil.LinkOption枚举是在 Java 7 中添加的。它用于指定是否应该跟随符号链接。

执行时,输出应如下所示:

URI 路径:file:///.../ConvertingPathsExample/invalidFileName.txt

绝对路径:...\ConvertingPathsExample\invalidFileName.txt

文件不存在

确定两个路径是否等效

有时可能需要比较路径。Path类允许您使用equals方法测试路径的相等性。您还可以使用compareTo方法使用Comparable接口的实现按字典顺序比较两个路径。最后,isSameFile方法可用于确定两个Path对象是否将定位到相同的文件。

准备工作

为了比较两个路径,您必须:

  1. 创建一个代表第一个路径的Path对象。

  2. 创建一个代表第二个路径的Path对象。

  3. 根据需要对路径应用equals, compareToisSameFile方法。

如何做...

  1. 创建一个新的控制台应用程序并添加一个main方法。声明三个Path对象变量,如path1,path2path3。将前两个设置为相同的文件,第三个设置为不同的路径。所有三个文件必须存在。接下来调用三个比较方法:
public class ComparingPathsExample {
public static void main(String[] args) {
Path path1 = null;
Path path2 = null;
Path path3 = null;
path1 = Paths.get("/home/docs/users.txt");
path2 = Paths.get("/home/docs/users.txt");
path3 = Paths.get("/home/music/Future Setting A.mp3");
testEquals(path1, path2);
testEquals(path1, path3);
testCompareTo(path1, path2);
testCompareTo(path1, path3);
testSameFile(path1, path2);
testSameFile(path1, path3);
}

  1. 添加三个静态方法如下:
private static void testEquals(Path path1, Path path2) {
if (path1.equals(path2)) {
System.out.printf("%s and %s are equal\n",
path1, path2);
}
else {
System.out.printf("%s and %s are NOT equal\n",
path1, path2);
}
}
private static void testCompareTo(Path path1, Path path2) {
if (path1.compareTo(path2) == 0) {
System.out.printf("%s and %s are identical\n",
path1, path2);
}
else {
System.out.printf("%s and %s are NOT identical\n",
path1, path2);
}
}
private static void testSameFile(Path path1, Path path2) {
try {
if (Files.isSameFile(path1, path2)) {
System.out.printf("%s and %s are the same file\n",
path1, path2);
}
else {
System.out.printf("%s and %s are NOT the same file\n",
path1, path2);
}
}
catch (IOException e) {
e.printStackTrace();
}
}

  1. 执行应用程序。您的输出应该类似于以下内容:

\home\docs\users.txt 和 \home\docs\users.txt 是相等的

\home\docs\users.txt 和 \home\music\Future Setting A.mp3 不相等

\home\docs\users.txt 和 \home\docs\users.txt 是相同的

\home\docs\users.txt 和 \home\music\Future Setting A.mp3 不相同

\home\docs\users.txt 和 \home\docs\users.txt 是相同的文件

\home\docs\users.txt 和 \home\music\Future Setting A.mp3 不是同一个文件

它是如何工作的...

testEquals方法中,我们确定了路径对象是否被视为相等。如果它们相等,equals方法将返回 true。但是,相等的定义是依赖于系统的。一些文件系统将使用大小写等因素来确定路径是否相等。

testCompareTo方法使用compareTo方法按字母顺序比较路径。如果路径相同,该方法返回零。如果路径小于参数,则该方法返回小于零的整数,如果路径按字典顺序跟随参数,则返回大于零的值。

testSameFile方法确定路径是否指向相同的文件。首先测试Path对象是否相同。如果是,则该方法将返回 true。如果Path对象不相等,则该方法确定路径是否指向相同的文件。如果Path对象是由不同的文件系统提供程序生成的,则该方法将返回 false。由于该方法可能引发IOException,因此使用了 try 块。

还有更多...

equalscompareTo方法将无法成功比较来自不同文件系统的路径。但是,只要文件位于同一文件系统上,所涉及的文件无需存在,文件系统也不会被访问。如果要测试的路径对象不相等,则isSameFile方法可能需要访问文件。在这种情况下,文件必须存在,否则该方法将返回 false。

另请参阅

Files类的existsnotExists方法可用于确定文件或目录是否存在。这在第三章的获取文件和目录信息中有所涵盖。

管理符号链接

符号链接用于创建对实际存在于不同目录中的文件的引用。在介绍中,详细列出了文件层次结构,其中users.txt文件在docs目录和music目录中分别列出。实际文件位于docs目录中。music目录中的users.txt文件是对真实文件的符号链接。对用户来说,它们看起来是不同的文件。实际上,它们是相同的。修改任一文件都会导致真实文件被更改。

从程序员的角度来看,我们经常想知道哪些文件是符号链接,哪些不是。在本教程中,我们将讨论 Java 7 中可用于处理符号链接的方法。重要的是要了解在与符号链接一起使用方法时方法的行为。

准备就绪

虽然几种方法可能根据Path对象是否表示符号链接而有所不同,但在本章中,只有toRealPath,existsnotExists方法接受可选的LinkOption枚举参数。此枚举只有一个元素:NOFOLLOW_LINKS。如果未使用该参数,则方法默认会跟随符号链接。

如何做...

  1. 创建一个新的控制台应用程序。使用以下main方法,在其中创建代表真实和符号users.txt文件的几个Path对象。演示了本章中几个Path-related方法的行为。
public static void main(String[] args) {
Path path1 = null;
Path path2 = null;
path1 = Paths.get("/home/docs/users.txt");
path2 = Paths.get("/home/music/users.txt");
System.out.println(Files.isSymbolicLink(path1));
System.out.println(Files.isSymbolicLink(path2));
try {
Path path = Paths.get("C:/home/./music/users.txt");
System.out.println("Normalized: " + path.normalize());
System.out.println("Absolute path: " + path.toAbsolutePath());
System.out.println("URI: " + path.toUri());
System.out.println("toRealPath (Do not follow links): " + path.toRealPath(LinkOption.NOFOLLOW_LINKS));
System.out.println("toRealPath: " + path.toRealPath());
Path firstPath = Paths.get("/home/music/users.txt");
Path secondPath = Paths.get("/docs/status.txt");
System.out.println("From firstPath to secondPath: " + firstPath.relativize(secondPath));
System.out.println("From secondPath to firstPath: " + secondPath.relativize(firstPath));
System.out.println("exists (Do not follow links): " + Files.exists(firstPath, LinkOption.NOFOLLOW_LINKS));
System.out.println("exists: " + Files.exists(firstPath));
System.out.println("notExists (Do not follow links): " + Files.notExists(firstPath, LinkOption.NOFOLLOW_LINKS));
System.out.println("notExists: " + Files.notExists(firstPath));
}
catch (IOException ex) {
Logger.getLogger(SymbolicLinkExample.class.getName()).log(Level.SEVERE, null, ex);
}
catch (InvalidPathException ex) {
System.out.println("Bad path: [" + ex.getInput() + "] at position " + ex.getIndex());
}
}

  1. 这些方法的行为可能因基础操作系统而异。当代码在 Windows 平台上执行时,我们会得到以下输出:

false

true

标准化:C:\ home \ music \ users.txt

绝对路径:C:\ home \。music \ users.txt

URI:file:///C:/home/./music/users.txt

toRealPath(不要跟随链接):C:\ home \ music \ users.txt

toRealPath:C:\ home \ docs \ users.txt

从 firstPath 到 secondPath:......\docs\status.txt

从 secondPath 到 firstPath:....\home\music\users.txt

exists(不要跟随链接):true

exists:true

notExists(不要跟随链接):false

notExists:false

它是如何工作的...

创建了path1path2对象,分别引用了真实文件和符号链接。针对这些对象执行了Files类的isSymbolicLink方法,指示哪个路径引用了真实文件。

使用多余的点符号创建了Path对象。针对符号链接执行的normalize方法的结果返回了对符号链接的标准化路径。使用toAbsolutePathtoUri方法会返回对符号链接而不是真实文件的路径。

toRealPath方法具有可选的LinkOption参数。我们使用它来获取真实文件的路径。当您需要真实路径时,这个方法非常有用,通常其他方法执行符号链接时不会返回真实路径。

firstPathsecondPath对象被用来探索relativize方法如何与符号链接一起工作。在这些例子中,使用了符号链接。最后一组例子使用了existsnotExists方法。使用符号链接并不影响这些方法的结果。

另请参阅

符号文件的使用对其他文件系统方法的影响将在后续章节中讨论。

第三章:获取文件和目录信息

在本章中,我们将涵盖以下内容:

  • 确定文件内容类型

  • 使用 getAttribute 方法逐个获取单个属性

  • 获取文件属性的映射

  • 获取文件和目录信息

  • 确定操作系统对属性视图的支持

  • 使用 BasicFileAttributeView 维护基本文件属性

  • 使用 PosixFileAttributeView 维护 POSIX 文件属性

  • 使用 DosFileAttributeView 维护 FAT 表属性

  • 使用 FileOwnerAttributeView 维护文件所有权属性

  • 使用 AclFileAttributeView 维护文件的 ACL

  • 使用 UserDefinedFileAttributeView 维护用户定义的文件属性

介绍

许多应用程序需要访问文件和目录信息。这些信息包括文件是否可以执行,文件的大小,文件的所有者,甚至其内容类型等属性。在本章中,我们将研究获取有关文件或目录信息的各种技术。我们根据所需的访问类型组织了配方。

使用java.nio.file.Files类获取文件和目录信息的五种一般方法如下:

  • 使用Files类的特定方法,如isDirectory方法,逐个获取单个属性。这在获取文件和目录信息配方中有详细说明。

  • 使用Files类的getAttribute方法逐个获取单个属性。这在使用 getAttribute 方法逐个获取单个属性配方中有详细说明。

  • 使用readAttributes方法返回使用String指定要返回的属性的映射。这在获取文件属性的映射配方中有解释。

  • 使用readAttributes方法与BasicFileAttributes派生类返回该属性集的属性类。这在使用 BasicFileAttributeView 维护基本文件属性配方中有详细说明。

  • 使用getFileAttributes方法返回提供对特定属性集的访问的视图。这也在使用 BasicFileAttributeView 方法维护基本文件属性配方中有详细说明。它在配方的还有更多..部分中找到。

通过几种方法支持对属性的动态访问,并允许开发人员使用String指定属性。Files类的getAttribute方法代表了这种方法。

Java 7 引入了一些基于文件视图的接口。视图只是关于文件或目录的信息的一种组织方式。例如,AclFileAttributeView提供了与文件的访问控制列表ACL)相关的方法。FileAttributeView接口是提供特定类型文件信息的其他接口的基接口。java.nio.file.attribute包中的子接口包括以下内容:

  • AclFileAttributeView:用于维护文件的 ACL 和所有权属性

  • BasicFileAttributeView:用于访问有关文件的基本信息并设置与时间相关的属性

  • DosFileAttributeView:设计用于与传统磁盘操作系统DOS)文件属性一起使用

  • FileOwnerAttributeView:用于维护文件的所有权

  • PosixFileAttributeView:支持便携式操作系统接口POSIX)属性

  • UserDefinedFileAttributeView:支持文件的用户定义属性

视图之间的关系如下所示:

注意

低级接口继承自它们上面的接口。

介绍

readAttributes 方法的第二个参数指定要返回的属性类型。支持三个属性接口,它们的关系如下图所示。这些接口提供了访问它们对应的视图接口的方法:

Introduction

每个视图都有一个专门的配方。这里不讨论 FileStoreAttributeView,但在 第四章 的 管理文件和目录 中有相关内容。

本章中示例使用的文件和目录结构在 第二章 的介绍中有描述,使用路径定位文件和目录

确定文件内容类型

文件的类型通常可以从其扩展名推断出来。但这可能会误导,具有相同扩展名的文件可能包含不同类型的数据。Files 类的 probeContentType 方法用于确定文件的内容类型(如果可能)。当应用程序需要一些指示文件内容以便处理时,这是很有用的。

准备工作

为了确定内容类型,需要完成以下步骤:

  1. 获取代表文件的 Path 对象。

  2. 使用 Path 对象作为 probeContentType 方法的参数。

  3. 使用结果处理文件。

操作步骤...

  1. 创建一个新的控制台应用程序。将三种不同类型的文件添加到 /home/docs 目录中。使用以下内容作为 main 方法。虽然你可以使用任何你选择的文件,但本示例使用了一个文本文件,一个 Word 文档和一个可执行文件:
public static void main(String[] args) throws Exception {
displayContentType("/home/docs/users.txt");
displayContentType("/home/docs/Chapter 2.doc");
displayContentType("/home/docs/java.exe");
}
static void displayContentType(String pathText) throws Exception {
Path path = Paths.get(pathText);
String type = Files.probeContentType(path);
System.out.println(type);
}

  1. 执行应用程序。你的输出应该如下所示。返回的类型取决于你使用的实际文件:

text/plain

application/msword

application/x-msdownload

工作原理...

创建了一个 java.nio.file.Path 变量,并分配给了三个不同的文件。对每个文件执行了 Files 类的 probeContentPath 方法。返回的结果是一个 String,用于说明目的。probeContentType 方法会抛出一个 java.io.IOException,我们通过让 displayConentType 方法和 main 方法抛出一个基类异常来处理这个异常。probeContentPath 方法也可能会抛出一个 java.lang.SecurityException,但你不需要处理它。

在本示例中使用的文件中,第一个文件是一个文本文件。返回的类型是 text/plain。另外两个是一个 Word 文档和可执行文件 java.exe。返回的类型分别是 application/mswordapplication/x-msdownload

还有更多...

该方法的结果是一个 String,由 多用途互联网邮件扩展 (MIME):RFC 2045:多用途互联网邮件扩展(MIME)第一部分:互联网消息正文的格式 定义。这允许使用 RFC 2045 语法规范解析 String。如果无法识别内容类型,则返回 null。

MIME 类型由类型和子类型以及一个或多个可选参数组成。类型和子类型之间使用斜杠分隔。在前面的输出中,文本文档的类型是 text,子类型是 plain。另外两种类型都是 application 类型,但子类型不同。以 x- 开头的子类型是非标准的。

probeContentType方法的实现取决于系统。该方法将使用java.nio.file.spi.FileTypeDetector实现来确定内容类型。它可能检查文件名或可能访问文件属性以确定文件内容类型。大多数操作系统将维护文件探测器列表。从此列表中加载并用于确定文件类型。FileTypeDetector类没有扩展,并且目前无法确定哪些文件探测器可用。

使用getAttribute方法一次获取一个属性

如果您有兴趣获取单个文件属性,并且知道属性的名称,则Files类的getAttribute方法简单且易于使用。它将返回基于表示属性的String的文件信息。本食谱的第一部分说明了getAttribute方法的简单用法。其他可用的属性列在本食谱的更多内容部分中。

准备就绪

获取单个文件属性值:

  1. 创建一个表示感兴趣的文件的Path对象。

  2. 将此对象用作getAttribute方法的第一个参数。

  3. 使用包含属性名称的String作为方法的第二个参数。

如何做...

  1. 创建一个新的控制台应用程序并使用以下main方法。在此方法中,我们确定文件的大小如下:
public static void main(String[] args) {
try {
Path path = FileSystems.getDefault().getPath("/home/docs/users.txt");
System.out.println(Files.getAttribute(path, "size"));
}
catch (IOException ex) {
System.out.println("IOException");
}
}

  1. 输出将如下所示,并将取决于所使用文件的实际大小:

30

它是如何工作的...

创建了一个表示users.txt文件的Path。然后将此路径用作Files类的getAttribute方法的第一个参数。执行代码时,将显示文件的大小。

更多内容...

Files类的getAttribute方法具有以下三个参数:

  • 一个表示文件的Path对象

  • 包含属性名称的String

  • 在处理符号文件时使用的可选LinkOption

以下表格列出了可以与此方法一起使用的有效属性名称:

属性名称 数据类型
lastModifiedTime FileTime
lastAccessTime FileTime
creationTime FileTime
size 长整型
isRegularFile 布尔值
isDirectory 布尔值
isSymbolicLink 布尔值
isOther 布尔值
fileKey 对象

如果使用无效的名称,则会发生运行时错误。这是这种方法的主要弱点。例如,如果名称拼写错误,我们将收到运行时错误。此方法如下所示,指定的属性在属性String末尾有一个额外的s

System.out.println(Files.getAttribute(path, "sizes"));

当应用程序执行时,您应该获得类似以下的结果:

线程"main"中的异常 java.lang.IllegalArgumentException:未识别'sizes'

在 sun.nio.fs.AbstractBasicFileAttributeView$AttributesBuilder.(AbstractBasicFile AttributeView.java:102)

在 sun.nio.fs.AbstractBasicFileAttributeView$AttributesBuilder.create(AbstractBasicFileAttributeView.java:112)

在 sun.nio.fs.AbstractBasicFileAttributeView.readAttributes(AbstractBasicFileAttributeView.java:166)

在 sun.nio.fs.AbstractFileSystemProvider.readAttributes(AbstractFileSystemProvider.java:92)

在 java.nio.file.Files.readAttributes(Files.java:1896)

在 java.nio.file.Files.getAttribute(Files.java:1801)

在 packt.SingleAttributeExample.main(SingleAttributeExample.java:15)

Java 结果:1

可以按照获取文件属性映射食谱中的描述获取文件属性列表。这可以用来避免使用无效名称。

获取文件属性映射

访问文件属性的另一种方法是使用Files类的readAttributes方法。该方法有两个重载版本,在第二个参数和返回的数据类型上有所不同。在本示例中,我们将探讨返回java.util.Map对象的版本,因为它允许在返回的属性上更灵活。该方法的第二个版本在一系列食谱中讨论,每个食谱都专门讨论一类属性。

准备就绪

要获取Map对象形式的属性列表,需要执行以下步骤:

  1. 创建一个表示文件的Path对象。

  2. Files类应用静态的readAttributes方法。

  3. 指定其参数的值:

  • 表示感兴趣文件的Path对象

  • 表示要返回的属性的String参数

  • 可选的第三个参数,指定是否应该跟踪符号链接

如何做...

  1. 创建一个新的控制台应用程序。使用以下main方法:
public static void main(String[] args) throws Exception {
Path path = Paths.getPath("/home/docs/users.txt");
try {
Map<String, Object> attrsMap = Files.readAttributes(path, "*");
Set<String> keys = attrsMap.keySet();
for(String attribute : keys) {
out.println(attribute + ": "
+ Files.getAttribute(path, attribute));
}
}
}

  1. 执行应用程序。您的输出应该类似于以下内容:

lastModifiedTime: 2011-09-06T01:26:56.501665Z

fileKey: null

isDirectory: false

lastAccessTime: 2011-09-06T21:14:11.214057Z

isOther: false

isSymbolicLink: false

isRegularFile: true

creationTime: 2011-09-06T21:14:11.214057Z

大小:30

它是如何工作的...

示例中使用了docs目录中的users.txt文件。声明了一个键类型为String,值类型为ObjectMap对象,然后为其赋予了readAttributes方法的值。使用Map接口的keySet方法创建了一个java.util.Set对象。这使我们可以访问Map的键和值。在 for each 循环中,将集合的每个成员用作getAttribute方法的参数。文件的相应属性和其值将被显示。getAttribute方法在使用 getAttribute 方法逐个获取属性食谱中有解释。

在这个例子中,我们使用字符串字面值"*"作为第二个参数。这个值指示方法返回文件的所有可用属性。正如我们很快将看到的,其他字符串值可以用来获得不同的结果。

readAttributes方法是一个原子文件系统操作。默认情况下,会跟踪符号链接。要指示该方法不要跟踪符号链接,使用java.nio.file包的LinkOption.NOFOLLOW_LINKS枚举常量,如下所示:

Map<String, Object> attrsMap = Files.readAttributes(path, "*", LinkOption.NOFOLLOW_LINKS);

还有更多...

该方法的有趣之处在于它的第二个参数。String参数的语法包括一个可选的viewName,后面跟着一个冒号,然后是属性列表。viewName通常是以下之一:

  • acl

  • 基本

  • 所有者

  • 用户

  • dos

  • posix

每个viewNames对应于一个视图接口的名称。

属性列表是一个逗号分隔的属性列表。属性列表可以包含零个或多个元素。如果使用无效的元素名称,则会被忽略。使用星号将返回与该viewName关联的所有属性。如果不包括viewName,则会返回所有基本文件属性,就像前面所示的那样。

以基本视图为例,以下表格说明了我们如何选择要返回的属性:

String 返回的属性
"*" 所有基本文件属性
"basic:*" 所有基本文件属性
"basic:isDirectory,lastAccessTime" isDirectorylastAccessTime属性
"isDirectory,lastAccessTime" isDirectorylastAccessTime属性
"" 无 - 会生成java.lang.IllegalArgumentException

String属性在除基本视图以外的视图中使用方式相同。

提示

属性String中不能有嵌入的空格。例如,String, "basic:isDirectory, lastAccessTime",逗号后面有一个空格会导致IllegalArgumentException

获取文件和目录信息

经常需要检索有关文件或目录的基本信息。本教程将介绍java.nio.file.Files类如何提供直接支持。这些方法仅提供对文件和目录信息的部分访问,并以isRegularFile等方法为代表。此类方法的列表可在本教程的更多信息部分找到。

准备就绪

要使用Files类的方法显示信息很容易,因为这些方法大多数(如果不是全部)都是静态的。这意味着这些方法可以轻松地针对Files类名称执行。要使用这种技术:

  1. 创建一个表示文件或目录的Path对象。

  2. Path对象用作适当的Files类方法的参数。

如何做...

  1. 为了演示如何获取文件属性,我们将开发一个方法来显示文件的属性。创建一个包含以下main方法的新控制台应用程序。在该方法中,我们创建一个文件的引用,然后调用displayFileAttribute方法。它使用几种方法来显示有关路径的信息,如下所示:
public static void main(String[] args) throws Exception {
Path path = FileSystems.getDefault().getPath("/home/docs/users.txt");
displayFileAttributes(path);
}
private static void displayFileAttributes(Path path) throws Exception {
String format =
"Exists: %s %n"
+ "notExists: %s %n"
+ "Directory: %s %n"
+ "Regular: %s %n"
+ "Executable: %s %n"
+ "Readable: %s %n"
+ "Writable: %s %n"
+ "Hidden: %s %n"
+ "Symbolic: %s %n"
+ "Last Modified Date: %s %n"
+ "Size: %s %n";
System.out.printf(format,
Files.exists(path, LinkOption.NOFOLLOW_LINKS),
Files.notExists(path, LinkOption.NOFOLLOW_LINKS),
Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS),
Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS),
Files.isExecutable(path),
Files.isReadable(path),
Files.isWritable(path),
Files.isHidden(path),
Files.isSymbolicLink(path),
Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS),
Files.size(path));
}

  1. 执行程序。您的输出应如下所示:

存在:true

不存在:false

目录:false

常规:true

可执行:true

可读:true

可写:true

隐藏:false

符号链接:false

上次修改日期:2011-10-20T03:18:20.338139Z

大小:29

它是如何工作的...

创建了指向users.txt文件的Path。然后将此Path对象传递给displayFileAttribute方法,该方法显示了文件的许多属性。返回这些属性的方法在以下表格中进行了总结:

方法 描述
exists 如果文件存在则返回true
notExists 如果文件不存在则返回true
isDirectory 如果路径表示目录则返回true
isRegularFile 如果路径表示常规文件则返回true
isExecutable 如果文件可执行则返回true
isReadable 如果文件可读则返回true
isWritable 如果文件可写则返回true
isHidden 如果文件是隐藏的且对非特权用户不可见则返回true
isSymbolicLink 如果文件是符号链接则返回true
getLastModifiedTime 返回文件上次修改的时间
size 返回文件的大小

其中几种方法具有第二个参数,指定如何处理符号链接。当存在LinkOption.NOFOLLOW_LINKS时,符号链接不会被跟踪。第二个参数是可选的。如果省略,则不会跟踪符号链接。符号链接在第二章的使用路径定位文件和目录中的管理符号链接教程中进行了讨论。

更多信息...

以下表格总结了抛出的异常以及方法是否为非原子操作。如果调用线程无权读取文件,则可能会抛出SecurityException

注意

当一个方法被称为非原子时,这意味着其他文件系统操作可能会与该方法同时执行。非原子操作可能导致不一致的结果。也就是说,在这些方法执行时,可能会导致对方法目标的并发操作可能修改文件的状态。在使用这些方法时应考虑到这一点。

这些方法标记为过时的结果在返回时不一定有效。也就是说,不能保证任何后续访问都会成功,因为文件可能已被删除或以其他方式修改。

被指定为无法确定的方法表示,如果无法确定结果,则可能返回 false。例如,如果 exists 方法无法确定文件是否存在,则会返回 false。它可能存在,但该方法无法确定它是否存在:

方法 SecurityException IOException 非原子 过时 无法确定
exists
notExists
isDirectory
isRegularFile
isExecutable
isReadable
isWritable
isHidden
isSymbolicLink
getLastModifiedTime
size

请注意,notExists 方法不是 exists 方法的反义词。使用任一方法,可能无法确定文件是否存在。在这种情况下,两种方法都将返回 false

isRegularFile 确定文件是否为常规文件。如果 isDirectory, isSymbolicLinkisRegularFile 方法返回 false,则可能是因为:

  • 它不是这些类型之一

  • 如果文件不存在或

  • 如果无法确定它是文件还是目录

对于这些方法,它们在 BasicFileAttributes 接口中对应的方法可能会提供更好的结果。这些方法在 使用 BasicFileAttributeView 维护基本文件属性 部分中有介绍。

isExecutable 方法检查文件是否存在,以及 JVM 是否有执行文件的访问权限。如果文件是一个目录,则该方法确定 JVM 是否有足够的权限来搜索该目录。如果:

  • 文件不存在

  • 文件不可执行

  • 如果无法确定是否可执行

隐藏的含义取决于系统。在 UNIX 系统上,如果文件名以句点开头,则文件是隐藏的。在 Windows 上,如果设置了 DOS 隐藏属性,则文件是隐藏的。

确定操作系统对属性视图的支持

操作系统可能不支持 Java 中的所有属性视图。有三种基本技术可以确定支持哪些视图。知道支持哪些视图可以让开发人员避免在尝试使用不受支持的视图时可能发生的异常。

准备工作

这三种技术包括使用:

  • 使用 java.nio.file.FileSystem 类的 supportedFileAttributeViews 方法返回一个包含所有支持的视图的集合。

  • 使用 java.nio.file.FileStore 类的 supportsFileAttributeView 方法和一个类参数。如果该类受支持,则该方法将返回 true

  • 使用 FileStore 类的 supportsFileAttributeView 方法和一个 String 参数。如果该 String 表示的类受支持,则该方法将返回 true

第一种方法是最简单的,将首先进行说明。

如何做...

  1. 创建一个新的控制台应用程序,其中包含以下 main 方法。在这个方法中,我们将显示当前系统支持的所有视图,如下所示:
public static void main(String[] args)
Path path = Paths.get("C:/home/docs/users.txt");
FileSystem fileSystem = path.getFileSystem();
Set<String> supportedViews = fileSystem.supportedFileAttributeViews();
for(String view : supportedViews) {
System.out.println(view);
}
}

  1. 当应用在 Windows 7 系统上执行时,应该会得到以下输出:

acl

basic

owner

user

dos

  1. 当应用在 Ubuntu 10.10 版本下执行时,应该会得到以下输出:

basic

owner

user

unix

dos

posix

请注意,acl视图不受支持,而unixposix视图受支持。在 Java 7 发布版中没有UnixFileAttributeView。但是,该接口可以作为 JSR203-backport 项目的一部分找到。

它是如何工作的...

users.txt文件创建了一个Path对象。接下来使用getFileSystem方法获取了该Path的文件系统。FileSystem类具有supportedFileAttributeViews方法,该方法返回一个表示支持的视图的字符串集合。然后使用 for each 循环显示每个字符串值。

还有更多...

还有另外两种方法可以用来确定支持哪些视图:

  • 使用带有类参数的supportsFileAttributeView方法

  • 使用带有String参数的supportsFileAttributeView方法

这两种技术非常相似。它们都允许您测试特定的视图。

使用带有类参数的 supportsFileAttributeView 方法

重载的supportsFileAttributeView方法接受表示所讨论的视图的类对象。将以下代码添加到上一个示例的main方法中。在这段代码中,我们确定支持哪些视图:

try {
FileStore fileStore = Files.getFileStore(path);
System.out.println("FileAttributeView supported: " + fileStore.supportsFileAttributeView(
FileAttributeView.class));
System.out.println("BasicFileAttributeView supported: " + fileStore.supportsFileAttributeView(
BasicFileAttributeView.class));
System.out.println("FileOwnerAttributeView supported: " + fileStore.supportsFileAttributeView(
FileOwnerAttributeView.class));
System.out.println("AclFileAttributeView supported: " + fileStore.supportsFileAttributeView(
AclFileAttributeView.class));
System.out.println("PosixFileAttributeView supported: " + fileStore.supportsFileAttributeView(
PosixFileAttributeView.class));
System.out.println("UserDefinedFileAttributeView supported: " + fileStore.supportsFileAttributeView(
UserDefinedFileAttributeView.class));
System.out.println("DosFileAttributeView supported: " + fileStore.supportsFileAttributeView(
DosFileAttributeView.class));
}
catch (IOException ex) {
System.out.println("Attribute view not supported");
}

在 Windows 7 机器上执行时,您应该获得以下输出:

FileAttributeView supported: false

BasicFileAttributeView supported: true

FileOwnerAttributeView supported: true

AclFileAttributeView supported: true

PosixFileAttributeView supported: false

UserDefinedFileAttributeView supported: true

DosFileAttributeView supported: true

使用带有String参数的supportsFileAttributeView方法

重载的supportsFileAttributeView方法接受一个String对象的工作方式类似。将以下代码添加到main方法的 try 块中:

System.out.println("FileAttributeView supported: " + fileStore.supportsFileAttributeView(
"file"));
System.out.println("BasicFileAttributeView supported: " + fileStore.supportsFileAttributeView(
"basic"));
System.out.println("FileOwnerAttributeView supported: " + fileStore.supportsFileAttributeView(
"owner"));
System.out.println("AclFileAttributeView supported: " + fileStore.supportsFileAttributeView(
"acl"));
System.out.println("PosixFileAttributeView supported: " + fileStore.supportsFileAttributeView(
"posix"));
System.out.println("UserDefinedFileAttributeView supported: " + fileStore.supportsFileAttributeView(
"user"));
System.out.println("DosFileAttributeView supported: " + fileStore.supportsFileAttributeView(
"dos"));

在 Windows 7 平台上执行时,您应该获得以下输出:

FileAttributeView supported: false

BasicFileAttributeView supported: true

FileOwnerAttributeView supported: true

AclFileAttributeView supported: true

PosixFileAttributeView supported: false

UserDefinedFileAttributeView supported: true

DosFileAttributeView supported: true

使用 BasicFileAttributeView 维护基本文件属性

java.nio.file.attribute.BasicFileAttributeView提供了一系列方法,用于获取有关文件的基本信息,例如其创建时间和大小。该视图具有一个readAttributes方法,该方法返回一个BasicFileAttributes对象。BasicFileAttributes接口具有几种用于访问文件属性的方法。该视图提供了一种获取文件信息的替代方法,而不是由Files类支持的方法。该方法的结果有时可能比Files类的结果更可靠。

准备工作

有两种方法可以获取BasicFileAttributes对象。第一种方法是使用readAttributes方法,该方法使用BasicFileAttributes.class作为第二个参数。第二种方法使用getFileAttributeView方法,并在本章的更多内容..部分中进行了探讨。

Files类的readAttributes方法最容易使用:

  1. 将表示感兴趣的文件的Path对象用作第一个参数。

  2. BasicFileAttributes.class用作第二个参数。

  3. 使用返回的BasicFileAttributes对象方法来访问文件属性。

这种基本方法用于本章中所示的其他视图。只有属性视图类不同。

操作步骤...

  1. 创建一个新的控制台应用程序。使用以下main方法。在该方法中,我们创建了一个BasicFileAttributes对象,并使用其方法来显示有关文件的信息:
public static void main(String[] args) {
Path path
= FileSystems.getDefault().getPath("/home/docs/users.txt");
try {
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Creation Time: " + attributes.creationTime());
System.out.println("Last Accessed Time: " + attributes.lastAccessTime());
System.out.println("Last Modified Time: " + attributes.lastModifiedTime());
System.out.println("File Key: " + attributes.fileKey());
System.out.println("Directory: " + attributes.isDirectory());
System.out.println("Other Type of File: " + attributes.isOther());
System.out.println("Regular File: " + attributes.isRegularFile());
System.out.println("Symbolic File: " + attributes.isSymbolicLink());
System.out.println("Size: " + attributes.size());
}
catch (IOException ex) {
System.out.println("Attribute error");
}
}

  1. 执行应用程序。您的输出应该类似于以下内容:

Creation Time: 2011-09-06T21:14:11.214057Z

Last Accessed Time: 2011-09-06T21:14:11.214057Z

Last Modified Time: 2011-09-06T01:26:56.501665Z

文件键:null

目录:false

其他类型的文件:false

常规文件:true

符号文件:false

大小:30

它是如何工作的...

首先,我们创建了一个代表users.txt文件的Path对象。接下来,我们使用Files类的readAttributes方法获取了一个BasicFileAttributes对象。该方法的第一个参数是一个Path对象。第二个参数指定了我们想要返回的对象类型。在这种情况下,它是一个BasicFileAttributes.class对象。

然后是一系列打印语句,显示有关文件的特定属性信息。readAttributes方法检索文件的所有基本属性。由于它可能会抛出IOException,代码序列被包含在 try 块中。

大多数BasicFileAttributes接口方法很容易理解,但有一些需要进一步解释。首先,如果isOther方法返回true,这意味着文件不是常规文件、目录或符号链接。此外,尽管文件大小以字节为单位,但由于文件压缩和稀疏文件的实现等问题,实际大小可能会有所不同。如果文件不是常规文件,则返回值的含义取决于系统。

fileKey方法返回一个唯一标识该文件的对象。在 UNIX 中,设备 ID 或 inode 用于此目的。如果文件系统及其文件发生更改,文件键不一定是唯一的。它们可以使用equals方法进行比较,并且可以用于集合。再次强调的是,假设文件系统没有以影响文件键的方式发生更改。两个文件的比较在第二章的确定两个路径是否等效中有所涉及,使用路径定位文件和目录

还有更多...

获取对象的另一种方法是使用Files类的getFileAttributeView方法。它根据第二个参数返回一个基于AttributeView的派生对象。要获取BasicFileAttributeView对象的实例:

  1. 使用代表感兴趣的文件的Path对象作为第一个参数。

  2. 使用BasicFileAttributeView作为第二个参数。

不要使用以下语句:

BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

我们可以用以下代码序列替换它:

BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
BasicFileAttributes attributes = view.readAttributes();

使用getFileAttributeView方法返回BasicFileAttributeView对象。然后readAttributes方法返回BasicFileAttributes对象。这种方法更长,但现在我们可以访问另外三种方法,如下所示:

  • name:这返回属性视图的名称

  • readAttributes:这返回一个BasicFileAttributes对象

  • setTimes:这用于设置文件的时间属性

  1. 然后我们使用如下所示的name方法:
System.out.println("Name: " + view.name());

这导致了以下输出:

名称:basic

然而,这并没有为我们提供太多有用的信息。setTimes方法在第四章的设置文件或目录的时间相关属性中有所说明,管理文件和目录

使用 PosixFileAttributeView 维护 POSIX 文件属性

许多操作系统支持可移植操作系统接口POSIX)标准。这提供了一种更便携的方式来编写可以在不同操作系统之间移植的应用程序。Java 7 支持使用java.nio.file.attribute.PosixFileAttributeView接口访问文件属性。

并非所有操作系统都支持 POSIX 标准。确定操作系统是否支持属性视图的示例说明了如何确定特定操作系统是否支持 POSIX。

准备工作

为了获取文件或目录的 POSIX 属性,我们需要执行以下操作:

  1. 创建一个代表感兴趣的文件或目录的Path对象。

  2. 使用getFileAttributeView方法获取PosixFileAttributeView接口的实例。

  3. 使用readAttributes方法获取一组属性。

如何做...

  1. 创建一个新的控制台应用程序。使用以下main方法。在此方法中,我们获取users.txt文件的属性如下:
public static void main(String[] args) throws Exception {
Path path = Paths.get("home/docs/users.txt");
FileSystem fileSystem = path.getFileSystem();
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
PosixFileAttributes attributes = view.readAttributes();
System.out.println("Group: " + attributes.group());
System.out.println("Owner: " + attributes.owner().getName());
Set<PosixFilePermission> permissions = attributes.permissions();
for(PosixFilePermission permission : permissions) {
System.out.print(permission.name() + " ");
}
}

  1. 执行应用程序。您的输出应如下所示。所有者名称可能会有所不同。在这种情况下,它是richard:

组:richard

所有者:richard

OWNER_READ OWNER_WRITE OTHERS_READ GROUP_READ

它是如何工作的...

users.txt文件创建了一个Path对象。这被用作Files类的getFileAttributeView方法的第一个参数。第二个参数是PosixFileAttributeView.class。返回了一个PosixFileAttributeView对象。

接下来,使用readAttributes方法获取了PosixFileAttributes接口的实例。使用groupgetName方法显示了文件的组和所有者。权限方法返回了一组PosixFilePermission枚举。这些枚举表示分配给文件的权限。

还有更多...

PosixFileAttributes接口扩展了java.nio.file.attribute.BasicFileAttributes接口,因此可以访问其所有方法。PosixFileAttributeView接口扩展了java.nio.file.attribute.FileOwnerAttributeViewBasicFileAttributeView接口,并继承了它们的方法。

PosixFileAttributeView接口具有setGroup方法,可用于配置文件的组所有者。可以使用setPermissions方法维护文件的权限。在第四章管理文件和目录中讨论了维护文件权限的管理 POSIX 属性配方。

另请参阅

Maintaining basic file attributes using the BasicFileAttributeView配方详细介绍了通过此视图可用的属性。使用 FileOwnerAttributeView 维护文件所有权属性配方讨论了所有权问题。要确定操作系统是否支持 POSIX,请查看确定属性视图的操作系统支持配方。

使用DosFileAttributeView维护 FAT 表属性

java.nio.file.attribute.DosFileAttributeView涉及较旧的磁盘操作系统DOS)文件。在今天的大多数计算机上,它的价值有限。但是,这是唯一可以用来确定文件是否标记为归档文件或系统文件的接口。

准备就绪

要使用DosFileAttributeView接口:

  1. 使用Files类的getFileAttributeView方法获取DosFileAttributeView的实例。

  2. 使用视图的readAttributes方法返回DosFileAttributes的实例。

  3. 使用DosFileAttributes类的方法获取文件信息。

此视图支持以下四种方法:

  • isArchive:关注文件是否需要备份

  • isHidden:如果文件对用户不可见,则返回true

  • isReadOnly:如果文件只能读取,则返回true

  • isSystem:如果文件是操作系统的一部分,则返回true

如何做...

  1. 创建一个新的控制台应用程序,并添加以下main方法。在此方法中,我们创建DosFileAttributes的一个实例,然后使用其方法显示有关文件的信息:
public static void main(String[] args) {
Path path = FileSystems.getDefault().getPath("/home/docs/users.txt");
try {
DosFileAttributeView view = Files.getFileAttributeView(path, DosFileAttributeView.class);
DosFileAttributes attributes = view.readAttributes();
System.out.println("isArchive: " + attributes.isArchive());
System.out.println("isHidden: " + attributes.isHidden());
System.out.println("isReadOnly: " + attributes.isReadOnly());
System.out.println("isSystem: " + attributes.isSystem());
}
catch (IOException ex) {
ex.printStackTrace();
}
}

  1. 执行程序。您的输出应如下所示:

isArchive:true

isHidden:false

isReadOnly:false

isSystem:false

它是如何工作的...

创建了一个代表users.txt文件的Path对象。将此对象用作Files类的getFileAttributeView方法的参数,以及DosFileAttributeView.class。返回了DosFileAttributeView接口的一个实例。这被用于创建DosFileAttributes接口的一个实例,该实例与接口的四个方法一起使用。

DosFileAttributeView扩展了BasicFileAttributes接口,并因此继承了其所有属性,如使用 BasicFileAttributeView 维护基本文件属性配方中所述。

另请参阅

有关其方法的更多信息,请参阅使用 BasicFileAttributeView 维护基本文件属性配方。

使用 FileOwnerAttributeView 来维护文件所有权属性

如果我们只对访问文件或目录的所有者的信息感兴趣,那么java.nio.file.attribute.FileOwnerAttributeView接口提供了检索和设置此类信息的方法。文件所有权的设置在第四章的设置文件和目录所有者配方中有所涵盖,管理文件和目录

准备就绪

检索文件的所有者:

  1. 获取FileOwnerAttributeView接口的实例。

  2. 使用其getOwner方法返回代表所有者的UserPrincipal对象。

如何做...

  1. 创建一个新的控制台应用程序。将以下main方法添加到其中。在此方法中,我们将确定users.txt文件的所有者如下:
public static void main(String[] args) {
Path path = Paths.get("C:/home/docs/users.txt");
try {
FileOwnerAttributeView view = Files.getFileAttributeView(path, FileOwnerAttributeView.class);
UserPrincipal userPrincipal = view.getOwner();
System.out.println(userPrincipal.getName());
}
catch (IOException e) {
e.printStackTrace();
}
}

  1. 执行应用程序。您的输出应该类似于以下内容,除了 PC 和用户名应该不同。

Richard-PC\Richard

它是如何工作的...

users.txt文件创建了一个Path对象。接下来,使用Path对象作为第一个参数调用了Files类的getFileAttributeView方法。第二个参数是FileOwnerAttributeView.class,这导致返回文件的FileOwnerAttributeView对象。

然后调用视图的getOwner方法返回一个UserPrincipal对象。它的getName方法返回用户的名称,然后显示出来。

另请参阅

有关其方法的更多信息,请参阅使用 BasicFileAttributeView 维护基本文件属性配方。

使用 AclFileAttributeView 维护文件的 ACL

java.nio.file.attribute.AclFileAttributeView接口提供了对文件或目录的 ACL 属性的访问。这些属性包括用户主体、属性类型以及文件的标志和权限。使用此接口的能力允许用户确定可用的权限并修改这些属性。

准备就绪

确定文件或目录的属性:

  1. 创建代表该文件或目录的Path对象。

  2. 使用此Path对象作为Files类的getFileAttributeView方法的第一个参数。

  3. 使用AclFileAttributeView.class作为其第二个参数。

  4. 使用返回的AclFileAttributeView对象访问该文件或目录的 ACL 条目列表。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,我们将检查users.txt文件的 ACL 属性。使用getFileAttributeView方法获取视图并访问 ACL 条目列表。使用两个辅助方法来支持此示例:displayPermissionsdisplayEntryFlags。使用以下main方法:
public static void main(String[] args) {
Path path = Paths.get("C:/home/docs/users.txt");
try {
AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
List<AclEntry> aclEntryList = view.getAcl();
for (AclEntry entry : aclEntryList) {
System.out.println("User Principal Name: " + entry.principal().getName());
System.out.println("ACL Entry Type: " + entry.type());
displayEntryFlags(entry.flags());
displayPermissions(entry.permissions());
System.out.println();
}
}
catch (IOException e) {
e.printStackTrace();
}
}

  1. 创建displayPermissions方法以显示文件的权限列表如下:
private static void displayPermissions(Set<AclEntryPermission> permissionSet) {
if (permissionSet.isEmpty()) {
System.out.println("No Permissions present");
}
else {
System.out.println("Permissions");
for (AclEntryPermission permission : permissionSet) {
System.out.print(permission.name() + " " );
}
System.out.println();
}
}

  1. 创建displayEntryFlags方法以显示文件的 ACL 标志列表如下:
private static void displayEntryFlags(Set<AclEntryFlag> flagSet) {
if (flagSet.isEmpty()) {
System.out.println("No ACL Entry Flags present");
}
else {
System.out.println("ACL Entry Flags");
for (AclEntryFlag flag : flagSet) {
System.out.print(flag.name() + " ");
}
System.out.println();
}
}

  1. 执行应用程序。您应该得到类似以下的输出:

用户主体名称:BUILTIN\Administrators

ACL 条目类型:允许

没有 ACL 条目标志

权限

WRITE_ATTRIBUTES EXECUTE DELETE READ_ATTRIBUTES WRITE_DATA READ_ACL READ_DATA WRITE_OWNER READ_NAMED_ATTRS WRITE_ACL APPEND_DATA SYNCHRONIZE DELETE_CHILD WRITE_NAMED_ATTRS

用户主体名称:NT AUTHORITY\SYSTEM

ACL 条目类型:允许

未出现 ACL 条目标志

权限

WRITE_ATTRIBUTES EXECUTE DELETE READ_ATTRIBUTES WRITE_DATA READ_ACL READ_DATA WRITE_OWNER READ_NAMED_ATTRS WRITE_ACL APPEND_DATA SYNCHRONIZE DELETE_CHILD WRITE_NAMED_ATTRS

用户主体名称:BUILTIN\Users

ACL 条目类型:允许

未出现 ACL 条目标志

权限

READ_DATA READ_NAMED_ATTRS EXECUTE SYNCHRONIZE READ_ATTRIBUTES READ_ACL

用户主体名称:NT AUTHORITY\Authenticated Users

ACL 条目类型:允许

未出现 ACL 条目标志

权限

READ_DATA READ_NAMED_ATTRS WRITE_ATTRIBUTES EXECUTE DELETE APPEND_DATA SYNCHRONIZE READ_ATTRIBUTES WRITE_NAMED_ATTRS WRITE_DATA READ_ACL

它是如何工作的...

创建了到users.txt文件的Path。然后将其与AclFileAttributeView.class参数一起用作getFileAttributeView方法的参数。这将返回AclFileAttributeView的一个实例。

AclFileAttributeView接口有三种方法:name, getAclsetAcl。在本例中,只使用了getAcl方法,它返回了一个AclEntry元素列表。每个条目代表文件的特定 ACL。

使用 for each 循环来遍历列表。显示了用户主体的名称和条目类型。接下来调用了displayEntryFlagsdisplayPermissions方法来显示有关条目的更多信息。

这两种方法在构造上相似。进行了检查以确定集合中是否有任何元素,并显示了适当的消息。接下来,将集合的每个元素显示在单独的一行上,以节省输出的垂直空间。

还有更多...

AclFileAttributeView源自java.nio.file.attribute.FileOwnerAttributeView接口。这提供了对getOwnersetOwner方法的访问。这些方法分别为文件或目录返回或设置UserPrincipal对象。

有三种AclFileAttributeView方法:

  • getAcl方法,返回 ACL 条目列表,如前所示

  • setAcl方法,允许我们向文件添加新属性

  • name方法,简单地返回acl

getAcl方法将返回一个AclEntrys列表。条目的一个元素是一个java.nio.file.attribute.UserPrincipal对象。正如我们在前面的示例中看到的,这代表了可以访问文件的用户。访问用户的另一种技术是使用java.nio.file.attribute.UserPrincipalLookupService类。可以使用FileSystem类的getUserPrincipalLookupService方法获取此类的实例,如下所示:

try {
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
GroupPrincipal groupPrincipal = lookupService.lookupPrincipalByGroupName("Administrators");
UserPrincipal userPrincipal = lookupService.lookupPrincipalByName("Richard");
System.out.println(groupPrincipal.getName());
System.out.println(userPrincipal.getName());
}
catch (IOException e) {
e.printStackTrace();
}

服务可用的两种方法可以按用户名或组名查找用户。在前面的代码中,我们使用了Administrators组和用户Richard

将此代码添加到上一个示例中,并更改名称以反映系统中的组和用户。当代码执行时,您应该收到类似以下的输出:

BUILTIN\Administrators

Richard-PC\Richard

然而,请注意,UserPrincipaljava.nio.file.attribute.GroupPrincipal对象的方法提供的信息比用户的名称更少。用户或组名称可能是大小写敏感的,这取决于操作系统。如果使用无效的名称,将抛出java.nio.file.attribute.UserPrincipalNotFoundException

另请参阅

在第四章中讨论了管理文件所有权和权限的内容,管理文件和目录中的设置文件和目录所有者配方。第四章还涵盖了在管理 ACL 文件权限配方中说明的 ACL 属性的设置。

使用 UserDefinedFileAttributeView 维护用户定义的文件属性

java.nio.file.attribute.UserDefinedFileAttributeView接口允许将非标准属性附加到文件或目录。这些类型的属性有时被称为扩展属性。通常,用户定义的属性存储有关文件的元数据。这些数据不一定被文件系统理解或使用。

这些属性存储为名称/值对。名称是一个String,值存储为ByteBuffer对象。该缓冲区的大小不应超过Integer.MAX_VALUE

准备就绪

用户定义的属性必须首先附加到文件上。这可以通过以下方式实现:

  1. 获取UserDefinedFileAttributeView对象的实例

  2. 创建一个以String名称和ByteBuffer值形式的属性

  3. 使用write方法将属性附加到文件

读取用户定义属性的过程在本配方的更多内容部分进行了说明。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,我们将创建一个名为publishable的用户定义属性,并将其附加到users.txt文件。使用以下main方法:
public static void main(String[] args) {
Path path = Paths.get("C:/home/docs/users.txt");
try {
UserDefinedFileAttributeView view = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
view.write("publishable", Charset.defaultCharset().encode("true"));
System.out.println("Publishable set");
}
catch (IOException e) {
e.printStackTrace();
}
}

  1. 执行应用程序。您的输出应如下所示:

设置为可发布

它是如何工作的...

首先,创建一个代表users.txt文件的Path对象。然后使用Files类的getFileAttributeView方法,使用Path对象和UserDefinedFileAttributeView.class作为第二个参数。这将返回文件的UserDefinedFileAttributeView的实例。

使用这个对象,我们对其执行write方法,使用属性publishable,创建了一个包含属性值truejava.nio.ByteBuffer对象。java.nio.Charset类的defaultCharset方法返回一个使用底层操作系统的区域设置和字符集的Charset对象。encode方法接受String并返回属性值的ByteBuffer。然后我们显示了一个简单的消息,指示进程成功完成。

还有更多...

read方法用于读取属性。要获取与文件关联的用户定义属性,需要按照以下步骤进行:

  1. 获取UserDefinedFileAttributeView对象的实例。

  2. 为属性名称创建一个String

  3. 分配一个ByteBuffer来保存值。

  4. 使用read方法获取属性值。

以下代码序列完成了先前附加的publishable属性的任务:

String name = "publishable";
ByteBuffer buffer = ByteBuffer.allocate(view.size(name));
view.read(name, buffer);
buffer.flip();
String value = Charset.defaultCharset().decode(buffer).toString();
System.out.println(value);

首先创建属性名称的String。接下来,创建一个ByteBuffer来保存要检索的属性值。allocate方法根据UserDefinedFileAttributeView接口的size方法指定的空间分配空间。此方法确定附加属性的大小并返回大小。

然后对view对象执行read方法。缓冲区填充了属性值。flip方法重置了缓冲区。使用decode方法将缓冲区转换为String对象,该方法使用操作系统的默认字符集。

main方法中,用这个read序列替换用户定义的属性write序列。当应用程序执行时,您应该得到类似以下的输出:

true

还有一个delete方法,用于从文件或目录中删除用户定义的属性。另外,需要注意使用UserDefinedFileAttributeView对象需要运行时权限accessUserDefinedAttributes

第四章:管理文件和目录

在这一章中,我们将涵盖以下内容:

  • 创建文件和目录

  • 控制文件复制方式

  • 管理临时文件和目录

  • 设置文件或目录的时间相关属性

  • 管理文件所有权

  • 管理 ACL 文件权限

  • 管理 POSIX 属性

  • 移动文件或目录

  • 删除文件和目录

  • 管理符号链接

介绍

通常需要执行文件操作,如创建文件,操作它们的属性和内容,或从文件系统中删除它们。Java 7 中java.lang.object.Files类的添加简化了这个过程。这个类在很大程度上依赖于新的java.nio.file.Path接口的使用,这在第二章中深入讨论,使用路径定位文件和目录。该类的方法在本质上都是静态的,并且通常将实际的文件操作分配给底层文件系统。

本章描述的许多操作在本质上是原子的,例如用于创建和删除文件或目录的操作。原子操作要么成功执行完成,要么失败并导致操作的有效取消。在执行过程中,它们不会从文件系统的角度受到干扰。其他并发文件操作不会影响该操作。

注意

要执行本章中的许多示例,应用程序需要以管理员身份运行。在 Windows 下以管理员身份运行应用程序,右键单击命令提示符菜单,选择以管理员身份运行。然后导航到适当的目录并使用java.exe命令执行。在 UNIX 系统上以管理员身份运行,使用终端窗口中的sudo命令,然后是java命令。

本章涵盖了基本的文件管理。创建文件和目录所需的方法在创建文件和目录教程中介绍。该教程侧重于普通文件。临时文件和目录的创建在管理临时文件和目录教程中介绍,链接文件的创建在管理符号链接教程中介绍。

复制文件和目录的可用选项在控制文件复制方式教程中找到。那里展示的技术提供了处理文件复制的强大方式。移动和删除文件和目录分别在移动文件或目录删除文件或目录教程中介绍。

设置文件或目录的时间相关属性教程说明了如何为文件分配时间属性。与此相关的还有其他属性,如文件所有权和权限。文件所有权在管理文件所有权教程中讨论。文件权限在两个教程中讨论:管理 ACL 文件权限管理 POSIX 文件权限

创建文件和目录

在 Java 7 中,创建新文件和目录的过程大大简化。Files类实现的方法相对直观,易于整合到您的代码中。在本教程中,我们将介绍如何使用createFilecreateDirectory方法创建新文件和目录。

准备工作

在我们的示例中,我们将使用几种不同的方法来创建代表文件或目录的Path对象。我们将执行以下操作:

  1. 创建Path对象。

  2. 使用Files类的createDirectory方法创建目录。

  3. 使用Files类的createFile方法创建文件。

FileSystem类的getPath方法可用于创建Path对象,Paths类的get方法也可以。Paths类的静态get方法基于字符串序列或URI对象返回Path的实例。FileSystem类的getPath方法也返回Path对象,但只使用字符串序列来标识文件。

如何做...

  1. 创建一个带有main方法的控制台应用程序。在main方法中,添加以下代码,为C目录中的/home/test目录创建一个Path对象。在 try 块内,使用您的Path对象作为参数调用createDirectory方法。如果路径无效,此方法将抛出IOException。接下来,使用此Path对象上的createFile方法创建文件newFile.txt,再次捕获IOException如下:
try {
Path testDirectoryPath = Paths.get("C:/home/test");
Path testDirectory = Files.createDirectory(testDirectoryPath);
System.out.println("Directory created successfully!");
Path newFilePath = FileSystems.getDefault().getPath("C:/home/test/newFile.txt");
Path testFile = Files.createFile(newFilePath);
System.out.println("File created successfully!");
}
catch (IOException ex) {
ex.printStackTrace();
}

  1. 执行程序。您的输出应如下所示:

目录创建成功!

文件创建成功!

  1. 验证新文件和目录是否存在于您的文件系统中。接下来,在两个方法之后添加一个IOException之前的 catch 块,并捕获FileAlreadyExistsException
}
catch (FileAlreadyExistsException a) {
System.out.println("File or directory already exists!");
}
catch (IOException ex) {
ex.printStackTrace();
}

  1. 当您再次执行程序时,您的输出应如下所示:

文件或目录已存在!

工作原理...

第一个Path对象被创建,然后被createDirectory方法用于创建一个新目录。创建第二个Path对象后,使用createFile方法在刚刚创建的目录中创建了一个文件。重要的是要注意,在创建目录之前无法实例化用于文件创建的Path对象,因为它将引用无效的路径。这将导致IOException

当调用createDirectory方法时,系统首先检查目录是否存在,如果不存在,则创建。createFile方法的工作方式类似。如果文件已经存在,该方法将失败。当我们捕获FileAlreadyExistsException时,我们看到了这一点。如果我们没有捕获该异常,将抛出IOException。无论哪种方式,现有文件都不会被覆盖。

还有更多...

createFilecreateDirectory方法在本质上是原子的。createDirectories方法可用于创建目录,如下所述。这三种方法都提供了传递文件属性参数以进行更具体文件创建的选项。

使用createDirectories方法创建目录层次结构

createDirectories方法用于创建目录和可能的其他中间目录。在此示例中,我们通过向test目录添加subtestsubsubtest目录来构建先前的目录结构。注释掉之前创建目录和文件的代码,并添加以下代码序列:

Path directoriesPath = Paths.get("C:/home/test/subtest/subsubtest");
Path testDirectory = Files.createDirectories(directoriesPath);

通过检查生成的目录结构来验证操作是否成功。

另请参阅

创建临时文件和目录在管理临时文件和目录中有所涉及。符号文件的创建在管理符号链接中有所说明。

控制文件复制的方式

在 Java 7 中,文件复制的过程也变得更加简化,并允许控制复制的方式。Files类的copy方法支持此操作,并提供了三种不同的复制技术。

准备就绪

在我们的示例中,我们将创建一个新文件,然后将其复制到另一个目标文件。这个过程涉及:

  1. 使用createFile方法创建一个新文件。

  2. 为目标文件创建一个路径。

  3. 使用copy方法复制文件。

如何做...

  1. 创建一个带有main方法的控制台应用程序。在main方法中,添加以下代码序列来创建一个新文件。指定两个Path对象,一个用于您的初始文件,另一个用于将其复制的位置。然后添加copy方法将该文件复制到目标位置,如下所示:
Path newFile = FileSystems.getDefault().getPath("C:/home/docs/newFile.txt");
Path copiedFile = FileSystems.getDefault().getPath("C:/home/docs/copiedFile.txt");
try {
Files.createFile(newFile);
System.out.println("File created successfully!");
Files.copy(newFile, copiedFile);
System.out.println("File copied successfully!");
}
catch (IOException e) {
System.out.println("IO Exception.");
}

  1. 执行程序。您的输出应如下所示:

文件创建成功!

文件复制成功!

它是如何工作的...

createFile方法创建了您的初始文件,copy方法将该文件复制到copiedFile变量指定的位置。如果您尝试连续两次运行该代码序列,您将遇到IOException,因为copy方法默认情况下不会替换现有文件。copy方法是重载的。使用带有java.lang.enum.StandardCopyOption枚举值REPLACE_EXISTINGcopy方法,允许替换文件,如下所示。

StandardCopyOption的三个枚举值列在下表中:

含义
ATOMIC_MOVE 原子性地执行复制操作
COPY_ATTRIBUTES 将源文件属性复制到目标文件
REPLACE_EXISTING 如果已存在,则替换现有文件

用以下代码序列执行前面的示例中的copy方法调用替换:

Files.copy(newFile, copiedFile, StandardCopyOption.REPLACE_EXISTING);

当代码执行时,文件应该被替换。在还有更多..部分的移动文件和目录配方中还有另一个使用复制选项的示例。

还有更多...

如果源文件和目标文件相同,则该方法会完成,但实际上不会发生复制。copy方法不是原子的。

还有另外两个重载的copy方法。一个是将java.io.InputStream复制到文件,另一个是将文件复制到java.io.OutputStream。在本节中,我们将更深入地研究以下过程:

  • 复制符号链接文件

  • 复制目录

  • 将输入流复制到文件

  • 将文件复制到输出流

复制符号链接文件

当复制符号链接文件时,会复制符号链接的目标。为了说明这一点,在music目录中创建一个名为users.txt的符号链接文件,指向docs目录中的users.txt文件。可以通过使用第二章中描述的管理符号链接配方中的过程,即使用路径定位文件和目录,或者使用本章中所示的管理符号链接配方中的方法来完成。

使用以下代码序列执行复制操作:

Path originalLinkedFile = FileSystems.getDefault().getPath("C:/home/music/users.txt");
Path newLinkedFile = FileSystems.getDefault().getPath("C:/home/music/users2.txt");
try {
Files.copy(originalLinkedFile, newLinkedFile);
System.out.println("Symbolic link file copied successfully!");
}
catch (IOException e) {
System.out.println("IO Exception.");
}

执行代码。您应该得到以下输出:

符号链接文件复制成功!

检查生成的music目录结构。user2.txt文件已添加,并且与链接文件或原始目标文件没有连接。修改user2.txt不会影响其他两个文件的内容。

复制目录

当复制目录时,会创建一个空目录。原始目录中的文件不会被复制。以下代码序列说明了这个过程:

Path originalDirectory = FileSystems.getDefault().getPath("C:/home/docs");
Path newDirectory = FileSystems.getDefault().getPath("C:/home/tmp");
try {
Files.copy(originalDirectory, newDirectory);
System.out.println("Directory copied successfully!");
}
catch (IOException e) {
e.printStackTrace();
}

执行此序列时,您应该得到以下输出:

目录复制成功!

检查tmp目录。它应该是空的,因为源目录中的任何文件都没有被复制。

将输入流复制到文件

copy方法有一个方便的重载版本,允许基于InputStream的输入创建新文件。该方法的第一个参数与原始copy方法不同,因为它是InputStream的实例。

以下示例使用此方法将jdk7.java.net网站复制到文件中:

Path newFile = FileSystems.getDefault().getPath("C:/home/docs/java7WebSite.html");
URI url = URI.create("http://jdk7.java.net/");
try (InputStream inputStream = url.toURL().openStream())
Files.copy(inputStream, newFile);
System.out.println("Site copied successfully!");
}
catch (MalformedURLException ex) {
ex.printStackTrace();
}
catch (IOException ex) {
ex.printStackTrace();
}

当代码执行时,您应该得到以下输出:

站点复制成功!

创建一个java.lang.Object.URI对象来表示网站。使用URI对象而不是java.lang.Object.URL对象立即避免了创建一个单独的 try-catch 块来处理MalformedURLException异常。

URL类的openStream方法返回一个InputStream,该流作为copy方法的第一个参数使用。请注意使用 try-with-resource 块。这个 try 块是 Java 7 中的新功能,并在第一章的使用 try-with-resource 块改进异常处理代码中有详细说明,Java 语言改进

然后执行了copy方法。现在可以使用浏览器打开新文件,或者根据需要进行处理。请注意,该方法返回一个表示写入的字节数的长整型值。

将文件复制到输出流

copy方法的第三个重载版本将打开一个文件并将其内容写入OutputStream。当需要将文件的内容复制到非文件对象(如PipedOutputStream)时,这可能很有用。当与其他线程通信或写入字节数组时,这也可能很有用,如本例所示。在这个例子中,users.txt文件的内容被复制到一个ByteArrayOutputStream的实例中。然后使用它的toByteArray方法来填充一个数组,如下所示:

Path sourceFile = FileSystems.getDefault().getPath("C:/home/docs/users.txt");
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Files.copy(sourceFile, outputStream);
byte arr[] = outputStream.toByteArray();
System.out.println("The contents of " + sourceFile.getFileName());
for(byte data : arr) {
System.out.print((char)data);
}
System.out.println();
}
catch (IOException ex) {
ex.printStackTrace();
}

执行这个序列。输出将取决于您的文件内容,但应该类似于以下内容:

users.txt 的内容

Bob

Jennifer

Sally

Tom

Ted

注意使用 try-with-resources 块来处理文件的打开和关闭。在复制操作完成或发生异常时关闭OutputStream总是一个好主意。try-with-resources 块很好地处理了这个问题。在某些情况下,该方法可能会阻塞,直到操作完成。它的行为大部分是特定于实现的。此外,输出流可能需要刷新,因为它实现了Flushable接口。请注意,该方法返回一个表示写入的字节数的长整型值。

另请参阅

有关使用符号链接的更多详细信息,请参阅管理符号链接食谱。

管理临时文件和目录

创建临时文件和目录的过程可能是许多应用程序的重要部分。临时文件可以用于中间数据或作为稍后清理的临时存储。通过Files类可以简单地完成管理临时文件和目录的过程。在本食谱中,我们将介绍如何使用createTempDirectorycreateTempFile方法创建临时文件和目录。

准备就绪

在我们的示例中,我们将创建一个临时目录,然后在目录中创建一个临时文件,如下所示:

  1. 创建代表临时文件和目录的Path对象。

  2. 使用createTempDirectory方法创建一个临时目录。

  3. 使用createTempFile方法创建一个临时文件。

如何做...

  1. 创建一个带有main方法的控制台应用程序。在main方法中,使用getPath方法创建一个Path对象rootDirectory。使用rootDirectory作为第一个参数,空字符串作为第二个参数调用createTempDirectory方法。然后使用toString方法将返回的Path对象dirPath转换为String并打印到屏幕上。接下来,使用dirPath作为第一个参数,空字符串作为第二和第三个参数添加createTempFile方法。再次使用toString方法打印出这个结果路径,如下所示:
try {
Path rootDirectory = FileSystems.getDefault().getPath("C:/home/docs");
Path tempDirectory = Files.createTempDirectory(rootDirectory, "");
System.out.println("Temporary directory created successfully!");
String dirPath = tempDirectory.toString();
System.out.println(dirPath);
Path tempFile = Files.createTempFile(tempDirectory,"", "");
System.out.println("Temporary file created successfully!");
String filePath = tempFile.toString();
System.out.println(filePath);
}
catch (IOException e) {
System.out.println("IO Exception.");
}

  1. 这段代码序列将产生类似于以下内容的输出:

临时目录创建成功!

C:\home\docs\7087436262102989339

临时文件创建成功!

C:\home\docs\7087436262102989339\3473887367961760381

工作原理...

createTempDirectory方法创建一个空目录并返回代表这个新目录位置的Path对象。同样,createTempFile方法创建一个空文件并返回代表这个新文件的Path对象。在我们之前的例子中,我们使用toString方法来查看我们的目录和文件创建的路径。之前的数字目录和文件名由系统分配,并且是特定于平台的。

这个createTempDirectory方法至少需要两个参数,即指向新目录位置的Path对象和指定目录前缀的String变量。在我们之前的例子中,我们留空了前缀。但是,如果我们想要指定文本以在系统分配的文件名之前出现,第二个变量可以用这个前缀字符串填充。

createTempFile方法的工作方式与createTempDirectory方法类似,如果我们想要为临时文件分配一个前缀,我们可以使用第二个参数来指定字符串。此方法的第三个参数也可以用来指定文件的后缀或类型,例如.txt

重要的是要注意,尽管在我们的例子中我们指定了我们想要创建目录和文件的Path,但每种方法还有另一个版本,其中初始参数,Path对象,可以被省略,目录和/或文件将被创建在系统的默认临时目录中。此外,这些方法在创建文件或目录之前不会检查文件或目录的存在,并且会覆盖具有相同临时、系统分配名称的任何现有文件或目录。

更多内容...

文件属性名称也可以传递给重载的createTempDirectorycreateTempFile方法。这些属性是可选的,但可以用来指定临时文件的处理方式,例如文件是否应在关闭时被删除。文件属性的创建在更多内容管理 POSIX 文件权限配方的部分中描述。

createTempDirectorycreateTempFile方法的存在是有限的。如果希望自动删除这些文件或目录,可以使用关闭挂钩或java.io.File类的deleteOnExit方法。这两种技术将导致在应用程序或 JVM 终止时删除元素。

设置文件或目录的与时间相关的属性

文件的时间戳对于某些应用程序可能至关重要。例如,操作执行的顺序可能取决于文件的最后更新时间。BasicFileAttributeView支持三种日期:

  • 最后修改时间

  • 最后访问时间

  • 创建时间

它们可以使用BasicFileAttributeView接口的setTimes方法进行设置。正如我们将在更多内容部分看到的,Files类可以用来设置或仅获取最后修改时间。

准备工作

为了使用setTimes方法设置时间,我们需要做以下操作:

  1. 获取代表感兴趣文件的Path对象。

  2. 获取BasicFileAttributeView对象。

  3. 为所需的时间创建FileTime对象。

  4. 使用这些FileTime对象作为setTimes方法的参数。

如何做...

  1. 使用以下main方法创建一个新的控制台应用程序。我们将更新我们最喜欢的文件users.txt的最后修改时间为当前时间:
public static void main(String[] args) throws Exception {
Path path = Paths.get("C:/home/docs/users.txt");
BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
FileTime lastModifedTime;
FileTime lastAccessTime;
FileTime createTime;
BasicFileAttributes attributes = view.readAttributes();
lastModifedTime = attributes.lastModifiedTime();
createTime = attributes.creationTime();
long currentTime = Calendar.getInstance().getTimeInMillis();
lastAccessTime = FileTime.fromMillis(currentTime);
view.setTimes(lastModifedTime, lastAccessTime, createTime);
System.out.println(attributes.lastAccessTime());
}

  1. 执行应用程序。除非您有时间机器的访问权限,或者以其他方式操纵了系统的时钟,否则您的输出应该反映出比以下显示的时间更晚的时间:

2011-09-24T21:34:55.012Z

工作原理...

首先为 users.txt 文件创建了一个 Path。接下来,使用 getFileAttributeView 方法获得了 BasicFileAttributeView 接口的一个实例。使用 try 块来捕获 readAttributessetTimes 方法可能抛出的任何 IOExceptions

在 try 块中,为三种类型的时间创建了 FileTime 对象。文件的 lastModifedTimecreateTime 时间没有改变。这些是使用 BasicFileAttributes 类的相应方法获得的,该类是使用 view 方法获得的。

currentTime 长变量被赋予以毫秒表示的当前时间。它的值是使用 Calendar 类的实例执行 getTimeInMillis 方法获得的。然后,三个 FileTime 对象被用作 setTimes 方法的参数,有效地设置了这些时间值。

还有更多...

FileTime 类的使用还不止以上所述。此外,Files 类提供了维护时间的替代方法。在这里,我们将进一步探讨以下内容:

  • 了解 FileTime

  • 使用 Files 类的 setLastModifiedTime 来维护最后修改时间

  • 使用 Files 类的 setAttribute 方法来设置单个属性

了解 FileTime

java.nio.file.attribute.FileTime 类表示用于 java.nio 包方法的时间。要创建一个 FileTime 对象,我们需要使用以下两个静态 FileTime 方法之一:

  • from 方法,接受一个表示持续时间的长数字和一个表示时间测量单位的 TimeUnit 对象

  • fromMillis 方法,接受一个基于纪元的毫秒数的长参数

TimeUnitjava.util.concurrent 包中的一个枚举。它表示如下表中定义的时间持续时间。它与另一个参数结合使用,其组合表示时间持续时间:

枚举值 含义
纳秒 千分之一微秒
微秒 千分之一毫秒
毫秒 千分之一秒
一秒
分钟 六十秒
小时 六十分钟
二十四小时

from 方法返回一个 TimeUnit 对象。它的值是通过将第一个长参数(其度量单位由第二个 TimeUnit 参数指定)加到纪元得到的。

注意

纪元是 1970-01-01T00:00:00Z,这是大多数计算机上用于指定时间的基本时间。这个基本时间代表 1970 年 1 月 1 日的协调世界时午夜。

例如,from 方法可以用来表示一个时间点,即从纪元开始的 1000 天,使用以下代码序列:

FileTime fileTime = FileTime.from(1000, TimeUnit.DAYS);
System.out.println(fileTime);

执行时应该得到以下输出:

1972-09-27T00:00:00Z

fromMillis 方法用于创建一个 FileTime 对象,其时间是通过将其参数加到纪元得到的,其中参数是以毫秒表示的长数字。如果我们使用以下 fromMillis 方法而不是如下所示的 from 方法:

FileTime fileTime = FileTime.fromMillis(1000L*60*60*24*1000);

我们将得到相同的结果。注意,第一个参数是一个长整型字面量,这迫使表达式的结果为长整数。如果我们没有将结果提升为长整数值,我们将得到一个整数值,这将导致溢出和错误的日期。任何方法的第一个参数都可以是负数。

注意

有关在 Java 中使用时间的更多细节,请参阅www3.ntu.edu.sg/home/ehchua/programming/java/DateTimeCalendar.html

使用 Files 类的 setLastModifiedTime 来维护最后修改时间

Files类的getLastModifiedTimesetLastModifiedTime方法提供了设置文件最后修改属性的另一种方法。在下面的代码序列中,setLastModifiedTime方法使用lastModifedTime对象来设置时间,如下所示:

Files.setLastModifiedTime(path, lastModifedTime);

Files类的getLastModifiedTime返回一个FileTime对象。我们可以使用这个方法将一个值赋给lastModifedTime变量,如下所示:

lastModifedTime = Files.getLastModifiedTime(path);

该方法有一个可选的LinkOption参数,指示是否应该跟随符号链接。

使用 Files 类的 setAttribute 方法来设置单个属性

setAttribute方法提供了一种灵活和动态的方法来设置某些文件属性。要设置最后修改时间,我们可以使用以下代码序列:

Files.setAttribute(path, "basic:lastAccessTime", lastAccessTime);

第三章中的使用 getAttribute 方法逐个获取属性配方详细介绍了可以设置的其他属性。

另请参阅

管理符号链接配方讨论了符号链接的使用。

管理文件所有权

文件或目录的所有者可以在文件创建后进行修改。这是通过使用java.nio.file.attribute.FileOwnerAttributeView接口的setOwner方法来实现的,当所有权发生变化并需要以编程方式进行控制时,这将非常有用。

使用java.nio.file.attribute.UserPrincipal对象表示一个用户。使用Path对象表示一个文件或目录。将这两个对象与Files类的setOwner方法一起使用,可以维护文件的所有权。

准备工作

为了更改文件或目录的所有者:

  1. 获取一个代表文件或目录的Path对象。

  2. 使用Path作为getFileAttributeView方法的参数。

  3. 创建一个代表新所有者的UserPrincipal对象。

  4. 使用FileOwnerAttributeView接口的setOwner方法来更改文件的所有者。

如何做...

  1. 在这个例子中,我们将假设users.txt文件的当前所有者是richard。我们将把所有者更改为一个名为jennifer的用户。为此,在系统上创建一个名为jennifer的新用户。创建一个包含以下main方法的新控制台应用程序。在该方法中,我们将使用FileOwnerAttributeViewUserPrincipal对象来更改所有者,如下所示:
public static void main(String[] args) throws Exception {
Path path = Paths.get("C:/home/docs/users.txt");
FileOwnerAttributeView view = Files.getFileAttributeView(path, FileOwnerAttributeView.class);
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal userPrincipal = lookupService.lookupPrincipalByName("jennifer");
view.setOwner(userPrincipal);
System.out.println("Owner: " + view.getOwner().getName());
}

  1. 为了修改文件的所有权,我们必须拥有适当的权限。本章的介绍解释了如何获取管理员权限。当应用程序在 Windows 7 上执行时,输出应该反映出 PC 名称和文件所有者,如下所示。PC 名称与所有者之间用反斜杠分隔:

所有者:Richard-PC\Richard

所有者:Richard-PC\Jennifer

工作原理...

首先为users.txt文件创建了一个Path。接下来,使用getFileAttributeView方法获取了FileOwnerAttributeView接口的一个实例。在 try 块内,使用默认的FileSystem类的getUserPrincipalLookupService方法创建了一个UserPrincipalLookupService对象。lookupPrincipalByName方法传递了字符串jennifer,返回了代表该用户的UserPrincipal对象。

最后一步是将UserPrincipal对象传递给setOwner方法。然后使用getOwner方法检索当前所有者以验证更改。

还有更多...

FileOwnerAttributeView派生的任何接口都可以使用getOwnersetOwner方法。这些包括AclFileAttributeViewPosixFileAttributeView接口。此外,Files类的setOwner方法也可以用于更改文件的所有权。

使用 Files 类的 setOwner 方法

Files类的setOwner方法与FileOwnerAttributeView接口的setOwner方法相同。不同之处在于它有两个参数,一个表示文件的Path对象和一个UserPrincipal对象。以下序列说明了将users.txt文件的所有者设置为jennifer的过程:

Path path = Paths.get("C:/home/docs/users.txt");
try {
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal userPrincipal = lookupService.lookupPrincipalByName("jennifer");
Files.setOwner(path, userPrincipal);
System.out.println("Owner: " + view.getOwner().getName());
}
catch (IOException ex) {
ex.printStackTrace();
}

管理 ACL 文件权限

在本教程中,我们将研究如何设置 ACL 权限。设置这些权限的能力对许多应用程序很重要。例如,当我们需要控制谁可以修改或执行文件时,我们可以通过编程方式影响这种变化。我们可以改变的内容由稍后列出的AclEntryPermission枚举值表示。

准备工作

为文件设置新的 ACL 权限:

  1. 为要更改其属性的文件创建Path对象。

  2. 获取该文件的AclFileAttributeView

  3. 为用户获取一个UserPrincipal对象。

  4. 获取当前分配给文件的 ACL 条目列表。

  5. 创建一个持有我们要添加的权限的新AclEntry.Builder对象。

  6. 将权限添加到 ACL 列表中。

  7. 使用setAcl方法用新的 ACL 列表替换当前的 ACL 列表。

操作步骤...

  1. 使用以下main方法创建一个新的控制台应用程序。在这个方法中,我们将首先简单地显示文件users.txt的当前 ACL 列表,如下所示:
public static void main(String[] args) throws Exception {
Path path = Paths.get("C:/home/docs/users.txt");
AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
List<AclEntry> aclEntryList = view.getAcl();
displayAclEntries(aclEntryList);
}

  1. 为了说明添加和删除 ACL 属性的过程,我们将使用一系列辅助方法:
  • displayAclEntries: 显示主体和条目类型,然后调用其他两个辅助方法

  • displayEntryFlags: 如果存在,显示条目标志

  • displayPermissions: 如果有的话,显示条目权限

  1. 按照以下代码向应用程序添加方法:
private static void displayAclEntries(List<AclEntry> aclEntryList) {
System.out.println("ACL Entry List size: " + aclEntryList.size());
for (AclEntry entry : aclEntryList) {
System.out.println("User Principal Name: " + entry.principal().getName());
System.out.println("ACL Entry Type: " + entry.type());
displayEntryFlags(entry.flags());
displayPermissions(entry.permissions());
System.out.println();
}
}
private static void displayPermissions(Set<AclEntryPermission> permissionSet) {
if (permissionSet.isEmpty()) {
System.out.println("No Permissions present");
}
else {
System.out.println("Permissions");
for (AclEntryPermission permission : permissionSet) {
System.out.print(permission.name() + " ");
}
System.out.println();
}
}
private static void displayEntryFlags(Set<AclEntryFlag> flagSet) {
if (flagSet.isEmpty()) {
System.out.println("No ACL Entry Flags present");
}
else {
System.out.println("ACL Entry Flags");
for (AclEntryFlag flag : flagSet) {
System.out.print(flag.name() + " ");
}
System.out.println();
}
}

  1. ACL 列表包含文件的 ACL 条目。当执行displayAclEntries方法时,它将方便地显示条目数量,然后每个条目将用空行分隔。以下是users.txt文件可能的列表:

所有者:Richard-PC\Richard

ACL 条目列表大小:4

用户主体名称:BUILTIN\Administrators

ACL 条目类型:允许

没有 ACL 条目标志

权限

读取数据 删除 读取命名属性 读取属性 写入所有者 删除子项 写入数据 追加数据 同步 执行 写入属性 写入 ACL 写入命名属性 读取 ACL

用户主体名称:NT AUTHORITY\SYSTEM

ACL 条目类型:允许

没有 ACL 条目标志

权限

读取数据 删除 读取命名属性 读取属性 写入所有者 删除子项 写入数据 追加数据 同步 执行 写入属性 写入 ACL 写入命名属性 读取 ACL

用户主体名称:BUILTIN\用户

ACL 条目类型:允许

没有 ACL 条目标志

权限

读取数据 同步 执行 读取命名属性 读取属性 读取 ACL

用户主体名称:NT AUTHORITY\已验证用户

ACL 条目类型:允许

没有 ACL 条目标志

权限

追加数据 读取数据 删除 同步 执行 读取命名属性 读取属性 写入属性 写入命名属性 读取 ACL 写入数据

  1. 接下来,使用UserPrincipalLookupService类的lookupService方法返回UserPrincipalLookupService类的实例。使用它的lookupPrincipalByName方法根据用户名称返回一个UserPrincipal对象。在调用displayAclEntries方法之后添加以下代码:
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal userPrincipal = lookupService.lookupPrincipalByName("users");

  1. 接下来,添加以下代码来创建和设置一个AclEntry.Builder对象。这将用于为用户添加WRITE_ACL 和 DELETE权限。将条目添加到 ACL 列表,并使用setAcl方法将其附加到当前文件,如下所示:
AclEntry.Builder builder = AclEntry.newBuilder();
builder.setType(AclEntryType.ALLOW);
builder.setPrincipal(userPrincipal);
builder.setPermissions(
AclEntryPermission.WRITE_ACL,
AclEntryPermission.DELETE);
AclEntry entry = builder.build();
aclEntryList.add(0, entry);
view.setAcl(aclEntryList);

  1. 执行应用程序。为了修改文件的一些 ACL 属性,我们必须具有适当的权限。本章的介绍详细介绍了如何以管理员身份运行应用程序的细节。接下来,注释掉添加 ACL 条目的代码,并验证是否已添加 ACL 条目。您应该看到以下条目添加到列表中:

ACL 条目列表大小:5

用户主体名称:BUILTIN\Users

ACL 条目类型:允许

没有 ACL 条目标志存在

权限

WRITE_ACL DELETE

它是如何工作的...

main方法中,我们创建了Path对象,然后使用它来获取java.nio.file.attribute.AclFileAttributeView接口的实例。Path对象表示的文件是users.txt文件。AclFileAttributeView对象可以用于多种目的。在这里,我们只对使用其getAcl方法返回与文件关联的 ACL 属性列表感兴趣。

我们只显示当前 ACL 的列表,以查看它们是什么,并最终验证文件的属性是否已更改。ACL 属性与用户关联。在这个例子中,我们创建了一个代表用户的UserPrincipal对象。

可以使用java.nio.file.attribute.AclEntry.Builder类的build方法创建新的 ACL 条目。静态的newBuilder方法创建了AclEntry.Builder类的一个实例。执行setPrincipal方法将用户设置为属性的主体。setPermissions方法接受一组AclEntryPermission对象或可变数量的AclEntryPermission对象。在这个例子中,我们使用了一个由逗号分隔的两个权限组成的列表:AclEntryPermission.WRITE_ACLAclEntryPermission.DELETE

然后将AclEntry.Builder对象添加到文件的现有 ACL 中。条目被添加到列表的开头。最后一步是使用setAcl方法用新的 ACL 列表替换旧的 ACL 列表。

还有更多...

要删除 ACL 属性,我们需要获取当前列表,然后确定要删除的属性的位置。我们可以使用java.util.List接口的remove方法来删除该项。然后可以使用setAcl方法用新列表替换旧列表。

ACL 属性在RFC 3530: Network File System (NFS) version 4 Protocol中有更详细的解释。以下表格提供了有关可用 ACL 权限的附加信息和见解。枚举AclEntryType具有以下值:

含义
ALARM 在尝试访问指定属性时,以系统特定的方式生成警报
ALLOW 授予权限
AUDIT 在尝试访问指定属性时,以系统相关的方式记录所请求的访问
DENY 拒绝访问

AclEntryPermission枚举值总结如下表所示:

含义
APPEND_DATA 能够向文件追加数据
DELETE 能够删除文件
DELETE_CHILD 能够删除目录中的文件或目录
EXECUTE 能够执行文件
READ_ACL 能够读取 ACL 属性
READ_ATTRIBUTES 能够读取(非 ACL)文件属性
READ_DATA 能够读取文件的数据
READ_NAMED_ATTRS 能够读取文件的命名属性
SYNCHRONIZE 能够在服务器上本地访问文件,进行同步读写
WRITE_ACL 能够写入 ACL 属性
WRITE_ATTRIBUTES 能够写入(非 ACL)文件属性
WRITE_DATA 能够修改文件的数据
WRITE_NAMED_ATTRS 能够写入文件的命名属性
WRITE_OWNER 能够更改所有者

AclEntryFlag枚举适用于目录条目。总结为四个值如下:

含义
DIRECTORY_INHERIT ACL 条目应添加到每个新创建的目录
FILE_INHERIT ACL 条目应添加到每个新创建的非目录文件
INHERIT_ONLY ACL 条目应添加到每个新创建的文件或目录
NO_PROPAGATE_INHERIT ACL 条目不应放置在新创建的目录上,该目录可被创建目录的子目录继承

目前,AclEntryType.AUDITAclEntryType.ALARM没有与之关联的标志。

管理 POSIX 属性

可用的 POSIX 属性包括组所有者、用户所有者和一组权限。在本示例中,我们将研究如何维护这些属性。管理这些属性使得开发应用程序在多个操作系统上执行更加容易。虽然属性的数量有限,但对于许多应用程序来说可能已经足够了。

有三种方法可以用来管理 POSIX 属性:

  • java.nio.file.attribute.PosixFileAttributeView接口

  • Files类的设置/获取 POSIX 文件权限方法

  • Files类的setAttribute方法

使用PosixFileAttributeView接口获取PosixFileAttributes对象的方法在第三章的使用 PosixFileAttributeView 维护 POSIX 文件属性中有详细说明。在这里,我们将首先说明如何使用PosixFileAttributeView接口方法,并在本示例的还有更多..部分演示最后两种方法。

准备工作

要维护文件的 POSIX 权限属性,我们需要:

  1. 创建一个表示感兴趣的文件或目录的Path对象。

  2. 获取该文件的PosixFileAttributes对象。

  3. 使用permissions方法获取该文件的一组权限。

  4. 修改权限集。

  5. 使用setPermissions方法替换权限。

如何做...

  1. 我们将创建一个应用程序,获取PosixFileAttributes对象并使用它来显示users.txt文件的当前权限集,然后向文件添加PosixFilePermission.OTHERS_WRITE权限。创建一个新的控制台应用程序,并添加以下main方法:
public static void main(String[] args) throws Exception {
Path path = Paths.get("home/docs/users.txt");
FileSystem fileSystem = path.getFileSystem();
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
PosixFileAttributes attributes = view.readAttributes();
Set<PosixFilePermission> permissions = attributes.permissions();
listPermissions(permissions);
permissions.add(PosixFilePermission.OTHERS_WRITE);
view.setPermissions(permissions);
System.out.println();
listPermissions(permissions);
}
private static void listPermissions(Set<PosixFilePermission> permissions) {
System.out.print("Permissions: ");
for (PosixFilePermission permission : permissions) {
System.out.print(permission.name() + " ");
}
System.out.println();
}

  1. 在支持 POSIX 的系统上执行应用程序。在Ubuntu 11.04下执行时,应该会得到类似以下的结果:

权限:GROUP_READ OWNER_WRITE OTHERS_READ OWNER_READ

权限:GROUP_READ OWNER_WRITE OTHERS_WRITE OTHERS_READ OWNER_READ

它是如何工作的...

main方法中,我们获取了users.txt文件的Path,然后使用getFileAttributeView方法获取了PosixFileAttributeView的实例。然后使用readAttributes方法获取了表示文件 POSIX 属性的PosixFileAttributes对象的实例。

使用listPermissions方法列出文件的权限。在添加新权限到文件之前和之后各执行一次此方法。我们这样做只是为了显示权限的变化。

使用add方法将PosixFilePermission.OTHERS_WRITE权限添加到权限集中。以下表列出了PosixFilePermission枚举值:

级别 授予权限
GROUP_EXECUTE 执行和搜索
GROUP_READ 读取
GROUP_WRITE 写入
OTHERS_EXECUTE 其他人 执行和搜索
OTHERS_READ 读取
OTHERS_WRITE 写入
OWNER_EXECUTE 拥有者 执行和搜索
OWNER_READ 读取
OWNER_WRITE 写入

在这个例子中,我们添加了一个PosixFilePermission.OTHERS_WRITE权限。在下一节中,我们将说明如何删除权限。

还有更多...

还有其他几个感兴趣的操作,包括:

  • 删除文件权限

  • 修改文件的 POSIX 所有权

  • 使用Files类的set/get POSIX 文件权限方法

  • 使用Files类的setAttribute方法

  • 使用PosixFilePermissions类创建PosixFilePermissions

删除文件权限

删除权限只是一个简单的事情:

  • 获取文件的一组权限

  • 使用Set接口的remove方法来删除权限

  • 将集合重新分配给文件

这在以下代码序列中有所体现,其中删除了PosixFilePermission.OTHERS_WRITE权限:

Set<PosixFilePermission> permissions = attributes.permissions();
Permissions.remove(PosixFilePermission.OTHERS_WRITE);
view.setPermissions(permissions);

修改文件的 POSIX 所有权

POSIX 所有者在组和用户级别指定。PosixFileAttributes方法的组和所有者将返回表示文件的组和用户所有者的对象。setGroupsetOwner方法将设置相应的成员资格。

在接下来的示例中,将显示users.txt文件的所有者,然后进行更改。创建UserPrincipal对象以支持set方法:

Path path = Paths.get("home/docs/users.txt");
try {
FileSystem fileSystem = path.getFileSystem();
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
PosixFileAttributes attributes = view.readAttributes();
Set<PosixFilePermission> permissions = attributes.permissions();
System.out.println("Old Group: " + attributes.group().getName());
System.out.println("Old Owner: " + attributes.owner().getName());
System.out.println();
UserPrincipalLookupService lookupService = FileSystems.getDefault().getUserPrincipalLookupService();
UserPrincipal userPrincipal = lookupService.lookupPrincipalByName("jennifer");
GroupPrincipal groupPrincipal = lookupService.lookupPrincipalByGroupName(("jennifer");
view.setGroup(groupPrincipal);
view.setOwner(userPrincipal);
attributes = view.readAttributes();
System.out.println("New Group: " + attributes.group().getName());
System.out.println("New Owner: " + attributes.owner().getName());
System.out.println();
POSIX attributesfile permission, removing}
catch (IOException ex) {
ex.printStackTrace();
}

执行时,输出应如下所示:

为 users.txt 设置所有者

旧组:richard

旧所有者:richard

新组:jennifer

新所有者:jennifer

您可能需要以管理员身份执行代码,详细信息请参见介绍。

使用 Files 类的 set/get POSIX 文件权限方法

此方法使用Files类的setPosixFilePermissionsgetPosixFilePermissions方法。getPosixFilePermissions方法返回指定其第一个参数的文件的PosixFilePermissions集合。它的第二个参数是LinkOption,用于确定如何处理符号链接文件。通常不会跟随链接,除非使用LinkOption.NOFOLLOW_LINKS。我们可以使用以下代码序列列出与文件关联的权限:

Path path = Paths.get("home/docs/users.txt");
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
System.out.print("Permissions: ");
for (PosixFilePermission permission : permissions) {
System.out.print(permission.name() + " ");
}
System.out.println();
}
catch (IOException ex) {
ex.printStackTrace();
}

setPermissions方法接受表示文件的Path对象和一组PosixFilePermission。而不是使用以前的方法:

view.setPermissions(path, permissions);

我们可以使用Files类的setPosixFilePermissions方法:

Files.setPosixFilePermissions(path, permissions);

使用Files类简化了该过程,避免了创建PosixFileAttributes对象。

使用 Files 类的 setAttribute 方法

Files类的getAttribute方法在第三章中详细介绍了使用 getAttribute 方法逐个获取属性配方。setAttribute方法将设置一个属性,并具有以下四个参数:

  • 表示文件的Path对象

  • 包含要设置的属性的String

  • 表示属性值的对象

  • 指定符号链接的可选LinkOption

以下说明了向users.txt文件添加PosixFilePermission.OTHERS_WRITE权限:

Path path = Paths.get("home/docs/users.txt");
try {
Files.setAttribute(path, "posix:permission, PosixFilePermission.OTHERS_WRITE);
}
catch (IOException ex) {
ex.printStackTrace();
}

在此示例中未使用LinkOption值。

使用 PosixFilePermissions 类创建 PosixFilePermissions

PosixFilePermissions类拥有三种方法:

  • asFileAttribute,返回一个包含一组PosixFilePermissionsFileAttribute对象

  • fromString,也返回一个基于String参数的PosixFilePermissions集合

  • toString,执行fromString方法的逆操作

所有三种方法都是静态的。第一种方法返回一个FileAttribute对象,可以与创建文件和目录配方中讨论的createFilecreateDirectory方法一起使用。

在 Unix 系统上,文件权限经常表示为一个九个字符的字符串。这个字符串分为三个字符组。第一组表示用户的权限,第二组表示组的权限,最后一组表示其他所有人的权限。这三个字符组中的每一个表示为该组授予的读、写或执行权限。在第一个位置的r表示读权限,第二个位置的w表示写权限,最后一个位置的x表示执行权限。在任何这些位置上的-表示权限未设置。

为了说明这些方法,执行以下代码序列:

Path path = Paths.get("home/docs/users.txt");
try {
FileSystem fileSystem = path.getFileSystem();
PosixFileAttributeView view = Files.getFileAttributeView(path, PosixFileAttributeView.class);
PosixFileAttributes attributes = view.readAttributes();
Set<PosixFilePermission> permissions = attributes.permissions();
for(PosixFilePermission permission : permissions) {
System.out.print(permission.toString() + ' ');
}
System.out.println();
FileAttribute<Set<PosixFilePermission>> fileAttributes = PosixFilePermissions.asFileAttribute(permissions);
Set<PosixFilePermission> fileAttributeSet = fileAttributes.value();
for (PosixFilePermission posixFilePermission : fileAttributeSet) {
System.out.print(posixFilePermission.toString() + ' ');
}
System.out.println();
System.out.println(PosixFilePermissions.toString(permissions));
permissions = PosixFilePermissions.fromString("rw-rw-r--");
for(PosixFilePermission permission : permissions) {
System.out.print(permission.toString() + ' ');
}
System.out.println();
}
catch (IOException ex) {
}

你的输出应该类似于以下内容:

OTHERS_READ OWNER_READ GROUP_READ OWNER_WRITE

OTHERS_READ OWNER_READ OWNER_WRITE GROUP_READ

rw-r--r--

OWNER_READ OWNER_WRITE GROUP_READ GROUP_WRITE OTHERS_READ

代码的第一部分获取了users.txt文件的权限集,就像在本食谱中早些时候详细介绍的那样。然后显示了权限。接下来,执行了asFileAttribute方法,返回了文件的FileAttribute。使用value方法获取了一组属性,然后显示了这些属性。两组权限被显示,但顺序不同。

接下来,使用toString方法将这组权限显示为字符串。注意每个字符反映了对users.txt文件授予的权限。

最后的代码段使用fromString方法创建了一个新的权限集。然后显示这些权限以验证转换。

移动文件和目录

移动文件或目录在重新组织用户空间结构时非常有用。这个操作由Files类的move方法支持。在移动文件或目录时,有几个因素需要考虑。这些包括符号链接文件是否存在,move是否应该替换现有文件,以及移动是否应该是原子的。

如果移动发生在相同的文件存储上,移动可能会导致资源的重命名。使用这个方法有时会使用Path接口的resolveSibling方法。这个方法将用它的参数替换路径的最后一部分。这在重命名文件时很有用。resolveSibling方法在第二章的使用路径解析合并路径食谱的还有更多..部分中有详细介绍。

做好准备

为了移动文件或目录:

  1. 获取一个Path对象,表示要移动的文件或目录。

  2. 获取一个Path对象,表示移动的目的地。

  3. 确定复制选项以控制移动。

  4. 执行move方法。

操作步骤...

  1. 使用以下main方法创建一个新的控制台应用程序。我们将把users.txt文件移动到music目录:
public static void main(String[] args) throws Exception {
Path sourceFile = Paths.get("C:/home/docs/users.txt");
Path destinationFile = Paths.get ("C:/home/music/users.txt");
Files.move(sourceFile, destinationFile);
}

  1. 执行应用程序。检查docsmusic目录的内容。users.txt文件应该不在docs目录中,但在music目录中。

工作原理...

move方法使用这两个Path对象,并且没有使用第三个可选参数。这个参数用于确定复制操作的工作方式。当它没有被使用时,文件复制操作默认为简单复制。

StandardCopyOption枚举实现了CopyOption接口,并定义了支持的复制操作类型。CopyOption接口与Files类的copymove方法一起使用。下表列出了这些选项。这些选项在还有更多..部分中有更详细的解释:

含义
ATOMIC_MOVE 移动操作是原子的
COPY_ATTRIBUTES 源文件属性被复制到新文件
REPLACE_EXISTING 如果目标文件存在,则替换目标文件

如果目标文件已经存在,则会抛出FileAlreadyExistsException异常。但是,如果CopyOption.REPLACE_EXISTING作为move方法的第三个参数使用,则不会抛出异常。当源是符号链接时,将复制链接而不是链接的目标。

还有更多...

有几个需要涵盖的变化和问题。这些包括:

  • 移动方法的琐碎用法

  • 标准复制选项枚举值的含义

  • 使用resolveSibling方法与move方法影响重命名操作

  • 移动目录

移动方法的琐碎用法

如果源文件和目标文件相同,则该方法不会产生任何效果。以下代码序列将不会产生任何效果:

Path sourceFile = ...;
Files.move(sourceFile, sourceFile);

不会抛出异常,文件也不会被移动。

标准复制选项枚举值的含义

标准复制选项枚举值需要更多的解释。StandardCopyOption.REPLACE_EXISTING的值将替换已存在的文件。如果文件是符号链接,则只替换符号链接文件,而不是其目标。

StandardCopyOption.COPY_ATTRIBUTES的值将复制文件的所有属性。StandardCopyOption.ATOMIC_MOVE的值指定移动操作要以原子方式执行。所有其他枚举值都将被忽略。但是,如果目标文件已经存在,则要么替换文件,要么抛出IOException。结果取决于实现。如果无法以原子方式执行移动操作,则会抛出AtomicMoveNotSupportedException。原子移动可能由于源文件和目标文件的文件存储器的差异而失败。

如果在 Windows 7 上执行以下代码序列:

Path sourceFile = Paths.get("C:/home/docs/users.txt");
Path destinationFile = Paths.get("C:/home/music/users. txt");
Files.move(sourceFile, destinationFile, StandardCopyOption.ATOMIC_MOVE);

如果目标文件已经存在,则会抛出AccessDeniedException异常。如果文件不存在,其执行将导致以下错误消息:

java.nio.file.AtomicMoveNotSupportedException: C:\home\docs\users.txt -> E:\home\music\users.txt: 系统无法将文件移动到不同的磁盘驱动器

使用resolveSibling方法与move方法影响重命名操作

resolveSibling方法将用不同的字符串替换路径的最后一部分。这可以用于在使用move方法时影响重命名操作。在以下序列中,users.txt文件被有效地重命名:

Path sourceFile = Paths.get("C:/home/docs/users.txt");
Files.move(sourceFile, sourceFile.resolveSibling(sourceFile.getFileName()+".bak"));

文件已重命名为users.txt.bak。请注意,源文件路径被使用了两次。要重命名文件并替换其扩展名,可以使用显式名称,如下所示:

Files.move(sourceFile, sourceFile.resolveSibling("users.bak"));

更复杂的方法可能使用以下序列:

Path sourceFile = Paths.get("C:/home/docs/users.txt");
String newFileName = sourceFile.getFileName().toString();
newFileName = newFileName.substring(0, newFileName.indexOf('.')) + ".bak";
Files.move(sourceFile, sourceFile.resolveSibling(newFileName));

substring方法返回一个以第一个字符开头,以紧接着句号之前的字符结尾的新文件名。

移动目录

当在同一文件存储器上移动目录时,目录和子目录也会被移动。以下将把docs目录、其文件和子目录移动到music目录中:

Path sourceFile = Paths.get("C:/home/docs");
Path destinationFile = Paths.get("C:/home/music/docs");
Files.move(sourceFile, destinationFile);

然而,执行此代码序列,其中docs目录将被移动到E驱动器上类似的文件结构,将导致DirectoryNotEmptyException异常:

Path sourceFile = Paths.get("C:/home/docs");
Path destinationFile = Paths.get("E:/home/music/docs");
Files.move(sourceFile, destinationFile);

在不同文件存储器之间移动目录将导致异常,如果目录不为空。如果在前面的例子中docs目录是空的,move方法将成功执行。如果需要在不同文件存储器之间移动非空目录,通常需要进行复制操作,然后进行删除操作。

删除文件或目录

删除文件或目录当它们不再需要时是一个常见的操作。它将节省系统空间并导致更干净的文件系统。Files类有两种方法可以用来删除文件或目录:deletedeleteIfExists。它们都以Path对象作为参数,并可能抛出IOException

准备工作

要删除文件或目录,需要执行以下操作:

  1. 获取一个代表文件或目录的Path对象。

  2. 使用deletedeleteIfExists方法来删除元素。

如何做...

  1. 创建一个新的控制台应用程序,并使用以下main方法:
public static void main(String[] args) throws Exception {
Path sourceFile = Paths.get("C:/home/docs/users.txt");
Files.delete(sourceFile);
}

  1. 执行应用程序。如果users.txt文件在程序运行时存在于目录中,那么在程序执行后它就不应该存在。如果文件不存在,那么你的程序输出应该类似于以下内容:

java.nio.file.NoSuchFileException: C:\home\docs\users.txt

它是如何工作的...

这种方法很简单。我们创建了一个代表users.txt方法的Path对象。然后我们将它作为delete方法的参数。由于delete方法可能会抛出IOException,所以代码被包含在 try-catch 块中。

为了避免如果文件不存在会抛出异常,我们可以使用deleteIfExists方法。用以下内容替换delete方法的调用:

Files.deleteIfExists(sourceFile);

确保文件不存在,然后执行此代码。程序应该正常终止,不会抛出任何异常。

还有更多...

如果我们尝试删除一个目录,那么该目录必须首先为空。如果目录不为空,则会抛出DirectoryNotEmptyException异常。执行以下代码序列来代替前面的示例:

Path sourceFile = Paths.get("C:/home/docs");
Files.delete(sourceFile);

假设docs目录不为空,应用程序应该抛出DirectoryNotEmptyException异常。

空目录的定义取决于文件系统的实现。在一些系统中,如果目录只包含特殊文件或符号链接,该目录可能被认为是空的。

如果一个目录不为空并且需要被删除,那么需要首先使用walkFileTree方法删除它的条目,就像在第五章的使用 SimpleFileVisitor 类遍历文件系统中所示的那样,管理文件系统

注意

如果要删除的文件是一个符号链接,只有链接被删除,而不是链接的目标。此外,如果文件被其他应用程序打开或正在使用中,可能无法删除文件。

管理符号链接

符号链接是文件,它们不是真正的文件,而是指向真正文件的链接,通常称为目标文件。当希望一个文件出现在多个目录中而不必实际复制文件时,这些链接是有用的。这样可以节省空间,并且所有更新都被隔离到一个单独的文件中。

Files类具有以下三种方法来处理符号链接:

  • createSymbolicLink方法,用于创建到可能不存在的目标文件的符号链接

  • createLink方法创建一个到现有文件的硬链接

  • readSymbolicLink检索到目标文件的Path

链接通常对文件的用户是透明的。对符号链接的任何访问都会被重定向到引用的文件。硬链接类似于符号链接,但有更多的限制。这些类型的链接在本食谱的还有更多..部分中有更详细的讨论。

准备工作

为了创建一个符号链接到一个文件:

  1. 获取一个代表链接的Path对象。

  2. 获取一个代表目标文件的Path对象。

  3. 将这些路径作为createSymbolicLink方法的参数。

如何做...

  1. 创建一个新的控制台应用程序。将以下main方法添加到应用程序中。在这个应用程序中,我们将在music目录中创建一个符号链接,指向docs目录中的实际users.txt文件。
public static void main(String[] args) throws Exception {
Path targetFile = Paths.get("C:/home/docs/users.txt");
Path linkFile = Paths.get("C:/home/music/users.txt");
Files.createSymbolicLink(linkFile, targetFile);
}

  1. 执行应用程序。如果应用程序没有足够的权限,那么将抛出异常。在 Windows 7 上执行时的示例如下:

java.nio.file.FileSystemException: C:\home\music\users.txt: A required privilege is not held by the client.

  1. 验证music目录中是否存在一个名为users.txt的新文件。检查文件的属性,以验证它是否是一个符号链接。在 Windows 7 上,右键单击文件名,然后选择属性。接下来,选择快捷方式选项卡。它应该显示如下截图所示:

操作步骤...

注意指定的目标是docs目录中的users.txt文件。

它是如何工作的...

我们创建了两个Path对象。第一个代表docs目录中的目标文件。第二个代表要在music目录中创建的链接文件。接下来,我们使用createSymbolicLink方法来实际创建符号链接。整个代码序列被包含在 try 块中,以捕获可能抛出的任何IOExceptions

createSymbolicLink方法的第三个参数可以是一个或多个FileAttribute值。这些值用于在创建链接文件时设置其属性。然而,目前它还没有得到完全支持。未来的 Java 版本将增强这一功能。FileAttribute可以按照还有更多..部分中管理 POSIX 文件权限配方中的详细说明来创建。

还有更多...

在这里,我们将更仔细地研究以下问题:

  • 创建硬链接

  • 创建对目录的符号链接

  • 确定链接文件的目标

创建硬链接

与符号链接相比,硬链接有更多的限制。这些限制包括以下内容:

  • 目标必须存在。如果不存在,就会抛出异常。

  • 不能对目录创建硬链接。

  • 硬链接只能在单个文件系统内建立。

硬链接的行为就像普通文件。文件没有明显的属性表明它是一个链接文件,而不是一个具有快捷方式选项卡的符号链接文件。硬链接的所有属性与目标文件的属性相同。

硬链接不像软链接那样经常使用。Path类的方法可以处理硬链接,不需要任何特殊考虑。使用createLink方法创建硬链接。它接受两个参数:链接文件的Path对象和目标文件的Path对象。在下面的示例中,我们在music目录中创建了一个硬链接,而不是一个符号链接:

try {
Path targetFile = Paths.get("C:/home/docs/users.txt");
Path linkFile = Paths.get("C:/home/music/users.txt");
Files.createLink(linkFile, targetFile);
}
catch (IOException ex) {
ex.printStackTrace();
}

执行应用程序。如果您检查链接文件的属性,您会发现它不显示为符号链接。但是,修改任一文件的内容将导致另一个文件也被修改。它们实际上是一样的。

创建对目录的符号链接

创建对目录的符号链接使用与文件相同的方法。在下面的示例中,创建了一个新目录tmp,它是docs目录的符号链接:

try {
Path targetFile = Paths.get("C:/home/docs");
Path linkFile = Paths.get("C:/home/tmp");
Files.createSymbolicLink(linkFile, targetFile);
}
catch (IOException ex) {
ex.printStackTrace();
}

tmp目录中的所有文件实际上都是指向docs目录中相应文件的符号链接。

确定链接文件的目标

isSymbolicLink方法,如第二章 使用路径定位文件和目录管理符号链接配方中所讨论的,用于确定文件是否是符号链接。readSymbolicLink方法接受一个代表链接文件的Path对象,并返回一个代表链接目标的Path对象。

以下代码序列说明了这一点,在music目录中的users.txt文件是一个符号链接:

try {
Path targetFile = Paths.get("C:/home/docs/users.txt");
Path linkFile = Paths.get("C:/home/music/users.txt");
System.out.println("Target file is: " + Files.readSymbolicLink(linkFile));
}
catch (IOException ex) {
ex.printStackTrace();
}

然而,如果users.txt链接文件是一个硬链接,就像使用createLink方法创建的那样,当执行代码时会得到以下异常:

java.nio.file.NotLinkException: 文件或目录不是一个重解析点

注意

重解析点是一个NTFS文件系统对象,它将特定数据与文件或目录关联起来。文件系统过滤器可以与重解析点类型关联。当文件系统打开文件时,它将传递这些信息给文件系统过滤器进行处理。这种方法是扩展文件系统功能的一种方式。

第五章:管理文件系统

在本章中,我们将涵盖以下内容:

  • 获取 FileStore 信息

  • 获取 FileSystem 信息

  • 使用 SimpleFileVisitor 类遍历文件系统

  • 使用 SimpleFileVisitor 类删除目录

  • 使用 SimpleFileVisitor 类复制目录

  • 使用 DirectoryStream 接口处理目录的内容,如使用 globbing 过滤目录教程中所述

  • 编写自己的目录过滤器

  • 使用 WatchEvents 监视文件事件

  • 理解 ZIP 文件系统提供程序

介绍

文件系统是一个或多个顶级根目录,包含文件层次结构。文件系统由文件存储支持,该文件存储是文件存储的提供者。本章涉及获取有关这些实体和典型文件系统任务的信息,例如确定目录的内容或监视文件系统事件。

文件存储表示存储单元。例如,它可能表示设备,比如C驱动器,驱动器的分区或卷。java.nio.file.FileStore类支持文件存储,并提供了几种方法。获取 FileStore 信息教程介绍了如何获取有关特定文件存储的基本信息。

文件系统支持访问目录和文件的层次结构。它在 Java 7 中用java.nio.file.FileSystem类表示。获取有关文件系统的一般信息在获取 FileSystem 信息教程中介绍。这包括如何获取文件系统的根目录列表和底层文件存储。

遍历目录层次结构对许多应用程序很有用。使用 SimpleFileVisitor 类遍历文件系统教程详细介绍了基本方法。这种方法在使用 SimpleFileVisitor 类删除目录使用 SimpleFileVisitor 类复制目录教程中使用。

当操作限制在单个目录时,java.nio.file.DirectoryStream接口提供了一种方便的技术,用于将目录中的每个元素作为java.nio.file.Path对象进行检查。使用 for each 循环处理这些路径非常容易。这种方法在使用 DirectoryStream 接口处理目录的内容教程中探讨。

有时我们不需要整个目录的内容,而是需要其元素的子集。Java 7 提供了几种过滤目录内容的方法,如使用 globbing 过滤目录编写自己的目录过滤器教程中所述。Globbing是一种类似于正则表达式但更容易使用的模式匹配技术。

使用 WatchEvents 监视文件事件教程中,我们了解到 Java 7 支持通过外部进程检测目录中文件的创建、修改和删除。当需要知道对目录进行更改时,这可能非常有用。

使用 Java 7,现在可以将 ZIP 文件的内容视为文件系统。这使得更容易管理 ZIP 文件的内容并操作 ZIP 文件中包含的文件。这种技术在理解 zip 文件系统提供程序教程中进行了演示。

获取 FileStore 信息

每个文件系统都支持文件存储机制。这可能是一个设备,比如C驱动器,一个驱动器的分区,一个卷,或者其他组织文件系统空间的方式。java.nio.file.FileStore类代表其中一个存储分区。本教程详细介绍了获取有关文件存储的信息的方法。

准备工作

要获取并使用FileStore对象:

  1. 获取正在使用的java.nio.file.FileSystem的实例。

  2. 使用FileSystem类的getFileStores方法返回可用的文件存储。

如何做…

  1. 创建一个新的控制台应用程序。在main方法中,我们将使用FileStore类的几种方法来演示该类提供的支持。让我们从添加main方法的第一部分开始,其中我们显示初始标题并获取一个FileSystem对象。还定义一个名为kiloBytelong变量:
static final long kiloByte = 1024;
public static void main(String[] args) throws IOException {
String format = "%-16s %-20s %-8s %-8s %12s %12s %12s\n";
System.out.printf(format,"Name", "Filesystem", "Type",
"Readonly", "Size(KB)", "Used(KB)",
"Available(KB)");
FileSystem fileSystem = FileSystems.getDefault();
}

  1. 接下来,我们需要使用getFileStores方法检索可用的文件存储,并显示它们。在代码块的第一部分中,我们使用了几个FileStore方法来获取相关信息。在最后一部分,我们按以下方式显示信息:
for (FileStore fileStore : fileSystem.getFileStores()) {
try {
long totalSpace = fileStore.getTotalSpace() / kiloByte;
long usedSpace = (fileStore.getTotalSpace() -
fileStore.getUnallocatedSpace()) / kiloByte;
long usableSpace = fileStore.getUsableSpace() / kiloByte;
String name = fileStore.name();
String type = fileStore.type();
boolean readOnly = fileStore.isReadOnly();
NumberFormat numberFormat = NumberFormat.getInstance();
System.out.printf(format,
name, fileStore, type, readOnly,
numberFormat.format(totalSpace),
numberFormat.format(usedSpace),
numberFormat.format(usableSpace));
}
catch (IOException ex) {
ex.printStackTrace();
}
}

  1. 执行应用程序。您的输出将与以下内容不同,但应反映系统上的驱动器:

名称 文件系统类型 只读 大小(KB)已用(KB)可用(KB)

HP HP (C:) NTFS false 301,531,984 163,041,420 138,490,564

FACTORY_IMAGE FACTORY_IMAGE (D:) NTFS false 11,036,652 9,488,108 1,548,544

HP_PAVILION HP_PAVILION (E:) NTFS false 312,568,640 66,489,184 246,079,456

TOSHIBA TOSHIBA (H:) FAT32 false 15,618,080 3,160,768 12,457,312

工作原理...

创建了一个格式化字符串,以简化文件存储信息的显示。此字符串在两个printf方法中都使用。两次使用相同的字符串可以确保输出的一致间距。使用此字符串显示了一个简单的标题。

使用FileSystems类的getDefault方法获取了一个FileSystem对象。对该对象执行getFileStores方法以获取FileStore对象的列表。

在循环中,使用 try 块捕获可能抛出的异常。按照下表详细说明的方式调用了几个方法。创建了NumberFormat类的实例以格式化文件存储大小信息。最后的printf方法显示了每个文件存储的文件存储信息:

方法 含义
getTotalSpace 文件存储中可用的总空间(以字节为单位)
getUnallocatedSpace 未分配的字节数
getUsableSpace JVM 可用的可用字节数
name 表示文件存储名称的特定于实现的字符串
type 表示文件存储类型的特定于实现的字符串
isReadOnly 如果方法返回true,则尝试创建文件或打开文件进行写入将导致抛出IOException

getUnallocatedSpacegetUsableSpace方法返回的值可能会在外部操作使用或释放文件存储空间时发生变化。

另请参阅

使用两种supportsFileAttributeView方法之一来确定FileStore支持的属性视图。这些方法在第三章的确定操作系统对属性视图的支持食谱的还有更多..部分中进行了说明,获取文件和目录信息

获取文件系统信息

文件系统由一系列目录和文件组成。通常有一些关于文件系统的有限信息是有用的。例如,我们可能想知道文件系统是否为只读,或者提供者是谁。在本示例中,我们将研究用于检索文件系统属性的可用方法。

准备就绪

要访问文件系统的方法,我们需要:

  1. 获取对java.nio.file.FileSystem对象的引用。

  2. 使用此对象的方法来访问文件系统信息。

如何做...

  1. 创建一个新的控制台应用程序。将以下代码添加到应用程序的main方法中。此序列显示了几个fileSystem属性,包括文件系统提供程序、文件打开状态、文件是否可读写、根目录和文件存储的名称:
FileSystem fileSystem = FileSystems.getDefault();
FileSystemProvider provider = fileSystem.provider();
System.out.println("Provider: " + provider.toString());
System.out.println("Open: " + fileSystem.isOpen());
System.out.println("Read Only: " + fileSystem.isReadOnly());
Iterable<Path> rootDirectories = fileSystem.getRootDirectories();
System.out.println();
System.out.println("Root Directories");
for (Path path : rootDirectories) {
System.out.println(path);
}
Iterable<FileStore> fileStores = fileSystem.getFileStores();
System.out.println();
System.out.println("File Stores");
for (FileStore fileStore : fileStores) {
System.out.println(fileStore.name());
}

  1. 执行应用程序。您的输出将取决于系统的配置。但是,它应该与以下输出类似:

提供程序:sun.nio.fs.WindowsFileSystemProvider@7b60e796

打开:true

只读:false

根目录

*C:*

*D:*

*E:*

*F:*

*G:*

*H:*

*I:*

*J:*

*K:*

*L:*

文件存储

HP

FACTORY_IMAGE

HP_PAVILION

TOSHIBA

工作原理...

getDefault方法返回 JVM 使用的默认文件系统。接下来,针对此对象执行了几种方法:

  • provider方法返回提供程序,即文件系统的实现者。在这种情况下,它是与 JVM 捆绑在一起的 Windows 文件系统提供程序。

  • isOpen方法指示文件系统已打开并准备就绪。

  • isReadOnly方法返回false,这意味着我们可以读写系统。

  • 我们使用getRootDirectories方法创建了一个Iterable对象,允许我们列出每个根目录。

  • getFileStores方法返回另一个Iterable对象,用于显示文件存储的名称。

还有...

虽然我们通常不需要关闭文件系统,但是close方法可以用于关闭文件系统。对文件系统执行的任何后续方法都将导致抛出ClosedFileSystemException。与文件系统关联的任何打开通道、目录流和监视服务也将被关闭。请注意,默认文件系统无法关闭。

FileSystems类的getFileSystem方法可用于访问特定的文件系统。此外,重载的newFileSystem方法将创建新的文件系统。close方法可以用于这些实例。

文件系统是线程安全的。但是,如果一个线程尝试关闭文件系统,而另一个线程正在访问filesystem对象,关闭操作可能会被阻塞。

直到访问完成。

使用SimpleFileVisitor类来遍历文件系统

在处理目录系统时,常见的需求是遍历文件系统,检查文件层次结构中的每个子目录。使用java.nio.file.SimpleFileVisitor类可以轻松完成这项任务。该类实现了在访问目录之前和之后执行的方法。此外,对于在目录中访问每个文件实例以及发生异常的情况,还会调用回调方法。

SimpleFileVisitor类或派生类与java.nio.file.Files类的walkFileTree方法一起使用。它执行深度优先遍历,从特定的根目录开始。

准备就绪

要遍历目录,我们需要:

  1. 创建一个代表根目录的Path对象。

  2. 创建一个派生自SimpleFileVisitor的类的实例。

  3. 将这些对象用作Files类的walkFileTree方法的参数。

如何做...

  1. 创建一个新的控制台应用程序,并使用以下main方法。在这里,我们将遍历home目录,并列出其每个元素如下:
public static void main(String[] args) {
try {
Path path = Paths.get("/home");
ListFiles listFiles = new ListFiles();
Files.walkFileTree(path, listFiles);
}
catch (IOException ex) {
ex.printStackTrace();
}
}

  1. 将以下ListFiles类添加到您的项目中。它说明了每个SimpleFileVisitor方法的用法:
class ListFiles extends SimpleFileVisitor<Path> {
private final int indentionAmount = 3;
private int indentionLevel;
public ListFiles() {
indentionLevel = 0;
}
private void indent() {
for(int i=0 ; i<indentionLevel; i++) { {
System.out.print(' ');
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
indent();
System.out.println("Visiting file:" + file.getFileName());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
indentionLevel -= indentionAmount;
indent();
System.out.println("Finished with the directory: " + directory.getFileName());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
indent();
System.out.println("About to traverse the directory: " + directory.getFileName());
indentionLevel += indentionAmount;
return FileVisitResult.CONTINUE;
}
SimpleFileVisitor classusing, for filesystem traverse@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
System.out.println("A file traversal error ocurred");
return super.visitFileFailed(file, exc);
}
}

  1. 执行应用程序。根据您的home目录的结构,您可能会得到与以下不同的结果:

即将遍历目录:home

即将遍历目录:docs

访问文件:users.bak

访问文件:users.txt

完成目录:docs

即将遍历目录:music

访问文件:Future Setting A.mp3

访问文件:Robot Brain A.mp3

访问文件:Space Machine A.mp3

完成目录:music

完成目录:home

检查backup目录,以验证它是否成功创建。

工作原理...

main方法中,我们为home目录创建了一个Path对象。接下来,创建了ListFiles类的一个实例。这些对象被用作walkFileTree方法的参数。该方法影响了home目录的遍历,并根据需要调用了ListFiles类的方法。

walkFileTree方法从根目录开始,并对目录层次结构进行深度优先遍历。在遍历目录之前,将调用preVisitDirectory方法。接下来,处理目录的每个元素。如果是文件,则调用visitFile方法。一旦处理了目录的所有元素,将调用postVisitDirectory方法。如果发生异常,则将调用visitFileFailed方法。

添加了私有辅助方法,使输出更易读。indentionAmount变量控制了每个缩进的深度。indentionLevel变量在访问每个子目录时递增和递减。indent方法执行实际的缩进。

还有更多...

有两个重载的walkFileTree方法。一个接受PathFileVisitor对象,之前已经说明过。它不会跟踪链接,并将访问目录的所有级别。第二个方法接受两个额外的参数:一个指定要访问的目录级别的数量,另一个用于配置遍历。目前,唯一可用的配置选项是FileVisitOption.FOLLOW_LINKS,它指示方法跟随符号链接。

默认情况下不会跟随符号链接。如果在walkFileTree方法的参数中指定了跟随它们,则会注意检测循环链接。如果检测到循环链接,则将其视为错误条件。

要访问的目录级别的数量由整数参数控制。值为 0 将导致只访问顶级目录。值为Integer.MAX_VALUE表示将访问所有级别。值为 2 表示只遍历前两个目录级别。

遍历将在以下条件之一发生时终止:

  • 所有文件都已被遍历

  • visit方法返回FileVisitResult.TERMINATE

  • visit方法以IOException或其他异常终止时,将被传播回来

任何不成功的操作通常会导致调用visitFileFailed方法并抛出IOException

当遇到文件时,如果它不是目录,则尝试读取其BasicFileAttributes。如果成功,将属性传递给visitFile方法。如果不成功,则调用visitFileFailed方法,并且除非处理,否则会抛出IOException

如果文件是目录并且目录可以打开,则调用preVisitDirectory,并访问目录及其后代的元素。

如果文件是目录且无法打开该目录,则将调用visitFileFailed方法,并将抛出IOException。但是,深度优先搜索将继续进行下一个兄弟节点。

以下表总结了遍历过程。

遇到的元素 可以打开 无法打开
文件 调用visitFile 调用visitFileFailed
目录 调用preVisitDirectory目录元素被处理调用postVisitDirectory 调用visitFileFailed

为方便起见,列出了枚举FileVisitResult的枚举常量如下:

含义
CONTINUE 继续遍历
SKIP_SIBLINGS 继续而不访问此文件或目录的兄弟节点
SKIP_SUBTREE 继续而不访问此目录中的条目
TERMINATE 终止

另请参阅

使用 SimpleFileVisitor 类删除目录使用 SimpleFileVisitor 类复制目录的方法利用了本方法中描述的方法来分别删除和复制目录。

使用 SimpleFileVisitor 类删除目录

删除目录是一些应用程序的要求。这可以通过使用walkFileTree方法和一个java.nio.file.SimpleFileVisitor派生类来实现。这个示例建立在使用 SimpleFileVisitor 类遍历文件系统示例提供的基础上。

准备工作

要删除一个目录,我们需要:

  1. 创建一个代表根目录的Path对象。

  2. 创建一个从SimpleFileVisitor派生的类的实例如下:

  • 重写visitFile方法来删除文件

  • 重写postVisitDirectory方法来删除目录

  1. 将这些对象作为参数传递给Files类的walkFileTree方法。

如何做...

  1. 创建一个新的控制台应用程序。在这里,我们将删除home目录及其所有元素。将以下代码添加到main方法中:
try {
Files.walkFileTree(Paths.get("/home"), new DeleteDirectory());
}
catch (IOException ex) {
ex.printStackTrace();
}

  1. DeleteDirectory类如下所示。在删除每个文件和目录时,都会显示相应的消息:
public class DeleteDirectory extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes)
throws IOException {
System.out.println("Deleting " + file.getFileName());
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path directory, IOException exception)
throws IOException {
if (exception == null) {
System.out.println("Deleting " + directory.getFileName());
Files.delete(directory);
return FileVisitResult.CONTINUE;
}
else {
throw exception;
}
}
}

  1. 备份home目录,然后执行应用程序。根据实际的目录结构,你应该会得到以下输出:

删除 users.bak

删除 users.txt

删除 docs

删除 Future Setting A.mp3

删除 Robot Brain A.mp3

删除 Space Machine A.mp3

删除音乐

删除 home

验证目录是否已被删除。

它是如何工作的...

main方法中,我们创建了一个代表home目录的Path对象。接下来,我们创建了DeleteDirectory类的一个实例。这两个对象被用作walkFileTree方法的参数,该方法启动了遍历过程。

当遇到一个文件时,visitFile方法被执行。在这个方法中,我们显示了一个指示文件正在被删除的消息,然后使用Files类的delete方法来删除文件。当遇到一个目录时,postVisitDirectory方法被调用。进行了一个测试以确保没有发生错误,然后显示了一个指示目录正在被删除的消息,随后调用了该目录的delete方法。这两个方法都返回了FileVisitResult.CONTINUE,这将继续删除过程。

另请参阅

使用 SimpleFileVisitor 类遍历文件系统示例提供了关于使用walkFileTree方法和SimpleFileVisitor类的更多细节。使用 SimpleFileVisitor 类复制目录示例也提供了这种方法的变体。

使用 SimpleFileVisitor 类复制目录

复制目录是一些应用程序的要求。这可以通过使用walkFileTree方法和一个java.nio.file.SimpleFileVisitor派生类来实现。这个示例建立在使用 SimpleFileVisitor 类遍历文件系统示例提供的基础上。

准备工作

要删除一个目录,我们需要:

  1. 创建一个代表根目录的Path对象。

  2. 创建一个从SimpleFileVisitor派生的类的实例如下:

  • 重写visitFile方法来复制文件

  • 重写preVisitDirectory方法来复制目录

  1. 将这些对象作为参数传递给Files类的walkFileTree方法。

如何做...

  1. 创建一个新的控制台应用程序。在这里,我们将把home目录及其所有元素复制到一个backup目录中。将以下代码添加到main方法中:
try {
Path source = Paths.get("/home");
Path target = Paths.get("/backup");
Files.walkFileTree(source,
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
Integer.MAX_VALUE,
new CopyDirectory(source, target));
}
catch (IOException ex) {
ex.printStackTrace();
}

  1. CopyDirectory类如下所示。在删除每个文件和目录时,都会显示相应的消息:
public class CopyDirectory extends SimpleFileVisitor<Path> {
private Path source;
private Path target;
public CopyDirectory(Path source, Path target) {
this.source = source;
this.target = target;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
SimpleFileVisitor classusing, for directory copySystem.out.println("Copying " + source.relativize(file));
Files.copy(file, target.resolve(source.relativize(file)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
Path targetDirectory = target.resolve(source.relativize(directory));
try {
System.out.println("Copying " + source.relativize(directory));
Files.copy(directory, targetDirectory);
}
catch (FileAlreadyExistsException e) {
if (!Files.isDirectory(targetDirectory)) {
throw e;
}
}
return FileVisitResult.CONTINUE;
}
}

  1. 执行应用程序。确切的输出取决于你使用的源文件结构,但应该类似于以下内容:

复制

复制 docs

复制 docs\users.bak

复制 docs\users.txt

复制音乐

复制 music\Future Setting A.mp3

复制 music\Robot Brain A.mp3

复制 music\Space Machine A.mp3

它是如何工作的...

main方法中,我们为homebackup目录创建了Path对象。我们使用这些对象创建了一个CopyDirectory对象。我们使用了一个两参数的CopyDirectory构造函数,这样它的方法就可以直接访问这两个路径。

使用源Path调用了walkFileTree方法。它还作为第二个参数传递,一个EnumSet,指定不要跟随符号链接。这个参数需要一组选项。EnumSet类的静态方法创建了这个集合。

walkFileTree方法的第三个参数是一个值,表示要跟随多少级。我们传递了一个Integer.MAX_VALUE的值,这将导致复制home目录的所有级别。最后一个参数是CopyDirectory对象的一个实例。

在遍历过程中遇到文件时,将调用CopyDirectory类的visitFile方法。显示一个消息指示正在复制文件,然后使用copy方法将源文件复制到目标目录。使用relativize方法获取到源文件的相对路径,然后用作resolve方法的参数。结果是一个代表目标目录和源文件名的Path对象。这些方法在第二章的使用路径解析组合路径在两个位置之间创建路径示例中进行了讨论,使用路径定位文件和目录

当在遍历过程中遇到一个目录时,将调用preVisitDirectory方法。它的工作方式与visitFile方法相同,只是我们复制的是一个目录而不是一个文件。这两种方法都返回FileVisitResult.CONTINUE,这将继续复制过程。仍然需要复制目录的各个文件,因为copy方法只能复制单个文件。

请注意,CopyDirectory类扩展了SimpleFileVisitor类,使用Path作为通用值。walkFileTree方法需要一个实现Path接口的对象。因此,我们必须使用Path或扩展Path的接口。

另请参阅

使用 SimpleFileVisitor 类遍历文件系统示例提供了更多关于walkFileTree方法和SimpleFileVisitor类的使用细节。使用 SimpleFileVisitor 类删除目录示例也提供了这种方法的变体。

使用DirectoryStream接口处理目录的内容

确定目录的内容是一个相当常见的需求。有几种方法可以做到这一点。在这个示例中,我们将研究使用java.nio.file.DirectoryStream接口来支持这个任务。

目录将包含文件或子目录。这些文件可能是常规文件,也可能是链接或隐藏文件。DirectoryStream接口将返回所有这些元素类型。我们将使用java.nio.file.Files类的newDirectoryStream方法来获取DirectoryStream对象。这个方法有三个重载版本。首先演示了这个方法的最简单用法。用于过滤目录内容的版本在使用 globbing 过滤目录示例和编写自己的目录过滤器示例中展示。

准备工作

为了使用DirectoryStream,我们需要:

  1. 获取DirectoryStream对象的实例。

  2. 通过DirectoryStream迭代处理其元素。

如何做...

  1. 创建一个新的控制台应用程序,并添加以下main方法。我们创建了一个新的DirectoryStream对象,然后使用 for each 循环来迭代目录元素,如下所示:
public static void main(String[] args) {
Path directory = Paths.get("/home");
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) {
for (Path file : directoryStream) {
System.out.println(file.getFileName());
}
}
catch (IOException | DirectoryIteratorException ex) {
ex.printStackTrace();
}
}

  1. 执行应用程序。您的输出应该反映出您的home目录的内容,并且应该类似于以下内容:

文档

音乐

它是如何工作的...

home目录创建了一个Path对象。这个对象与newDirectoryStream方法一起使用,该方法返回了一个目录的DirectoryStream对象。DirectoryStream接口扩展了Iterable接口。这允许DirectoryStream对象与 for each 语句一起使用,它简单地打印了home目录的每个元素的名称。在这种情况下,只有两个子目录:docsmusic

注意使用 try-with-resource 块。这是 Java 7 中的新功能,并在第一章中的使用 try-with-resource 块改进异常处理代码中进行了讨论,Java 语言改进。这保证了目录流将被关闭。如果没有使用这种 try 块,则在不再需要流之后关闭流是很重要的。

使用的Iterable对象不是通用的iterator。它在几个重要方面有所不同,如下所示:

  • 它只支持单个Iterator

  • hasNext方法执行至少一个元素的预读

  • 它不支持remove方法

DirectoryStream接口有一个方法iterator,它返回一个Iterator类型的对象。第一次调用该方法时,将返回一个Iterator对象。对该方法的后续调用将抛出IllegalStateException

hasNext方法将至少提前读取一个元素。如果该方法返回true,则对其 next 方法的下一次调用将保证返回一个元素。返回的元素的顺序没有指定。此外,许多操作系统在许多 shell 中以"."".."表示对自身和/或其父级的链接。这些条目不会被返回。

iterator有时被称为弱一致。这意味着虽然iterator是线程安全的,但在iterator返回后对目录的任何更新都不会导致iterator的更改。

还有更多...

有两个重载的newDirectoryStream方法,允许该方法的结果通过通配符模式或DirectoryStream.Filter对象进行过滤。通配符模式是一个包含一系列字符的字符串,用于定义模式。该模式用于确定要返回哪些目录元素。DirectoryStream.Filter接口有一个方法accept,它返回一个布尔值,指示是否应返回目录元素。

另请参阅

使用通配符过滤目录示例说明了通配符模式的使用。编写自己的目录过滤器示例展示了如何创建和使用DirectoryStream.Filter对象来过滤目录的内容。

使用通配符过滤目录

通配符模式类似于正则表达式,但更简单。与正则表达式一样,它可以用于匹配特定的字符序列。我们可以将通配符与newDirectoryStream方法结合使用,以过滤目录的内容。该方法的使用在使用 DirectoryStream 接口处理目录的内容示例中进行了演示。

准备工作

要使用这种技术,我们需要:

  1. 创建一个符合我们过滤要求的 globbing 字符串。

  2. 为感兴趣的目录创建一个java.nio.file.Path对象。

  3. 将这两个对象用作newDirectoryStream方法的参数。

如何做...

  1. 创建一个新的控制台应用程序,并使用以下main方法。在这个例子中,我们将只列出那些以java开头并以.exe结尾的目录元素。我们将使用 Java 7 的bin目录。globbing字符串使用特殊字符*来表示零个或多个字符,如下所示:
Path directory = Paths.get("C:/Program Files/Java/jdk1.7.0/bin");
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory,"java*.exe")) {
for (Path file : directoryStream) {
System.out.println(file.getFileName());
}
}
catch (IOException | DirectoryIteratorException ex) {
ex.printStackTrace();
}

  1. 执行应用程序。输出应该类似于以下内容:

java-rmi.exe

java.exe

javac.exe

javadoc.exe

javah.exe

javap.exe

javaw.exe

javaws.exe

它是如何工作的...

首先,创建了一个代表bin目录的Path对象。然后将其用作newDirectoryStream方法的第一个参数。第二个参数是globbing字符串。在这种情况下,它匹配以java开头并以.exe结尾的目录元素。允许任意数量的中间字符。然后使用 for each 循环显示过滤后的文件。

还有更多...

Globbing 字符串基于模式,使用特殊字符来匹配字符串序列。这些特殊字符在Files类的getPathMatcher方法的文档中定义。在这里,我们将更深入地研究这些字符串。以下表格总结了几个特殊字符:

特殊符号 意义
* 匹配不跨越目录边界的名称组件的零个或多个字符
** 匹配跨越目录边界的零个或多个字符
? 匹配名称组件的一个字符
\ 用于匹配特殊符号的转义字符
[ ] 匹配括号内找到的单个字符。A - 匹配一个范围。!表示否定。*、?和\字符匹配它们自己,-如果是括号内的第一个字符或!后的第一个字符,则匹配它自己。
可以同时指定多个子模式。这些模式使用花括号分组在一起,但在花括号内部用逗号分隔。

匹配通常以实现相关的方式执行。这包括匹配是否区分大小写。**符号在这里不适用,因为newDirectoryStream方法返回单独的元素。在这里没有机会匹配跨越目录边界的序列。其他方法将使用这种能力。

以下表格列出了几个可能有用的 glob 模式示例:

Globbing 字符串 将匹配
*.java .java结尾的任何文件名
*.{java,class,jar} .java, .class.jar结尾的任何文件
java*[ph].exe 仅匹配以 java 开头并以p.exeh.exe结尾的文件
j*r.exe j开头并以r.exe结尾的文件

现在,让我们讨论PathMatcher接口的使用。

使用 PathMatcher 接口来过滤目录

java.nio.file.PathMatcher接口提供了使用glob匹配文件名的方法。它有一个名为matches的方法,接受一个Path参数。如果文件与 glob 模式匹配,则返回true。否则返回false

在以下代码序列中,我们通过使用 glob 模式glob:java?.exe创建了一个PathMatcher对象。在 for 循环中,我们使用matches方法进一步过滤以java开头,后跟一个字符,然后以.exe结尾的文件的子集:

Path directory = Paths.get("C:/Program Files/Java/jdk1.7.0/bin");
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:java?.exe");
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(directory,"java*.exe")) {
for (Path file : directoryStream) {
if(pathMatcher.matches(file.getFileName())) {
System.out.println(file.getFileName());
}
}
}
catch (IOException | DirectoryIteratorException ex) {
ex.printStackTrace();
}

当您执行此序列时,您应该得到以下输出:

javac.exe

javah.exe

javap.exe

javaw.exe

注意matches方法中使用的glob:前缀。这种方法需要使用这个前缀,但newDirectoryStream方法不需要。此外,matches方法接受一个Path参数。但是,请注意我们使用了从Path类的getFileName方法返回的String。仅使用Path对象或使用String文字均不起作用。

与使用 glob:前缀不同,我们可以改用正则表达式。为此,请使用reg:前缀,后跟正则表达式。

通常,对于简单的目录过滤,我们会在newDirectoryStream方法中使用更严格的 glob 模式。我们在这里使用它是为了举例说明。然而,如果我们想要在循环的一部分执行多个过滤操作,那么使用模式作为newDirectoryStream方法的一部分,然后使用一个或多个matches方法调用是一种可行的策略。

另请参阅

编写自己的目录过滤器配方探讨了如何创建更强大的过滤器,以匹配基于文件名以外的属性的文件名。

编写自己的目录过滤器

在使用java.nio.file.Files类的newDirectoryStream方法时,目录过滤器用于控制返回哪些目录元素。当我们需要限制流的输出时,这是很有用的。例如,我们可能只对超过一定大小或在某个日期后修改的文件感兴趣。正如本配方中描述的java.nio.file.DirectoryStream.Filter接口,它将限制流的输出。它比使用 globbing 更强大,因为决策可以基于文件名以外的因素。

准备工作

要使用这种技术,我们需要:

  1. 创建一个满足我们过滤要求的DirectoryStream.Filter对象。

  2. 为感兴趣的目录创建一个Path对象。

  3. 使用这两个对象作为newDirectoryStream方法的参数。

如何做...

  1. 创建一个新的控制台应用程序,并将以下序列添加到main方法中。在这个例子中,我们将只过滤出那些隐藏的目录元素。我们将使用 Windows 系统目录。然而,任何其他适当的目录都可以工作:
DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>() {
public boolean accept(Path file) throws IOException {
return (Files.isHidden(file));
}
};
Path directory = Paths.get("C:/Windows");
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory,filter)){
own directory filterwritingfor (Path file : directoryStream) {
System.out.println(file.getFileName());
}
}
catch (IOException | DirectoryIteratorException ex) {
ex.printStackTrace();
}

  1. 执行时,您的输出应该只列出那些隐藏的文件。以下是一个可能的输出:

SwSys1.bmp

SwSys2.bmp

WindowsShell.Manifest

工作原理...

首先,我们创建了一个匿名内部类来定义一个实现DirectoryStream.Filter接口的对象。在accept方法中,使用isHidden方法来确定元素文件是否隐藏。DirectoryStream.Filter接口使用其accept方法来确定是否应该返回目录元素。该方法返回truefalse,指示newDirectoryStream方法是否应该返回该元素。因此,它过滤掉了不需要的元素,这种情况下是非隐藏元素。使用 for each 循环来显示隐藏元素。当声明filter变量时,它是使用Path作为其泛型值声明的。扩展Path接口的接口也可以使用。

另请参阅

这种技术过滤单个目录。如果需要过滤多个目录,则可以根据使用 SimpleFileVisitor 类遍历文件系统配方中使用的示例来适应多个目录。

使用 WatchEvents 监视文件事件

当应用程序需要了解目录中的更改时,监视服务可以监听这些更改,然后通知应用程序这些更改。服务将根据感兴趣的事件类型注册要监视的目录。事件发生时,将排队一个监视事件,随后可以根据应用程序的需求进行处理。

准备工作

要监视目录的事件,我们需要执行以下操作:

  1. 创建一个代表目录的java.nio.file.Path对象。

  2. 使用java.nio.file.FileSystem类的newWatchService方法创建一个新的监视服务。

  3. 确定我们感兴趣监视的事件。

  4. 使用监视服务注册目录和事件。

  5. 处理事件发生时的事件。

如何做...

  1. 创建一个新的控制台应用程序。我们将在main方法中添加代码来创建一个观察服务,确定我们想要观察的事件,将docs目录注册到服务中,然后处理事件。让我们从创建观察服务和目录的Path对象开始。将以下代码添加到main方法中:
try {
FileSystem fileSystem = FileSystems.getDefault();
WatchService watchService = fileSystem.newWatchService();
Path directory = Paths.get("/home/docs");

  1. 接下来,创建一个监视文件创建、删除和修改的事件数组,如下所示:
WatchEvent.Kind<?>[] events = {
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY};
directory.register(watchService, events);

  1. 添加以下 while 循环以监视和处理任何目录事件:
while (true) {
System.out.println("Waiting for a watch event");
WatchKey watchKey = watchService.take();
System.out.println("Path being watched: " + watchKey.watchable());
System.out.println();
if (watchKey.isValid()) {
for (WatchEvent<?> event : watchKey.pollEvents()) {
System.out.println("Kind: " + event.kind());
System.out.println("Context: " + event.context());
System.out.println("Count: " + event.count());
System.out.println();
}
boolean valid = watchKey.reset();
if (!valid) {
// The watchKey is not longer registered
}
}
}
}
catch (IOException ex) {
ex.printStackTrace();
}
catch (InterruptedException ex) {
ex.printStackTrace();
}

  1. 执行应用程序。您应该得到以下输出:

等待观察事件

  1. 使用文本编辑器,在docs目录中创建一个名为temp.txt的新文件并保存。然后应用程序应该显示类似以下的输出。如果这是您第一次在目录中创建文件,则您的输出可能会有所不同。这些条目表示文件已被创建,其内容随后被保存:

正在观察的路径:\home\docs

类型:ENTRY_CREATE

上下文:temp.txt

计数:1

等待观察事件

正在观察的路径:\home\docs

类型:ENTRY_MODIFY

上下文:temp.txt

计数:2

等待观察事件

  1. 接下来,再次保存文件。现在您应该得到以下输出:

正在观察的路径:\home\docs

类型:ENTRY_MODIFY

上下文:temp.txt

计数:1

等待观察事件

  1. 从文件管理器中删除文件。您的输出应该反映出它的删除:

类型:ENTRY_DELETE

上下文:temp1.txt

计数:1

等待观察事件

它是如何工作的...

我们需要的第一件事是一个WatchService对象。这是通过获取默认文件系统,然后对其应用newWatchService方法来获得的。接下来,我们创建了一个代表docs目录的Path对象和一个涵盖创建、删除和修改类型事件的事件数组。

然后进入了一个无限循环,以监视和处理docs目录中发生的文件事件。循环开始时显示一个消息,指示它正在等待事件。执行了WatchService类的take方法。此方法将阻塞,直到发生事件。

当事件发生时,它返回一个WatchKey对象,其中包含有关事件的信息。它的watchable方法返回被观察的对象,然后为了信息目的而显示。

使用isValid方法验证了观察键的有效性,并且它的pollEvents方法被用作 for each 循环的一部分。pollEvents方法返回所有待处理事件的列表。显示了与事件相关的类型、上下文和计数值。

我们监视的事件的上下文是目标目录和引起事件的条目之间的相对路径。计数值取决于事件,并在下一节中讨论。

最后的活动重置了观察键。这是为了将键重新置于就绪状态,直到再次需要它。如果方法返回false,则键不再有效。

还有更多...

WatchService接口具有获取观察键和关闭服务的方法。polltake方法检索下一个观察键,就像我们之前看到的那样。如果没有观察键存在,poll方法将返回null。但是,take方法将阻塞,直到观察键可用。有一个重载的poll方法,它接受额外的参数来指定在返回之前等待事件的时间。这些参数包括超时值和TimeUnit值。TimeUnit枚举的使用在第四章的理解 FileTime 类部分中讨论,管理文件和目录

Path类的register方法将注册由其执行的Path对象指定的文件。该方法接受参数:

  • 指定监视服务

  • 要监视的事件类型

  • 确定Path对象注册方式的修饰符

WatchEvent.Modifier接口指定了如何使用监视服务注册Path对象。在此版本的 Java 中,没有定义修饰符。

java.nio.file.StandardWatchEventKinds类定义了标准事件类型。此接口的字段总结在以下表中:

类型 含义 计数
ENTRY_CREATE 创建目录条目 总是 1
ENTRY_DELETE 删除目录条目 总是 1
ENTRY_MODIFY 修改目录条目 大于 1
OVERFLOW 表示事件可能已丢失或被丢弃的特殊事件 大于 1

当事件发生时,监视服务将返回一个代表事件的WatchKey对象。此键用于同一类型事件的多次发生。当发生该类型的事件时,与事件关联的计数将增加。如果在处理事件之前发生了该类型的多个事件,则每次增加的计数值取决于事件类型。

在前面的示例中使用reset方法将重新排队监视键并将计数重置为零。对于重复事件,上下文是相同的。每个目录条目将为该事件类型拥有自己的监视键。

可以使用WatchKey接口的cancel方法取消事件。这将取消事件在监视服务中的注册。队列中的任何待处理事件将保留在队列中,直到被移除。如果监视服务关闭,监视事件也将被取消。

监视服务是线程安全的。这意味着如果多个线程正在访问事件,那么在使用reset方法时应该小心。在所有使用该事件的线程完成处理事件之前,不应该使用该方法。

可以使用close方法关闭监视服务。如果多个线程正在使用此服务,那么后续尝试检索监视键将导致ClosedWatchServiceException

文件系统可能能够比监视服务更快地报告事件。一些监视服务的实现可能会对排队的事件数量施加限制。当有意忽略事件时,将使用OVERFLOW类型的事件来报告此问题。溢出事件会自动为目标注册。溢出事件的上下文取决于实现。

监视服务的许多方面都依赖于实现,包括:

  • 是否使用本机事件通知服务或模拟

  • 事件被排队的及时性

  • 处理事件的顺序

  • 是否报告短暂事件

了解 ZIP 文件系统提供程序

处理 ZIP 文件比 Java 7 之前要简单得多。在这个版本中引入的 ZIP 文件系统提供程序处理 ZIP 和 JAR 文件,就好像它们是文件系统一样,因此您可以轻松访问文件的内容。您可以像处理普通文件一样操作文件,包括复制、删除、移动和重命名文件。您还可以修改文件的某些属性。本教程将向您展示如何创建 ZIP 文件系统的实例并向系统添加目录。

准备就绪

我们必须首先创建一个java.net.URI对象的实例来表示我们的 ZIP 文件,然后创建新的java.nio.file.FileSystem,然后才能对 ZIP 文件的内容进行任何操作。在这个例子中,我们还将使用java.util.HashMap来设置FileSystem的可选属性如下:。

  1. 创建一个URI对象来表示 ZIP 文件。

  2. 创建一个HashMap对象来指定create属性为true

  3. 使用newFileSystem方法创建FileSystem对象。

如何做...

  1. 创建一个带有main方法的控制台应用程序。在main方法中,添加以下代码序列。我们将在 ZIP 文件中创建一个新的文件系统,然后将一个目录添加到其中,如下所示:
Map<String, String> attributes = new HashMap<>();
attributes.put("create", "true");
try {
URI zipFile = URI.create("jar:file:/home.zip");
try (FileSystem zipFileSys = FileSystems.newFileSystem(zipFile, attributes);) {
Path path = zipFileSys.getPath("docs");
Files.createDirectory(path);
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(zipFileSys.getPath("/"));) {
for (Path file : directoryStream) {
System.out.println(file.getFileName());
}
}
}
}
catch (IOException e) {
e.printStackTrace();
}

  1. 执行程序。您的输出应如下所示:

docs/

它是如何工作的...

URI对象通过使用HashMap对象指定了 ZIP 文件的位置,我们指定如果 ZIP 文件不存在,它应该被创建。FileSystem对象zipFileSys是在 try-with-resources 块中创建的,因此资源将自动关闭,但如果您不希望使用嵌套的 try-with-resources 块,您必须使用FileSystem类的close方法手动关闭资源。try-with-resources 块在第一章中有详细介绍,Java 语言改进,配方:使用 try-with-resources 块改进异常处理代码

为了演示如何将 ZIP 文件作为FileSystem对象进行操作,我们调用了createDirectory方法在我们的 ZIP 文件中添加了一个文件夹。在这一点上,我们还有选择执行其他FileSystem操作的选项,比如复制文件、重命名文件和删除文件。我们使用了一个java.nio.file.DirectoryStream来浏览我们的 ZIP 文件结构并打印出我们的docs目录,但您也可以在计算机上导航到 ZIP 文件的位置来验证其创建。

另请参阅

有关DirectoryStream类的更多信息,请参阅使用 DirectoryStream 接口处理目录的内容配方。

第六章:Java 7 中的流 IO

在本章中,我们将涵盖:

  • 管理简单文件

  • 使用缓冲 IO 处理文件

  • 使用SeekableByteChannel进行随机访问 IO

  • 使用AsynchronousServerSocketChannel类管理异步通信

  • 使用AsynchronousFileChannel类写入文件

  • 使用AsynchronousFileChannel类从文件中读取

  • 使用SecureDirectoryStream

介绍

在 Java 7 中,我们发现它的 IO 功能有许多改进。其中大部分都在java.nio包中,被称为NIO2。在本章中,我们将专注于对流和基于通道的 IO 的新支持。是一系列连续的数据。流 IO一次处理一个字符,而通道 IO对每个操作使用一个缓冲区。

我们从用于处理简单文件的新技术开始。这些技术由Files类支持,并在管理简单文件配方中有讨论。缓冲 IO通常更有效,并在使用缓冲 IO 处理文件配方中有解释。

java.nio.channels包的ByteChannel接口是一个可以读写字节的通道。SeekableByteChannel接口扩展了ByteChannel接口以在通道内保持位置。位置可以使用寻找类型的随机 IO 操作进行更改。这个功能在使用 SeekableByteChannel 进行随机访问 IO配方中有讨论。

Java 7 增加了对异步通道功能的支持。这些操作的异步性在于它们不会阻塞。异步应用可以继续执行而不需要等待 IO 操作完成。当 IO 完成时,应用的一个方法会被调用。有四个新的java.nio.channels包异步通道类:

  • AsynchronousSocketChannel

  • AsynchronousServerSocketChannel

  • AsynchronousFileChannel

  • AsynchronousChannelGroup

前两者在服务器/客户端环境中一起使用,并在使用 AsynchronousServerSocketChannel 类管理异步通信配方中有详细说明。

AsynchronousFileChannel类用于需要以异步方式执行的文件操作。支持写和读操作的方法分别在使用 AsynchronousFileChannel 类写入文件使用 AsynchronousFileChannel 类从文件中读取配方中有说明。

AsynchronousChannelGroup类提供了一种将异步通道组合在一起以共享资源的方法。这个类的使用在更多内容部分的使用 AsynchronousFileChannel 类从文件中读取配方中有展示。

java.nio.file包的SecureDirectoryStream类提供了对目录的更安全访问的支持。这个类的使用在使用 SecureDirectoryStream配方中有解释。然而,底层操作系统必须为这个类提供本地支持。

users.txt文件在本章中的几个示例中使用。假定users.txt文件的内容最初包含以下内容:

  • Bob

  • Mary

  • Sally

  • Tom

  • Ted

如果您的文件内容不同,那么示例的输出将相应地有所不同。

本章中的一些配方打开了一个文件。其中一些打开方法将使用一个枚举参数来指定文件应该如何打开。java.nio.file包的OpenOption接口指定了文件的打开方式,StandardOpenOption枚举实现了这个接口。枚举的值总结在下表中:

枚举 含义
APPEND 字节被写入文件的末尾
CREATE 如果文件不存在则创建一个新文件
CREATE_NEW 仅在文件不存在时创建新文件
DELETE_ON_CLOSE 关闭文件时删除文件
DSYNC 对文件的每次更新都是同步写入的
READ 以读取访问权限打开
SPARSE 稀疏文件
SYNC 对文件或元数据的每次更新都是同步写入的
TRUNCATE_EXISTING 打开文件时将文件长度截断为 0
WRITE 以写入访问权限打开文件

虽然这里没有讨论,但是java.nio.channels包的NetworkChannel接口是在 Java 7 中引入的。这代表了一个到网络套接字的通道。包括AsynchronousServerSocketChannelAsynchronousSocketChannel在内的几个类在本章中实现了它。它有一个bind方法,用于将套接字绑定到本地地址,允许检索和设置各种查询套接字选项。它允许使用操作系统特定的选项,这可以用于高性能服务器。

java.nio.channels包的MulticastChannel也是 Java 7 中的新功能。它用于支持组的多播操作。它由DatagramChannel类实现。该接口的方法支持从组中加入和离开成员。

Sockets Direct ProtocolSDP)是一种网络协议,支持使用InfiniBandIB)进行流连接。IB 技术支持高速外围设备之间的点对点双向串行链接,例如磁盘。IB 的一个重要部分是它能够将数据从一台计算机的内存直接移动到另一台计算机的内存。

SDP 在 Solaris 和 Linux 操作系统上的 Java 7 中得到支持。java.netjava.nio.channels包中的几个类支持它的透明使用。但是,在使用之前必须启用 SDP。有关如何启用 IB,然后创建 SDP 配置文件的详细信息,请参阅download.oracle.com/javase/tutorial/sdp/sockets/index.html

管理简单文件

一些文件很小,包含简单的数据。这通常适用于文本文件。当可以一次性读取或写入文件的全部内容时,有一些Files类的方法可以很好地工作。

在本教程中,我们将研究处理简单文件的技术。首先,我们将研究如何读取这些类型文件的内容。在还有更多部分,我们将演示如何向它们写入。

准备工作

一次性读取文件的全部内容:

  1. 创建一个代表文件的java.nio.file.Path对象。

  2. 使用java.nio.file.Files类的readAllBytes方法。

如何做...

  1. 创建一个新的控制台应用程序。我们将读取并显示在 docs 目录中找到的users.txt文件的内容。将以下主要方法添加到应用程序中:
public static void main(String[] args) throws IOException {
Path path = Paths.get("/home/docs/users.txt");
byte[] contents = Files.readAllBytes(path);
for (byte b : contents) {
System.out.print((char)b);
}
}

  1. 执行应用程序。您的输出应该反映文件的内容。以下是一个可能的输出:

鲍勃

玛丽

莎莉

汤姆

泰德

它是如何工作的...

我们首先创建了一个代表users.txt文件的Path对象。使用Files类的readAllBytes方法,使用path对象作为其参数执行了该方法。该方法返回一个字节数组。

接下来,使用 for 语句来遍历数组。每个byte都被转换为char,然后显示出来。

还有更多...

一旦所有字节都被读取或发生异常,该方法将自动关闭文件。除了可能发生的IOException之外,还可能抛出OutOfMemoryError,如果不可能创建足够大的数组来容纳文件的内容。如果发生这种情况,则应使用另一种方法。

我们还关注:

  • 写入简单文件

  • 将文件的所有行作为列表返回

写入简单文件

在下面的例子中,我们将获取users.txt文件的内容,并向列表中添加一个新的名字。使用前面的代码,在打印出内容值的 for 循环之后加上注释。然后,在Path对象上调用readAllBytes方法后,创建一个指向一个新的不存在的文本文件的新path对象。然后声明一个名为nameString变量,并在字符串上调用getBytes方法返回一个新的byte数组。

Path newPath = Paths.get("/home/docs/newUsers.txt");
byte[] newContents = "Christopher".getBytes();

接下来,我们将使用Files类的写入方法创建一个与我们的users.txt文件内容相同的新文件,然后将我们的String名字追加到这个列表中。在第一次调用write方法时,我们使用newPath指定文件应该创建在哪里,使用内容字节数组指定应该使用什么信息,使用StandardOpenOption.CREATE参数指定如果文件不存在则应该创建文件。在第二次调用write方法时,我们再次使用newPath,然后使用字节数组newContentsStandardOpenOption.APPEND指定应该将名字追加到现有文件中。

Files.write(newPath, contents, StandardOpenOption.CREATE);
Files.write(newPath, newContents, StandardOpenOption.APPEND);

如果你打开newUsers.txt文件,你会看到从你的users.txt文件中获取的名单,以及使用newContents字节数组指定的名字。

还有一个重载的write方法,它使用相同的Path对象作为第一个参数,并使用Iterable接口作为第二个参数来迭代CharSequence。该方法的第三个参数定义了要使用的CharsetStandardOpenOptions作为可选参数可用,如前一个版本所示。在本章的介绍中列出了打开选项。

将文件的所有行作为列表返回

在你希望从一个简单的文件中读取时,使用readAllLines方法可能是有效的。该方法接受两个参数,即Path对象和Charset。该方法可能会抛出IOException。在下面的例子中,我们使用我们的users.txt文件的路径和Charset类的defaultCharset方法来执行readAllLines方法。该方法返回一个字符串列表,我们在 for 循环中打印出来。

try {
Path path = Paths.get("/home/docs/users.txt");
List<String> contents = Files.readAllLines(path, Charset.defaultCharset());
for (String b : contents) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
}

你的输出应该类似于这样:

Bob
Mary
Sally
Tom
Ted

注意,readAllLines方法返回的字符串不包括行结束符。

readAllLines方法识别以下行终止符:

  • \u000D后跟\u000A (CR/LF)

  • \u000A,(LF)

  • \u000D,(CR)

另请参阅

在本章中:

  • 使用缓冲 IO 处理文件:这个示例说明了在 Java 7 中如何处理缓冲 IO

  • 使用 AsynchronousFileChannel 类写入文件:这个示例说明了如何以异步方式对文件进行 IO 操作

  • 使用 AsynchronousFileChannel 类从文件中读取:这个示例说明了如何以异步方式对文件进行 IO 操作

使用缓冲 IO 处理文件

缓冲 IO 提供了一种更有效的访问文件的技术。java.nio.file包的Files类的两种方法返回java.io包的BufferedReaderBufferedWriter对象。这些类提供了一种易于使用和高效的处理文本文件的技术。

我们将首先说明读取操作。在还有更多部分,我们将演示如何写入文件。

准备工作

使用BufferedReader对象从文件中读取:

  1. 创建一个代表感兴趣的文件的Path对象

  2. 使用newBufferedReader方法创建一个新的BufferedReader对象

  3. 使用适当的read方法从文件中读取

操作步骤…

  1. 使用以下main方法创建一个新的控制台应用程序。在这个方法中,我们将读取users.txt文件的内容,然后显示它的内容。
public static void main(String[] args) throws IOException {
Path path = Paths.get("/home/docs/users.txt");
Charset charset = Charset.forName("ISO-8859-1");
try (BufferedReader reader = Files.newBufferedReader(path, charset)) {
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}

  1. 执行应用程序。你的输出应该反映出users.txt文件的内容,应该类似于以下内容:
Bob
Mary
Sally
Tom
Ted

工作原理...

创建代表users.txt文件的Path对象,然后创建Charset。在此示例中使用 ISO Latin Alphabet No. 1。可以根据所使用的平台使用其他字符集。

使用 try-with-resource 块创建了BufferedReader对象。这种类型的try块是 Java 7 中的新功能,并在第一章的使用 try-with-resource 块改进异常处理代码中有详细说明,Java 语言改进。这将导致BufferedReader对象在块完成时自动关闭。

while 循环读取文件的每一行,然后将每一行显示到控制台。任何IOExceptions都将根据需要抛出。

还有更多...

当字节存储在文件中时,其含义可能取决于预期的编码方案。java.nio.charset包的Charset类提供了字节序列和 16 位 Unicode 代码单元之间的映射。newBufferedReader方法的第二个参数指定要使用的编码。JVM 支持一组标准字符集,详细信息请参阅Charset类的 Java 文档。

我们还需要考虑:

  • 使用BufferedWriter类写入文件

  • Files类中的非缓冲 IO 支持

使用 BufferedWriter 类写入文件

newBufferedWriter方法打开或创建一个文件进行写入,并返回一个BufferedWriter对象。该方法需要两个参数,一个是Path对象,一个是指定的Charset,还可以使用可选的第三个参数。第三个参数指定一个OpenOption,如Introduction中的表中所述。如果未指定选项,该方法将表现为CREATE, TRUNCATE_EXISTINGWRITE选项被指定,将创建一个新文件或截断现有文件。

在以下示例中,我们指定一个包含要添加到我们的users.txt文件中的名称的新String对象。声明了我们的Path对象后,我们使用 try-with-resource 块打开了一个新的BufferedWriter。在此示例中,我们使用默认系统字符集和StandardOpenOption.APPEND来指定我们要将名称追加到我们的users.txt文件的末尾。在 try 块内,我们首先针对我们的BufferedWriter对象调用newline方法,以确保我们的名称放在新行上。然后我们针对我们的BufferedWriter对象调用write方法,使用我们的String作为第一个参数,零来表示字符串的开始字符,以及我们的String的长度来表示整个String应该被写入。

String newName = "Vivian";
Path file = Paths.get("/home/docs/users.txt");
try (BufferedWriter writer = Files.newBufferedWriter(file, Charset.defaultCharset(), StandardOpenOption.APPEND)) {
writer.newLine();
writer.write(newName, 0, newName.length());
}

如果您检查users.txt文件的内容,新名称应该附加到文件中的其他名称的末尾。

Files类中的非缓冲 IO 支持

虽然非缓冲 IO 不如缓冲 IO 高效,但有时仍然很有用。Files类通过其newInputStreamnewOutputStream方法为InputStreamOutputStream类提供支持。这些方法在需要访问非常小的文件或方法或构造函数需要InputStreamOutputStream作为参数时非常有用。

在以下示例中,我们将执行一个简单的复制操作,将users.txt文件的内容复制到newUsers.txt文件中。我们首先声明两个Path对象,一个引用源文件users.txt,一个指定我们的目标文件newUsers.txt。然后,在 try-with-resource 块内,我们打开了一个InputStream和一个OutputStream,使用newInputStreamnewOutputStream方法。在块内,我们从源文件中读取数据并将其写入目标文件。

Path file = Paths.get("/home/docs/users.txt");
Path newFile = Paths.get("/home/docs/newUsers.txt");
try (InputStream in = Files.newInputStream(file);
OutputStream out = Files.newOutputStream( newFile,StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
int data = in.read();
while (data != -1){
out.write(data);
data = in.read();
fileunbuffered IO}
}

检查newUsers.txt文件后,您应该看到内容与users.txt文件相匹配。

另请参阅

在本章中:

  • 管理简单文件:此示例显示了如何在 Java 7 中处理非缓冲 IO

  • 使用 AsynchronousFileChannel 类向文件写入:此示例说明了如何以异步方式对文件进行 IO 操作

  • 使用 AsynchronousFileChannel 类从文件中读取:此示例说明了如何以异步方式对文件进行 IO 操作

使用 SeekableByteChannel 进行随机访问 IO

对文件的随机访问对于更复杂的文件很有用。它允许以非顺序方式访问文件中的特定位置。java.nio.channels包的SeekableByteChannel接口提供了这种支持,基于通道 IO。通道提供了用于大容量数据传输的低级方法。在此示例中,我们将使用SeekableByteChannel来访问文件。

准备工作

要获取SeekableByteChannel对象:

  1. 创建一个表示文件的Path对象。

  2. 使用Files类的静态newByteChannel方法获取SeekableByteChannel对象。

操作步骤...

  1. 使用以下main方法创建一个新的控制台应用程序。我们将定义一个bufferSize变量来控制通道使用的缓冲区的大小。我们将创建一个SeekableByteChannel对象,并使用它来显示users.txt文件中的前两个名称。
public static void main(String[] args) throws IOException {
int bufferSize = 8;
Path path = Paths.get("/home/docs/users.txt");
try (SeekableByteChannel sbc = Files.newByteChannel(path)) {
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
sbc.position(4);
sbc.read(buffer);
for(int i=0; i<5; i++) {
System.out.print((char)buffer.get(i));
}
System.out.println();
buffer.clear();
sbc.position(0);
sbc.read(buffer);
for(int i=0; i<4; i++) {
System.out.print((char)buffer.get(i));
}
System.out.println();
}
}

确保users.txt文件包含以下内容:

Bob
Mary
Sally
Tom
Ted

  1. 执行应用程序。输出应显示前两个名称的相反顺序:
Mary
Bob

工作原理...

我们创建了一个bufferSize变量来控制通道使用的缓冲区的大小。接下来,为users.txt文件创建了一个Path对象。该路径被用作newByteChannel方法的参数,该方法返回了一个SeekableByteChannel对象。

我们将文件中的读取位置移动到第四个位置。这将我们放置在文件中第二个名称的开头。然后使用read方法,将大约八个字节读入缓冲区。然后显示缓冲区的前五个字节。

我们重复了这个序列,但将位置移动到零,即文件的开头。然后再次执行了一个read操作,然后显示了前四个字符,这是文件中的第一个名称。

此示例使用了对文件中名称大小的明确了解。通常,除非通过其他技术获得,否则不会获得这种了解。我们在这里使用这些知识只是为了演示SeekableByteChannel接口的性质。

还有更多...

read方法将从文件中的当前位置开始读取。它将读取直到缓冲区填满或达到文件末尾。该方法返回一个整数,指示读取了多少字节。当达到流的末尾时,返回-1

读取和写入操作可能会访问由多个线程使用的相同的SeekableByteChannel对象。因此,当另一个线程关闭通道或以其他方式中断当前线程时,可能会抛出AsynchronousCloseExceptionClosedByInterruptException异常。

有一个返回流大小的size方法。还有一个可用的truncate方法,它会丢弃文件中特定位置之后的所有字节。该位置作为长参数传递给该方法。

Files类的静态newByteChannel方法可以接受第二个参数,该参数指定打开文件时使用的选项。这些选项在还有更多部分的使用 Buffered IO for files示例的使用 BufferedWriter 类向文件写入中有详细说明。

此外,我们需要考虑:

  • 处理整个文件的内容

  • 使用SeekableByteChannel接口向文件写入

  • 查询位置

处理整个文件的内容

将以下代码添加到应用程序中。其目的是演示如何以顺序方式处理整个文件,并了解各种SeekableByteChannel接口方法。

// Read the entire file
System.out.println("Contents of File");
sbc.position(0);
buffer = ByteBuffer.allocate(bufferSize);
String encoding = System.getProperty("file.encoding");
int numberOfBytesRead = sbc.read(buffer);
System.out.println("Number of bytes read: " + numberOfBytesRead);
while (numberOfBytesRead > 0) {
buffer.rewind();
System.out.print("[" + Charset.forName(encoding). decode(buffer) + "]");
buffer.flip();
numberOfBytesRead = sbc.read(buffer);
System.out.println("\nNumber of bytes read: " + numberOfBytesRead);
}

执行应用程序。您的输出应该类似于以下内容:

Contents of File
Number of bytes read: 8
[Bob
Mar]
Number of bytes read: 8
[y
Sally]
Number of bytes read: 8
[
Tom
T]
Number of bytes read: 2
[edTom
T]
Number of bytes read: -1

我们首先通过使用position方法将read重新定位到文件的开头。通过访问system属性:file.encoding来确定系统的编码字符串。我们跟踪了每次读取操作读取了多少字节,并显示了这个计数。

在 while 循环中,我们通过将其括在一对括号中显示了缓冲区的内容。这样更容易看到读取的内容。rewind方法将缓冲区内的位置设置为0。这不应与文件内的位置混淆。

要显示实际的缓冲区,我们需要应用forName方法来获取Charset对象,然后使用decode方法将缓冲区中的字节转换为 Unicode 字符。然后是flip方法,它将缓冲区的限制设置为当前位置,然后将缓冲区的位置设置为0。这为后续读取设置了缓冲区。

您可能希望调整bufferSize值,以查看应用程序在不同值下的行为。

使用 SeekableByteChannel 接口向文件写入

write方法接受java.nio包的ByteBuffer对象,并将其写入通道。操作从文件中的当前位置开始。例如,如果文件以追加选项打开,则第一次写入将在文件末尾进行。该方法返回写入的字节数。

在下一个示例中,我们将向users.txt文件的末尾追加三个名称。我们使用StandardOpenOption.APPEND作为newByteChannel方法的打开选项。这将把光标移动到文件的末尾,并从该位置开始写入。创建了一个ByteBuffer,其中包含三个名称,由系统行分隔符属性分隔。使用此属性使代码更具可移植性。然后执行write方法。

final String newLine = System.getProperty("line.separator");
try (SeekableByteChannel sbc = Files.newByteChannel(path, StandardOpenOption.APPEND)) {
String output = newLine + "Paul" + newLine + "Carol" + newLine + "Fred";
ByteBuffer buffer = ByteBuffer.wrap(output.getBytes());
sbc.write(buffer);
}

users.txt文件的初始内容应该是:

Bob
Mary
Sally
Tom
Ted

将代码序列添加到应用程序并执行该程序。检查users.txt文件的内容。现在应该如下所示:

Bob
Mary
Sally
Tom
Ted
Paul
Carol
Fred 

查询位置

重载的position方法返回一个长值,指示文件内的位置。这由一个接受长参数的position方法补充,并将位置设置为该值。如果该值超过流的大小,则位置将设置为流的末尾。size方法将返回通道使用的文件的大小。

为了演示这些方法的使用,我们将复制上一节中的示例。这意味着我们将把文件光标定位到users.txt文件的末尾,然后在单独的行上写入三个不同的名称。

在下面的代码序列中,我们使用size方法来确定文件的大小,然后将此值作为position方法的参数。这将把光标移动到文件的末尾。

接下来,创建了三次ByteBuffer,并且每次使用不同的名称写入文件。位置是为了信息目的而显示的。

Path path = Paths.get("/home/docs/users.txt");
final String newLine = System.getProperty("line.separator");
try (SeekableByteChannel sbc = Files.newByteChannel(path, StandardOpenOption.WRITE)) {
ByteBuffer buffer;
long position = sbc.size();
sbc.position(position);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Paul").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Carol").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
buffer = ByteBuffer.wrap((newLine + "Fred").getBytes());
sbc.write(buffer);
System.out.println("Position: " + sbc.position());
}

users.txt文件的内容应该最初包含:

Bob
Mary
Sally
Tom
Ted

将此序列添加到应用程序并执行该程序。检查users.txt文件的内容。现在应该如下所示:

Bob

Mary

Sally

Tom

Ted

Paul

Carol

Fred

另请参阅

在本章中

  • 随后使用 SeekableByteChannel 进行随机访问 IO配方:此配方简要介绍了用于打开文件的选项

  • 使用 BufferedWriter 类向文件写入使用缓冲 IO 进行文件配方。

使用 AsynchronousServerSocketChannel 类管理异步通信

Java 7 支持服务器和客户端之间的异步通信。java.nio.channels包的AsynchronousServerSocketChannel类支持以线程安全的方式进行流 IO 的服务器操作。通信是使用充当客户端的AsynchronousSocketChannel对象进行的。我们可以一起使用这些类来构建一个以异步方式通信的服务器/客户端应用程序。

准备就绪

需要创建服务器和客户端。要创建服务器:

  1. 使用静态的AsynchronousServerSocketChannel类的open方法来获取AsynchronousServerSocketChannel对象的实例

  2. 将通道绑定到本地地址和端口号

  3. 使用accept方法来接受来自客户端的连接请求

  4. 在接收到消息时处理消息

要创建客户端:

  1. 使用静态的open方法创建一个AsynchronousSocketChannel对象

  2. 为服务器创建一个InetSocketAddress对象的实例

  3. 连接到服务器

  4. 根据需要发送消息

如何做...

我们将创建两个应用程序:一个在服务器上,一个在客户端上。它们将一起支持一个简单的服务器/客户端应用程序,这将解释如何使用AsynchronousSocketChannel执行异步通信。

  1. 创建一个新的控制台应用程序,将作为服务器,并添加以下main方法。服务器将简单地显示发送到它的任何消息。它打开一个服务器套接字并将其绑定到一个地址。然后,它将使用accept方法和CompletionHandler来处理来自客户端的任何请求。
public static void main(String[] args) {throws Exception final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("localhost", 5000);
listener.bind(address);
listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
public void completed(AsynchronousSocketChannel channel, Void attribute) {
AsynchronousServerSocketChannel classasynchronous communication, managingtry {
System.out.println("Server: completed method executing");
while(true) {
ByteBuffer buffer = ByteBuffer.allocate(32);
Future<Integer> readFuture = channel.read(buffer);
Integer number = readFuture.get();
System.out.println("Server: Message received: " + new String(buffer.array()));
}
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
}
public void failed(Throwable ex, Void atttribute) {
System.out.println("Server: CompletionHandler exception");
ex.printStackTrace();
}
});
while(true) {
// wait — Prevents the program from
// terminating before the client can connect
}
} catch (IOException ex) {
ex.printStackTrace();
}
}

  1. 接下来,创建一个作为客户端的第二个控制台应用程序。它将使用open方法来创建一个AsynchronousSocketChannel对象,然后连接到服务器。使用java.util.concurrent包的Future对象的get方法来阻塞,直到连接完成,然后向服务器发送消息。
public static void main(String[] args) {throws Exception try {
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("localhost", 5000);
Future<Void> future = client.connect(address);
System.out.println("Client: Waiting for the connection to complete");
future.get();
AsynchronousServerSocketChannel classasynchronous communication, managingString message;
do {
System.out.print("Enter a message: ");
Scanner scanner = new Scanner(System.in);
message = scanner.nextLine();
System.out.println("Client: Sending ...");
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
System.out.println("Client: Message sent: " + new String(buffer.array()));
client.write(buffer);
} while(!"quit".equals(message)) {
}
}

您需要执行这两个应用程序。根据您的环境,您可能需要在命令窗口中执行一个应用程序,然后在 IDE 中执行第二个应用程序。如果您一次只能运行一个 IDE 实例,就会出现这种情况。

首先执行服务器应用程序。然后执行客户端应用程序。它应提示您输入消息,然后将消息发送到服务器,在那里将显示。您的输出应该具有以下一般输出。客户端和服务器的输出显示在以下表格中的不同列中:

客户端 服务器
客户端:等待连接完成输入消息:第一条消息客户端:发送...客户端:消息已发送:第一条消息
服务器:完成方法执行中服务器:收到消息:第一条消息
输入消息:最优秀的消息客户端:发送...客户端:消息已发送:最优秀的消息
服务器:收到消息:最优秀的消息
输入消息:退出客户端:发送...客户端:消息已发送:退出
服务器:收到消息:退出 java.util.concurrent.ExecutionException: java.io.IOException:指定的网络名称不再可用...
  • 请注意,当客户端应用程序终止时,服务器中发生了ExecutionException。通常,我们会在生产应用程序中更优雅地处理此异常。

它是如何工作的...

让我们首先检查服务器应用程序。使用open方法创建了一个AsynchronousServerSocketChannel对象。然后使用bind方法将套接字与由系统确定的套接字地址和端口号5000关联起来。

接下来,调用accept方法来接受一个连接。第一个参数指定了一个null值,用于附件。稍后,我们将看到如何使用附件。第二个参数是一个CompletionHandler对象。这个对象被创建为一个匿名内部类,当客户端发出通信请求时,它的方法将被调用。

completed方法中,我们显示了一个消息,指示该方法正在执行。然后我们进入了一个无限循环,在循环中我们为一个缓冲区分配了 32 个字节,然后尝试从客户端读取。read方法返回了一个Future对象,随后我们使用get方法。这有效地阻塞了执行,直到客户端发送了一条消息。然后显示了这条消息。

注意get方法返回了一个泛型Future 对象,类型为Integer。我们可以使用这个来确定实际读取了多少字节。这里只是用来阻塞,直到 IO 完成。如果通道通信发生异常,将调用failed方法。

在 try 块的末尾进入了一个无限循环,防止服务器终止。这在这里是可以接受的,为了简单起见,但通常我们会以更优雅的方式处理这个问题。

在客户端应用程序中,我们使用open方法创建了一个AsynchronousSocketChannel对象。创建了一个与服务器对应的网络地址,然后与connect方法一起使用以连接到服务器。这个方法返回了一个Future对象。我们使用这个对象与get方法来阻塞,直到与服务器建立连接。

注意connect方法返回了一个Void类型的Future对象。Void类位于java.lang包中,是void的包装类。这里使用它是因为connect方法实际上没有返回任何东西。

进入了一个 while 循环,当用户输入quit时终止。用户被提示输入一条消息,然后使用该消息创建了一个ByteBuffer对象。然后将缓冲区写入服务器。

注意在两个应用程序的 catch 块中使用了多个异常。这是 Java 7 的新语言改进,可以在第一章的Catching multiple exception types to improve type checking中找到。

还有更多...

bind方法是重载的。两个版本的第一个参数都是一个SocketAddress对象,对应一个本地地址。可以使用null值,这将自动分配一个套接字地址。第二个bind方法接受一个整数值作为第二个参数。这样可以以实现相关的方式配置允许的最大挂起连接数。小于或等于零的值将使用特定于实现的默认值。

有两个方面的通信技术我们应该注意:

  • 在服务器中使用Future对象

  • 理解AsynchronousServerSocketChannel类的选项

在服务器中使用 Future 对象

AsynchronousServerSocketChannel类的accept方法是重载的。有一个不带参数的方法接受一个连接并返回通道的Future对象。Future对象的get方法将返回一个连接的AsynchronousSocketChannel对象。这种方法的优势是返回一个AsynchronousSocketChannel对象,可以在其他上下文中使用。

与使用CompletionHandleraccept方法不同,我们可以使用以下顺序来做同样的事情。注释掉之前的accept方法,添加以下代码:

try {
Future<AsynchronousSocketChannel> future = listener.accept();
AsynchronousSocketChannel worker = future.get();
while (true) {
// Wait
stem.out.println("Server: Receiving ...");
ByteBuffer buffer = ByteBuffer.allocate(32);
Future<Integer> readFuture = worker.read(buffer);
Integer number = readFuture.get();
ystem.out.println("Server: Message received: " + new String(buffer.array()));
}
} catch (IOException | InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}

再次执行应用程序。你应该得到与之前相同的输出。

理解 AsynchronousServerSocketChannel 类的选项

supportedOptions方法返回AsynchronousServerSocketChannel类使用的一组选项。getOption方法将返回选项的值。在上一个示例中的bind方法之后添加以下代码:

Set<SocketOption<?>> options = listener.supportedOptions();
for (SocketOption<?> socketOption : options) {
System.out.println(socketOption.toString() + ": " + listener.getOption(socketOption));
}

执行代码。将显示默认值,并且应该类似于以下内容:

SO_RCVBUF: 8192
SO_REUSEADDR: false

可以使用setOption方法设置选项。此方法接受选项的名称和值。以下说明了如何将接收缓冲区大小设置为 16,384 字节:

listener.setOption(StandardSocketOptions.SO_RCVBUF, 16384);

StandardSocketOptions类定义了套接字选项。仅支持AsynchronousServerSocketChannel通道的SO_REUSEADDRSO_RCVBUF选项。

另请参见

  • 在本章中:使用AsynchronousFileChannel类从文件中读取部分的还有更多部分:本示例解释了完成处理程序的附件使用以及AsynchronousChannelGroup类的使用

使用AsynchronousFileChannel类写入文件

java.nio.channels包的AsynchronousFileChannel类允许以异步方式执行文件 IO 操作。当调用 IO 方法时,它将立即返回。实际操作可能会在其他时间发生(可能使用不同的线程)。在本示例中,我们将探讨AsynchronousFileChannel类如何执行异步操作。操作将在使用AsynchronousFileChannel类从文件中读取示例中进行演示。

准备工作

执行写操作:

  1. 创建一个代表要从中读取的文件的Path对象。

  2. 使用此路径和open方法打开文件通道。

  3. 使用write方法向文件写入数据,可以选择使用完成处理程序。

如何做...

在这个例子中,我们将对文件执行一系列写操作。有两个重载的write方法。它们的初始参数都是java.nio包的ByteBuffer,包含要写入的数据,以及指定要写入文件的位置的第二个参数。

两个参数的write方法返回一个java.util.concurrent包的Future<Integer>对象,也可以用于向文件写入,如还有更多部分所示。第二个write方法有第三个参数,一个附件,和第四个参数,一个CompletionHandler对象。当写操作完成时,执行完成处理程序。

  1. 创建一个新的控制台应用程序。使用以下main方法。我们打开一个名为asynchronous.txt的文件进行写入。创建并使用了一个完成处理程序与write方法。执行了两次写操作。显示线程信息以解释操作的异步性质以及完成处理程序的工作原理。
public static void main(String[] args) {throws Exception try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get( "/home/docs/asynchronous.txt"),
READ, WRITE,
StandardOpenOption.CREATE)) {
CompletionHandler<Integer, Object> handler = new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println("Attachment: " + attachment + " " + result + " bytes written");
System.out.println("CompletionHandler Thread ID: " + Thread.currentThread().getId());
}
@Override
public void failed(Throwable e, Object attachment) {
System.err.println("Attachment: " + attachment + " failed with:");
e.printStackTrace();
}
};
AsynchronousFileChannel classfile, writing toSystem.out.println("Main Thread ID: " + Thread.currentThread().getId());
fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write", handler);
fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write", handler);
}
}

  1. 执行应用程序。您的应用程序可能不会按您的预期行为。由于操作的异步性质,各个元素的执行顺序可能会因执行而异。以下是一个可能的输出:
Main Thread ID: 1
Attachment: Second Write 3 bytes written
Attachment: First Write 6 bytes written
CompletionHandler Thread ID: 13
CompletionHandler Thread ID: 12

重新执行应用程序可能会给出不同的执行顺序。这种行为在下一节中有解释。

它是如何工作的...

我们首先使用docs目录中的asynchronous.txt文件的Path对象创建了一个AsynchronousFileChannel对象。该文件被打开以进行读写操作,并且如果文件不存在,则应该被创建。创建了一个CompletionHandler对象。在本例中,它用于确认写操作的执行。

write方法被执行了两次。第一次将字符串Sample从文件的位置0开始写入。第二次写操作将字符串Box写入文件,也从位置0开始。这导致覆盖,文件的内容包含字符串Boxple。这是有意的,并且说明了write方法的position参数的使用。

当前线程的 ID 在代码的各个地方都有显示。它显示了一个线程用于main方法,另外两个线程用于内容处理程序。当执行write方法时,它是以异步方式执行的。write方法执行并立即返回。实际的写操作可能会在稍后发生。写操作完成后,成功完成会导致内容处理程序的completed方法执行。这会显示其线程的 ID,并显示一个消息,显示附件和写入的字节数。如果发生异常,将执行failed方法。

从输出中可以看到,一个单独的线程被用来执行完成处理程序。完成处理程序被定义为返回一个Integer值。这个值代表写入的字节数。附件可以是任何需要的对象。在这种情况下,我们用它来显示哪个write方法已经完成。写操作的异步性导致内容处理程序的执行顺序不可预测。然而,write方法确实按预期的顺序执行了。

注意使用 try-with-resource 块。这是 Java 7 的一个特性,在第一章的使用 try-with-resource 块改进异常处理代码示例中进行了探讨,Java 语言改进

还有更多...

两个参数的write方法返回一个Future<Integer>对象。稍后在程序中,我们可以使用它的get方法,它会阻塞,直到写操作完成。注释掉前面示例的写操作,并用以下代码序列替换它们:

Future<Integer> writeFuture1 = fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0);
Future<Integer> writeFuture2 = fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0);
int result = writeFuture1.get();
System.out.println("Sample write completed with " + result + " bytes written");
result = writeFuture2.get();
System.out.println("Box write completed with " + result + " bytes written");

执行应用程序。输出应该类似于以下内容:

Main Thread ID: 1
Sample write completed with 6 bytes written
Box write completed with 3 bytes written 

write方法返回了一个Future对象。get方法被阻塞,直到写操作完成。我们用结果来显示一个消息,指示哪个写操作执行了,以及写入了多少字节。

还有许多关于异步文件通道 IO 的方面可以讨论。可能感兴趣的其他方面包括:

  • 强制将对通道的更新写入

  • 锁定文件的部分或全部以独占方式访问

  • 使用AsynchronousChannelGroup来管理相关的异步操作

另请参阅

  • 在本章使用 AsynchronousFileChannel 类从文件中读取: 这个示例演示了如何执行异步读取,并使用AsynchronousChannelGroup类。

使用 AsynchronousFileChannel 类从文件中读取

也可以使用两个重载的read方法来进行异步读取操作。我们将演示如何使用java.nio.channels包的AsynchronousChannelGroup对象来实现这一点。这将为我们提供一种观察这些方法的方式,并提供一个AsynchronousChannelGroup的示例。

准备工作

执行写操作:

  1. 创建一个代表要从中读取的文件的Path对象。

  2. 使用这个路径和open方法来打开一个文件通道。

  3. 使用read方法从文件中读取数据。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,创建一个大小为三的java.util.concurrent包的ScheduledThreadPoolExecutor对象的实例。我们将主要使用ScheduledThreadPoolExecutor类,因为它很容易创建。大小为三将有助于说明线程是如何管理的。
ExecutorService pool = new ScheduledThreadPoolExecutor(3);

  1. 接下来,添加一个 try-with-resource 块,并为文件items.txt创建一个AsynchronousFileChannel对象。使用StandardOpenOption.READopen选项,以及之前创建的 pool 对象。
try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( Paths.get("/home/docs/items.txt"), EnumSet.of(StandardOpenOption.READ), pool)) {

  1. 接下来,显示主线程 ID,然后创建一个CompletionHandler对象,我们将用它来显示异步读操作的结果。
System.out.println("Main Thread ID: " + Thread.currentThread().getId());
CompletionHandler<Integer, ByteBuffer> handler = new CompletionHandler<Integer, ByteBuffer>() {
@Override
public synchronized void completed(Integer result, ByteBuffer attachment) {
for (int i = 0; i < attachment.limit(); i++) {
System.out.print((char) attachment.get(i));
}
System.out.println("");
System.out.println("CompletionHandler Thread ID: " + Thread.currentThread().getId());
System.out.println("");
}
@Override
public void failed(Throwable e, ByteBuffer attachment) {
System.out.println("Failed");
}
};

  1. 接下来,添加代码来创建一个ByteBuffer对象数组。为每个缓冲区分配10字节,然后使用一个缓冲区作为read方法的第一个参数和附件。将其用作附件,允许我们在完成处理程序中访问读操作的结果。第二个参数指定了起始读取位置,并设置为读取文件的每个 10 字节段。
final int bufferCount = 5;
ByteBuffer buffers[] = new ByteBuffer[bufferCount];
for (int i = 0; i < bufferCount; i++) {
buffers[i] = ByteBuffer.allocate(10);
fileChannel.read(buffers[i], i * 10, buffers[i], handler);
}

  1. 添加一个调用awaitTermination方法,以允许读取操作完成。然后再次显示缓冲区。
pool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("Byte Buffers");
for (ByteBuffer byteBuffer : buffers) {
for (int i = 0; i < byteBuffer.limit(); i++) {
System.out.print((char) byteBuffer.get(i));
}
System.out.println();
}

  1. 使用以下内容作为items.txt文件的内容,其中每个条目都是一个包含商品和数量的 10 字节序列:
Nail 34Bolt 12Drill 22Hammer 24Auger 24

  1. 执行应用程序。您的输出应该类似于以下内容:
Main Thread ID: 1
Nail 34
CompletionHandler Thread ID: 10
Drill 22
CompletionHandler Thread ID: 12
Bolt 12
CompletionHandler Thread ID: 11
Auger 24
CompletionHandler Thread ID: 12
Hammer 24
CompletionHandler Thread ID: 10
Byte Buffers
Nail 34
Bolt 12
Drill 22
Hammer 24
Auger 24 

注意完成处理程序线程的三个 ID 的使用。这些对应于作为线程池的一部分创建的三个线程。

它的工作原理...

使用大小为三的线程池创建了一个java.util.concurrent包的ExecutorService,以演示线程组的使用并强制重用线程。items.txt文件包含了相等长度的数据。这简化了示例。

在内容处理程序中,成功完成后,将执行completed方法。附件包含了缓冲区read,然后与内容处理程序的线程 ID 一起显示。请注意completed方法中synchronized关键字的使用。虽然不是每个方法都需要,但在这里使用了,以使输出更易读。删除关键字将导致缓冲区输出交错,使其无法阅读。

注意完成处理程序线程的非确定性行为。它们并没有按照相应的read方法执行的顺序执行。重复执行应用程序应该产生不同的输出。

知道输入文件只包含五个项目,我们创建了五个大小为10ByteBuffer对象。read方法使用不同的缓冲区执行了五次。

执行了awaitTermination方法,有效地暂停了应用程序一秒钟。这允许完成处理程序的线程完成。然后再次显示缓冲区以验证操作。

还有更多...

每当创建一个异步通道时,它都被分配到一个通道组。通过定义自己的组,可以更好地控制组中使用的线程。使用open方法创建通道时,它属于全局通道组。

异步通道组提供了完成绑定到组的异步 IO 操作所需的技术。每个组都有一个线程池。这些线程用于 IO 操作和CompletionHandler对象。

在上一个例子中,我们使用open方法将线程池与异步操作关联起来。也可以使用以下静态AsynchronousChannelGroup方法之一来创建异步通道组:

  • withFixedThreadPool: 使用ThreadFactory创建新线程的固定大小池。池的大小由其第一个参数指定。

  • withCachedThreadPool: 这个池使用ExecutorService来创建新线程。第二个参数指定了池的建议初始线程数。

  • withThreadPool: 这也使用ExecutorService,但没有指定初始大小。

异步通道组提供了对组进行有序关闭的能力。一旦关闭被启动:

  • 它尝试创建一个新通道的结果是ShutdownChannelGroupException

  • 运行完成处理程序的线程不会被中断

当组终止时:

  • 所有通道都已关闭

  • 所有完成处理程序都已经完成

  • 所有组资源都已被释放

其他感兴趣的方法包括:

  • isShutdown方法,用于确定组是否已关闭。

  • isTerminated方法,用于确定组是否已终止。

  • shutdownNow方法,用于强制关闭组。所有通道都将关闭,但内容处理程序不会被中断。

另请参阅

在本章中:

  • 使用 AsynchronousFileChannel 类写入文件:此示例演示了如何执行异步写入

使用 SecureDirectoryStream 类

java.nio.file包的SecureDirectoryStream类设计用于与依赖于比其他 IO 类提供的更严格安全性的应用程序一起使用。它支持在目录上进行无竞争(顺序一致)操作,其中操作与其他应用程序同时进行。

该类需要操作系统的支持。通过将Files类的newDirectoryStream方法的返回值转换为SecureDirectoryStream对象来获取类的实例。如果转换失败,则底层操作系统不支持此类型的流。

准备就绪

获取并使用SecureDirectoryStream对象:

  1. 创建表示感兴趣目录的Path对象。

  2. 使用Files类的newDirectoryStream方法,并将结果转换为SecureDirectoryStream

  3. 使用此对象来影响SecureDirectoryStream操作。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,添加以下代码。我们将为docs目录创建一个Path对象,然后获取一个SecureDirectoryStream对象。这将用于查看目录的 POSIX 权限。
public static void main(String args[]) throws IOException {
Path path = Paths.get("home/docs");
SecureDirectoryStream<Path> sds = (SecureDirectoryStream) Files.newDirectoryStream(path);
PosixFileAttributeView view = sds.getFileAttributeView(PosixFileAttributeView.class);
PosixFileAttributes attributes = view.readAttributes();
Set<PosixFilePermission> permissions = attributes.permissions();
for (PosixFilePermission permission : permissions) {
System.out.print(permission.toString() + ' ');
}
System.out.println();
}

  1. 在支持SecureDirectoryStream类的系统上执行应用程序。在 Ubuntu 系统上运行应用程序后获得以下输出:
GROUP_EXECUTE OWNER_WRITE OWNER_READ OTHERS_EXECUTE GROUP_READ OWNER_EXECUTE OTHERS_READ 

工作原理...

获取docs目录的Path对象,然后将其用作Files类的newDirectoryStream方法的参数。该方法的结果被转换为SecureDirectoryStream类。然后执行getFileAttributeView方法以获取一个视图,该视图用于显示目录的 POSIX 文件权限。PosixFileAttributeView类的使用在使用 PosixFileAttributeView 维护 POSIX 文件属性中有所讨论,在第三章 获取文件和目录信息

还有更多...

SecureDirectoryStream 类支持的其他方法包括删除文件或目录的能力,将文件移动到不同目录的移动方法,以及创建SeekableByteChannel以访问文件。

第七章:图形用户界面改进

在本章中,我们将涵盖以下内容:

  • 混合重量级和轻量级组件

  • 管理窗口类型

  • 管理窗口的不透明度

  • 创建变化的渐变半透明窗口

  • 管理窗口的形状

  • 在 Java 7 中使用新的边框类型

  • 在 FileDialog 类中处理多个文件选择

  • 控制打印对话框类型

  • 使用新的 JLayer 装饰器为密码字段

介绍

在 Java 7 中增强了开发具有图形用户界面GUI)界面的应用程序的能力。其中一些是较小的改进,并在本介绍中进行了讨论。其他的,如使用javax.swing.JLayer装饰器类,更为复杂,分别在单独的配方中进行了讨论。

现在可以在应用程序中混合重量级和轻量级组件,而无需添加特殊代码来使其按预期工作。这一改进对 Java 7 的用户来说基本上是透明的。然而,这种方法的本质以及可能由于它们的使用而出现的特殊情况在混合重量级和轻量级组件配方中有详细介绍。

为了简化应用程序的开发,引入了三种基本的窗口类型。这些应该简化某些类型应用程序的创建,并在“管理窗口类型”配方中进行了讨论。

应用程序的整体外观可能包括其不透明度和形状等特征。管理窗口的不透明度配方说明了如何控制窗口的不透明度,创建变化的渐变半透明窗口配方则探讨了为这样的窗口创建渐变。详细介绍了控制窗口的形状,例如使其圆形或某种不规则形状,管理窗口的形状配方中有详细说明。

Java 6 Update 10发布一起,透明度相关的功能最初是作为私有的com.sun.awt.AWTUtilities类的一部分添加的。然而,这些功能已经移动到了java.awt包中。

Javax.swing.JComponents具有可以控制外观的边框。在 Java 7 中,添加了几种新的边框。这些在在 Java 7 中使用新的边框类型配方中有详细说明。

文件对话框和打印对话框的使用也进行了改进。这些增强功能分别在处理文件对话框类中的多个文件选择控制打印对话框类型配方中进行了讨论。

现在可以在JComponent上绘制。这允许使用特殊效果,这在早期版本的 Java 中并不容易实现。使用新的 JLayer 装饰器为密码字段配方说明了这个过程,并演示了如何为窗口创建水印。

本章的所有配方都使用了基于JFrame的应用程序。以下是用于开发基于最小窗口的应用程序的代码,这些代码是配方示例的基础。使用ApplicationDriver类来启动和显示JFrame派生的ApplicationWindow类。ApplicationDriver类如下所示:

public class ApplicationDriver {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ApplicationWindow window = new ApplicationWindow();
window.setVisible(true);
}
});
}
}

invokeLater方法使用内部类来创建并显示ApplicationWindow。这个窗口在其构造函数中设置。这是一个简单的窗口,有一个退出按钮,我们将在后续的配方中用来关闭应用程序并进行增强:

public class ApplicationWindow extends JFrame {
public ApplicationWindow() {
this.setTitle("Example");
this.setSize(200, 100);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton exitButton = new JButton("Exit");
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
this.add(exitButton);
}
}

当执行此代码时,输出应该如下截图所示:

Introduction

在 Java 7 中引入了一些较小的改进。例如,受保护的静态java.awt.Cursor数组已被弃用。而是使用getPredefinedCursor方法。此方法接受一个整数参数并返回一个Cursor对象。

java.swing.JColorChooser对话框引入了一个新的HSV选项卡。如下截图所示:

Introduction

在 Java 7 中,可以自定义拖动的 JApplet 的标题,并指定是否应该装饰。这是通过script标签来实现的:

<script src="img/javascript source file"></script>
<script>
var attributes = { code:'AppletName', width:100, height:100 };
var parameters = {jnlp_href: 'appletname.jnlp',
java_decorated_frame: 'true',
java_applet_title: 'A Custom Title'
};
deployJava.runApplet(attributes, parameters, '7'7);
</script>

java_decorated_frame参数设置为true,以指定窗口应该装饰。使用java_applet_title参数指定窗口的标题。

此示例改编自download.oracle.com/javase/tutorial/deployment/applet/draggableApplet.html。可以在该网站上找到有关如何创建可拖动小程序的更多详细信息。

还需要注意一些杂项更改。Nimbus 外观已从com.sun.java.swing包移动到javax.swing包。isValidateRoot方法已添加到Applet类中,以指示容器是有效的根。最后,基于X11 XRender扩展的新Java2D图形管道已添加,以提供更好的访问图形处理单元GPU)。

混合重量级和轻量级组件

Java 提供了两种基本的组件集用于开发 GUI 应用程序:抽象窗口工具包AWT)和Swing。 AWT 依赖于本地系统的底层代码,因此这些组件被称为重量级组件。另一方面,Swing 组件完全独立于本地系统运行,完全由 Java 代码实现,因此被称为轻量级组件。在以前的 Java 版本中,混合重量级和轻量级组件是低效且麻烦的。在Java 6 Update 12中,并持续到 Java 7,JVM 处理了重量级和轻量级组件的混合。

准备工作

如果您正在使用同时实现重量级和轻量级组件的代码,无需对代码进行任何更改,因为 Java 7 会自动处理这些组件。我们将修改本章开头的代码来演示这一点:

  1. 使用介绍部分的代码示例创建一个新的应用程序。

  2. 修改代码以使用重量级和轻量级示例。

  3. 使用旧版本的 Java 运行应用程序,然后再次使用 Java 7 运行。

如何做...

  1. 按照本章的介绍指定创建一个新的窗口应用程序。将以下代码段添加到ApplicationWindow构造函数中:
JMenuBar menuBar = new JMenuBar();
JMenu menu = new JMenu("Overlapping Menu");
JMenuItem menuItem = new JMenuItem("Overlapping Item");
menu.add(menuItem);
menuBar.add(menu);
this.setJMenuBar(menuBar);
this.validate();

  1. 接下来,修改Exit按钮的声明,使其现在使用重量级的Button而不是轻量级的JButton,如下所示:
Button exitButton = new Button("Exit");

  1. 执行应用程序。您需要使用Java 6 Build 10之前的版本运行应用程序,否则重叠问题将不会显示。当窗口打开时,点击菜单,注意虽然菜单项重叠了Exit按钮,但按钮显示出来并覆盖了菜单文本。以下是重叠的示例:如何做...

  2. 现在,使用 Java 7 再次运行应用程序。当您这次点击菜单时,您应该注意到重叠问题已经解决,如下面的截图所示:

如何做...

它是如何工作的...

JVM 会自动处理组件的混合。在这个例子中,我们创建了一个场景来说明重叠问题,然后展示了如何在最新的 Java 版本中解决了这个问题。然而,调用顶层框架的validate方法以确保所有形状正确重绘是一个好的做法。以前用于混合组件的解决方法也可能需要被移除。

还有更多...

在使用 Java 7 时,有一些特定的地方需要考虑,当使用混合组件时。

  • 高级 Swing 事件可能无法正常工作,特别是由javax.swing.InputMap维护的事件。

  • 不支持部分透明的轻量级组件,这些组件旨在允许重量级组件透过它们看到。重量级项目将不会显示在半透明像素下面。

  • 重量级组件必须作为框架或小程序的一部分创建。

  • 如果在您的应用程序中已经处理了重量级和轻量级组件的混合,并且 Java 7 的新增功能引起了问题,您可以使用私有的sun.awt.disableMixing系统属性来关闭混合支持。

管理窗口类型

JFrame类支持setType方法,该方法将窗口的一般外观配置为三种类型之一。这可以简化窗口外观的设置。在本教程中,我们将研究这些类型及其在 Windows 和 Linux 平台上的外观。

准备工作

要设置窗口类型,使用setType方法,其中包括java.awt.Window类中的三种窗口类型之一:

  • Type.NORMAL: 这代表一个正常的窗口,是窗口的默认值

  • Type.POPUP: 这是一个临时窗口,用于小区域,如工具提示

  • Type.UTILITY: 这也是一个用于对象的小窗口,例如调色板

如何做...

  1. 按照本章介绍中的说明创建一个新的窗口应用程序。在退出按钮创建之前添加以下语句:
this.setType(Type.POPUP);

  1. 执行应用程序。在 Windows 系统上,窗口应如下所示:

如何做...

它是如何工作的...

该方法的使用相当简单。Type枚举可以在java.awt包中找到。在 Windows 上,窗口显示如下截图所示。正常和弹出样式具有相同的外观。实用程序类型缺少最小化和最大化按钮:

以下截图显示了Type.NORMAL窗口类型的示例:

它是如何工作的...

以下截图显示了Type.POPUP窗口类型的示例:

它是如何工作的...

以下截图显示了Type.UTILITY窗口类型的示例:

它是如何工作的...

在 Ubuntu 上,窗口显示如下截图所示。正常和实用程序具有相同的外观,而弹出类型缺少其按钮:

以下截图显示了Type.NORMAL窗口类型的示例:

它是如何工作的...

以下截图显示了Type.POPUP窗口类型的示例:

它是如何工作的...

以下截图显示了Type.UTILITY窗口类型的示例:

它是如何工作的...

管理窗口的不透明度

窗口的不透明度指的是窗口的透明程度。当窗口完全不透明时,屏幕上窗口后面的东西是看不见的。部分不透明的窗口允许背景透过。在本教程中,我们将学习如何控制窗口的不透明度。

准备工作

要控制窗口的不透明度,使用JFrame类的setOpacity方法,使用表示窗口应该有多不透明的浮点值。

如何做...

  1. 按照本章介绍中的说明创建一个新的标准 GUI 应用程序。将invokeLater方法调用替换为以下代码:
JFrame.setDefaultLookAndFeelDecorated(true);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ApplicationWindow window = new ApplicationWindow();
window.setOpacity(0.75f);
window.setVisible(true);
}
});

  1. 执行应用程序。窗口应如下所示:

如何做...

注意这个应用程序后面的窗口是可以看到的。在这种情况下,背景是应用程序的代码。

它是如何工作的...

setOpacity使用0.75f来设置窗口的不透明度。这导致它变得 75%透明,可以通过代码看到。

不透明度的值范围是 0.0f 到 1.0f。值为 1.0f 表示完全不透明的窗口,值为 0.0f 表示完全透明的窗口。如果不透明度设置为 0.0f,则可能启用或禁用鼠标。这由底层系统决定。要设置小于 1.0f 的值:

  • 必须支持透明度

  • 窗口必须是无装饰的

  • 窗口不能处于全屏模式

下一节将介绍如何确定是否支持透明度。getOpacity方法可用于确定当前不透明度级别。

还有更多...

要确定平台是否支持不透明度,我们需要使用java.awt.GraphicsDevice类的一个实例。java.awt.GraphicsEnvironment类包含当前平台的GraphicsDevice对象列表。GraphicsDevice通常指的是可用的屏幕,但也可以包括打印机或图像缓冲区。每个GraphicsDevice还可以包含一组GraphicsConfiguration对象,用于指定设备可能的配置,例如分辨率和支持的颜色模型。

在以下代码序列中,我们获取GraphicsEnvironment对象的一个实例,然后使用它的getDefaultScreenDevice方法获取一个GraphicsDevice对象。使用isWindowTranslucencySupported方法针对GraphicsDevice对象来确定是否支持透明度:

GraphicsEnvironment graphicsEnvironment =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
if (!graphicsDevice.isWindowTranslucencySupported(
GraphicsDevice.WindowTranslucency.TRANSLUCENT)) {
System.err.println(
"Translucency is not supported on this platform");
System.exit(0);
}

GraphicsDevice.WindowTranslucency枚举表示平台可能支持的透明度类型。其值总结在以下表中。alpha 值指的是透明度级别:

意义
PERPIXEL_TRANSLUCENT 表示系统支持一些像素设置为可能不同的 alpha 值
PERPIXEL_TRANSPARENT 表示系统支持所有像素设置为 0.0f 或 1.0f
TRANSLUCENT 表示系统支持所有像素设置为 alpha 值

另请参阅

使用新的 JLayer 装饰器为密码字段配方解决了如何在JComponent上绘制。

创建一个变化的渐变半透明窗口

有时,通过添加特殊的图形特性,应用程序窗口可以在美学上得到增强。Java 7 支持使用渐变半透明窗口,透明度既可以在视觉上有趣,也可以在功能上有用。

这个配方将演示如何在窗口上同时使用透明度特性和颜色渐变。

准备工作

为了创建一个半透明的渐变颜色窗口,您需要:

  1. 执行检查以确保系统环境支持每像素半透明。

  2. 设置背景颜色,使窗口最初完全透明。

  3. 创建一个java.awt.GradientPaint对象来指定渐变的颜色和位置。

如何做...

  1. 按照本章介绍中的描述创建一个新的标准 GUI 应用程序。在线程开始之前,将以下代码添加到ApplicationDriver类中:
GraphicsEnvironment envmt =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice device = envmt.getDefaultScreenDevice();
if (!device.isWindowTranslucencySupported (WindowTranslucency.PERPIXEL_TRANSLUCENT)) {
System.out.println("Translucent windows are not supported on your system.");
System.exit(0);
}
JFrame.setDefaultLookAndFeelDecorated(true);

  1. 接下来,用以下代码序列替换ApplicationWindow构造函数的主体:
this.setTitle("Gradient Translucent Window");
setBackground(new Color(0, 0, 0, 0));
this.setSize(500, 700);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics gradient) {
if (gradient instanceof Graphics2D) {
final int Red = 120;
final int Green = 50;
final int Blue = 150;
Paint paint = new GradientPaint(0.0f, 0.0f,
new Color(Red, Green, Blue, 0),
getWidth(), getHeight(),
new Color(Red, Green, Blue, 255));
Graphics2D gradient2d = (Graphics2D) gradient;
gradient2d.setPaint(paint);
gradient2d.fillRect(0, 0, getWidth(), getHeight());
}
}
};
this.setContentPane(panel);
this.setLayout(new FlowLayout());
JButton exitButton = new JButton("Exit");
this.add(exitButton);
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});

  1. 执行应用程序。您的窗口应该类似于以下内容:

如何做...

它是如何工作的...

首先,我们在ApplicationDriver类中添加了代码,以测试系统是否支持每像素半透明。在我们的示例中,如果不支持,应用程序将退出。这在还有更多...部分的管理窗口的不透明度配方中有更详细的讨论。

不应在装饰窗口上使用渐变。我们调用setDefaultLookAndFeelDecorated方法来确保使用默认外观。在 Windows 7 上执行时,这会导致一个无装饰的窗口。

ApplicationDriver类中,我们首先设置了窗口的背景颜色。我们使用(0, 0, 0, 0)来指定每种颜色的饱和度级别,红色、绿色和蓝色的 alpha 值都为零。颜色值可以是 0 到 255 之间的任何整数,但我们希望我们的窗口起始时没有颜色。alpha 值为零意味着我们的窗口将完全透明。

接下来,我们创建了一个新的JPanel。在JPanel中,我们重写了paintComponent方法,并创建了一个新的GradientPaint对象。GradientPaint类有四个构造函数。我们选择使用需要浮点数的 X 和 Y 坐标以及Color对象来指定渐变颜色的构造函数。您还可以选择传递Point2D对象而不是浮点数。

首先指定的点,可以是浮点数,也可以是Point2D对象,表示渐变的起点。在我们的示例中,第二个点由getWidthgetHeight方法确定,确定了渐变的终点。在我们的示例中,结果是一个从左上角开始浅色,随着向下和向右移动逐渐变暗的渐变。

最后,我们将渐变强制转换为Graphics2D对象,并调用setPaintfillRect方法在整个窗口上绘制我们的渐变。

另请参阅

使用GraphicsDevice对象来确定透明度支持级别的讨论在还有更多..部分的管理窗口的不透明度配方中有更详细的介绍。

管理窗口的形状

在应用程序开发中,有时创建特殊形状的窗口可能很有趣和有用。从 Java 7 版本开始,这个功能现在已经可用。在这个配方中,我们将开发一个停止标志形状的窗口,以确保用户想要继续某些操作。

准备工作

要创建一个特殊形状的窗口,您必须:

  1. 验证给定系统是否支持逐像素透明度。

  2. 创建一个组件监听器来捕获componentResized事件。

  3. 创建一个形状的实例并将其传递给setShape方法。

如何做...

  1. 按照本章介绍中描述的方式创建一个新的标准 GUI 应用程序。在main方法中,在启动线程之前,通过添加以下代码来测试系统是否支持有形窗口:
GraphicsEnvironment envmt =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice device = envmt.getDefaultScreenDevice();
if (!device.isWindowTranslucencySupported(
WindowTranslucency.PERPIXEL_TRANSLUCENT)) {
System.out.println("Shaped windows not supported");
System.exit(0);
}

  1. 创建一个名为StopPanel的新类,它是从JPanel派生的,并向其添加以下构造函数:
public StopPanel() {
this.setBackground(Color.red);
this.setForeground(Color.red);
this.setLayout(null);
JButton okButton = new JButton("YES");
JButton cancelButton = new JButton("NO");
okButton.setBounds(90, 225, 65, 50);
cancelButton.setBounds(150, 225, 65, 50);
okButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
this.add(okButton);
this.add(cancelButton);
}

  1. 您还需要为StopPanel类实现一个paintComponent方法。它负责在我们的窗口中显示文本。以下是实现此方法的一种方式:
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int pageHeight = this.getHeight();
int pageWidth = this.getWidth();
int bigHeight = (pageHeight+80)/2;
int bigWidth = (pageWidth-305)/2;
int smallHeight = (pageHeight+125)/2;
int smallWidth = (pageWidth-225)/2;
Font bigFont = new Font("Castellar", Font.BOLD, 112);
Font smallFont = new Font("Castellar", Font.PLAIN, 14);
g2d.setFont(bigFont);
g2d.setColor(Color.white);
g2d.drawString("STOP", bigWidth, bigHeight);
g2d.setFont(smallFont);
g2d.drawString("Are you sure you want to continue?", smallWidth, smallHeight);
}

  1. ApplicationWindow类中,在创建Exit按钮之前,创建一个StopPanel的新实例。接下来,创建一个Shape的新实例。在我们的示例中,我们使用getPolygon方法创建了一个Polygon对象,如下所示:
this.add(new StopPanel());
final Polygon myShape = getPolygon();

  1. 然后在创建Exit按钮的代码前添加一个componentListener来捕获componentResized事件。在监听器中,对Shape对象调用setShape方法。我们还将在这一点上设置前景色和背景色:
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
setShape(myShape);
((JFrame) e.getSource()).setForeground(Color.red);
((JFrame) e.getSource()).setBackground(Color.red);
}
});

  1. 添加一个调用setUndecorated方法并将属性设置为true:
setUndecorated(true);

  1. 接下来,将getPolygon方法添加到类中。该方法使用两个整数数组和Polygon类的addPoint方法创建一个八边形:
private Polygon getPolygon() {
int x1Points[] = {0, 0, 100, 200, 300, 300, 200, 100};
int y1Points[] = {100, 200, 300, 300, 200, 100, 0, 0};
Polygon polygon = new Polygon();
for (int i = 0; i < y1Points.length; i++) {
polygon.addPoint(x1Points[i], y1Points[i]);
}
return polygon;
}

  1. 当应用程序执行时,您应该看到一个八边形窗口,格式如下:

如何做...

它是如何工作的...

我们最初的测试验证了逐像素的透明度,使我们能够根据系统的需求定制应用程序。在我们的例子中,如果该属性不受支持,我们只是退出应用程序,尽管在现实世界的环境中,您可能希望打开一个不太复杂的窗口。在更多内容部分的管理窗口的不透明度配方中更详细地讨论了检测操作系统支持的内容。

StopPanel类实现了JPanel接口,并允许我们在窗口中添加自定义文本和按钮。因为我们在窗口中使用了特殊的形状,所以我们选择使用null参数调用setLayout方法,这样就可以使用setBounds方法来明确地放置我们想要的按钮在窗口上。重要的是要注意,虽然窗口显示为八边形,或者您选择的其他形状,但实际上窗口仍然是一个矩形,由setSize方法指定。因此,按钮和其他对象可以放置在窗口上,但如果它们超出了您的形状设置的边界,它们就不可见了。

paintComponent方法用于自定义窗口上的文本。在这个方法中,我们设置了文本的大小、样式和位置,并调用drawString方法将其实际绘制到屏幕上。

要实际创建一个八边形窗口,我们创建了我们的getPolygon方法并手动绘制了多边形。然而,如果您想要使用一个已经由实现Shape接口的类定义形状的窗口,您就不需要创建一个单独的方法。您只需将Shape对象传递给setShape方法。如果setShape方法的参数是null,窗口将调整为给定系统的默认大小,通常是一个矩形。

componentResized事件中执行setShape方法非常重要。这确保了每次窗口被重绘时,setShape方法都会被调用并且形状会被保持。调用setUndecorated方法也很重要,因为目前,特殊形状的窗口会丢失装饰。此外,窗口可能不是全屏模式。

另请参阅

使用GraphicsDevice对象来确定透明度支持的级别在更多内容部分的管理窗口的不透明度配方中有更详细的讨论。

在 Java 7 中使用新的边框类型

边框用于 swing 组件的轮廓。在 Java 7 中,有几种新的边框选项可用。在这个配方中,我们将开发一个简单的应用程序来演示如何创建边框以及这些边框的外观。

准备工作

创建和使用边框:

  1. 使用javax.swing.BorderFactory方法创建一个新的边框。

  2. 将边框对象作为setBorder方法的参数应用于JComponent对象。

如何做...

  1. 按照本章介绍中的描述创建一个新的标准 GUI 应用程序。修改ApplicationWindow类以替换以下行:
JButton exitButton = new JButton("Exit");
this.add(exitButton);

  1. 使用以下代码:
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createRaisedSoftBevelBorder());
this.setLayout(new FlowLayout());
JButton exitButton = new JButton("Exit");
panel.add(exitButton);
this.add(panel);

  1. 执行应用程序。窗口应该如下所示:如何做...

它是如何工作的...

setBorder方法将JPanel的边框更改为凸起的软斜角边框。BorderFactory方法具有许多静态方法来创建边框。下表总结了 Java 7 中可用的新边框:

方法 视觉效果
默认边框 它是如何工作的...
createRaisedSoftBevelBorder() 它是如何工作的...
createLineBorder(Color.BLACK, 1, true)第一个参数是边框的颜色。第二个是它的厚度,而第三个参数指定边角是否应该是圆角的。 它是如何工作的...
createLoweredSoftBevelBorder() 它是如何工作的...
createSoftBevelBorder(BevelBorder.LOWERED)这与createLoweredSoftBevelBorder()具有相同的效果。 工作原理...
createSoftBevelBorder(BevelBorder.RAISED)这与createRaisedSoftBevelBorder()具有相同的效果。 工作原理...
createSoftBevelBorder(BevelBorder.LOWERED, Color.lightGray, Color.yellow)第一个参数是边框的类型:RAISEDLOWERED。第二个参数是外部突出区域的颜色。第三个参数是内边缘的颜色。 工作原理...
createSoftBevelBorder(BevelBorder.RAISED,Color.lightGray, Color.yellow)createSoftBevelBorder相同的参数。 工作原理...
createSoftBevelBorder(BevelBorder.LOWERED, Color.lightGray, Color.lightGray, Color.white, Color.orange)这些参数用于边框的高亮和阴影区域的内部和外部边缘。 工作原理...
createStrokeBorder(new BasicStroke(1.0f))第二个重载的方法将Paint对象作为第二个参数,并用于生成颜色。 工作原理...
createDashedBorder(Color.red) 工作原理...
createDashedBorder(Color.red, 4.0f, 1.0f)第二个参数是虚线的相对长度,第三个参数是空格的相对长度。 工作原理...
createDashedBorder(Color.red, 2.0f, 10.0f, 1.0f, true)第二个参数指定线条的厚度。第三和第四个参数分别指定长度和间距,而最后的布尔参数确定端点是否是圆形的。 工作原理...

边框可以更改为任何JComponent类。然而,外观并不总是令人满意。就像我们在这个例子中所做的那样,有时最好在一个封闭的JPanel对象上更改边框。

在 FileDialog 类中处理多个文件选择

使用Ctrl和/或Shift键与鼠标结合来在文件对话框中选择两个或多个文件或目录。在 Java 7 中,文件对话框使用java.awt.FileDialog类的setMultipleMode方法启用或禁用此功能。这个简单的增强功能在这个示例中有所体现。

准备工作

在打印对话框中启用或禁用多个文件的选择:

  1. 创建一个新的FileDialog对象。

  2. 使用其setMultipleMode方法来确定其行为。

  3. 显示对话框。

  4. 使用返回值来确定选择了哪些文件。

如何操作...

  1. 按照本章介绍中的描述创建一个新的标准 GUI 应用程序。修改ApplicationWindow类以添加一个按钮来显示文件对话框,如下面的代码所示。在匿名内部类中,我们将显示对话框:
public ApplicationWindow() {
this.setTitle("Example");
this.setSize(200, 100);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
final FileDialog fileDialog = new FileDialog(this, "FileDialog");
fileDialog.setMultipleMode(true);
JButton fileDialogButton = new JButton("File Dialog");
fileDialogButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
fileDialog.setVisible(true);
}
});
this.add(fileDialogButton);
JButton exitButton = new JButton("Exit");
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
this.add(exitButton);
}

  1. 执行应用程序。应用程序窗口应该如下所示:如何操作...

  2. 选择文件对话框按钮,应该出现以下对话框。转到一个目录并选择一些文件。在接下来的窗口中,已选择了/home/music目录的两个文件:如何操作...

工作原理...

fileDialog类的setMultipleMode方法使用true参数执行。这使得可以选择多个文件。创建了一个匿名内部类来处理文件按钮事件的选择。在actionPerformed方法中,对话框被显示出来。

还有更多...

要确定选择了哪些文件,我们可以使用fileDialog类的getFiles方法。在fileDialog类的setVisible方法之后添加以下代码:

File files[] = fileDialog.getFiles();
for (File file : files) {
System.out.println("File: " + file.getName());
}

该方法返回一个File对象数组。使用 for each 循环,我们可以显示每个选定文件的名称。执行应用程序并选择几个文件。所选音乐文件的输出应如下所示:

文件:Future Setting A.mp3

文件:Space Machine A.mp3

控制打印对话框类型

java.awt.PrintJob类的标准打印对话框允许使用通用和本机对话框。这提供了更好地适应平台的能力。对话框类型的规范很简单。

准备工作

要指定打印对话框类型并使用打印对话框,需要按照以下步骤进行:

  1. 创建一个javax.print.attribute.PrintRequestAttributeSet对象。

  2. 将所需的对话框类型分配给此对象。

  3. 创建一个PrinterJob对象。

  4. PrintRequestAttributeSet对象用作PrinterJob类的printDialog方法的参数。

如何做...

  1. 创建一个新的标准 GUI 应用程序,如章节介绍中所述。修改ApplicationWindow类以添加一个按钮,显示如下所示的打印对话框。在一个匿名内部类中,我们将显示一个打印对话框:
public ApplicationWindow() {
this.setTitle("Example");
this.setSize(200, 100);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
JButton printDialogButton = new JButton("Print Dialog");
printDialogButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
final PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
attributes.add(DialogTypeSelection.COMMON);
PrinterJob printJob = PrinterJob.getPrinterJob();
printJob.printDialog(attributes);
}
});
this.add(printDialogButton);
JButton exitButton = new JButton("Exit");
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
this.add(exitButton);
}

  1. 执行应用程序并选择打印按钮。出现的对话框应该使用通用外观类型,如下面的屏幕截图所示:

如何做...

它是如何工作的...

创建了一个新的打印按钮,允许用户显示打印对话框。在用于处理按钮动作事件的匿名内部类中,我们创建了一个基于javax.print.attribute.HashPrintRequestAttributeSet类的PrintRequestAttributeSet对象。这使我们能够向集合添加DialogTypeSelection.NATIVE属性。DialogTypeSelection类是 Java 7 中的新类,提供了两个字段:COMMONNATIVE

接下来,我们创建了一个PrinterJob对象,并对该对象执行了printDialog方法。然后打印对话框将显示出来。如果我们使用了NATIVE类型,将如下所示:

attributes.add(DialogTypeSelection.NATIVE);

然后在 Windows 平台上,打印对话框将如下所示:

它是如何工作的...

使用新的 JLayer 装饰器为密码字段

Java 7 支持装饰 GUI 组件,如文本框和面板。装饰是在组件顶部绘制的过程,使其具有特殊外观。例如,我们可能希望在界面上加水印,以显示它是测试版,或者可能为文本框中的错误提供图形 X 的指示,而这在其他情况下是不可能的。

javax.swing.JLayer类提供了一种将显示的组件、在组件上绘制额外图形以及拦截事件的方法绑定在一起的方式。事件的处理和显示被委托给一个javax.swing.plaf.LayerUI派生对象。当事件发生时,将执行一个处理事件的方法。当组件被绘制时,将执行LayerUI派生对象的paint方法,根据需要显示图形。

在本教程中,我们将学习 Java 如何支持此功能。在第一部分中,我们将演示如何为密码字段显示错误消息。在还有更多..部分,我们将展示如何为窗口创建水印。

准备工作

要装饰一个组件:

  1. 创建要装饰的组件。

  2. 创建一个实现装饰图形操作的LayerUI派生类。

  3. 创建一个基于组件和LayerUI派生类的JLayer对象。

  4. JLayer对象添加到应用程序中。

如何做...

  1. 按照本章介绍中的描述创建一个新的标准 GUI 应用程序。使用以下ApplicationWindow。在它的构造函数中,我们将使用getPanel方法执行必要的步骤来返回我们的密码JPanel对象。当用户输入密码时,窗口将被装饰,显示密码太短的消息,直到至少输入六个字符:
public ApplicationWindow() {
this.setTitle("Example");
this.setSize(300, 100);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
LayerUI<JPanel> layerUI = new PasswordLayerUI();
JLayer<JPanel> jlayer = new JLayer<JPanel>(getPanel(), layerUI);
this.add(jlayer);
}
private JPanel getPanel() {
JPanel panel = new JPanel(new BorderLayout());
JPanel gridPanel = new JPanel(new GridLayout(1, 2));
JLabel quantityLabel = new JLabel("Password");
gridPanel.add(quantityLabel);
JPasswordField passwordField = new JPasswordField();
gridPanel.add(passwordField);
panel.add(gridPanel, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton okButton = new JButton("OK");
buttonPanel.add(okButton);
JButton cancelButton = new JButton("Cancel");
buttonPanel.add(cancelButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}

  1. 接下来,按照以下代码创建PasswordLayerUI类。paint方法将执行实际的装饰。其余的方法用于启用键盘事件并在发生时处理它们:
class PasswordLayerUI extends LayerUI<JPanel> {
private String errorMessage = "Password too short";
@Override
public void paint(Graphics g, JComponent c) {
FontMetrics fontMetrics;
Font font;
int height;
int width;
super.paint(g, c);
Graphics2D g2d = (Graphics2D) g.create();
int componentWidth = c.getWidth();
int componentHeight = c.getHeight();
// Display error message
g2d.setFont(c.getFont());
fontMetrics = g2d.getFontMetrics(c.getFont());
height = fontMetrics.getHeight();
g2d.drawString(errorMessage,
componentWidth / 2 + 10, componentHeight / 2 + height);
g2d.dispose();
}
@Override
public void installUI(JComponent component) {
super.installUI(component);
((JLayer) component).setLayerEventMask(AWTEvent.KEY_EVENT_MASK);
}
@Override
public void uninstallUI(JComponent component) {
new JLayer decoratorusingsuper.uninstallUI(component);
((JLayer) component).setLayerEventMask(0);
}
protected void processKeyEvent(KeyEvent event, JLayer layer) {
JTextField f = (JTextField) event.getSource();
if (f.getText().length() < 6) {
errorMessage = "Password too short";
}
else {
errorMessage = "";
}
layer.repaint();
}
}

  1. 执行应用程序。在文本框中输入一些字符。您的窗口应该看起来类似于以下内容:操作步骤...

  2. 输入至少六个字符。此时装饰应该消失如下:

操作步骤...

工作原理...

ApplicationWindow中,我们创建了PasswordLayerUI类的一个实例。我们使用这个对象以及getPanel方法返回的JPanel来创建JLayer对象。然后将JLayer对象添加到窗口中。

注意在LayerUIJLayer对象中使用泛型。这是为了确保元素都是兼容的。我们使用JPanel,因为这是我们要装饰的组合组件。

JLayer类提供了一种将密码框、错误消息的显示和键盘事件拦截绑定在一起的方法。键盘事件的处理和错误消息的显示被委托给了PasswordLayerUI对象。按下键时,将执行processKeyEvent方法。当组件被绘制时,将执行paint方法,通过密码框显示错误消息。

PasswordLayerUI类中,我们声明了一个私有的String变量来保存我们的错误消息。它被声明在这个级别,因为它在多个方法中被使用。

paint方法执行实际的装饰。它接收一个代表我们可以绘制的区域的Graphics对象,以及一个组件JComponent,在这种情况下是一个JPanel。在paint方法中,我们使用了组件的字体,还为错误消息创建了一个新的font。计算并使用了组件和错误字符串的高度和宽度来定位显示的错误字符串。

installUIuninstallUI方法用于执行装饰所需的任何初始化。在这种情况下,它们被用来使键盘事件能够被拦截并由该类处理。setLayerEventMask方法与AWTEvent.KEY_EVENT_MASK参数一起使用,以启用键盘事件的处理。processKeyEvent方法执行实际的键盘事件处理。在这个方法中,密码文本字段内容的长度被用来确定要显示哪个错误消息。

还有更多...

这个例子可以考虑使用标签来执行。然而,这个例子旨在提供如何使用装饰的简单演示。创建其他装饰,比如水印,如果没有使用JLayerLayerUI类,就不容易执行。

dispose方法之前添加以下代码。这个序列将在窗口上添加一个水印,指示这是界面的测试版。使用Castellar字体提供更多的模板化文本外观。使用Composite对象来改变字符串的 alpha 值。这有效地控制了显示的字符串的透明度。getComposite方法用于获取窗口的当前复合体,然后用于确定正在使用的规则。规则以及0.25f的 alpha 值用于使水印淡入背景,如下所示:

// Display watermark
String displayText = "Beta Version";
font = new Font("Castellar",Font.PLAIN, 16);
fontMetrics = g2d.getFontMetrics(font);
g2d.setFont(font);
width = fontMetrics.stringWidth(displayText);
height = fontMetrics.getHeight();
Composite com = g2d.getComposite();
AlphaComposite ac = AlphaComposite.getInstance(
((AlphaComposite)com).getRule(),0.25f);
g2d.setComposite(ac);
g2d.drawString(displayText,
(componentWidth - width) / 2,
(componentHeight - height) / 2);

当执行时,您的应用程序应该看起来类似于以下屏幕截图。请注意,水印是全大写的。这是使用Castellar字体的结果,这是 一种全大写字母字体,模仿了奥古斯都纪念罗马柱上使用的字母。

更多内容...

第八章:处理事件

在本章中,我们将涵盖以下内容:

  • 管理额外的鼠标按钮和高分辨率鼠标滚轮

  • 在显示窗口时控制焦点

  • 使用辅助循环模拟模态对话框

  • 处理虚假线程唤醒

  • 使用事件处理程序处理小程序初始化状态

介绍

Java 7 还增加了几个与事件相关的事件或与事件相关的事件。这包括对鼠标事件的处理,其中提供了增强的支持来检测鼠标按钮和使用高分辨率鼠标滚轮,正如我们将在管理额外的鼠标按钮和高分辨率鼠标滚轮示例中看到的。

当使用setVisibletoFront方法使窗口可见时,现在我们可以控制它们是否应该获得焦点。有些窗口可能是为了信息或状态而显示的,并不一定需要或有权获得焦点。如何控制这种行为在控制 AutoRequestFocus示例中有解释。

读者应该熟悉模态对话框的行为。基本上,模态对话框在关闭之前不会将焦点返回到主窗口。有时候,希望模仿这种行为而不使用对话框。例如,执行相对较长的计算的按钮的选择可能会受益于这种行为。使用辅助循环模拟模态对话框示例探讨了如何实现这一点。

虽然不常见,但在使用wait方法时可能会发生虚假中断。java.awt.event.InvocationEvent类的isDispatched方法可用于处理虚假中断,详细信息请参阅处理虚假线程唤醒示例。

小程序在与 JavaScript 代码通信方面也得到了增强。使用事件处理程序处理小程序初始化状态示例描述了 JavaScript 代码如何能够意识到并利用小程序加载的时间。

Java 7 中还有一些与事件相关的小改进,不值得列入示例的包括访问扩展键代码和为JSlider类实现java.awt.iamg.ImageObserver接口的可用性。

KeyEvent类已增加了两个新方法:getExtendedKeyCodegetExtendedKeyCodeForChar。第一个方法返回一个键的唯一整数,但与getKeyCode方法不同,它的值取决于键盘当前的配置。第二个方法返回给定 Unicode 字符的扩展键代码。

imageUpdate方法已添加到JSlider类中。这允许该类监视正在加载的图像的状态,尽管这种能力可能最好与从JSlider派生的类一起使用。

管理额外的鼠标按钮和高分辨率鼠标滚轮

Java 7 提供了更多处理鼠标事件的选项。java.awt.Toolkit类的areExtraMouseButtonsEnabled方法允许您确定系统是否支持标准按钮集之外的更多按钮。java.awt.event.MouseWheelEvent类的getPreciseWheelRotation方法可用于控制高分辨率鼠标滚轮的操作。在这个示例中,我们将编写一个简单的应用程序来确定启用的鼠标按钮数量并测试鼠标滚轮旋转。

准备工作

首先,使用第七章图形用户界面改进中的入门处找到的ApplicationWindowApplicationDriver起始类创建一个新的应用程序。

  1. 实现MouseListenerMouseWheelListener接口以捕获鼠标事件。

  2. 使用areExtraMouseButtonsEnabledgetPreciseWheelRotation方法来确定鼠标的具体信息。

如何做...

  1. 首先,我们将使用以下代码示例设置关于我们正在创建的JFrame的基本信息:
public class ApplicationWindow extends JFrame {
public ApplicationWindow() {
this.setTitle("Example");
this.setSize(200, 100);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
JButton exitButton = new JButton("Exit");
this.add(exitButton);
}
}

  1. 接下来,我们想要收集有关鼠标的一些信息。我们执行getNumberOfButtons方法来确定鼠标上有多少个按钮。然后我们使用areExtraMouseButtonsEnabled方法来确定我们鼠标上有多少个按钮可供我们使用。我们将这些信息打印到控制台上,如下所示:
int totalButtons = MouseInfo.getNumberOfButtons();
System.out.println(Toolkit.getDefaultToolkit().areExtraMouseButtonsEnabled());
System.out.println("You have " + totalButtons + " total buttons");

  1. 接下来,我们启用我们的监听器:
this.addMouseListener(this);
this.addMouseWheelListener(this);
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});

  1. mousePressed事件方法中,只需使用getButton方法打印出按下的按钮编号,如下所示:
public void mousePressed(MouseEvent e) {
System.out.println("" + e.getButton());
}

  1. 实现MouseListener接口方法的其余部分。在mouseWheelMoved事件方法中,使用getPreciseWheelRotationgetWheelRotation方法打印有关鼠标滚轮移动的具体信息:
public void mouseWheelMoved(MouseWheelEvent e) {
System.out.println("" + e.getPreciseWheelRotation() +
" - " + e.getWheelRotation());
}

  1. 执行应用程序。您应该看到一个类似以下的JFrame窗口:操作步骤...

  2. 当您在窗口中单击时,您将在控制台中看到不同的输出,具体取决于您的鼠标、您单击的按钮以及您移动鼠标滚轮的方向。以下是可能的输出之一:

true

您总共有 5 个按钮

1

2

3

4

5

0.75 - 0

1.0 - 1

1.0 - 1

1.1166666666666667 - 1

-1.0 - 0

-1.0 - -1

-1.2916666666666667 - -1

-1.225 - -1

它是如何工作的...

getNumberOfButtons方法返回了鼠标上的按钮总数。在先前的示例中,有五个按钮,但如果在没有鼠标的系统上执行该应用程序,它将返回-1。在我们的mousePressed方法中,我们打印了由getButton方法返回的点击的按钮的名称。

我们执行了areExtraMouseButtonsEnabled方法来确定实际上支持额外的按钮,并且允许将它们添加到EventQueue中。如果要更改此值,必须在Toolkit类初始化之前进行,如还有更多..部分所述。

因为启用了多个鼠标按钮,我们的输出显示了所有五个鼠标按钮的编号。在大多数情况下,鼠标滚轮也被视为按钮,并包括在计数中。

先前控制台输出的最后几行是鼠标滚轮的移动指示。第一行,0.75 - 0,表示鼠标滚轮向后移动,或者向用户方向移动。这是通过getPreciseWheelRotation方法返回值 0.75 和getWheelRotation方法返回值 0 来表明的。输出的最后一行,-1.225 - -1,相反表示鼠标滚轮向前移动,或者远离用户。这是通过两种方法的负返回值来表示的。

使用高分辨率鼠标滚轮执行了此应用程序。低分辨率鼠标滚轮将只返回整数值。

还有更多...

有两种控制是否启用额外鼠标按钮的方法。第一种技术是使用以下命令行启动应用程序,并将sun.awt.enableExtraMouseButtons属性设置为truefalse

java -Dsun.awt.enableExtraMouseButtons=false ApplicationDriver

选项D使用了一个false值,指定不启用额外的鼠标按钮。第二种方法是在Toolkit类初始化之前设置相同的属性。可以使用以下代码实现:

System.setProperty("sun.awt.enableExtraMouseButtons", "true");

在显示窗口时控制焦点

setAutoRequestFocus方法已添加到java.awt.Window类中,用于指定窗口在使用setVisibletoFront方法显示时是否应该接收焦点。有时候,窗口被显示出来,但我们不希望窗口获得焦点。例如,如果显示的窗口包含状态信息,使其可见就足够了。让它获得焦点可能没有意义,并且可能会让用户感到沮丧,因为他们被迫将焦点切换回原始窗口。

做好准备

在窗口可见时控制焦点,如果应该接收焦点则调用setAutoRequestFocus方法并传入true,否则传入false

如何做...

  1. 为了演示这种技术,我们将创建两个窗口。一个用于隐藏然后显示第二个窗口。通过在第二个窗口中使用setAutoRequestFocus方法,我们可以控制它是否接收焦点。

  2. 首先,使用以下驱动程序创建一个新项目。在驱动程序中,我们将创建第一个窗口如下:

public class ApplicationDriver {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ApplicationWindow window = new ApplicationWindow();
window.setVisible(true);
}
});
}
}

  1. 接下来,添加ApplicationWindow类。在这个类中,我们添加了两个按钮来隐藏和显示第二个窗口,以及一个用于退出应用程序的第三个按钮,如下所示:
public class ApplicationWindow extends JFrame {
private SecondWindow second;
public ApplicationWindow() {
this.setTitle("Example");
this.setBounds(100, 100, 200, 200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
second = new SecondWindow();
second.setVisible(true);
JButton secondButton = new JButton("Hide");
this.add(secondButton);
secondButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
second.setVisible(false);
});
JButton thirdButton = new JButton("Reveal");
this.add(thirdButton);
thirdButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
second.setVisible(true);
}
});
JButton exitButton = new JButton("Exit");
this.add(exitButton);
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.exit(0);
}
});
}
}

  1. 接下来添加SecondWindow类。这个简单的窗口除了使用setAutoRequestFocus方法来控制其行为外,什么也不做:
public class SecondWindow extends JFrame {
public SecondWindow() {
this.setTitle("Second Window");
this.setBounds(400, 100, 200, 200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setAutoRequestFocus(false);
}
}

  1. 执行应用程序。两个窗口应该都出现,并且焦点在第一个窗口上,如下截图所示:如何做...

  2. 第二个窗口显示如下:如何做...

  3. 选择隐藏按钮。第二个窗口应该消失。接下来,选择显示按钮。第二个窗口应该重新出现,并且不应该有焦点。这是setAutoRequestFocus方法与false值一起使用时的效果。

  4. 停止应用程序并将setAutoRequestFocus方法的参数更改为true。重新执行应用程序,隐藏然后显示第二个窗口。当它显示时,第二个窗口应该接收焦点。

工作原理...

应用程序驱动程序显示了应用程序窗口。在ApplicationWindow类中,创建并显示了第二个窗口。此外,创建了三个按钮和内部类来影响它们的操作。setAutoRequestFocus方法传递了一个false值,指定在窗口显示时不保留焦点。

还有更多...

这种方法可能对从系统托盘运行的应用程序有用。

注意

请注意,isAutoRequestFocus方法可用于确定autoRequestFocus值的值。

使用次要循环模拟模态对话框

java.awt.EventQueue类的SecondaryLoop接口提供了一种方便的技术来模拟模态对话框的行为。模态对话框有两种行为。第一种是从用户的角度来看。在对话框完成之前,用户不被允许与主窗口交互。第二个角度是从程序执行的角度来看。调用对话框的线程在对话框关闭之前被阻塞。

SecondaryLoop允许在阻塞当前线程的同时执行某些任务,直到次要循环完成。它可能没有与之关联的用户界面。当用户选择一个按钮时,虽然它不显示对话框,但涉及到长时间运行的计算时,这可能会很有用。在本教程中,我们将演示如何使用次要循环并检查其行为。

准备工作

要创建和使用次要循环,需要按照以下步骤进行:

  1. 获取应用程序的默认java.awt.Toolkit实例。

  2. 使用此方法获取系统事件队列的引用。

  3. 使用事件队列创建一个SecondaryLoop对象。

  4. 使用SecondaryLoop接口的enter方法来启动循环。

  5. 在次要循环中实现所需的行为。

  6. 使用SecondaryLoop接口的exit方法来终止循环。

如何做...

  1. 使用以下ApplicationDriver类创建一个新的应用程序。它简单地显示应用程序的窗口如下:
public class ApplicationDriver {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ApplicationWindow window = new ApplicationWindow();
window.setVisible(true);
}
});
}
}

  1. 添加以下ApplicationWindow类。它创建了两个按钮,用于演示次要循环的行为:
public class ApplicationWindow extends JFrame implements ActionListener {
private JButton firstButton;
private JButton secondButton;
public ApplicationWindow() {
this.setTitle("Example");
this.setBounds(100, 100, 200, 200);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(new FlowLayout());
firstButton = new JButton("First");
this.add(firstButton);
firstButton.addActionListener(this);
secondButton = new JButton("Second");
this.add(secondButton);
secondButton.addActionListener(this);
}
}

  1. 接下来,添加以下的actionPerformed方法。创建一个SecondaryLoop对象,并根据所选的按钮创建WorkerThread对象如下:
@Override
public void actionPerformed(ActionEvent e) {
Thread worker;
JButton button = (JButton) e.getSource();
Toolkit toolkit = Toolkit.getDefaultToolkit();
EventQueue eventQueue = toolkit.getSystemEventQueue();
SecondaryLoop secondaryLoop = eventQueue.createSecondaryLoop();
Calendar calendar = Calendar.getInstance();
String name;
if (button == firstButton) {
name = "First-"+calendar.get(Calendar.MILLISECOND);
}
else {
name = "Second-"+calendar.get(Calendar.MILLISECOND);
}
worker = new WorkerThread(secondaryLoop, name);
worker.start();
if (!secondaryLoop.enter()) {
System.out.println("Error with the secondary loop");
}
else {
System.out.println(name + " Secondary loop returned");
}
}

  1. 添加以下的WorkerThread类作为内部类。它的构造函数保存了SecondaryLoop对象,并传递了一条消息。这条消息将被用来帮助我们解释结果。run方法在睡眠两秒之前显示消息:
class WorkerThread extends Thread {
private String message;
private SecondaryLoop secondaryLoop;
public WorkerThread(SecondaryLoop secondaryLoop, String message) {
this.secondaryLoop = secondaryLoop;
this.message = message;
}
@Override
public void run() {
System.out.println(message + " Loop Sleeping ... ");
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(message + " Secondary loop completed with a result of " +
secondaryLoop.exit());
}
}

  1. 执行应用程序。应该会出现以下窗口。这里已经调整了大小:操作步骤...

  2. 接下来,选择First按钮。以下控制台输出应该说明了次级循环的执行。跟在First-后面的数字可能与您的输出不同:

First-433 Loop Sleeping ...

First-433 Secondary loop completed with a result of true

First-433 Secondary loop returned

  1. 虽然次级循环阻塞了当前线程,但并不妨碍窗口继续执行。窗口的 UI 线程仍然活动。为了证明这一点,重新启动应用程序并选择First按钮。在两秒内未过去之前,选择Second按钮。控制台输出应该类似于以下内容:

First-360 Loop Sleeping ...

Second-416 Loop Sleeping ...

First-360 Secondary loop completed with a result of true

Second-416 Secondary loop completed with a result of true

Second-416 Secondary loop returned

First-360 Secondary loop returned

这说明了次级循环的两个方面。第一是应用程序仍然可以与用户交互,第二是同时执行两个次级循环的行为。具体来说,如果在第一个次级循环完成之前启动第二个次级循环,第一个次级循环将不会恢复,直到嵌套的(第二个)循环终止。

请注意,应用程序仍然响应用户输入。另外,请注意Second-416循环在First-360之后开始执行。然而,虽然First-360Second-416之前完成,正如你所期望的那样,First-360循环直到Second-416循环返回后才返回并恢复被阻塞的线程的执行。如果在两秒内两次选择First按钮,将会看到相同的行为。

工作原理...

ApplicationWindow中,我们创建了两个按钮。这些按钮被添加到应用程序中,并与应用程序对ActionListener接口的实现相关联。我们使用First按钮来说明执行次级循环。

actionPerformed方法中,我们使用Toolkit类的getSystemEventQueue方法来获取EventQueue的实例。然后使用createSecondaryLoop方法创建了一个次级循环。

为了跟踪潜在的多个次级循环,我们创建了Calendar类的一个实例,并根据当前毫秒数派生了一个以First-Second-为后缀的唯一名称。虽然这种技术不能保证唯一的名称,但是两个循环具有相同的名称是不太可能的,这对我们的示例来说已经足够了。

根据按下的按钮,使用secondaryLoop对象和一个唯一的名称创建了WorkerThread的实例。然后启动了工作线程,并对secondaryLoop执行了enter方法。

此时,次级循环将执行,当前线程将被阻塞。在WorkerThread类中,显示了执行了哪个次级循环的消息。然后暂停两秒,随后显示了次级循环完成以及exit方法的返回值。

然后actionPerformed方法的线程被解除阻塞,并显示了一条最后的消息,指示次级循环已完成。请注意,此线程在次级循环完成之前被阻塞。

这模仿了从应用程序角度看模态对话框的行为。创建次级循环的线程将被阻塞,直到循环完成。虽然其他线程方法也可以用来实现类似的结果,但这种方法方便且易于使用。

还有更多...

如果一个SecondaryLoop对象已经处于活动状态,则不可能使用相同的SecondaryLoop对象启动一个新的循环。任何尝试这样做都将导致enter方法返回false。然而,一旦循环完成,循环可以被重用于其他循环。这意味着enter方法随后可以针对相同的SecondaryLoop对象执行。

另请参阅

在第七章中查看使用新的 JLayer 装饰器为密码字段的示例,图形用户界面改进。如果需要创建一个可以显示在指示长时间运行进程的按钮上的计时器-沙漏类型动画,这个示例可能会有用。

处理虚假线程唤醒

当使用多个线程时,一个线程可能需要等待一个或多个其他线程完成。在这种情况下,一种方法是使用Object类的wait方法等待其他线程完成。这些其他线程需要使用Object类的notifynotifyAll方法来允许等待的线程继续。

然而,在某些情况下可能会发生虚假唤醒。在 Java 7 中,引入了java.awt.event.InvocationEvent类的isDispatched方法来解决这个问题。

准备工作

避免虚假唤醒:

  1. 添加一个同步块。

  2. 根据特定于应用程序的条件和isDispatched方法创建一个while循环。

  3. 在循环体中使用wait方法。

如何做...

  1. 由于虚假中断的性质,不可能创建一个能够始终展示这种行为的演示应用程序。处理wait的推荐方法如下所示:
synchronized (someObject) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
EventQueue eventQueue = toolkit.getSystemEventQueue();
while(someCondition && !eventQueue.isDispatchThread()) {
try {
wait();
}
catch (InterruptedException e) {
}
}
// Continue processing
}

  1. 这种方法将消除虚假中断。

它是如何工作的...

首先,我们为我们正在处理的对象使用了一个同步块。接下来,我们获取了EventQueue的一个实例。while循环将测试一个特定于应用程序的条件,以确定是否应处于wait状态。这可能只是一个布尔变量,指示队列已准备好被处理。循环将在条件为trueisDispatched方法返回false时继续执行。这意味着如果方法返回true,则事件实际上是从事件队列中分派出来的。这也将发生在EventQueue.invokeAndWait方法中。

线程可能会无缘无故地从wait方法中醒来。可能没有调用notifynotifyAll方法。这可能是由于通常是低级和微妙的 JVM 外部条件引起的。

在早期版本的Java 语言规范中,没有提到这个问题。然而,在 Java 5 中,wait方法的文档中包括了对这个问题的讨论。对这个问题的澄清可以在 Java 语言规范的第三版中找到,第 17.8.1 节等待,位于java.sun.com/docs/books/jls/third_edition/html/memory.html#17.8.1

使用事件处理程序处理小程序初始化状态

JavaScript 代码能够调用小程序方法。但是,在小程序初始化之前是不可能的。任何尝试与小程序通信都将被阻塞,直到小程序加载完成。为了确定小程序何时已加载,Java 7 引入了一个加载状态变量,可以从 JavaScript 代码中访问。我们将探讨如何设置 HTML 文件以检测和响应这些事件。

准备工作

使用小程序的加载状态:

  1. 创建 JavaScript 函数来处理 applet 加载事件。

  2. 部署 applet,将参数java_status_events设置为true

如何做...

  1. 为 Java applet 创建一个新的应用程序。在java.applet.Applet类的init方法中,我们将创建一个Graphics对象来显示一个简单的蓝色矩形,然后延迟两秒。这个延迟将模拟 applet 的加载:
public class SampleApplet extends Applet {
BufferedImage image;
Graphics2D g2d;
public void init() {
int width = getWidth();
int height = getHeight();
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
g2d = image.createGraphics();
g2d.setPaint(Color.BLUE);
g2d.fillRect(0, 0, width, height);
try {
Thread.sleep(2000);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
}
public void paint(Graphics g) {
g.drawImage(image, 0, 0, this);
}
}

  1. 将 applet 打包在SampleApplet.jar文件中。接下来,创建一个 HTML 文件如下。第一部分包括声明一个标题和创建determineAppletState函数来检查 applet 的加载状态如下:
<HTML>
<HEAD>
<TITLE>Checking Applet Status</TITLE>
<SCRIPT>
function determineAppletState() {
if (sampleApplet.status == 1) {
document.getElementById("statediv").innerHTML = "Applet loading ...";
sampleApplet.onLoad = onLoadHandler;
}
else if (sampleApplet.status == 2) {
document.getElementById("statediv").innerHTML = "Applet already loaded";
}
else {
document.getElementById("statediv").innerHTML = "Applet entered error while loading";
}
}
function onLoadHandler() {
document.getElementById("loadeddiv").innerHTML = "Applet has loaded";
}
</SCRIPT>
</HEAD>

  1. 在 HTML 文件的 body 部分之后。它使用onload事件调用determineAppletState函数。然后是一个标题字段和两个分区标签。这些分区将用于显示目的如下:
<BODY onload="determineAppletState()">
<H3>Sample Applet</H3>
<DIV ID="statediv">state</DIV>
<DIV ID="loadeddiv"></DIV>

  1. 使用 JavaScript 序列完成 HTML 文件,配置和执行 applet 如下:
<DIV>
<SCRIPT src="img/deployJava.js"></SCRIPT>
<SCRIPT>
var attributes = {id:'sampleApplet', code:'SampleApplet.class', archive:'SampleApplet.jar', width:200,
height:100};
var parameters = {java_status_events: 'true'};
deployJava.runApplet(attributes, parameters, '7'7);
</SCRIPT>
</DIV>
</BODY>
</HTML>

  1. 将 applet 加载到浏览器中。这里,它加载到 Chrome 中如下:

如何做...

它是如何工作的...

SampleApplet拥有两个方法:initpaintinit方法创建了一个BufferedImage对象,用于显示一个蓝色的正方形,其大小由分配给 applet 的区域确定。最初,使用sleep方法延迟加载两秒,以模拟加载缓慢的 applet。paint方法只是显示图像。当状态为加载时,指定onLoadHandler作为 applet 加载完成时要调用的函数。执行此函数时,在loadeddiv分区元素中显示相应的消息。

在 HTML 文件的 body 标签中,指定了determineAppletState函数作为在 HTML 加载到浏览器时执行的函数。这确保了在加载 HTML 文件时检查加载状态。

将变量和属性与SampleApplet类相关联的sampleApplet ID。还指定了包含类的存档文件和 applet 的大小。为了利用这一功能,applet 需要使用java_status_events参数设置为true进行部署。

determineAppletState函数使用加载状态变量 status 来显示加载过程的状态。在 HTML 分区元素中显示的消息显示了操作的顺序。

deployJava.jsJava 部署工具包的一部分,用于检测 JRE 的存在,如果需要则安装一个,然后运行 applet。它也可以用于其他Web Start程序。在这种情况下,它被用来使用属性和参数以及要使用的 JRE 版本(即 Java 7)来执行 applet。

注意

有关使用deployJava.js执行 Java 应用程序部署的更多信息,请访问download.oracle.com/javase/7/docs/technotes/guides/jweb/index.html.

以下表格详细介绍了三种 applet 状态值:

状态 含义
LOADING 1 applet 正在加载
READY 2 applet 已加载

第九章:数据库、安全和系统增强

在本章中,我们将涵盖以下内容:

  • 使用 RowSetFactory 类

  • Java 7 数据库增强

  • 使用 ExtendedSSLSession 接口

  • 使用平台 MXBeans 监视 JVM 或系统进程负载

  • 重定向操作系统进程的输入和输出

  • 在 HTML 页面中嵌入 JNLP 文件

介绍

本章涵盖了 Java 7 中对数据库、安全和系统类型的增强。其中一些增强较小,将在本介绍中进行讨论。其他一些增强更为重要,将在本章的配方中详细介绍。由于某些主题的专业性相当特殊,比如一些安全增强的特点,它们将被提及但不在此处解释。

Java 7 中对 JDBC 进行了多项增强,现在支持 JDBC 4.1。一些改进取决于早期驱动程序版本中不可用的第三方驱动程序支持。当发生这种情况时,您可能会收到AbstractMethodException。在测试本章的数据库配方时,请确保您使用支持 JDBC 4.1 功能的驱动程序。驱动程序可以在developers.sun.com/product/jdbc/drivers找到。

使用 RowSetFactory配方涉及使用javax.sql.rowset.RowSetFactory接口和javax.sql.rowset.RowSetProvider类,允许根据给定的 JDBC 驱动程序创建任何行集。Java 7 中还包括数据库支持的其他改进。这些在Java 7 数据库增强配方中进行了讨论,包括确定当前模式的名称和提供对隐藏列的访问。Derby 数据库引擎将用于数据库示例。如果您希望使用其他数据库和表,可以通过调整不同数据库的代码来实现。

除了这些数据库配方之外,try-with-resource 语句可以与实现java.sql包的Connection, ResultSetStatement接口的任何对象一起使用。这种语言改进简化了打开和关闭资源的过程。try-with-resource 语句的一般用法在第一章的使用 try-with-resource 块改进异常处理代码配方中进行了详细介绍,Java 语言改进。使用ResultSet-derived类的示例显示在使用 RowSetFactory 类配方中。

Statement接口已增强了两种新方法。第一种方法closeOnCompletion用于指定当使用连接的结果集关闭时,Statement对象将被关闭。第二种方法isCloseOnCompletion返回一个布尔值,指示在满足此条件时语句是否将被关闭。

Java 7 的网络增强包括向java.net.URLClassLoader类添加了两种方法:

还提供了支持使用 InfiniBand(IB)的流连接的帮助。这项技术使用远程直接内存访问(RDMA)直接在不同计算机的内存之间传输数据。这种支持是通过 Sockets Direct Protocol(SDP)网络协议提供的。这项技术的专业性使其无法进一步讨论。

使用平台 MXBeans 监视 JVM 或系统进程负载示例检查了对MXBeans支持的改进。这包括访问这些管理类型 bean 的不同方法。

java.lang.ProcessBuilder类通过ProcessBuilder.Redirect类引入了改进的重定向功能。这个主题在重定向操作系统进程的输入和输出示例中进行了探讨。

Java 7 还改进了 applet 嵌入 HTML 页面的方式。在 HTML 页面中嵌入 JNLP 文件示例演示了这种技术。

Java Secure Socket ExtensionJSSE)用于使用安全套接字层SSL)和传输层安全性TLS)保护互联网通信。JSSE 有助于数据加密、身份验证和维护消息完整性。在 Java 7 中,发生了几项增强。使用 ExtendedSSLSession 接口示例使用 SSL,并用于说明如何使用ExtendedSSLSession接口和新的安全功能。

安全增强包括椭圆曲线加密ECC)算法的整合。这类加密算法更抵抗暴力攻击。提供了算法的便携式实现。

还添加或增强了新的异常类以增强安全性。新的java.security.cert.CertificateRevokedException在抛出时表示X.509证书已被吊销。java.security.cert.CertPathValidatorException类通过添加一个接受CertPathValidatorException.Reason对象的新构造函数进行了增强。此对象实现了CertPathValidatorException.BasicReason枚举,列举了异常的原因。CertPathValidatorException类的getReason方法返回一个CertPathValidatorException.Reason对象。

Java 7 还支持 TLS 1.1 和 1.2 规范,并对此提供了改进支持。Sun JSSE提供程序支持 RFC 4346(tools.ietf.org/html/rfc4346)和 RFC 5246(tools.ietf.org/html/rfc5246)中定义的 TLS 1.1 和 TLS 1.2。这包括支持防范密码块链接攻击和新的加密算法。

此外,还有一些其他与 TKS 相关的增强:

可以使用Sun提供程序的jdk.certpath.disabledAlgorithms属性来禁用弱加密算法。默认情况下,MD2 算法被禁用。此属性在jre/lib/security/java.security文件中指定。默认设置如下所示:

jdk.certpath.disabledAlgorithms=MD2

还可以指定算法,还可以限制密钥大小。

算法限制也可以放置在 TLS 级别。这是通过jre/lib/security/java.security文件中的jdk.tls.disabledAlgorithms安全属性来实现的。示例如下:

jdk.tls.disabledAlgorithms=MD5, SHA1, RSA keySize < 2048

目前,此属性仅适用于Oracle JSSE实现,可能不被其他实现所识别。

服务器名称指示SNI)JSSE 扩展(RFC 4366)使 TLS 客户端能够连接到虚拟服务器,即使用相同支持网络地址的不同网络名称的多个服务器。这在默认情况下设置为true,但可以在不支持该扩展的系统上设置为false

jsse.enableSNIExtension系统属性用于控制此设置。可以使用如下所示的-Djava 命令选项进行设置:

java -D jsse.enableSNIExtension=true ApplicationName

还可以使用如下所示的setProperty方法设置此属性:

System.setProperty("jsse.enableSNIExtension", "true");

请注意,属性名称可能会在将来更改。

使用RowSetFactory

现在可以使用新的javax.sql.rowset包的RowSetFactoryInterface接口和RowSetProvider类来创建行集。这允许创建 JDBC 支持的任何类型的行集。我们将使用 Derby 数据库来说明创建行集的过程。将使用COLLEAGUES表。如何创建此表的说明可在netbeans.org/kb/docs/ide/java-db.html找到。创建表的 SQL 代码如下:

CREATE TABLE COLLEAGUES (
"ID" INTEGER not null primary key,
"FIRSTNAME" VARCHAR(30),
"LASTNAME" VARCHAR(30),
"TITLE" VARCHAR(10),
"DEPARTMENT" VARCHAR(20),
"EMAIL" VARCHAR(60)
);
INSERT INTO COLLEAGUES VALUES (1,'Mike','Johnson','Manager','Engineering','mike.johnson@foo.com');
INSERT INTO COLLEAGUES VALUES
(2, 'James', 'Still', 'Engineer', 'Engineering', 'james.still@foo.com');
INSERT INTO COLLEAGUES VALUES
(3, 'Jerilyn', 'Stall', 'Manager', 'Marketing', 'jerilyn.stall@foo.com');
INSERT INTO COLLEAGUES VALUES
(4, 'Jonathan', 'Smith', 'Manager', 'Marketing', 'jonathan.smith@foo.com');

准备工作

创建新的行集:

  1. 创建RowSetFactory的实例。

  2. 使用几种create方法之一来创建RowSet对象。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,添加以下代码序列。我们将创建一个新的javax.sql.rowset.JdbcRowSet对象,并使用它来显示COLLEAGUES表中的一些字段。首先设置String变量以建立与数据库的连接,并创建RowSetFactory对象如下:
String databaseUrl = "jdbc:derby://localhost:1527/contact";
String username = "userName";
String password = "password";
RowSetFactory rowSetFactory = null;
try {
rowSetFactory = RowSetProvider.newFactory("com.sun.rowset.RowSetFactoryImpl", null);
}
catch (SQLException ex) {
ex.printStackTrace();
return;
}

  1. 接下来,添加一个 try 块来捕获任何SQLExceptions,然后使用createJdbcRowSet方法创建行集。接下来,显示表的选定元素。
try (JdbcRowSet rowSet = rowSetFactory.createJdbcRowSet();) {
rowSet.setUrl(databaseUrl);
rowSet.setUsername(username);
rowSet.setPassword(password);
rowSet.setCommand("SELECT * FROM COLLEAGUES");
rowSet.execute();
while (rowSet.next()) {
System.out.println(rowSet.getInt("ID") + " - "
+ rowSet.getString("FIRSTNAME"));
}
}
catch (SQLException ex) {
ex.printStackTrace();
}

  1. 执行应用程序。输出应如下所示:

1 - Mike

2 - James

3 - Jerilyn

4 - Jonathan

工作原理...

为数据库 URL、用户名和密码创建了字符串变量。使用静态的newFactory方法创建了RowSetFactory对象。任何生成的异常都将导致应用程序终止。

在 try-with-resources 块中,使用createJdbcRowSet方法创建了JdbcRowSet类的实例。然后将 URL、用户名和密码分配给行集。选择命令从COLLEAGUES表中检索所有字段。然后执行查询。

接下来,使用while循环显示了行集的每一行的 ID 和名字。

还有更多...

可能有多个可用的RowSetFactory实现。newFactory方法将按以下顺序查找RowSetFactory类:

  1. 如果定义了系统属性javax.sql.rowset.RowSetFactory中指定的。

  2. 使用ServiceLoader API。

  3. 平台默认实例。

除了创建JdbcRowSet行集之外,还有其他方法可用于创建不同类型的行集,如下表所示:

方法 创建的行集
createCachedRowSet CachedRowSet
createFilteredRowSet FilteredRowSet
createJdbcRowSet JdbcRowSet
createJoinRowSet JoinRowSet
createWebRowSet WebRowSet

还可以使用带有两个参数的重载的newFactory方法创建RowSetFactory,如下所示:

rowSetFactory = RowSetProvider.newFactory("com.sun.rowset.RowSetFactoryImpl", null);

这种方法为应用程序提供了更多的控制,使其能够指定要使用的提供程序。当类路径中有多个提供程序时,这可能很有用。第一个参数指定提供程序的类名,第二个参数指定要使用的类加载器。将null用作第二个参数指定要使用上下文类加载器。

Java 7 数据库增强

Java 7 提供了对数据库支持的许多小的增强。本示例介绍了这些增强,并在实际情况下提供了示例。由于许多 JDBC 4.1 驱动程序的不成熟,不是所有的代码示例都能完全正常运行。

准备工作

大多数示例都是从以下开始:

  1. 创建 Derby 数据库的连接。

  2. 使用连接方法访问所需功能。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,添加以下代码序列。它将建立与数据库的连接,并确定自动生成的键是否总是被返回,以及当前模式是什么:
try {
Connection con = DriverManager.getConnection(
"jdbc:derby://localhost:1527/contact", "userName", "password");
System.out.println("Schema: " + con.getSchema());
System.out.println("Auto Generated Keys: " + metaData.generatedKeyAlwaysReturned());
}
catch (SQLException ex) {
ex.printStackTrace();
}

  1. 执行时,输出应类似于以下内容:

自动生成的键:true

模式:SchemaName

工作原理...

Statement接口的getGeneratedKeys方法是在 Java 1.4 中引入的,用于返回该语句的任何自动生成的键。java.sql.DatabaseMetaData接口的generatedKeyAlwaysReturned方法返回一个布尔值,指示自动生成的键将始终被返回。

可以使用Connection接口的setSchemagetSchema方法来设置和获取连接的模式。执行了getSchema方法,返回了模式名称。

还有更多...

其他三个主题需要进一步讨论:

  • 检索伪列

  • 控制OUT参数的类型值

  • 其他数据库增强

检索伪列

数据库通常会使用隐藏列来表示表的每一行的唯一键。这些隐藏列有时被称为伪列。在 Java 7 中,已添加了两种新方法来处理伪列。DatabaseMetaData接口的getPseudoColumns方法将检索一个ResultSet。该方法要求以下内容:

  • 目录:这需要与数据库中使用的目录名称匹配。如果不使用目录,则使用空字符串。空值表示在搜索列时不使用目录名称。

  • 模式名称:这需要与数据库中使用的模式名称匹配。如果不使用模式,则使用空字符串。空值表示在搜索列时不使用模式名称。

  • 表名称模式:这需要与数据库中使用的表名称匹配

  • 列名称模式:这需要与数据库中使用的列名称匹配

返回的ResultSet将按照以下表格所示的组织结构:

类型 意义
TABLE_CAT 字符串 可能为空的目录名称
TABLE_SCHEM 字符串 可能为空的模式名称
TABLE_NAME 字符串 表的名称
COLUMN_NAME 字符串 列的名称
DATA_TYPE 整数 SQL 类型(java.sql.Types
COLUMN_SIZE 整数 列的大小
DECIMAL_DIGITS 整数 小数位数。空值表示没有小数位数。
NUM_PREC_RADIX 整数 基数
COLUMN_USAGE 字符串 指定列的使用方式,由新的 PsuedoColumnUsage 枚举定义
REMARKS 字符串 关于列的评论
CHAR_OCTET_LENGTH 整数 char 列的最大字符数
IS_NULLABLE 字符串 YES: 列可以包含空值NO: 列不能包含空值"": 未知

隐藏列表示一个唯一键,提供了一种快速访问行的方式。Derby 不支持隐藏列。但是,以下代码序列说明了如何实现这一点:

try {
Connection con = DriverManager.getConnection(
"jdbc:derby://localhost:1527/contact", "userName", "password");
DatabaseMetaData metaData = con.getMetaData();
ResultSet resultSet = metaData.getPseudoColumns("", "schemaName", "tableName", "");
while (rs.next()) {
System.out.println(
resultSet.getString("TABLE_SCHEM ")+" - "+
resultSet.getString("COLUMN_NAME "));
}
}
catch (SQLException ex) {
ex.printStackTrace();
}

Derby 将返回一个空的ResultSet,其中包含先前列出的列。

控制OUT参数的类型值

java.sql.CallableStatement有两个重载的getObject方法,返回一个给定列名或索引的对象。目前支持有限。但是,基本方法如下所示:

try {
Connection conn = DriverManager.getConnection(
"...", "username", "password");
String query = "{CALL GETDATE(?,?)}";
CallableStatement callableStatement = (CallableStatement) conn.prepareCall(query);
callableStatement.setInt(1,recordIdentifier);
callableStatement.registerOutParameter(1, Types.DATE);
callableStatement.executeQuery();
date = callableStatement.getObject(2,Date.class));
}
catch (SQLException ex) {
ex.printStackTrace();
}

查询字符串包含对存储过程的调用。假定该存储过程使用整数值作为第一个参数来标识表中的记录。第二个参数将被返回,并且是Date类型。

一旦查询被执行,getObject方法将使用指定的数据类型返回指定的列。该方法将把 SQL 类型转换为 Java 数据类型。

其他数据库增强

java.sql包的Driver接口有一个新方法,返回驱动程序的父记录器。下面的代码序列说明了这一点:

try {
Driver driver = DriverManager.getDriver("jdbc:derby://localhost:1527");
System.out.println("Parent Logger" + driver.getParentLogger());
}
catch (SQLException ex) {
ex.printStackTrace();
}

但是,当执行时,当前版本的驱动程序将生成以下异常:

Java.sql.SQLFeatureNotSupportedException: Feature not implemented: getParentLogger

Derby 不使用java.util.logging包,因此会抛出此异常。javax.sql.CommonDataSource接口还添加了getParentLogger方法。

此外,当一系列数据库操作与Executor一起执行时,有三种方法可用于支持这些操作,如下所示:

  • abort:此方法将使用传递给方法的Executor中止打开的连接

  • setNetworkTimeout:此方法指定等待响应的超时时间(以毫秒为单位)。它还使用一个Executor对象。

  • getNetworkTimeout:此方法返回连接等待数据库请求的毫秒数

最后两个方法是可选的,Derby 不支持它们。

使用ExtendedSSLSession接口

javax.net.ssl包提供了一系列用于实现安全套接字通信的类。Java 7 中引入的改进包括添加了ExtendedSSLSession接口,该接口可用于确定所使用的特定本地和对等支持的签名算法。此外,创建SSLSession时,可以使用端点识别算法来确保主机计算机的地址与证书的地址匹配。这个算法可以通过SSLParameters类访问。

准备工作

为了演示ExtendedSSLSession接口的使用,我们将:

  1. 创建一个基于SSLServerSocketEchoServer应用程序,以接受来自客户端的消息。

  2. 创建一个客户端应用程序,该应用程序使用SSLSocket实例与服务器通信。

  3. 使用EchoServer应用程序获取ExtendedSSLSession接口的实例。

  4. 使用SimpleConstraints类来演示算法约束的使用。

如何做...

  1. 让我们首先创建一个名为SimpleConstraints的类,该类改编自Java PKI 程序员指南(download.oracle.com/javase/7/docs/technotes/guides/security/certpath/CertPathProgGuide.html)。我们将使用这个类来将算法约束关联到应用程序。将以下类添加到您的项目中:
public class SimpleConstraints implements AlgorithmConstraints {
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, AlgorithmParameters parameters) {
return permits(primitives, algorithm, null, parameters);
}
public boolean permits(Set<CryptoPrimitive> primitives, Key key) {
return permits(primitives, null, key, null);
}
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, Key key, AlgorithmParameters parameters) {
if (algorithm == null) algorithm = key.getAlgorithm();
if (algorithm.indexOf("RSA") == -1) return false;
if (key != null) {
RSAKey rsaKey = (RSAKey)key;
int size = rsaKey.getModulus().bitLength();
if (size < 2048) return false;
}
return true;
}
}

  1. 创建EchoServer应用程序,创建一个新的控制台应用程序。将以下代码添加到main方法中。在这个初始序列中,我们创建并启动服务器:
try {
SSLServerSocketFactory sslServerSocketFactory =
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
SSLServerSocket sslServerSocket =
(SSLServerSocket) sslServerSocketFactory.createServerSocket(9999);
System.out.println("Waiting for a client ...");
SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
}
catch (Exception exception) {
exception.printStackTrace();
}

  1. 接下来,添加以下代码序列以设置应用程序的算法约束。它还返回端点算法的名称:
SSLParameters parameters = sslSocket.getSSLParameters();
parameters.setAlgorithmConstraints (new SimpleConstraints());
String endPoint = parameters.getEndpointIdentificationAlgorithm();
System.out.println("End Point: " + endPoint);

  1. 添加以下代码以显示本地支持的算法:
System.out.println("Local Supported Signature Algorithms");
if (sslSocket.getSession() instanceof ExtendedSSLSession) {
ExtendedSSLSession extendedSSLSession =
(ExtendedSSLSession) sslSocket.getSession();
ExtendedSSLSession interfaceusingString algorithms[] =
extendedSSLSession.getLocalSupportedSignatureAlgorithms();
for (String algorithm : algorithms) {
System.out.println("Algorithm: " + algorithm);
}
}

  1. 以下序列显示了对等支持的算法:
System.out.println("Peer Supported Signature Algorithms");
if (sslSocket.getSession() instanceof ExtendedSSLSession) {
String algorithms[] = ((ExtendedSSLSession) sslSocket.getSession()).getPeerSupportedSignatureAlgorithms();
for (String algorithm : algorithms) {
System.out.println("Algorithm: " + algorithm);
}
}

  1. 添加以下代码来缓冲来自客户端应用程序的输入流:
InputStream inputstream = sslSocket.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader (inputstreamreader);

  1. 通过添加代码显示来自客户端的输入来完成该方法:
String stringline = null;
while ((stringline = bufferedreader.readLine()) != null) {
System.out.println(string);
System.out.flush();
}

  1. 要执行服务器,我们需要创建密钥库。这可以通过在命令提示符中执行以下命令来完成:
keytool -genkey -keystore mySrvKeystore -keyalg RSA

  1. 提供程序请求的密码和其他信息。接下来,转到回声服务器的位置并输入以下命令:
java -Djavax.net.ssl.keyStore=mySrvKeystore
Djavax.net.ssl.keyStorePassword=password package.EchoServer

  1. 上面的密码是您用来创建密钥库的密码,而package是您的 EchoServer 的包(如果有的话)。当程序执行时,您会得到以下输出:

等待客户端...

  1. 现在我们需要创建一个名为EchoClient的客户端控制台应用程序。在main方法中,添加以下代码,我们创建与服务器的连接,然后将键盘输入发送到服务器:
try {
SSLSocketFactory sslSocketFactory =
(SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslSocket = (SSLSocket)
sslSocketFactory.createSocket("localhost", 9999);
InputStreamReader inputStreamReader =
new InputStreamReader(System.in);
BufferedReader bufferedReader =
new BufferedReader(inputStreamReader);
OutputStream outputStream = sslSocket.getOutputStream();
OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(outputStream);
BufferedWriter bufferedwriter =
new BufferedWriter(outputStreamWriter);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
ExtendedSSLSession interfaceusingbufferedwriter.write(line + '\n');
bufferedwriter.flush();
}
}
catch (Exception exception) {
exception.printStackTrace();
}

  1. 将密钥库文件复制到客户端应用程序的目录中。在单独的命令窗口中,执行以下命令:
java -Djavax.net.ssl.trustStore=mySrvKeystore
-Djavax.net.ssl.trustStorePassword=password package.EchoClient

  1. 上面的密码是您用来创建密钥库的密码,而package是您的 EchoServer 的包(如果有的话)。程序执行时,输入单词cat,然后按Enter键。在服务器命令窗口中,您应该看到一个终点名称,可能为空,一个本地支持的签名算法列表,以及类似以下内容的cat

终点:null

本地支持的签名算法

算法:SHA512withECDSA

算法:SHA512withRSA

算法:SHA384withECDSA

算法:SHA384withRSA

算法:SHA256withECDSA

算法:SHA256withRSA

算法:SHA224withECDSA

算法:SHA224withRSA

算法:SHA1withECDSA

算法:SHA1withRSA

算法:SHA1withDSA

算法:MD5withRSA

对等支持的签名算法

cat

  1. 当您输入更多的输入行时,它们应该在服务器命令窗口中反映出来。要终止程序,在客户端命令窗口中输入Ctrl + C

它是如何工作的...

SimpleConstraints类只允许 RSA 算法,然后使用 2048 位或更多的密钥。这被用作setAlgorithmConstraints方法的参数。该类实现了java.security.AlgorithmConstraints接口,表示算法的限制。

首先创建一个SSLServerSocketFactory实例,然后创建一个SSLServerSocket。对套接字执行accept方法,该方法会阻塞,直到客户端连接到它。

接下来设置了SimpleConstraints,然后使用了getEndpointIdentificationAlgorithm方法,返回了一个空字符串。在这个例子中,没有使用终点识别算法。

列出了本地和对等支持的签名算法。剩下的代码涉及读取并显示客户端发送的字符串。

EchoClient应用程序更简单。它创建了SSLSocket类的一个实例,然后使用它的getOutputStream方法将用户的输入写入回显服务器。

使用平台 MXBeans 进行 JVM 或系统进程负载监控

Java 管理扩展JMX)是一种向应用程序添加管理接口的标准方式。托管 beanMBean)为应用程序提供管理服务,并向javax.management.MBeanServer注册,该服务器保存和管理 MBean。javax.management.MXBean是一种 MBean 类型,允许客户端访问 bean 而无需访问特定类。

java.lang.management包的ManagementFactory类添加了几种新方法来访问 MBean。然后可以用这些方法来访问进程和负载监控。

准备就绪

访问MXBean

  1. 使用getPlatformMXBean方法和应用程序所需的MXBean类型。

  2. 根据需要使用MXBean方法。

如何做...

  1. 创建一个新的控制台应用程序。使用以下main方法。在这个应用程序中,我们将获取运行时环境的MXBean并显示关于它的基本信息:
public static void main(String[] args) {
RuntimeMXBean mxBean = ManagementFactory.getPlatformMXBean(RuntimeMXBean.class);
System.out.println("JVM Name: " + mxBean.getName());
System.out.println("JVM Specification Name: " + mxBean.getSpecName());
System.out.println("JVM Specification Version: " + mxBean.getSpecVersion());
System.out.println("JVM Implementation Name: " + mxBean.getVmName());
System.out.println("JVM Implementation Vendor: " + mxBean.getVmVendor());
System.out.println("JVM Implementation Version: " + mxBean.getVmVersion());
}

  1. 执行应用程序。您的输出应该类似于以下内容:

JVM 名称:5584@name-PC

JVM 规范名称:Java 虚拟机规范

JVM 规范版本:1.7

JVM 实现名称:Java HotSpot(TM) 64 位服务器 VM

JVM 实现供应商:Oracle Corporation

JVM 实现版本:21.0-b17

它是如何工作的...

我们使用了ManagementFactory类的静态getPlatformMXBean方法,参数为RuntimeMXBean.class。这返回了一个RuntimeMXBean的实例。然后应用了该实例的特定方法,并显示了它们的值。

还有更多...

ManagementFactory在 Java 7 中引入了几种新方法:

  • getPlatformMXBean: 这是一个重载的方法,它返回一个支持特定管理接口的PlatformManagedObject派生对象,使用Class参数

  • getPlatformMXBeans: 这是一个重载的方法,它返回一个支持特定管理接口的PlatformManagedObject派生对象,使用MBeanServerConnection对象和一个Class参数

  • getPlatformManagementInterfaces: 该方法返回当前 Java 平台上的PlatformManagedObject派生对象的Class对象集

此外,java.lang.management包中添加了一个新的接口。PlatformManagedObject接口用作所有MXBeans的基本接口。

使用getPlatformMXBeans方法

getPlatformMXBeans方法传递MXBean类型并返回实现MXBean类型的平台MXBeans列表。在下面的示例中,我们获取了OperatingSystemMXBean的列表。然后显示了MXBean的几个属性:

List<OperatingSystemMXBean> list =
ManagementFactory.getPlatformMXBeans(OperatingSystemMXBean.class);
for (OperatingSystemMXBean bean : list) {
System.out.println("Operating System Name: " + bean.getName());
System.out.println("Operating System Architecture: " + bean.getArch());
System.out.println("Operating System Version: " + bean.getVersion());
}

执行时,您应该获得类似以下的输出。确切的输出取决于用于执行应用程序的操作系统和硬件:

操作系统名称:Windows 7

操作系统架构:amd64

操作系统版本:6.1

获取平台的管理接口

ManagementFactory类的静态getPlatformManagementInterfaces方法返回表示平台支持的MXBeansClass对象集。然而,在运行 JDK 7.01 版本时,该方法在 Windows 7 和 Ubuntu 平台上都生成了ClassCastException。未来的版本应该纠正这个问题。

作为 JDK 的一部分提供的jconsole应用程序,提供了一种确定可用的MXBeans的替代技术。以下是控制台显示操作系统属性,特别是ProcessCpuLoad属性:

获取平台的管理接口

重定向操作系统进程的输入和输出

java.lang.ProcessBuilder类有几个有用于重定向从 Java 应用程序执行的外部进程的输入和输出的新方法。嵌套的ProcessBuilder.Redirect类已被引入以提供这些额外的重定向功能。为了演示这个过程,我们将从文本文件向 DOS 提示符发送命令行参数,并将输出记录在另一个文本文件中。

准备就绪

为了控制外部进程的输入和输出,您必须:

  1. 创建一个新的ProcessBuilder对象。

  2. 将进程的输入和输出定向到适当的位置。

  3. 通过start方法执行进程。

操作步骤…

  1. 首先,创建一个新的控制台应用程序。创建三个新的文件实例来表示我们的进程执行涉及的三个文件:输入,输出和错误,如下所示:
File commands = new File("C:/Projects/ProcessCommands.txt");
File output = new File("C:/Projects/ProcessLog.txt");
File errors = new File("C:/Projects/ErrorLog.txt");

  1. 使用指定文件的路径创建文件ProcessCommands.txt并输入以下文本:

*cd C:*

dir

mkdir "Test Directory"

dir

  1. 确保在最后一行之后有一个回车。

  2. 接下来,创建一个ProcessBuilder的新实例,将字符串"cmd"传递给构造函数,以指定我们要启动的外部进程,即操作系统命令窗口。调用redirectInput, redirectOutputredirectError方法,不带参数,并打印出默认位置:

ProcessBuilder pb = new ProcessBuilder("cmd");
System.out.println(pb.redirectInput());
System.out.println(pb.redirectOutput());
System.out.println(pb.redirectError());

  1. 然后,我们想调用前面方法的重载形式,将各自的文件传递给每个方法。再次调用每个方法的无参数形式,使用toString方法来验证 IO 源是否已更改:
pb.redirectInput(commands);
pb.redirectError(errors);
pb.redirectOutput(output);
System.out.println(pb.redirectInput());
System.out.println(pb.redirectOutput());
System.out.println(pb.redirectError());

  1. 最后,调用start方法来执行进程,如下所示:
pb.start();

  1. 运行应用程序。您应该看到类似以下的输出:

PIPE

PIPE

PIPE

重定向以从文件"C:\Projects\ProcessCommands.txt"读取

重定向以写入文件"C:\Projects\ProcessLog.txt"

重定向以写入文件"C:\Projects\ErrorLog.txt"

  1. 检查每个文本文件。您的输出文件应该有类似于以下文本:

Microsoft Windows [版本 6.7601]

版权所有(c)2009 年微软公司。保留所有权利。

C:\Users\Jenn\Documents\NetBeansProjects\ProcessBuilderExample>cd C:\

C:>dir

驱动器 C 中没有标签的卷。

卷序列号为 927A-1F77

C:\的目录

03/05/2011 10:56

戴尔

11/08/2011 16:04

其他

11/08/2011 11:08

移动

10/31/2011 10:57

音乐

11/08/2011 19:44

项目

10/27/2011 21:09

临时

10/28/2011 10:46

用户

11/08/2011 17:11

窗户

0 个文件 0 字节

34 个目录 620,819,542,016 字节可用

在 C:>中创建"测试目录"

C:>dir

驱动器 C 中没有标签的卷。

卷序列号为 927A-1F77

C:\的目录

03/05/2011 10:56

戴尔

11/08/2011 16:04

其他

11/08/2011 11:08

移动

10/31/2011 10:57

音乐

11/08/2011 19:44

项目

10/27/2011 21:09

临时

10/28/2011 10:46

测试目录

10/28/2011 10:46

用户

11/08/2011 17:11

窗户

  1. 再次执行程序并检查您的错误日志的内容。因为您的测试目录已经在第一次进程执行时创建,所以现在应该看到以下错误消息:

子目录或文件"测试目录"已经存在。

它是如何工作的...

我们创建了三个文件来处理我们进程的输入和输出。当我们创建ProcessBuilder对象的实例时,我们指定要启动的应用程序为命令窗口。在应用程序中执行操作所需的信息存储在我们的输入文件中。

当我们首次调用redirectInput, redirectOutputredirectError方法时,我们没有传递任何参数。这些方法都返回一个ProcessBuilder.Redirect对象,我们打印了它。这个对象代表默认的 IO 源,在所有三种情况下都是Redirect.PIPEProcessBuilder.Redirect.Type枚举值之一。管道将一个源的输出发送到另一个源。

我们使用的方法的第二种形式涉及将java.io.File实例传递给redirectInput, redirectOutputredirectError方法。这些方法也返回一个ProcessBuilder对象,但它们还具有设置 IO 源的功能。在我们的示例中,我们再次调用了每种方法的无参数形式,以验证 IO 是否已被重定向。

程序第一次执行时,您的错误日志应该是空的,假设您为每个File对象使用了有效的文件路径,并且您在计算机上有写权限。第二次执行旨在显示如何将错误捕获定向到单独的文件。如果未调用redirectError方法,错误将继承标准位置,并将显示在 IDE 的输出窗口中。有关继承标准 IO 位置的信息,请参阅还有更多..部分。

重要的是要注意,必须在重定向方法之后调用start方法。在重定向输入或输出之前启动进程将导致进程忽略您的重定向,并且应用程序将使用标准 IO 位置执行。

还有更多...

在本节中,我们将研究ProcessBuilder.Redirect类和inheritIO方法的使用。

使用 ProcessBuilder.Redirect 类

ProcessBuilder.Redirect类提供了另一种指定 IO 数据重定向的方法。使用前面的示例,在调用start方法之前添加一行:

pb.redirectError(Redirect.appendTo(errors));

这种redirectError方法的形式允许你指定错误应该追加到错误日志文本文件中,而不是覆盖。如果你使用这个改变来执行应用程序,当进程再次尝试创建Test Directory目录时,你会看到错误的两个实例:

子目录或文件 Test Directory 已经存在

子目录或文件 Test Directory 已经存在

这是使用redirectError方法的重载形式的一个例子,传递了一个ProcessBuilder.Redirect对象而不是一个文件。所有三种方法,redirectError, redirectInputredirectOutput,都有这种重载形式。

ProcessBuilder.Redirect类有两个特殊值,即Redirect.PIPERedirect.INHERIT。Redirect.PIPE是处理外部进程 IO 的默认方式,简单地意味着 Java 进程将通过管道连接到外部进程。Redirect.INHERIT值意味着外部进程将具有与当前 Java 进程相同的输入或输出位置。你也可以使用Redirect.toRedirect.from方法重定向数据的输入或输出。

使用 inheritIO 方法继承默认的 IO 位置

如果你从 Java 应用程序执行外部进程,你可以设置源和目标数据的位置与当前 Java 进程的位置相同。ProcessBuilder类的inheritIO方法是实现这一点的一种便捷方式。如果你有一个ProcessBuilder对象pb,执行以下代码:

pb.inheritIO()

然后它具有执行以下三个语句的相同效果:

pb.redirectInput(Redirect.INHERIT)
pb.redirectOutput(Redirect.INHERIT)
pb.redirectError(Redirect.INHERIT)

在这两种情况下,输入、输出和错误数据将位于与当前 Java 进程的输入、输出和错误数据相同的位置。

在 HTML 页面中嵌入 JNLP 文件

Java 7 提供了一个新选项,可以加快在网页中部署小程序的速度。在 7 之前,当使用Java 网络启动协议JNLP)启动小程序时,必须先从网络下载 JNLP 文件,然后才能启动小程序。有了新版本,JNLP 文件可以直接嵌入到 HTML 代码中,减少了小程序启动所需的时间。在这个例子中,我们将构建一个基本的小程序,并使用一个嵌入了 JNLP 的 HTML 页面来启动它。

准备工作

为了加快 Java 7 中小程序的启动速度,你必须:

  1. 创建一个新的小程序。

  2. 创建并编码一个 JNLP 文件。

  3. 将 JNLP 文件的引用添加到 HTML 页面。

如何做...

  1. 首先创建一个小程序,用于在 HTML 窗口中使用。以下是一个简单的小程序,可以用于本教程的目的。这个小程序有两个输入字段,subtotaltaxRate,还有一个calculate按钮用于计算总额:
public class JNLPAppletExample extends Applet {
TextField subtotal = new TextField(10);
TextField taxRate = new TextField(10);
Button calculate = new Button("Calculate");
TextArea grandTot = new TextArea("Total = $", 2, 15, TextArea.SCROLLBARS_NONE);
@Override
public void init() {
this.setLayout(new GridLayout(3,2));
this.add(new Label("Subtotal = "));
this.add(subtotal);
this.add(new Label("Tax Rate = "));
this.add(taxRate);
this.add(calculate);
grandTot.setEditable(false);
this.add(grandTot);
calculate.addActionListener(new CalcListener());
}
class CalcListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
JNLP fileembedding, in HTML pagedouble subTot;
double tax;
double grandTot;
subTot = validateSubTot(subtotal.getText());
tax = validateSubTot(taxRate.getText());
grandTot = calculateTotal(subTot, tax);
JNLPAppletExample.this.grandTot.setText("Total = $" + grandTot);
}
}
double validateSubTot(String s) {
double answer;
Double d;
try {
d = new Double(s);
answer = d.doubleValue();
}
catch (NumberFormatException e) {
answer = Double.NaN;
}
return answer;
}
double calculateTotal(double subTot, double taxRate) {
double grandTotal;
taxRate = taxRate / 100;
grandTotal = (subTot * taxRate) + subTot;
return grandTotal;
}
}

  1. 接下来,创建一个名为JNLPExample.jnlp的 JNLP 文件。以下是一个示例 JNLP 文件,用于配合我们之前的小程序。请注意,在资源标签中引用了一个 JAR 文件。这个 JAR 文件,包含你的小程序,必须与你的 JNLP 文件和 HTML 文件在同一个位置,我们马上就会创建:
<?xml version="1.0" encoding="UTF-8"?>
<jnlp href="http://JNLPExample.jnlp">
<information>
<title>Embedded JNLP File</title>
<vendor>Sample Vendor</vendor>
</information>
<resources>
<j2se version="7" />
<jar href="http://JNLPAppletExample.jar"
main="true" />
</resources>
<applet-desc
name="Embedded JNLP Example"
main-class="packt.JNLPAppletExample"
width="500"
height="500">
</applet-desc>
<update check="background"/>
</jnlp>

  1. 创建 JNLP 文件后,必须对其进行编码。有几种在线资源可用于将 JNLP 文件转换为 BASE64,但本例中使用的是base64encode.org/。使用 UTF-8 字符集。一旦你有了编码的数据,你将在创建 HTML 文件时使用它。创建一个如下所示的 HTML 文件。请注意,高亮显示的 BASE64 编码字符串已经为简洁起见而缩短,但你的字符串会更长:
<HTML>
<HEAD>
<TITLE>Embedded JNLP File Example</TITLE>
</HEAD>
<BODY>
<H3>Embedded JNLP Applet</H3>
<script src="img/deployJava.js"></script>
<script>
var jnlpFile = "http://JNLPExample.jnlp";
deployJava.createWebStartLaunchButtonEx(jnlpFile);
</script>
<script>
var attributes = {} ;
var parameters = {jnlp_href: 'JNLPExample.jnlp',
jnlp_embedded: 'PD94bWw...'};
deployJava.runApplet(attributes, parameters, '7');
</script>
</BODY>
</HTML>

  1. 另外,请注意第一个脚本标签。为了避免使用codebase属性,我们利用了 Java 7 的另一个新特性,使用了一个开发工具包脚本。

  2. 在浏览器窗口中加载你的应用程序。根据你当前的浏览器设置,你可能需要启用 JavaScript。你的小程序应该快速加载,并且看起来类似于以下的截图:

How to do it...

它是如何工作的...

将 JNLP 文件嵌入 HTML 页面中允许 applet 立即加载,而不必首先从服务器下载。JNLP 文件在href属性中必须有相对路径,而且不应该指定codebase。通过将codebase属性留空,可以由 applet 网页的 URL 确定。

resources标签指定了 JAR 文件的位置和要使用的 Java 版本。JAR 文件的路径被假定为默认工作目录,JNLP 文件的位置也是如此。JNLP 文件中还包括了 applet 的描述,被applet-desc标签包围。在这个标签中指定了 applet 的名称和主类文件的名称。

HTML 文件包含了加载 applet 所需的信息,而不必从服务器下载 applet 信息。我们首先指定要使用 JavaScript 调用加载应用程序。然后,在我们的第一个 script 标签中,我们添加了一个部分,允许我们在没有codebase的情况下调用 applet。这是有利的,因为应用程序可以在不同的环境中加载和测试,而不必更改codebase属性。相反,它是从应用程序所在的网页继承而来。

部署工具包有两个函数可以在没有codebase属性的情况下在网页中部署 Java applet:launchWebStartApplicationcreateWebStartLaunchButtonEx。我们选择在这个示例中使用createWebStartLaunchButtonEx,但launchWebStartApplication选项也会在下文中讨论。在这两种情况下,客户端必须具有 Java SE 7 版本才能启动 applet,如果没有,他们将被引导到 Java 网站下载最新版本。

createWebStartLaunchButtonEx函数创建了一个应用程序的启动按钮。在script标签中,jnlpFile变量指定了 JNLP 文件的名称,并且是相对于 applet 网页的。然后将此文件名传递给deployJava.createWebStartLaunchButtonEx函数。

或者,launchWebStartApplication函数可以嵌入到 HTML 链接中。该函数在href标签中被调用,如下所示:

<script src="img/deployJava.js"></script>
<a href="javascript:deployJava.launchWebStartApplication('JNLPExample.jnlp');">Launch</a>
</script>

HTML 文件中的第二个script标签包含了有关 JNLP 文件的信息。jnlp_href变量存储了 JNLP 文件的名称。JNLP 文件的编码形式由jnlp_embedded参数指定。BASE64 编码器对需要在文本媒介中存储和传输数据的二进制数据进行编码,比如电子邮件和 XML 文件。

第十章:并发处理

在本章中,我们将涵盖以下内容:

  • 在 Java 7 中使用 join/fork 框架

  • 使用可重用的同步障碍 Phaser

  • 在多个线程中安全地使用 ConcurrentLinkedDeque 类

  • 使用 LinkedTransferQueue 类

  • 使用 ThreadLocalRandom 类支持多个线程

介绍

Java 7 中改进了并发应用程序的支持。引入了几个新类,支持任务的并行执行。ForkJoinPool类用于使用分而治之技术解决问题的应用程序。每个子问题都被分叉(分割)为一个单独的线程,然后在必要时合并以提供解决方案。该类使用的线程通常是java.util.concurrent.ForkJoinTask类的子类,是轻量级线程。在 Java 中使用 join/fork 框架示例中说明了这种方法的使用。

此外,引入了java.util.concurrent.Phaser类,以支持一系列阶段中线程集合的执行。一组线程被同步,以便它们都执行然后等待其他线程的完成。一旦它们都完成了,它们可以重新执行第二阶段或后续阶段。使用可重用的同步障碍 Phaser示例说明了在游戏引擎设置中使用此类的情况。

使用 java.util.concurrent.ConcurrentLinkedDeque 类安全地与多个线程一起使用使用 java.util.concurrent.LinkedTransferQueue 类示例介绍了两个设计用于安全地与多个线程一起工作的新类。展示了它们在支持生产者/消费者框架的使用示例。

java.util.concurrent.ThreadLocalRandom类是新的,并提供更好地支持在多个线程之间使用的随机数生成。在使用 ThreadLocalRandom 类支持多个线程示例中进行了讨论。

java.util.ConcurrentModificationException类中添加了两个新的构造函数。它们都接受一个Throwable对象,用于指定异常的原因。其中一个构造函数还接受一个提供有关异常的详细信息的字符串。

Java 7 通过修改锁定机制改进了类加载器的使用,以避免死锁。在 Java 7 之前的多线程自定义类加载器中,某些自定义类加载器在使用循环委托模型时容易发生死锁。

考虑以下情景。Thread1 尝试使用 ClassLoader1(锁定 ClassLoader1)加载 class1。然后将加载 class2 的委托给 ClassLoader2。与此同时,Thread2 使用 ClassLoader2(锁定 ClassLoader2)加载 class3,然后将加载 class4 的委托给 ClassLoader1。由于两个类加载器都被锁定,而两个线程都需要这两个加载器,因此发生死锁情况。

并发类加载器的期望行为是从同一实例的类加载器并发加载不同的类。这需要以更细粒度的级别进行锁定,例如通过正在加载的类的名称锁定类加载器。

同步不应该在类加载器级别进行。相反,应该在类级别上进行锁定,其中类加载器只允许由该类加载器一次加载一个类的单个实例。

一些类加载器能够并发加载类。这种类型的类加载器称为并行可用的类加载器。它们在初始化过程中需要使用registerAsParallelCapable方法进行注册。

如果自定义类加载器使用无环层次委托模型,则在 Java 中不需要进行任何更改。在层次委托模型中,首先委托给其父类加载器。不使用层次委托模型的类加载器应该在 Java 中构造为并行可用的类加载器。

为自定义类加载器避免死锁:

  • 在类初始化序列中使用registerAsParallelCapable方法。这表示类加载器的所有实例都是多线程安全的。

  • 确保类加载器代码是多线程安全的。这包括:

  • 使用内部锁定方案,例如java.lang.ClassLoader使用的类名锁定方案

  • 删除类加载器锁上的任何同步

  • 确保关键部分是多线程安全的

  • 建议类加载器覆盖findClass(String)方法

  • 如果defineClass方法被覆盖,则确保每个类名只调用一次

有关此问题的更多详细信息,请访问openjdk.java.net/groups/core-libs/ClassLoaderProposal.html

在 Java 中使用 join/fork 框架

join/fork框架是一种支持将问题分解为更小的部分,以并行方式解决它们,然后将结果合并的方法。新的java.util.concurrent.ForkJoinPool类支持这种方法。它旨在与多核系统一起工作,理想情况下有数十个或数百个处理器。目前,很少有桌面平台支持这种并发性,但未来的机器将会支持。少于四个处理器时,性能改进将很小。

ForkJoinPool类源自java.util.concurrent.AbstractExecutorService,使其成为ExecutorService。它旨在与ForkJoinTasks一起工作,尽管它也可以与普通线程一起使用。ForkJoinPool类与其他执行程序不同,其线程尝试查找并执行其他当前运行任务创建的子任务。这称为工作窃取

ForkJoinPool类可用于计算子问题上的计算要么被修改,要么返回一个值。当返回一个值时,使用java.util.concurrent.RecursiveTask派生类。否则,使用java.util.concurrent.RecursiveAction类。在本教程中,我们将说明使用RecursiveTask派生类的用法。

准备工作

要为返回每个子任务结果的任务使用分支/合并框架:

  1. 创建一个实现所需计算的RecursiveTask的子类。

  2. 创建ForkJoinPool类的实例。

  3. 使用ForkJoinPool类的invoke方法与RecursiveTask类的子类的实例。

如何做...

该应用程序并非旨在以最有效的方式实现,而是用于说明分支/合并任务。因此,在处理器数量较少的系统上,可能几乎没有性能改进。

  1. 创建一个新的控制台应用程序。我们将使用一个派生自RecursiveTask的静态内部类来计算numbers数组中整数的平方和。首先,声明numbers数组如下:
private static int numbers[] = new int[100000];

  1. 如下添加SumOfSquaresTask类。它创建数组元素的子范围,并使用迭代循环计算它们的平方和,或者根据阈值大小将数组分成更小的部分:
private static class SumOfSquaresTask extends RecursiveTask<Long> {
private final int thresholdTHRESHOLD = 1000;
private int from;
private int to;
public SumOfSquaresTask(int from, int to) {
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
long sum = 0L;
int mid = (to + from) >>> 1;
if ((to - from) < thresholdTHRESHOLD) {
for (int i = from; i < to; i++) {
sum += numbers[i] * numbers[i];
}
return sum;
}
else {
List<RecursiveTask<Long>> forks = new ArrayList<>();
SumOfSquaresTask task1 =
new SumOfSquaresTask(from, mid);
SumOfSquaresTask task2 =
new SumOfSquaresTask(mid, to);
forks.add(task1);
task1.fork();
forks.add(task2);
task2.fork();
for (RecursiveTask<Long> task : forks) {
sum += task.join();
}
return sum;
}
}
}

  1. 添加以下main方法。为了比较,使用 for 循环计算平方和,然后使用ForkJoinPool类。执行时间如下计算并显示:
public static void main(String[] args) {
for (int i = 0; i < numbers.length; i++) {
numbers[i] = i;
}
long startTime;
long stopTime;
long sum = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < numbers.length; i++) {
sum += numbers[i] * numbers[i];
}
System.out.println("Sum of squares: " + sum);
stopTime = System.currentTimeMillis();
System.out.println("Iterative solution time: " + (stopTime - startTime));
ForkJoinPool forkJoinPool = new ForkJoinPool();
startTime = System.currentTimeMillis();
long result = forkJoinPool.invoke(new SumOfSquaresTask(0, numbers.length));
System.out.println("forkJoinPool: " + forkJoinPool.toString());
stopTime = System.currentTimeMillis();
System.out.println("Sum of squares: " + result);
System.out.println("Fork/join solution time: " + (stopTime - startTime));
}

  1. 执行应用程序。您的输出应该类似于以下内容。但是,根据您的硬件配置,您应该观察到不同的执行时间:

平方和:18103503627376

迭代解决方案时间:5

平方和:18103503627376

分支/合并解决方案时间:23

请注意,迭代解决方案比使用分支/合并策略的解决方案更快。如前所述,除非有大量处理器,否则这种方法并不总是更有效。

重复运行应用程序将导致不同的结果。更积极的测试方法是在可能不同的处理器负载条件下重复执行解决方案,然后取结果的平均值。阈值的大小也会影响其性能。

工作原理...

numbers数组声明为一个包含 100,000 个元素的整数数组。SumOfSquaresTask类是从RecursiveTask类派生的,使用了泛型类型Long。设置了阈值为 1000。任何小于此阈值的子数组都使用迭代解决。否则,该段被分成两半,并创建了两个新任务,每个任务处理一半。

ArrayList用于保存两个子任务。这严格来说是不需要的,实际上会减慢计算速度。但是,如果我们决定将数组分成两个以上的段,这将是有用的。它提供了一个方便的方法,在子任务加入时重新组合元素。

fork方法用于拆分子任务。它们进入线程池,最终将被执行。join方法在子任务完成时返回结果。然后将子任务的总和相加并返回。

main方法中,第一个代码段使用for循环计算了平方的和。开始和结束时间基于以毫秒为单位测量的当前时间。第二段创建了ForkJoinPool类的一个实例,然后使用其invoke方法与SumOfSquaresTask对象的新实例。传递给SumOfSquaresTask构造函数的参数指示它从数组的第一个元素开始,直到最后一个元素。完成后,显示执行时间。

还有更多...

ForkJoinPool类有几种报告池状态的方法,包括:

  • getPoolSize:该方法返回已启动但尚未完成的线程数

  • getRunningThreadCount:该方法返回未被阻塞但正在等待加入其他任务的线程数的估计值

  • getActiveThreadCount:该方法返回执行任务的线程数的估计值

ForkJoinPool类的toString方法返回池的几个方面。在invoke方法执行后立即添加以下语句:

out.println("forkJoinPool: " + forkJoinPool);

当程序执行时,将获得类似以下的输出:

forkJoinPool: java.util.concurrent.ForkJoinPool@18fb53f6[Running, parallelism = 4, size = 55, active = 0, running = 0, steals = 171, tasks = 0, submissions = 0]

另请参阅

使用可重用同步障碍Phaser的方法提供了执行多个线程的不同方法。

使用可重用的同步障碍Phaser

java.util.concurrent.Phaser类涉及协调一起工作的线程在循环类型阶段中的同步。线程将执行,然后等待组中其他线程的完成。当所有线程都完成时,一个阶段就完成了。然后可以使用Phaser来协调再次执行相同一组线程。

java.util.concurrent.CountdownLatch类提供了一种方法来做到这一点,但需要固定数量的线程,并且默认情况下只执行一次。java.util.concurrent.CyclicBarrier,它是在 Java 5 中引入的,也使用了固定数量的线程,但是可重用。但是,不可能进入下一个阶段。当问题以一系列基于某些标准的步骤/阶段进行推进时,这是有用的。

随着 Java 7 中Phaser类的引入,我们现在有了一个结合了CountDownLatchCyclicBarrier功能并支持动态线程数量的并发抽象。术语“phase”指的是线程可以协调执行不同阶段或步骤的想法。所有线程将执行,然后等待其他线程完成。一旦它们完成,它们将重新开始并完成第二个或后续阶段的操作。

屏障是一种阻止任务继续进行的类型的块,直到满足某些条件。一个常见的条件是当所有相关线程都已完成时。

Phaser类提供了几个功能,使其非常有用:

  • 可以动态地向线程池中添加和删除参与者

  • 每个阶段都有一个唯一的阶段号。

  • Phaser可以被终止,导致任何等待的线程立即返回

  • 发生的异常不会影响屏障的状态

register方法增加了参与的方数量。当内部计数达到零或根据其他条件确定时,屏障终止。

准备好了

我们将开发一个模拟游戏引擎操作的应用程序。第一个版本将创建一系列代表游戏中参与者的任务。我们将使用Phaser类来协调它们的交互。

使用Phaser类来同步一组任务的开始:

  1. 创建一个将参与PhaserRunnable对象集合。

  2. 创建Phaser类的一个实例。

  3. 对于每个参与者:

  • 注册参与者

  • 使用参与者的Runnable对象创建一个新线程

  • 使用arriveAndAwaitAdvance方法等待其他任务的创建

  • 执行线程

  1. 使用Phaser对象的arriveAndDeregister来启动参与者的执行。

如何做...

  1. 创建一个名为GamePhaserExample的新控制台应用程序类。我们将创建一系列内部类的简单层次结构,这些类代表游戏中的参与者。将Entity类添加为基本抽象类,定义如下。虽然不是绝对必要的,但我们将使用继承来简化这些类型应用程序的开发:
private static abstract class Entity implements Runnable {
public abstract void run();
}

  1. 接下来,我们将创建两个派生类:PlayerZombie。这些类实现run方法和toString方法。run方法使用sleep方法来模拟执行的工作。预期地,僵尸比人类慢:
private static class Player extends Entity {
private final static AtomicInteger idSource = new AtomicInteger();
private final int id = idSource.incrementAndGet();
public void run() {
System.out.println(toString() + " started");
try {
Thread.currentThread().sleep(
ThreadLocalRandom.current().nextInt(200, 600));
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(toString() + " stopped");
}
@Override
public String toString() {
return "Player #" + id;
}
}
private static class Zombie extends Entity {
private final static AtomicInteger idSource = new AtomicInteger();
private final int id = idSource.incrementAndGet();
public void run() {
System.out.println(toString() + " started");
try {
Thread.currentThread().sleep(
ThreadLocalRandom.current().nextInt(400, 800));
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(toString() + " stopped");
}
@Override
public String toString() {
return "Zombie #" + id;
}
}

  1. 为了使示例更清晰,将以下main方法添加到GamePhaserExample类中:
public static void main(String[] args) {
new GamePhaserExample().execute();
}

  1. 接下来,添加以下execute方法,我们在其中创建参与者列表,然后调用gameEngine方法:
private void execute() {
List<Entity> entities = new ArrayList<>();
entities = new ArrayList<>();
entities.add(new Player());
entities.add(new Zombie());
entities.add(new Zombie());
entities.add(new Zombie());
gameEngine(entities);
}

  1. 接下来是gameEngine方法。for each循环为每个参与者创建一个线程:
private void gameEngine(List<Entity> entities) {
final Phaser phaser = new Phaser(1);
for (final Entity entity : entities) {
synchronization barrier Phaserusingfinal String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread() {
@Override
public void run() {
System.out.println(member +
" waiting for the remaining participants");
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
System.out.println(member + " starting run");
entity.run();
}
}.start();
}
phaser.arriveAndDeregister(); //Deregister and continue
System.out.println("Phaser continuing");
}

  1. 执行应用程序。输出是不确定的,但应该类似于以下内容:

玩家#1 加入游戏

僵尸#1 加入游戏

僵尸#2 加入游戏

玩家#1 等待剩余参与者

僵尸#1 等待剩余参与者

僵尸#3 加入游戏

Phaser 继续

僵尸#3 等待剩余参与者

僵尸#2 等待剩余参与者

僵尸#1 开始奔跑

僵尸#1 开始

僵尸#3 开始奔跑

僵尸#3 开始

僵尸#2 开始奔跑

僵尸#2 开始

玩家#1 开始奔跑

玩家#1 开始

玩家#1 停止

僵尸#1 停止

僵尸#3 停止

僵尸#2 停止

注意Phaser对象会等待直到所有参与者都加入游戏。

它是如何工作的...

sleep 方法用于模拟实体所涉及的工作。请注意 ThreadLocalRandom 类的使用。其 nextInt 方法返回其参数中指定的值之间的随机数。在使用并发线程时,这是生成随机数的首选方式,如使用 ThreadLocalRandom 类支持多个线程配方中所述。

AtomicInteger 类的一个实例用于为每个创建的对象分配唯一的 ID。这是在线程中生成数字的安全方式。toString 方法返回实体的简单字符串表示形式。

execute 方法中,我们创建了一个 ArrayList 来保存参与者。请注意在创建 ArrayList 时使用了菱形操作符。这是 Java 7 语言改进,在第一章的使用菱形操作符进行构造类型推断配方中有解释,Java 语言改进。添加了一个玩家和三个僵尸。僵尸似乎总是比人类多。然后调用了 gameEngine 方法。

使用参数为一的 Phaser 对象创建了一个代表第一个参与者的对象。它不是一个实体,只是作为帮助控制阶段器的机制。

在每个循环中,使用 register 方法将阶段器中的方的数量增加一。使用匿名内部类创建了一个新线程。在其 run 方法中,直到所有参与者到达之前,实体才会开始。arriveAndAwaitAdvance 方法导致通知参与者已到达,并且该方法在所有参与者到达并且阶段完成之前不返回。

while循环的每次迭代开始时,注册参与者的数量比已到达的参与者数量多一个。register 方法将内部计数增加一。然后内部计数比已到达的数量多两个。当执行 arriveAndAwaitAdvance 方法时,现在等待的参与者数量将比已注册的多一个。

循环结束后,仍然有一个比已到达的参与者多的注册方。但是,当执行 arriveAndDeregister 方法时,已到达的参与者数量的内部计数与参与者数量匹配,并且线程开始。此外,注册方的数量减少了一个。当所有线程终止时,应用程序终止。

还有更多...

可以使用 bulkRegister 方法注册一组方。此方法接受一个整数参数,指定要注册的方的数量。

在某些情况下,可能希望强制终止阶段器。forceTermination 方法用于此目的。

在执行阶段器时,有几种方法可以返回有关阶段器状态的信息,如下表所述。如果阶段器已终止,则这些方法将不起作用:

方法 描述
getRoot 返回根阶段器。与阶段器树一起使用
getParent 返回阶段器的父级
getPhase 返回当前阶段编号
getArrivedParties 已到达当前阶段的方的数量
getRegisteredParties 注册方的数量
getUnarrivedParties 尚未到达当前阶段的方的数量

可以构建阶段器树,其中阶段器作为任务的分支创建。在这种情况下,getRoot 方法非常有用。阶段器构造在www.cs.rice.edu/~vs3/PDF/SPSS08-phasers.pdf中讨论。

使用阶段器重复一系列任务

我们还可以使用Phaser类来支持一系列阶段,其中执行任务,执行可能的中间操作,然后再次重复一系列任务。

为了支持这种行为,我们将修改gameEngine方法。修改将包括:

  • 添加一个iterations变量

  • 覆盖Phaser类的onAdvance方法

  • 在每个任务的run方法中使用while循环,由isTerminated方法控制

添加一个名为iterations的变量,并将其初始化为3。这用于指定我们将使用多少个阶段。还要重写如下所示的onAdvance方法:

final int iterations = 3;
final Phaser phaser = new Phaser(1) {
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("Phase number " + phase + " completed\n")
return phase >= iterations-1 || registeredParties == 0;
}
};

每个阶段都有唯一的编号,从零开始。调用onAdvance传递当前阶段编号和注册到 phaser 的当前参与方数量。当注册方数量变为零时,此方法的默认实现返回true。这将导致 phaser 被终止。

该方法的实现导致仅当阶段编号超过iterations值(即减 1)或没有使用 phaser 的注册方时,该方法才返回true

根据以下代码中突出显示的内容修改run方法:

for (final Entity entity : entities) {
final String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread() {
@Override
public void run() {
do {
System.out.println(member + " starting run");
entity.run();
System.out.println(member +
" waiting for the remaining participants during phase " +
phaser.getPhase());
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
}
while (!phaser.isTerminated());
}
}.start();
}

实体被允许先运行,然后等待其他参与者完成和到达。只要通过isTerminated方法确定的 phaser 尚未终止,当每个人准备好时,下一阶段将被执行。

最后一步是使用arriveAndAwaitAdvance方法将 phaser 推进到下一个阶段。同样,只要 phaser 尚未终止,当每个参与者到达时,phaser 将推进到下一个阶段。使用以下代码序列来完成此操作:

while (!phaser.isTerminated()) {
phaser.arriveAndAwaitAdvance();
}
System.out.println("Phaser continuing");

仅使用一个玩家和一个僵尸执行程序。这将减少输出量,并且应与以下内容类似:

玩家#1 加入游戏

僵尸#1 加入游戏

玩家#1 开始运行

玩家#1 开始

僵尸#1 开始运行

僵尸#1 开始

玩家#1 停止

玩家#1 在第 0 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 0 阶段等待剩余参与者

第 0 阶段完成

玩家#1 开始运行

玩家#1 开始

僵尸#1 开始运行

僵尸#1 开始

玩家#1 停止

玩家#1 在第 1 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 1 阶段等待剩余参与者

第 1 阶段完成

僵尸#1 开始运行

玩家#1 开始运行

僵尸#1 开始

玩家#1 开始

玩家#1 停止

玩家#1 在第 2 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 2 阶段等待剩余参与者

第 2 阶段完成

Phaser 继续

另请参阅

有关为多个线程生成随机数的更多信息,请参阅使用当前线程隔离的随机数生成器

安全地使用新的ConcurrentLinkedDeque与多个线程

java.util.concurrent.ConcurrentLinkedDeque类是 Java 集合框架的成员,它允许多个线程安全地同时访问相同的数据集合。该类实现了一个双端队列,称为deque,并允许从 deque 的两端插入和删除元素。它也被称为头尾链接列表,并且与其他并发集合一样,不允许使用空元素。

在本示例中,我们将演示ConcurrentLinkedDeque类的基本实现,并说明一些最常用方法的使用。

准备好了

在生产者/消费者框架中使用ConcurrentLinkedDeque

  1. 创建ConcurrentLinkedDeque的实例。

  2. 定义要放入双端队列的元素。

  3. 实现一个生产者线程来生成要放入双端队列中的元素。

  4. 实现一个消费者线程来从双端队列中删除元素。

如何做...

  1. 创建一个新的控制台应用程序。使用Item的泛型类型声明一个私有静态实例的ConcurrentLinkedDequeItem类被声明为内部类。包括获取方法和构造函数,如下面的代码所示,使用两个属性descriptionitemId
private static ConcurrentLinkedDeque<Item> deque = new ConcurrentLinkedDeque<>();
static class Item {
privateublic final String description;
privateublic final int itemId;
public Item() {
"this(Default Item";, 0)
}
public Item(String description, int itemId) {
this.description = description;
this.itemId = itemId;
}
}

  1. 然后创建一个生产者类来生成Item类型的元素。为了这个示例的目的,我们只会生成七个项目,然后打印出一个声明来证明该项目已添加到双端队列中。我们使用ConcurrentLinkedDeque类的add方法来添加元素。每次添加后,线程会短暂休眠:
static class ItemProducer implements Runnable {
@Override
public void run() {
String itemName = "";
int itemId = 0;
try {
for (int x = 1; x < 8; x++) {
itemName = "Item" + x;
itemId = x;
deque.add(new Item(itemName, itemId));
System.out.println("New Item Added:" + itemName + " " + itemId);
Thread.currentThread().sleep(250);
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 接下来,创建一个消费者类。为了确保在消费者线程尝试访问它之前,双端队列中将有元素,我们让线程在检索元素之前睡眠一秒钟。然后我们使用pollFirst方法来检索双端队列中的第一个元素。如果元素不为空,那么我们将元素传递给generateOrder方法。在这个方法中,我们打印有关该项目的信息:
static class ItemConsumer implements Runnable {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
Item item;
while ((item = deque.pollFirst()) != null) {
{
generateOrder(item);
}
}
private void generateOrder(Item item) {
System.out.println("Part Order");
System.out.println("Item description: " + item.getDescriptiond());
System.out.println("Item ID # " + item.getItemIdi());
System.out.println();
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 最后,在我们的main方法中,启动两个线程:
public static void main(String[] args) {
new Thread(new ItemProducer());.start()
new Thread(new ItemConsumer());.start()
}

  1. 当您执行程序时,您应该看到类似以下的输出:

新项目已添加:Item1 1

新项目已添加:Item2 2

新项目已添加:Item3 3

新项目已添加:Item4 4

零件订单

项目描述:Item1

项目 ID#1

新项目已添加:Item5 5

新项目已添加:Item6 6

新项目已添加:Item7 7

零件订单

项目描述:Item2

项目 ID#2

零件订单

项目描述:Item3

项目 ID#3

零件订单

项目描述:Item4

项目 ID#4

零件订单

项目描述:Item5

项目 ID#5

零件订单

项目描述:Item6

项目 ID#6

零件订单para

项目描述:Item7

项目 ID#7

它是如何工作的...

当我们启动两个线程时,我们让生产者线程提前一点时间来填充我们的双端队列。一秒钟后,消费者线程开始检索元素。使用ConcurrentLinkedDeque类允许两个线程同时安全地访问双端队列的元素。

在我们的示例中,我们使用了addpollFirst方法来添加和删除双端队列的元素。有许多可用的方法,其中许多方法基本上以相同的方式运行。还有更多...部分提供了有关访问双端队列元素的各种选项的更多详细信息。

还有更多...

我们将涵盖几个主题,包括:

  • 异步并发线程存在问题

  • 向双端队列添加元素

  • 从双端队列中检索元素

  • 访问双端队列的特定元素

异步并发线程存在问题

由于多个线程可能在任何给定时刻访问集合,因此size方法并不总是会返回准确的结果。当使用iteratordescendingIterator方法时,情况也是如此。此外,任何批量数据操作,例如addAllremoveAll,也不总是会达到预期的结果。如果一个线程正在访问集合中的一个项目,而另一个线程尝试拉取所有项目,则批量操作不能保证以原子方式运行。

有两种toArray方法可用于检索双端队列的所有元素并将它们存储在数组中。第一个返回表示双端队列所有元素的对象数组,并且可以转换为适当的数据类型。当双端队列的元素是不同的数据类型时,这是有用的。以下是如何使用toArray方法的第一种形式的示例,使用我们之前的线程示例:

Item[] items = (Item[]) deque.toArray();

另一个toArray方法需要一个特定数据类型的初始化数组作为参数,并返回该数据类型的元素数组。

Item[] items = deque.toArray(new Item[0]);

向双端队列添加元素

以下表格列出了一些可用于向双端队列中添加元素的方法。在下表中分组在一起的方法本质上执行相同的功能。这种类似方法的多样性是ConcurrentLinkedDeque类实现略有不同接口的结果:

方法名 添加元素到
add(Element e)``offer(Element e)``offerLast(Element e)``addLast(Element e) 双端队列的末尾
addFirst(Element e)``offerFirst(Element e)``push(Element e) 双端队列的前端

从双端队列中检索元素

以下是一些用于从双端队列中检索元素的方法:

方法名 错误操作 功能
element() 如果双端队列为空则抛出异常 检索但不移除双端队列的第一个元素
getFirst()
getLast()
peek() 如果双端队列为空则返回 null
peekFirst()
peekLast()
pop() 如果双端队列为空则抛出异常 检索并移除双端队列的第一个元素
removeFirst()
poll() 如果双端队列为空则返回 null
pollFirst()
removeLast() 如果双端队列为空则抛出异常 检索并移除双端队列的最后一个元素
pollLast() 如果双端队列为空则返回 null

访问双端队列的特定元素

以下是一些用于访问双端队列特定元素的方法:

方法名 功能 注释
contains(Element e) 如果双端队列包含至少一个等于Element e的元素则返回true
remove(Element e)``removeFirstOccurrence(Element e) 移除双端队列中第一个等于Element e的元素 如果元素在双端队列中不存在,则双端队列保持不变。如果e为 null 则抛出异常
removeLastOccurrence(Element e) 移除双端队列中最后一个等于Element e的元素

使用新的 LinkedTransferQueue 类

java.util.concurrent.LinkedTransferQueue类实现了java.util.concurrent.TransferQueue接口,是一个无界队列,遵循先进先出模型。该类提供了用于检索元素的阻塞方法和非阻塞方法,并且适合于多个线程的并发访问。在本示例中,我们将创建一个LinkedTransferQueue的简单实现,并探索该类中的一些可用方法。

准备工作

要在生产者/消费者框架中使用LinkedTransferQueue

  1. 创建一个LinkedTransferQueue的实例。

  2. 定义要放入队列的元素类型。

  3. 实现一个生产者线程来生成要放入队列的元素。

  4. 实现一个消费者线程来从队列中移除元素。

如何做...

  1. 创建一个新的控制台应用程序。使用Item的泛型类型声明一个LinkedTransferQueue的私有静态实例。然后创建内部类Item,并包括如下代码所示的 get 方法和构造函数,使用descriptionitemId这两个属性:
private static LinkedTransferQueue<Item>
linkTransQ = new LinkedTransferQueue<>();
static class Item {
public final String description;
public final int itemId;
public Item() {
this("Default Item", 0) ;
}
public Item(String description, int itemId) {
this.description = description;
this.itemId = itemId;
}
}

  1. 接下来,创建一个生产者类来生成Item类型的元素。为了本示例的目的,我们只会生成七个项目,然后打印一条语句来演示该项目已被添加到队列中。我们将使用LinkedTransferQueue类的offer方法来添加元素。在每次添加后,线程会短暂休眠,然后我们打印出添加的项目的名称。然后我们使用hasWaitingConsumer方法来确定是否有任何消费者线程正在等待可用的项目:
static class ItemProducer implements Runnable {
@Override
public void run() {
try {
for (int x = 1; x < 8; x++) {
String itemName = "Item" + x;
int itemId = x;
linkTransQ.offer(new Item(itemName, itemId));
System.out.println("New Item Added:" + itemName + " " + itemId);
Thread.currentThread().sleep(250);
if (linkTransQ.hasWaitingConsumer()) {
System.out.println("Hurry up!");
}
}
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

  1. 接下来,创建一个消费者类。为了演示hasWaitingConsumer方法的功能,我们让线程在检索元素之前睡眠一秒钟,以确保一开始没有等待的消费者。然后,在while循环内,我们使用take方法来移除列表中的第一个项目。我们选择了take方法,因为它是一个阻塞方法,会等待直到队列有可用的元素。一旦消费者线程能够取出一个元素,我们将元素传递给generateOrder方法,该方法打印有关项目的信息:
static class ItemConsumer implements Runnable {
@Override
public void run() {
try {
Thread.currentThread().sleep(1000);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
while (true) {
try {
generateOrder(linkTransQ.take());
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
private void generateOrder(Item item) {
System.out.println();
System.out.println("Part Order");
System.out.println("Item description: " + item.description());
System.out.println("Item ID # " + item.itemId());
}
}

  1. 最后,在我们的main方法中,我们启动了两个线程:
public static void main(String[] args) {
new Thread(new ItemProducer()).start();
new Thread(new ItemConsumer()).start();
}

  1. 当您执行程序时,您应该看到类似以下的输出:

新添加的项目:Item1 1

新添加的项目:Item2 2

新添加的项目:Item3 3

新添加的项目:Item4 4

零件订单

项目描述:Item1

项目编号#1

零件订单

项目描述:Item2

项目编号#2

零件订单

项目描述:Item3

项目编号#3

零件订单

项目描述:Item4

项目编号#4

快点!

新添加的项目:Item5 5

零件订单

项目描述:Item5

项目编号#5

快点!

零件订单

项目描述:Item6

项目编号#6

新添加的项目:Item6 6

快点!

零件订单

项目描述:Item7

项目编号#7

新添加的项目:Item7 7

快点!

它是如何工作的...

当我们启动了两个线程时,我们让生产者线程有一个领先,通过在ItemConsumer类中睡眠一秒钟来填充我们的队列。请注意,hasWaitingConsumer方法最初返回false,因为消费者线程尚未执行take方法。一秒钟后,消费者线程开始检索元素。在每次检索时,generateOrder方法打印有关检索到的元素的信息。在检索队列中的所有元素之后,请注意最后的快点!语句,表示仍有消费者在等待。在这个例子中,因为消费者在while循环中使用了一个阻塞方法,线程永远不会终止。在现实生活中,线程应该以更优雅的方式终止,比如向消费者线程发送终止消息。

在我们的例子中,我们使用了offertake方法来添加和移除队列的元素。还有其他可用的方法,这些方法在还有更多..部分中讨论。

还有更多...

在这里,我们将讨论以下内容:

  • 异步并发线程的问题

  • 向队列添加元素

  • 从双端队列中检索元素

异步并发线程的问题

由于多个线程可能在任何给定时刻访问集合,因此size方法不总是会返回准确的结果。此外,任何批量数据操作,如addAllremoveAll,也不总能达到期望的结果。如果一个线程正在访问集合中的一个项目,另一个线程尝试拉取所有项目,则不保证批量操作会以原子方式运行。

向队列添加元素

以下是一些可用于向队列添加元素的方法:

方法名称 添加元素到 评论
add(Element e) 队列末尾 队列是无界的,因此该方法永远不会返回false或抛出异常
offer(Element e) 队列是无界的,因此该方法永远不会返回false
put(Element e) 队列是无界的,因此该方法永远不会阻塞
offer(Element``e, Long t,TimeUnit u) 队列末尾等待 t 个时间单位的类型 u 然后放弃 队列是无界的,因此该方法将始终返回true

从双端队列中检索元素

以下是一些可用于从双端队列中检索元素的方法:

方法名称 功能 评论
peek() 检索队列的第一个元素,但不移除 如果队列为空,则返回 null
poll() 移除队列的第一个元素 如果队列为空,则返回 null
poll(Long t, TimeUnit u) 从队列前面移除元素,在时间 t(以单位 u 计)之前放弃 如果时间限制在元素可用之前到期,则返回 null
remove(Object e) 从队列中移除等于Object e的元素 如果找到并移除元素,则返回true
take() 移除队列的第一个元素 如果在阻塞时被中断,则抛出异常
transfer(Element e) 将元素传输给消费者线程,必要时等待 将元素插入队列末尾,并等待消费者线程检索它
tryTransfer(Element e) 立即将元素传输给消费者 如果消费者不可用,则返回false
tryTransfer(Element e, Time t, TimeUnit u) 立即将元素传输给消费者,或在 t(以单位 u 计)指定的时间内 如果消费者在时间限制到期时不可用,则返回false

使用 ThreadLocalRandom 类支持多个线程

java.util.concurrent包中有一个新的类ThreadLocalRandom,它支持类似于Random类的功能。然而,使用这个新类与多个线程将导致较少的争用和更好的性能,与Random类相比。当多个线程需要使用随机数时,应该使用ThreadLocalRandom类。随机数生成器是局部的。本食谱将介绍如何使用这个类。

准备就绪

使用这个类的推荐方法是:

  1. 使用静态的current方法返回ThreadLocalRandom类的一个实例。

  2. 使用该对象的方法。

如何做...

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法中:
System.out.println("Five random integers");
for(int i = 0; i<5; i++) {
System.out.println(ThreadLocalRandom.current(). nextInt());
}
System.out.println();
System.out.println("Random double number between 0.0 and 35.0");
System.out.println(ThreadLocalRandom.current().nextDouble(35.0));
System.out.println();
System.out.println("Five random Long numbers between 1234567 and 7654321");
for(int i = 0; i<5; i++) {
System.out.println(
ThreadLocalRandom.current().nextLong(1234567L, 7654321L));
}

  1. 执行程序。您的输出应该类似于以下内容:

五个随机整数

0

4232237

178803790

758674372

1565954732

0.0 和 35.0 之间的随机双精度数

3.196571144914888

1234567 和 7654321 之间的五个随机长整数

7525440

2545475

1320305

1240628

1728476

它是如何工作的...

nextInt方法被执行了五次,其返回值被显示出来。注意该方法最初返回 0。ThreadLocalRandom类扩展了Random类。然而,不支持setSeed方法。如果尝试使用它,将抛出UnsupportedOperationException

然后执行了nextDouble方法。这个重载方法返回了一个介于 0.0 和 35.0 之间的数字。使用两个参数执行了五次nextLong方法,指定了其起始(包括)和结束(不包括)的范围值。

还有更多...

该类的方法返回均匀分布的数字。以下表总结了它的方法:

提示

当指定范围时,起始值是包含的,结束值是不包含的。

方法 参数 返回
current 线程的当前实例
next 代表返回值位数的整数值 位数范围内的整数
nextDouble doubledouble, double 0.0 和其参数之间的双精度数 0.0 和其参数之间的双精度数
nextInt int, int 其参数之间的整数
nextLong longlong, long 0 和其参数之间的长整数 0 和其参数之间的长整数
setSeed long 抛出 UnsupportedOperationException

另请参阅

使用可重用同步障碍 Phaser食谱中找到了它的用法示例。

第十一章:杂项

在本章中,我们将涵盖以下内容:

  • 在 Java 7 中处理周

  • 在 Java 7 中使用货币

  • 使用 NumericShaper.Range 枚举支持数字显示

  • Java 7 中的 JavaBean 改进

  • 在 Java 7 中处理区域设置和 Locale.Builder 类

  • 处理空引用

  • 在 Java 7 中使用新的 BitSet 方法

介绍

本章将介绍 Java 7 中许多不适合前几章的新内容。其中许多增强功能具有潜在的广泛应用,例如在在 Java 7 中处理区域设置和 Locale.Builder 类中讨论的java.lang.Objects类和java.util.Locale类的改进。其他更专业,例如对java.util.BitSet类的改进,这在在 Java 7 中使用新的 BitSet 方法中有所涉及。

在处理周和货币方面进行了许多改进。当前周数和每年的周数计算受区域设置的影响。此外,现在可以确定平台上可用的货币。这些问题在在 Java 7 中处理周在 Java 7 中使用货币中有所说明。

添加了一个新的枚举,以便在不同语言中显示数字。讨论了使用java.awt.font.NumericShaper类来支持此工作的使用 NumericShaper.Range 枚举支持数字显示配方。在 JavaBeans 的支持方面也有改进,这在Java 7 中的 JavaBean 改进配方中有所讨论。

还有许多增强功能,不值得单独列为配方。本介绍的其余部分都致力于这些主题。

Unicode 6.0

Unicode 6.0是 Unicode 标准的最新修订版。Java 7 通过添加数千个更多的字符和许多新方法来支持此版本。此外,正则表达式模式匹配使用\u\x转义序列支持 Unicode 6.0。

Character.UnicodeBlock类中添加了许多新的字符块。Java 7 中添加了Character.UnicodeScript枚举,用于表示Unicode 标准附录#24:脚本名称中定义的字符脚本。

注意

有关 Unicode 标准附录#24:脚本名称的更多信息,请访问download.oracle.com/javase/7/docs/api/index.html

Character类中添加了几种方法,以支持 Unicode 操作。以下是它们在字符串朝鲜圆上的使用示例,这是基于区域设置的朝鲜圆的中文显示名称,以及在中国大陆使用的简化脚本。将以下代码序列添加到新应用程序中:

int codePoint = Character.codePointAt("朝鲜圆", 0);
System.out.println("isBmpCodePoint: " + Character.isBmpCodePoint(codePoint));
System.out.println("isSurrogate: " + Character.isSurrogate('朝'));
System.out.println("highSurrogate: " + (int)Character.highSurrogate(codePoint));
System.out.println("lowSurrogate: " + (int)Character.lowSurrogate(codePoint));
System.out.println("isAlphabetic: " + Character.isAlphabetic(codePoint));
System.out.println("isIdeographic: " + Character.isIdeographic(codePoint));
System.out.println("getName: " + Character.getName(codePoint));

执行时,您的输出应如下所示:

isBmpCodePoint: true

isSurrogate: false

highSurrogate: 55257

lowSurrogate: 57117

isAlphabetic: true

isIdeographic: true

getName: CJK UNIFIED IDEOGRAPHS 671D

由于字符不是 Unicode 代理代码,因此highSurrogatelowSurrogate方法的结果是无用的。

注意

有关 Unicode 6.0 的更多信息,请访问www.unicode.org/versions/Unicode6.0.0/

原始类型和比较方法

Java 7 引入了用于比较原始数据类型Boolean, byte, long, shortint的新静态方法。每个包装类现在都有一个compare方法,它接受两个数据类型的实例作为参数,并返回表示比较结果的整数。例如,您以前需要使用compareTo方法来比较两个布尔变量 x 和 y,如下所示:

Boolean.valueOf(x).compareTo(Boolean.valueOf(y))

现在可以使用compare方法如下:

Boolean.compare(x,y);

虽然这对于布尔数据类型是 Java 的新功能,但compare方法以前已经适用于doublefloat。此外,在 7 中,parse, valueofdecode方法用于将字符串转换为数值,将接受Byte, Short, Integer, LongBigInteger的前导加号(+)标记,以及Float, DoubleBigDecimal,这些类型以前接受该标记。

全局记录器

java.util.logging.Logger类有一个新方法getGlobal,用于检索名为GLOBAL_LOGGER_NAME的全局记录器对象。Logger类的静态字段globalLogger类与LogManager类一起使用时容易发生死锁,因为两个类都会等待对方完成初始化。getGlobal方法是访问全局记录器对象的首选方式,以防止这种死锁。

JavaDocs 改进

从结构上讲,JavaDocs 在 Java 7 中有了重大改进。现在,通过使用HTMLTree类来创建文档树来生成 HTML 页面,从而实现了更准确的 HTML 生成和更少的无效页面。

JavaDocs 的外部变化也有一些,其中一些是为了符合新的第五百零八部分可访问性指南。这些指南旨在确保屏幕阅读器能够准确地将 HTML 页面翻译成可听的输出。主要结果是在表格上添加了更多的标题和标题。JavaDocs 现在还使用 CSS 样式表来简化页面外观的更改。

JVM 性能增强

Java HotSpotTM 虚拟机的性能已经得到了改进。这些改进大多数不在开发人员的控制范围之内,而且具有专业性质。感兴趣的读者可以在docs.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html找到有关这些增强的更多详细信息。

在 Java 7 中处理周

一些应用程序关心一年中的周数和本年的当前周数。众所周知,一年有 52 周,但 52 周乘以每周 7 天等于每年 364 天,而不是实际的 365 天。周数用于指代一年中的周。但是如何计算呢?Java 7 引入了几种方法来支持确定一年中的周。在本教程中,我们将检查这些方法,并看看如何计算与周相关的值。ISO 8601标准提供了表示日期和时间的方法。java.util.GregorianCalendar类支持此标准,除了以下部分中描述的内容。

准备工作

使用这些基于周的方法,我们需要:

  1. 创建Calendar类的实例。

  2. 根据需要使用其方法。

如何做...

某些抽象java.util.Calendar类的实现不支持周计算。要确定Calendar实现是否支持周计算,我们需要执行isWeekDateSupported方法。如果提供支持,则返回true。要返回当前日历年的周数,请使用getWeeksInWeekYear方法。要确定当前日期的周,请使用get方法,并将WEEK_OF_YEAR作为其参数。

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法:
Calendar calendar = Calendar.getInstance();
if(calendar.isWeekDateSupported()) {
System.out.println("Number of weeks in this year: " + calendar.getWeeksInWeekYear());
System.out.println("Current week number: " + calendar.get(Calendar.WEEK_OF_YEAR));
}

  1. 执行应用程序。您的输出应如下所示,但值将取决于应用程序执行的日期:

今年的周数:53

当前周数:48

工作原理...

创建了Calendar类的一个实例。这通常是GregorianCalendar类的一个实例。if语句由isWeekDateSupported方法控制。它返回true,导致执行getWeeksInWeekYearget方法。get方法传入了字段WEEK_OF_YEAR,返回当前的周数。

还有更多...

可以使用setWeekDate方法设置日期。此方法有三个参数,指定年、周和日。它提供了一种根据周设置日期的便捷技术。以下是通过将年份设置为 2012 年,将周设置为该年的第 16 周,将日期设置为该周的第三天来说明此过程:

calendar.setWeekDate(2012, 16, 3);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));

执行此代码时,我们得到以下输出:

2012 年 4 月 17 日下午 12:00:08 CDT

一年中第一周和最后一周的计算方式取决于区域设置。GregorianCalendar类的WEEK_OF_YEAR字段范围从 1 到 53,其中 53 代表闰周。一年中的第一周是:

  • 最早的七天周期

  • 从一周的第一天开始(getFirstDayOfWeek

  • 其中至少包含一周的最小天数(getMinimalDaysInFirstWeek

getFirstDayOfWeekgetMinimalDaysInFirstWeek方法是与区域设置相关的。例如,getFirstDayOfWeek方法返回一个整数,表示该区域设置的一周的第一天。在美国,它是星期日,但在法国是星期一。

一年中的第一周和最后一周可能有不同的日历年。考虑以下代码序列。日历设置为 2022 年第一周的第一天:

calendar.setWeekDate(2022, 1, 1);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));

执行时,我们得到以下输出:

2021 年 12 月 26 日下午 12:15:39 CST

这表明这周实际上是从上一年开始的。

此外,TimeZoneSimpleTimeZone类有一个observesDaylightTime方法,如果时区遵守夏令时,则返回true。以下代码序列创建了一个SimpleTimeZone类的实例,然后确定是否支持夏令时。使用的时区是中央标准时间CST):

SimpleTimeZone simpleTimeZone = new SimpleTimeZone(
-21600000,
"CST",
Calendar.MARCH, 1, -Calendar.SUNDAY,
7200000,
Calendar.NOVEMBER, -1, Calendar.SUNDAY,
7200000,
3600000);
System.out.println(simpleTimeZone.getDisplayName() + " - " +
simpleTimeZone.observesDaylightTime());

执行此序列时,您应该获得以下输出:

中央标准时间-真

在 Java 7 中使用 Currency 类

java.util.Currency类引入了四种检索有关可用货币及其属性的信息的新方法。本示例说明了以下方法的使用:

  • getAvailableCurrencies:此方法返回一组可用的货币

  • getNumericCode:此方法返回货币的 ISO 4217 数字代码

  • getDisplayName:此重载方法返回表示货币显示名称的字符串。一个方法传递了一个Locale对象。返回的字符串是特定于该区域设置的。

准备就绪

getAvailableCurrencies方法是静态的,因此应该针对类名执行。其他方法针对Currency类的实例执行。

如何做...

  1. 创建一个新的控制台应用程序。将以下代码添加到main方法中:
Set<Currency> currencies = Currency.getAvailableCurrencies();
for (Currency currency : currencies) {
System.out.printf("%s - %s - %s\n", currency.getDisplayName(),
currency.getDisplayName(Locale.GERMAN),
currency.getNumericCode());
}

  1. 执行应用程序时,您应该获得类似以下内容的输出。但是,每个的第一部分可能会有所不同,这取决于当前的区域设置。

朝鲜元 - 朝鲜元 - 408

欧元 - 欧元 - 978

荷兰盾 - 荷兰盾 - 528

福克兰群岛镑 - 福克兰-镑 - 238

丹麦克朗 - 丹麦克朗 - 208

伯利兹元 - 伯利兹元 - 84

它是如何工作的...

代码序列从生成代表当前系统配置的Currency对象的Set开始。对每个集合元素执行了重载的getDisplayName方法。使用了Locale.GERMAN参数来说明此方法的使用。显示的最后一个值是货币的数字代码。

使用 NumericShaper.Range 枚举来支持数字的显示

在本示例中,我们将演示使用java.awt.font.NumericShaper.Range枚举来支持使用java.awt.font.NumericShaper类显示数字。有时希望使用不同于当前使用的语言显示数字。例如,在关于蒙古语的英语教程中,我们可能希望用英语解释数字系统,但使用蒙古数字显示数字。NumericShaper类提供了这种支持。新的NumericShaper.Range枚举简化了这种支持。

准备工作

使用NumericShaper.Range枚举来显示数字:

  1. 创建一个HashMap来保存显示属性信息。

  2. 创建一个Font对象来定义要使用的字体。

  3. 指定要显示文本的 Unicode 字符范围。

  4. 创建一个FontRenderContext对象来保存有关如何测量要显示的文本的信息。

  5. 创建一个TextLayout的实例,并在paintComponent方法中使用它来渲染文本。

操作步骤...

我们将演示使用NumericShaper.Range枚举来显示蒙古数字。这是在download.oracle.com/javase/tutorial/i18n/text/shapedDigits.html中找到的示例的简化版本。

  1. 创建一个扩展JFrame类的应用程序,如下所示。我们将在NumericShaperPanel类中演示NumericShaper类的使用:
public class NumericShaperExample extends JFrame {
public NumericShaperExample() {
Container container = this.getContentPane();
container.add("Center", new NumericShaperPanel());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("NumericShaper Example");
this.setSize(250, 120);
}
public static void main(String[] args) {
new NumericShaperExample();.setVisible(true)
}
NumericShaper.Range enumeration using, for digit display}

  1. 接下来,将NumericShaperPanel类添加到项目中,如下所示:
public class NumericShaperPanel extends JPanel {
private TextLayout layout;
public NumericShaperPanel() {
String text = "0 1 2 3 4 5 6 7 8 9";
HashMap map = new HashMap();
Font font = new Font("Mongolian Baiti", Font.PLAIN, 32);
map.put(TextAttribute.FONT, font);
map.put(TextAttribute.NUMERIC_SHAPING,
NumericShaper.getShaper(NumericShaper.Range. MONGOLIAN));
FontRenderContext fontRenderContext =
new FontRenderContext(null, false, false);
layout = new TextLayout(text, map, fontRenderContext);
}
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
layout.draw(g2d, 10, 50);
}
}

  1. 执行应用程序。您的输出应该如下所示:

操作步骤...

工作原理...

main方法中,创建了NumericShaperExample类的一个实例。在其构造函数中,创建了NumericShaperPanel类的一个实例,并将其添加到窗口的中心。设置了窗口的标题、默认关闭操作和大小。接下来,窗口被显示出来。

NumericShaperPanel类的构造函数中,创建了一个文本字符串以及一个HashMap来保存显示的基本特性。将此映射用作TextLayout构造函数的参数,以及要显示的字符串和映射。使用蒙古 Baiti 字体和 MONGOLIAN 范围显示蒙古文。我们使用这种字体来演示NumericShaper类的新方法。

NumericShaper类已添加了新方法,使得在不同语言中显示数字值更加容易。getShaper方法被重载,其中一个版本接受一个NumericShaper.Range枚举值。该值指定要使用的语言。NumericShaper.Range枚举已添加以表示给定语言中数字的 Unicode 字符范围。

paintComponent方法中,使用Graphics2D对象作为draw方法的参数来将字符串渲染到窗口中。

还有更多...

getContextualShaper方法用于控制在与不同脚本一起使用时如何显示数字。这意味着如果在数字之前使用日语脚本,则会显示日语数字。该方法接受一组NumericShaper.Range枚举值。

shape方法还使用范围来指定要在数组中的起始和结束索引处使用的脚本。getRangeSet方法返回NumericShaper实例使用的一组NumericShaper.Range

Java 7 中的 JavaBean 增强功能

JavaBean是构建 Java 应用程序可重用组件的一种方式。它们是遵循特定命名约定的 Java 类。在 Java 7 中添加了几个 JavaBean 增强功能。在这里,我们将重点关注java.beans.Expression类,它在执行方法时非常有用。execute方法已经添加以实现这一功能。

准备工作

使用Expression类来执行方法:

  1. 为方法创建参数数组,如果需要的话。

  2. 创建Expression类的一个实例,指定要执行方法的对象、方法名称和任何需要的参数。

  3. 针对表达式调用execute方法。

  4. 如有必要,使用getValue方法获取方法执行的结果。

如何做...

  1. 创建一个新的控制台应用程序。创建两个类:JavaBeanExample,其中包含main方法和Person类。Person类包含一个用于名称的单个字段,以及构造函数、getter 方法和 setter 方法:
public class Person {
private String name;
public Person() {
this("Jane", 23);
}
public Person(String name, int age) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

  1. JavaBeanExample类的main方法中,我们将创建Person类的一个实例,并使用Expression类来执行其getNamesetName方法:
public static void main(String[] args) throws Exception {
Person person = new Person();
String arguments[] = {"Peter"};
Expression expression = new Expression(null, person, "setName", arguments);
System.out.println("Name: " + person.getName());
expression.execute();
System.out.println("Name: " + person.getName());
System.out.println();
expression = new Expression(null, person, "getName", null);
System.out.println("Name: " + person.getName());
expression.execute();
System.out.println("getValue: " + expression.getValue());
}

  1. 执行应用程序。其输出应如下所示:

名称:Jane

名称:Peter

名称:Peter

getValue:Peter

它是如何工作的...

Person类使用了一个名为 name 的字段。getNamesetName方法是从main方法中使用的,其中创建了一个Person实例。Expression类的构造函数有四个参数。第一个参数在本例中没有使用,但可以用来定义方法执行的返回值。第二个参数是方法将被执行的对象。第三个参数是包含方法名称的字符串,最后一个参数是包含方法使用的参数的数组。

在第一个序列中,使用Peter作为参数执行了setName方法。应用程序的输出显示名称最初为Jane,但在执行execute方法后更改为Peter

在第二个序列中,执行了getName方法。getValue方法返回方法执行的结果。输出显示getName方法返回了Peter

还有更多...

java.bean包的类还有其他增强。例如,FeatureDescriptorPropertyChangeEvent类中的toString方法已被重写,以提供更有意义的描述。

Introspector类提供了一种了解 Java Bean 的属性、方法和事件的方式,而不使用可能很繁琐的反射 API。该类已添加了一个getBeanInfo方法,该方法使用Inspector类的控制标志来影响返回的BeanInfo对象。

Transient注解已添加以控制包含什么。属性的true值意味着应忽略带注解的特性。

XMLDecoder类中添加了一个新的构造函数,接受一个InputSource对象。此外,添加了createHandler方法,返回一个DefaultHandler对象。此处理程序用于解析XMLEncoder类创建的 XML 存档。

XMLEncoder类中添加了一个新的构造函数。这允许使用特定的字符集和特定的缩进将 JavaBeans 写入OutputStream

在 Java 7 中处理区域设置和Locale.Builder

java.util.Locale.Builder类已添加到 Java 7 中,并提供了一种简单的创建区域设置的方法。Locale.Category枚举也是新的,使得在显示和格式化目的上使用不同的区域设置变得容易。我们首先将看一下Locale.Builder类的使用,然后检查其他区域设置的改进以及在还有更多..部分中使用Locale.Category枚举。

准备工作

构建和使用新的Locale对象:

  1. 创建Builder类的一个实例。

  2. 使用类的相关方法设置所需的属性。

  3. 根据需要使用Locale对象。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,添加以下代码。我们将创建一个基于东亚美尼亚语的区域设置,使用意大利的拉丁文。通过使用setWeekDate方法,演示了该区域设置,显示了 2012 年第 16 周的第三天的日期。这种方法在Java 7 中处理周中有更详细的讨论:
Calendar calendar = Calendar.getInstance();
calendar.setWeekDate(2012, 16, 3);
Builder builder = new Builder();
builder.setLanguage("hy");
builder.setScript("Latn");
builder.setRegion("IT");
builder.setVariant("arevela");
Locale locale = builder.build();
Locale.setDefault(locale);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println("" + locale.getDisplayLanguage());

  1. 第二个示例构建了一个基于中国语言的区域设置,使用了在中国大陆使用的简体字:
builder.setLanguage("zh");
builder.setScript("Hans");
builder.setRegion("CN");
locale = builder.build();
Locale.setDefault(locale);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println("" + locale.getDisplayLanguage());

  1. 执行时,输出应如下所示:

April 17, 2012 7:25:42 PM CDT

亚美尼亚语

2012 年 4 月 17 日 下午 07 时 25 分 42 秒

中文

工作原理...

创建了Builder对象。使用该对象,我们应用了方法来设置区域设置的语言、脚本和地区。然后执行了build方法,并返回了一个Locale对象。我们使用这个区域设置来显示日期和区域设置的显示语言。这是两次执行的。首先是亚美尼亚语,然后是中文。

还有更多...

能够标记一条信息以指示所使用的语言是很重要的。为此目的使用了一个标签。一组标准标签由IETF BCP 47标准定义。Java 7 符合这一标准,并添加了几种方法来处理标签。

该标准支持对标签的扩展概念。这些扩展可用于提供有关区域设置的更多信息。有两种类型:

  • Unicode 区域设置扩展

  • 私有使用扩展

Unicode 区域设置扩展由Unicode 通用区域设置数据存储库CLDR)(cldr.unicode.org/)定义。这些扩展涉及非语言信息,如货币和日期。CLDR 维护了一个区域设置信息的标准存储库。私有使用扩展用于指定特定于平台的信息,例如与操作系统或编程语言相关的信息。

注意

有关 IETF BCP 47 标准的更多信息,请访问tools.ietf.org/rfc/bcp/bcp47.txt

扩展由键/值对组成。键是一个单个字符,值遵循以下格式:

SUBTAG ('-' SUBTAG)*

SUBTAG由一系列字母数字字符组成。对于 Unicode 区域设置扩展,值必须至少为两个字符,但不超过 8 个字符的长度。对于私有使用扩展,允许 1 到 8 个字符。所有扩展字符串不区分大小写。

Unicode 区域设置扩展的键为u,私有使用扩展的键为x。这些扩展可以添加到区域设置中,以提供额外的信息,例如要使用的日历编号类型。

可以使用的键列在下表中:

键代码 描述
ca 用于确定日期的日历算法
co 整理—语言中使用的排序
ka 整理参数—用于指定排序
cu 货币类型信息
nu 编号系统
va 常见变体类型

键和类型的示例列在下表中:

键/类型 含义
nu-armnlow 亚美尼亚小写数字
ca-indian 印度日历

已添加了几种方法来使用这些扩展。getExtensionKeys方法返回一个包含区域设置中使用的所有键的Character对象集。同样,getUnicodeLocaleAttributesgetUnicodeLocaleKeys方法返回一个列出属性和可用的 Unicode 键的字符串集。如果没有可用的扩展,这些方法将返回一个空集。如果已知键,则getExtension方法或getUnicodeLocaleType方法将返回一个包含该键值的字符串。

对于给定的区域设置,getScript, getDisplayScripttoLanguageTag方法分别返回脚本、脚本的可显示名称和区域设置的格式良好的BCP 47标签。getDisplayScript方法还将返回给定区域设置的脚本的可显示名称。

接下来的部分讨论了使用setDefault方法同时控制使用两种不同区域设置显示信息的方法。

使用Locale.Category枚举来使用两种不同的区域设置显示信息

Locale.Category枚举已添加到 Java 7。它有两个值,DISPLAYFORMAT。这允许为格式类型资源(日期、数字和货币)和显示资源(应用程序的 GUI 方面)设置默认区域设置。例如,应用程序的一部分可以将格式设置为适应一个区域设置,比如JAPANESE,同时在另一个区域设置中显示相关信息,比如GERMAN

考虑以下示例:

Locale locale = Locale.getDefault();
Calendar calendar = Calendar.getInstance();
calendar.setWeekDate(2012, 16, 3);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println(ocale.getDisplayLanguage());
Locale.setDefault(Locale.Category.FORMAT, Locale.JAPANESE);
Locale.setDefault(Locale.Category.DISPLAY, Locale.GERMAN);
System.out.println(DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.LONG).format(calendar.getTime()));
System.out.println(locale.getDisplayLanguage());

当执行此代码序列时,您应该会得到类似以下的输出。初始日期和显示语言可能会因默认区域设置而有所不同。

2012 年 4 月 17 日下午 7:15:14 CDT

英语

2012/04/17 19:15:14 CDT

英语

已检索默认区域设置,并使用setWeekDate方法设置了一个日期。这个方法在在 Java 7 中使用星期示例中有更详细的讨论。接下来,打印日期和显示语言。显示被重复,只是使用setDefault方法更改了默认区域设置。显示资源已更改为使用Locale.JAPANESE,格式类型资源已更改为Locale.GERMAN。输出反映了这一变化。

处理 null 引用

java.lang.NullPointerException是一个相当常见的异常。当尝试对包含 null 值的引用变量执行方法时,就会发生这种情况。在这个示例中,我们将研究各种可用的技术来解决这种类型的异常。

java.util.Objects类已被引入,并提供了许多静态方法来处理需要处理 null 值的情况。使用这个类简化了对 null 值的测试。

还有更多..部分讨论了使用空列表的情况,这可以用来代替返回 null。java.util.Collections类有三个返回空列表的方法。

准备就绪

使用Objects类来覆盖equalshashCode方法:

  1. 覆盖目标类中的方法。

  2. 使用Objects类的equals方法来避免在equals方法中检查 null 值的显式代码。

  3. 使用Objects类的hashCode方法来避免在hashCode方法中检查 null 值的显式代码。

如何做...

  1. 创建一个新的控制台应用程序。我们将创建一个Item类来演示Objects类的使用。在Item类中,我们将覆盖equalshashCode方法。这些方法是由 NetBeans 的插入代码命令生成的。我们使用这些方法,因为它们说明了Objects类的方法并且结构良好。首先按以下方式创建类:
public class Item {
private String name;
private int partNumber;
public Item() {
this("Widget", 0);
}
public Item(String name, int partNumber) {
this.name = Objects.requireNonNull(name);
this.partNumber = partNumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = Objects.requireNonNull(name);
}
public int getPartNumber() {
return partNumber;
null referenceshandling}
public void setPartNumber(int partNumber) {
this.partNumber = partNumber;
}
}

  1. 接下来,按以下方式覆盖equalshashCode方法。它们提供了检查 null 值的代码:
@Override
public boolean equals(Object obj){
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Item other = (Item) obj;
if (!Objects.equals(this.name, other.name)) {
return false;
}
if (this.partNumber != other.partNumber) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 47 * hash + Objects.hashCode(this.name);
hash = 47 * hash + this.partNumber;
return hash;
}

  1. 通过添加toString方法完成类:
@Override
public String toString() {
return name + " - " + partNumber;
}

  1. 接下来,在main方法中添加以下内容:
Item item1 = new Item("Eraser", 2200);
Item item2 = new Item("Eraser", 2200);
Item item3 = new Item("Pencil", 1100);
Item item4 = null;
System.out.println("item1 equals item1: " + item1.equals(item1));
System.out.println("item1 equals item2: " + item1.equals(item2));
System.out.println("item1 equals item3: " + item1.equals(item3));
System.out.println("item1 equals item4: " + item1.equals(item4));
item2.setName(null);
System.out.println("item1 equals item2: " + item1.equals(item2));

  1. 执行应用程序。您的输出应如下所示:

item1 等于 item1:true

item1 等于 item2:true

item1 等于 item3:false

item1 等于 item4:false

线程"main"中的异常 java.lang.NullPointerException

在 java.util.Objects.requireNonNull(Objects.java:201)

在 packt.Item.setName(Item.java:23)

在 packt.NullReferenceExamples.main(NullReferenceExamples.java:71)

正如我们将很快看到的,NullPointerException是尝试将 null 值分配给 Item 的名称字段的结果。

它是如何工作的...

equals方法中,首先进行了一个测试,以确定传递的对象是否为 null。如果是,则返回false。进行了一个测试,以确保类是相同类型的。然后使用equals方法来查看两个名称字段是否相等。

Objects类的equals方法的行为如下表所示。相等性的含义由第一个参数的equals方法确定:

第一个参数 第二个参数 返回
非 null 非 null 如果它们是相同的对象,则为true,否则为false
非 null null false
null 非 null false
null null true

最后的测试比较了两个整数partNumber字段的相等性。

Item类的hashCode方法中,Objects类的hashCode方法被应用于名称字段。如果其参数为 null,则该方法将返回 0,否则返回参数的哈希码。然后使用partNumber来计算哈希码的最终值。

注意在两个参数构造函数和setName方法中使用了requireNonNull方法。该方法检查非空参数。如果参数为 null,则抛出NullPointerException。这有效地在应用程序中更早地捕获潜在的错误。

requireNonNull方法有两个版本,第二个版本接受第二个字符串参数。当发生异常时,此参数会改变生成的消息。用以下代码替换setName方法的主体:

this.name = Objects.requireNonNull(name, "The name field requires a non-null value");

重新执行应用程序。异常消息现在将显示如下:

Exception in thread "main" java.lang.NullPointerException: The name field requires a non-null value

还有更多...

有几个其他Objects类的方法可能会引起兴趣。此外,第二部分将讨论使用空迭代器来避免空指针异常。

其他Objects类方法

Objects类的hashCode方法是重载的。第二个版本接受可变数量的对象作为参数。该方法将使用这些对象的序列生成哈希码。例如,Item类的hashCode方法可以这样编写:

@Override
public int hashCode() {
return Objects.hash(name,partNumber);
}

deepEquals方法深度比较两个对象。这意味着它比较的不仅仅是引用值。两个 null 参数被认为是深度相等的。如果两个参数都是数组,则调用Arrays.deepEqual方法。对象的相等性由第一个参数的equals方法确定。

compare方法用于比较前两个参数,根据参数之间的关系返回负值、零或正值。通常,返回 0 表示参数相同。负值表示第一个参数小于第二个参数。正值表示第一个参数大于第二个参数。

如果其参数相同,或者两个参数都为 null,则该方法将返回零。否则,返回值将使用Comparator接口的compare方法确定。

Objects类的toString方法用于确保即使对象为 null 也返回字符串。以下序列说明了这个重载方法的使用:

Item item4 = null;
System.out.println("toString: " + Objects.toString(item4));
System.out.println("toString: " + Objects.toString(item4, "Item is null"));

当执行时,该方法的第一次使用将显示单词null。在第二个版本中,字符串参数显示如下:

toString: null

toString: Item is null

使用空迭代器来避免空指针异常

避免NullPointerException的一种方法是在无法创建列表时返回非空值。返回空的Iterator可能是有益的。

在 Java 7 中,Collections类添加了三种新方法,返回一个Iterator、一个ListIterator或一个Enumeration,它们都是空的。通过返回空,它们可以在不引发空指针异常的情况下使用。

演示使用空列表迭代器,创建一个新的方法,返回一个通用的ListIterator<String>,如下所示。使用if语句来返回ListIterator或空的ListIterator

public static ListIterator<String> returnEmptyListIterator() {
boolean someConditionMet = false;
if(someConditionMet) {
ArrayList<String> list = new ArrayList<>();
// Add elements
ListIterator<String> listIterator = list.listIterator();
return listIterator;
}
else {
return Collections.emptyListIterator();
}
}

使用以下main方法来测试迭代器的行为:

public static void main(String[] args) {
ListIterator<String> list = returnEmptyListIterator();
while(())String item: list {
System.out.println(item);
}
}

执行时,不应有输出。这表示迭代器是空的。如果我们返回 null,我们将收到NullPointerException

Collections类的静态emptyListIterator方法返回一个ListIterator,其方法如下表所列:

方法 行为
hasNext``hasPrevious 总是返回false
next``Previous 总是抛出NoSuchElementException
remove``set 总是抛出IllegalStateException
add 总是抛出UnsupportedOperationException
nextIndex 总是返回 0
previousIndex 总是返回-1

emptyIterator方法将返回一个具有以下行为的空迭代器:

方法 行为
hasNext 总是返回false
next 总是抛出NoSuchElementException
remove 总是抛出IllegalStateException

emptyEnumeration方法返回一个空枚举。它的hasMoreElements将始终返回false,它的nextElement将始终抛出NoSuchElementException异常。

在 Java 7 中使用新的 BitSet 方法

java.util.BitSet类在最新的 Java 版本中增加了几种新方法。这些方法旨在简化大量位的操作,并提供更容易访问有关位位置的信息。位集可用于优先级队列或压缩数据结构。本示例演示了一些新方法。

准备工作

要使用新的BitSet方法:

  1. 创建一个BitSet的实例。

  2. 根据需要对BitSet对象执行方法。

如何做...

  1. 创建一个新的控制台应用程序。在main方法中,创建一个BitSet对象的实例。然后声明一个长数字的数组,并使用静态的valueOf方法将我们的BitSet对象设置为这个长数组的值。添加一个println语句,这样我们就可以看到我们的长数字在BitSet中的表示方式:
BitSet bitSet = new BitSet();
long[] array = {1, 21, 3};
bitSet = BitSet.valueOf(array);
System.out.println(bitSet);

  1. 然后,使用toLongArray方法将BitSet转换回长数字的数组。使用 for 循环打印数组中的值:
long[] tmp = bitSet.toLongArray();
for (long number : tmp) {
System.out.println(number);
}

  1. 执行应用程序。您应该看到以下输出:

{0, 64, 66, 68, 128, 129}

1

21

3

它是如何工作的...

创建BitSet对象后,我们创建了一个包含三个long数字的数组,这些数字用作我们在BitSet中希望使用的位序列的表示。valueOf方法接受这个表示并将其转换为位序列。

当我们打印出BitSet时,我们看到了序列{0, 64, 66, 68, 128, 129}。这个BitSet中的每个数字代表了在我们的位序列中设置的位的索引。例如,0 代表数组中的long数字 1,因为用于表示 1 的位的索引在位置 0。同样,位 64、66 和 68 被设置为表示我们的long数字 21。序列中的第 128 和 129 位被设置为表示我们的long数字 3。在下一节中,我们使用toLongArray方法将BitSet返回到其原始形式。

在我们的示例中,我们使用了一个long数字的数组。类似的valueOf方法也适用于byte, LongBufferByteBuffer数组。当使用LongBufferByteBuffer数组时,缓冲区不会被valueOf方法修改,并且BitSet不能被转换回缓冲区。相反,必须使用toLongArray方法或类似的toByteArray方法将BitSet转换为字节数组。

还有更多...

有两种有用的方法用于定位BitSet中的设置或清除位。方法previousSetBit以整数表示特定索引作为其参数,并返回表示BitSet中最接近的设置位的整数。例如,将以下代码序列添加到我们的先前示例中(使用由长数字{1, 21, 3}表示的BitSet):

System.out.println(bitSet.previousSetBit(1));

这将导致输出整数 0。这是因为我们将索引 1 的参数传递给previousSetBit方法,而我们的BitSet中最接近的前一个设置位是在索引 0 处。

previousClearBit方法以类似的方式运行。如果我们在上一个示例中执行以下代码:

System.out.println(bitSet.previousClearBit(66));

我们将得到整数 65 的输出。位于索引 65 的位是最接近我们的参数 66 的最近的清除位。如果在BitSet中不存在这样的位,则两种方法都将返回-1。

posted @ 2025-09-10 15:06  绝不原创的飞龙  阅读(8)  评论(0)    收藏  举报