Java 中文官方教程 2022 版(九)(1)https://developer.aliyun.com/article/1486342
观察服务概述
WatchService
API 相当低级,允许您自定义它。您可以直接使用它,或者您可以选择在此机制之上创建一个高级 API,以使其适合您的特定需求。
下面是实现观察服务所需的基本步骤:
- 为文件系统创建一个
WatchService
“观察者”。 - 对于要监视的每个目录,请将其注册到观察者中。在注册目录时,指定要接收通知的事件类型。您为每个注册的目录收到一个
WatchKey
实例。 - 实现一个无限循环以等待传入事件。当事件发生时,键被标记并放入观察者队列中。
- 从观察者队列中检索键。您可以从键中获取文件名。
- 检索键的每个待处理事件(可能有多个事件)并根据需要处理。
- 重置键,并恢复等待事件。
- 关闭服务:当线程退出或调用其
closed
方法关闭服务时,监视服务将退出。
WatchKeys
是线程安全的,可以与java.nio.concurrent
包一起使用。您可以为此目的专门分配一个线程池。
试一试
由于此 API 更为高级,请在继续之前先尝试一下。将WatchDir
示例保存到您的计算机上,并对其进行编译。创建一个将传递给示例的test
目录。WatchDir
使用单个线程处理所有事件,因此在等待事件时会阻止键盘输入。要么在单独的窗口中运行程序,要么在后台运行,如下所示:
java WatchDir test &
在test
目录中创建、删除和编辑文件。当发生任何这些事件时,将在控制台上打印消息。完成后,删除test
目录,WatchDir
退出。或者,如果您愿意,也可以手动终止进程。
您还可以通过指定-r
选项来监视整个文件树。当您指定-r
时,WatchDir
遍历文件树,将每个目录注册到监视服务中。
创建监视服务并注册事件
第一步是通过FileSystem
类中的newWatchService
方法创建一个新的WatchService
,如下所示:
WatchService watcher = FileSystems.getDefault().newWatchService();
接下来,向监视服务注册一个或多个对象。任何实现了Watchable
接口的对象都可以注册。Path
类实现了Watchable
接口,因此要监视的每个目录都被注册为一个Path
对象。
与任何Watchable
一样,Path
类实现了两个register
方法。本页使用了两个参数版本的register(WatchService, WatchEvent.Kind...)
。(三个参数版本接受一个WatchEvent.Modifier
,目前尚未实现。)
在向监视服务注册对象时,您需要指定要监视的事件类型。支持的StandardWatchEventKinds
事件类型如下:
ENTRY_CREATE
– 创建目录条目。ENTRY_DELETE
– 删除目录条目。ENTRY_MODIFY
– 修改目录条目。OVERFLOW
– 表示事件可能已丢失或被丢弃。您无需注册OVERFLOW
事件即可接收它。
以下代码片段显示了如何为所有三种事件类型注册Path
实例:
import static java.nio.file.StandardWatchEventKinds.*; Path dir = ...; try { WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); } catch (IOException x) { System.err.println(x); }
处理事件
事件处理循环中事件的顺序如下:
- 获取一个监视键。提供了三种方法:
poll
– 如果可用,则返回一个排队的键。如果不可用,则立即返回null
值。poll(long, TimeUnit)
– 如果有排队的键可用,则返回一个。如果没有立即可用的排队键,则程序将等待指定的时间。TimeUnit
参数确定指定的时间是纳秒、毫秒还是其他时间单位。take
– 返回一个排队的键。如果没有可用的排队键,此方法将等待。
- 处理键的待处理事件。您从
pollEvents
方法中获取WatchEvents
的List
。 - 使用
kind
方法检索事件的类型。无论键注册了什么事件,都有可能收到OVERFLOW
事件。您可以选择处理溢出或忽略它,但应该对其进行测试。 - 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用
context
方法来检索它。 - 处理键的事件后,需要通过调用
reset
将键放回ready
状态。如果此方法返回false
,则键不再有效,循环可以退出。这一步非常重要。如果未调用reset
,则此键将不会接收到进一步的事件。
观察键具有状态。在任何给定时间,其状态可能是以下之一:
Ready
表示键已准备好接受事件。创建时,键处于准备状态。Signaled
表示有一个或多个事件排队。一旦键被标记,它就不再处于准备状态,直到调用reset
方法。Invalid
表示键不再活动。当发生以下事件之一时,会出现此状态:
这里是一个事件处理循环的示例。它取自于 Email
示例,该示例监视一个目录,等待新文件出现。当新文件可用时,通过使用 probeContentType(Path)
方法来检查它是否是一个 text/plain
文件。意图是将 text/plain
文件发送到一个别名,但具体实现细节留给读者。
Watch service API 特定的方法用粗体显示:
for (;;) { // wait for key to be signaled WatchKey key; try { key = watcher.take(); } catch (InterruptedException x) { return; } for (WatchEvent<?> event: key.pollEvents()) { WatchEvent.Kind<?> kind = event.kind(); // This key is registered only // for ENTRY_CREATE events, // but an OVERFLOW event can // occur regardless if events // are lost or discarded. if (kind == OVERFLOW) { continue; } // The filename is the // context of the event. WatchEvent<Path> ev = (WatchEvent<Path>)event; Path filename = ev.context(); // Verify that the new // file is a text file. try { // Resolve the filename against the directory. // If the filename is "test" and the directory is "foo", // the resolved name is "test/foo". Path child = dir.resolve(filename); if (!Files.probeContentType(child).equals("text/plain")) { System.err.format("New file '%s'" + " is not a plain text file.%n", filename); continue; } } catch (IOException x) { System.err.println(x); continue; } // Email the file to the // specified email alias. System.out.format("Emailing file %s%n", filename); //Details left to reader.... } // Reset the key -- this step is critical if you want to // receive further watch events. If the key is no longer valid, // the directory is inaccessible so exit the loop. boolean valid = key.reset(); if (!valid) { break; } }
检索文件名
文件名是从事件上下文中检索的。Email
示例使用以下代码检索文件名:
WatchEvent<Path> ev = (WatchEvent<Path>)event; Path filename = ev.context();
当你编译 Email
示例时,会生成以下错误:
Note: Email.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
这个错误是由将 WatchEvent
强制转换为 WatchEvent
的代码行引起的。WatchDir
示例通过创建一个抑制未经检查警告的实用 cast
方法来避免这个错误,如下所示:
@SuppressWarnings("unchecked") static <T> WatchEvent<T> cast(WatchEvent<?> event) { return (WatchEvent<Path>)event; }
如果你对 @SuppressWarnings
语法不熟悉,请参见 Annotations。
何时使用和不使用这个 API
Watch Service API 适用于需要通知文件更改事件的应用程序。它非常适合任何可能有许多打开文件并需要确保文件与文件系统同步的应用程序,比如编辑器或 IDE。它也非常适合监视目录的应用服务器,也许等待 .jsp
或 .jar
文件的出现,以便部署它们。
这个 API 不 是为了索引硬盘而设计的。大多数文件系统实现都原生支持文件更改通知。Watch Service API 利用了这种支持(如果可用)。然而,当文件系统不支持这种机制时,Watch Service 将轮询文件系统,等待事件发生。
其他有用的方法
本课程中未涵盖的一些有用方法在此处介绍。本节涵盖以下内容:
- 确定 MIME 类型
- 默认文件系统
- 路径字符串分隔符
- 文件系统的文件存储器
确定 MIME 类型
要确定文件的 MIME 类型,您可能会发现probeContentType(Path)
方法很有用。例如:
try { String type = Files.probeContentType(filename); if (type == null) { System.err.format("'%s' has an" + " unknown filetype.%n", filename); } else if (!type.equals("text/plain") { System.err.format("'%s' is not" + " a plain text file.%n", filename); continue; } } catch (IOException x) { System.err.println(x); }
注意,如果无法确定内容类型,probeContentType
会返回 null。
此方法的实现高度依赖于平台,并不是绝对可靠的。内容类型由平台的默认文件类型检测器确定。例如,如果检测器根据.class
扩展名确定文件的内容类型为application/x-java
,可能会被欺骗。
如果默认的方法不符合您的需求,您可以提供自定义的FileTypeDetector
。
电子邮件
示例使用probeContentType
方法。
默认文件系统
要检索默认文件系统,请使用getDefault
方法。通常,此FileSystems
方法(注意是复数形式)链接到FileSystem
方法之一(注意是单数形式),如下所示:
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.*");
路径字符串分隔符
POSIX 文件系统的路径分隔符是正斜杠/
,Microsoft Windows 的路径分隔符是反斜杠\
。其他文件系统可能使用其他分隔符。要检索默认文件系统的Path
分隔符,可以使用以下方法之一:
String separator = File.separator; String separator = FileSystems.getDefault().getSeparator();
getSeparator
方法也用于检索任何可用文件系统的路径分隔符。
文件系统的文件存储器
文件系统有一个或多个文件存储器来保存其文件和目录。文件存储器代表底层存储设备。在 UNIX 操作系统中,每个挂载的文件系统都由一个文件存储器表示。在 Microsoft Windows 中,每个卷都由一个文件存储器表示:C:
、D:
等等。
要检索文件系统的所有文件存储器列表,可以使用getFileStores
方法。此方法返回一个Iterable
,允许您使用增强的 for 语句遍历所有根目录。
for (FileStore store: FileSystems.getDefault().getFileStores()) { ... }
如果要检索特定文件所在的文件存储器,请使用Files
类中的getFileStore
方法,如下所示:
Path file = ...; FileStore store= Files.getFileStore(file);
DiskUsage
示例使用getFileStores
方法。
传统文件 I/O 代码
与旧代码的互操作性
在 Java SE 7 发布之前,java.io.File
类是文件 I/O 的机制,但它有一些缺点。
- 许多方法在失败时不会抛出异常,因此无法获得有用的错误消息。例如,如果文件删除失败,程序将收到“删除失败”,但不知道是因为文件不存在、用户没有权限还是其他问题。
rename
方法在各个平台上的工作不一致。- 没有对符号链接的真正支持。
- 需要更多对元数据的支持,如文件权限、文件所有者和其他安全属性。
- 访问文件元数据效率低下。
- 许多
File
方法不具备可扩展性。在服务器上请求大型目录列表可能导致挂起。大型目录也可能导致内存资源问题,导致拒绝服务。 - 不可能编写可靠的代码,可以递归遍历文件树,并在存在循环符号链接时做出适当响应。
也许您有使用 java.io.File
的旧代码,并希望最小影响地利用 java.nio.file.Path
功能。
java.io.File
类提供了 toPath
方法,将旧式 File
实例转换为 java.nio.file.Path
实例,如下所示:
Path input = file.toPath();
然后,您可以利用 Path
类提供的丰富功能集。
例如,假设您有一些删除文件的代码:
file.delete();
您可以修改此代码以使用 Files.delete
方法,如下所示:
Path fp = file.toPath(); Files.delete(fp);
相反,Path.toFile
方法为 Path
对象构造一个 java.io.File
对象。
将 java.io.File 功能映射到 java.nio.file
由于 Java SE 7 发布中的文件 I/O 实现已完全重新架构,因此不能将一个方法替换为另一个方法。如果您想使用 java.nio.file
包提供的丰富功能,最简单的解决方案是使用前一节中建议的 File.toPath
方法。但是,如果您不想使用该方法或该方法不符合您的需求,您必须重写文件 I/O 代码。
两个 API 之间没有一对一对应关系,但以下表格给出了 java.io.File
API 中的功能在 java.nio.file
API 中的映射,并告诉您可以在哪里获取更多信息。
java.io.File 功能 | java.nio.file 功能 | 教程覆盖范围 |
java.io.File |
java.nio.file.Path |
Path 类 |
java.io.RandomAccessFile |
SeekableByteChannel 功能。 |
随机访问文件 |
File.canRead 、canWrite 、canExecute |
Files.isReadable 、Files.isWritable 和 Files.isExecutable 。在 UNIX 文件系统上,使用 管理元数据(文件和文件存储属性) 包来检查九个文件权限。 |
检查文件或目录 管理元数据 |
File.isDirectory() 、File.isFile() 和 File.length() |
Files.isDirectory(Path, LinkOption...) 、Files.isRegularFile(Path, LinkOption...) 和 Files.size(Path) |
管理元数据 |
File.lastModified() 和 File.setLastModified(long) |
Files.getLastModifiedTime(Path, LinkOption...) 和 Files.setLastMOdifiedTime(Path, FileTime) |
管理元数据 |
设置各种属性的 File 方法:setExecutable 、setReadable 、setReadOnly 、setWritable |
这些方法被 Files 方法 setAttribute(Path, String, Object, LinkOption...) 替代。 |
管理元数据 |
new File(parent, "newfile") |
parent.resolve("newfile") |
路径操作 |
File.renameTo |
Files.move |
移动文件或目录 |
File.delete |
Files.delete |
删除文件或目录 |
File.createNewFile |
Files.createFile |
创建文件 |
File.deleteOnExit |
由 createFile 方法中指定的 DELETE_ON_CLOSE 选项替代。 |
创建文件 |
| File.createTempFile
| Files.createTempFile(Path, String, FileAttributes)
、Files.createTempFile(Path, String, String, FileAttributes)
| 创建文件 通过流 I/O 创建和写入文件
通过通道 I/O 读写文件 |
File.exists |
Files.exists 和 Files.notExists |
验证文件或目录的存在性 |
File.compareTo and equals |
Path.compareTo and equals |
比较两个路径 |
File.getAbsolutePath and getAbsoluteFile |
Path.toAbsolutePath |
转换路径 |
| File.getCanonicalPath
and getCanonicalFile
| Path.toRealPath
或 normalize
| 转换路径 (toRealPath
) 从路径中删除冗余部分 (normalize
)
|
File.toURI |
Path.toURI |
转换路径 |
File.isHidden |
Files.isHidden |
检索路径信息 |
File.list and listFiles |
Path.newDirectoryStream |
列出目录内容 |
File.mkdir 和 mkdirs |
Files.createDirectory |
创建目录 |
File.listRoots |
FileSystem.getRootDirectories |
列出文件系统的根目录 |
File.getTotalSpace 、File.getFreeSpace 、File.getUsableSpace |
FileStore.getTotalSpace 、FileStore.getUnallocatedSpace 、FileStore.getUsableSpace 、FileStore.getTotalSpace |
文件存储属性 |
摘要
原文:
docs.oracle.com/javase/tutorial/essential/io/summary.html
java.io
包包含许多类,您的程序可以使用这些类来读取和写入数据。大多数类实现顺序访问流。顺序访问流可以分为两组:那些读取和写入字节的流以及读取和写入 Unicode 字符的流。每个顺序访问流都有其特长,例如从文件中读取或写入数据,过滤读取或写入的数据,或者序列化对象。
java.nio.file
包提供了广泛的文件和文件系统 I/O 支持。这是一个非常全面的 API,但关键入口点如下:
Path
类具有操作路径的方法。Files
类具有文件操作的方法,例如移动、复制、删除,以及检索和设置文件属性的方法。FileSystem
类具有各种方法用于获取有关文件系统的信息。
关于 NIO.2 的更多信息可以在 OpenJDK: NIO 项目网站上找到。该网站包括 NIO.2 提供的超出本教程范围的功能资源,例如多播、异步 I/O 和创建自己的文件系统实现。
问题和练习:基本 I/O
原文:
docs.oracle.com/javase/tutorial/essential/io/QandE/questions.html
问题
1. 你会使用什么类和方法来读取大文件末尾附近已知位置的几个数据片段?
2. 在调用format
时,如何最好地指示一个新行?
3. 如何确定文件的 MIME 类型?
4. 您会使用什么方法来确定文件是否是符号链接?
练习
1. 编写一个示例,计算文件中特定字符(如e
)出现的次数。可以在命令行指定字符。您可以使用xanadu.txt
作为输入文件。
2. 文件datafile
以一个告诉你同一文件中一个int
数据偏移量的long
开头。编写一个程序获取这个int
数据。这个int
数据是什么?
检查你的答案。
课程:并发编程
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/index.html
计算机用户认为他们的系统可以同时执行多项任务是理所当然的。他们认为他们可以在一个文字处理器中继续工作,同时其他应用程序可以下载文件,管理打印队列和流式传输音频。甚至单个应用程序通常也被期望同时执行多项任务。例如,流式传输音频应用程序必须同时从网络上读取数字音频,解压缩它,管理播放和更新显示。即使文字处理器也应该始终准备好响应键盘和鼠标事件,无论它是在重新格式化文本还是更新显示。能够执行这些操作的软件被称为并发软件。
Java 平台从头开始就设计用于支持并发编程,在 Java 编程语言和 Java 类库中具有基本的并发支持。自 5.0 版本以来,Java 平台还包括高级并发 API。本课程介绍了平台的基本并发支持,并总结了java.util.concurrent
包中的一些高级 API。
进程和线程
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/procthread.html
在并发编程中,有两个基本的执行单位:进程和线程。在 Java 编程语言中,并发编程主要涉及线程。然而,进程也很重要。
计算机系统通常有许多活动进程和线程。即使在只有一个执行核心的系统中,因此在任何给定时刻只有一个线程实际执行,也是如此。单核心的处理时间通过操作系统的时间片特性在进程和线程之间共享。
现在越来越普遍的是计算机系统具有多个处理器或具有多个执行核心的处理器。这极大地增强了系统对进程和线程并发执行的能力 — 但即使在简单系统上,没有多个处理器或执行核心,也可以实现并发。
进程
一个进程有一个独立的执行环境。一个进程通常有一个完整的,私有的基本运行时资源集;特别是,每个进程都有自己的内存空间。
进程通常被视为与程序或应用程序同义。然而,用户所看到的单个应用程序实际上可能是一组协作的进程。为了促进进程之间的通信,大多数操作系统支持进程间通信(IPC)资源,如管道和套接字。IPC 不仅用于同一系统上进程之间的通信,还用于不同系统上的进程。
大多数 Java 虚拟机的实现作为一个单独的进程运行。Java 应用程序可以使用ProcessBuilder
对象创建额外的进程。多进程应用程序超出了本课程的范围。
线程
线程有时被称为轻量级进程。进程和线程都提供执行环境,但创建一个新线程所需的资源比创建一个新进程少。
线程存在于一个进程中 — 每个进程至少有一个。线程共享进程的资源,包括内存和打开的文件。这样做可以实现高效的,但潜在的有问题的通信。
多线程执行是 Java 平台的一个重要特性。每个应用程序至少有一个线程 — 或者多个,如果计算“系统”线程,执行诸如内存管理和信号处理等任务。但从应用程序员的角度来看,你从一个称为主线程的线程开始。这个线程有能力创建额外的线程,我们将在下一节中演示。
线程对象
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/threads.html
每个线程都与类Thread
的实例相关联。使用Thread
对象创建并发应用程序有两种基本策略。
- 要直接控制线程的创建和管理,只需在应用程序需要启动异步任务时实例化
Thread
即可。 - 要将线程管理与应用程序的其余部分抽象出来,将应用程序的任务传递给一个执行器。
本节介绍了Thread
对象的使用。执行器与其他高级并发对象一起讨论。
定义和启动线程
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
创建 Thread
实例的应用程序必须提供将在该线程中运行的代码。 有两种方法可以做到这一点:
- 提供一个
Runnable
对象.Runnable
接口定义了一个名为run
的方法,用于包含在线程中执行的代码。Runnable
对象被传递给Thread
构造函数,就像HelloRunnable
示例中那样:
public class HelloRunnable implements Runnable { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new Thread(new HelloRunnable())).start(); } }
- 子类
Thread
.Thread
类本身实现了Runnable
,尽管它的run
方法什么也不做。 应用程序可以子类化Thread
,提供自己的run
实现,就像HelloThread
示例中那样:
public class HelloThread extends Thread { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new HelloThread()).start(); } }
请注意,这两个示例都调用了 Thread.start
来启动新线程。
你应该使用哪种习语?第一个习语使用了一个 Runnable
对象,更通用,因为 Runnable
对象可以是 Thread
以外的类的子类。 第二个习语在简单应用程序中更容易使用,但受到任务类必须是 Thread
的后代的限制。 本课程重点介绍第一种方法,它将 Runnable
任务与执行任务的 Thread
对象分开。 这种方法不仅更灵活,而且适用于后面介绍的高级线程管理 API。
Thread
类定义了一些对线程管理有用的方法。 这些方法包括 static
方法,提供有关调用方法的线程的信息或影响其状态。 其他方法是从参与管理线程和 Thread
对象的其他线程调用的。 我们将在以下部分中检查其中一些方法。
暂停执行与睡眠
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/sleep.html
Thread.sleep
会导致当前线程暂停执行一段指定的时间。这是一种有效的方式,可以让处理器时间可用于应用程序的其他线程或者可能在计算机系统上运行的其他应用程序。sleep
方法也可以用于节奏控制,就像下面的示例中展示的那样,以及等待另一个线程,该线程的任务被理解为具有时间要求,就像稍后章节中的SimpleThreads
示例一样。
提供了两个重载版本的sleep
:一个指定以毫秒为单位的睡眠时间,另一个指定以纳秒为单位的睡眠时间。然而,这些睡眠时间不能保证是精确的,因为它们受到底层操作系统提供的设施的限制。此外,睡眠时间可以被中断,我们将在稍后的章节中看到。无论如何,你不能假设调用sleep
会精确地暂停线程指定的时间段。
SleepMessages
示例使用sleep
以四秒的间隔打印消息:
public class SleepMessages { public static void main(String args[]) throws InterruptedException { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; for (int i = 0; i < importantInfo.length; i++) { //Pause for 4 seconds Thread.sleep(4000); //Print a message System.out.println(importantInfo[i]); } } }
注意,main
声明了它会throws InterruptedException
。这是一个异常,当另一个线程在sleep
处于活动状态时中断当前线程时会抛出。由于这个应用程序没有定义另一个线程来引起中断,所以它不会去捕获InterruptedException
。
中断
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html
中断是对线程的指示,告诉它应该停止当前操作并执行其他操作。程序员需要决定线程如何响应中断,但通常线程会终止。这是本课程强调的用法。
一个线程通过在要中断的线程的Thread
对象上调用interrupt
来发送中断。为了使中断机制正常工作,被中断的线程必须支持自身的中断。
支持中断
一个线程如何支持自身的中断?这取决于它当前正在做什么。如果线程频繁调用抛出InterruptedException
的方法,它只需在捕获异常后从run
方法返回。例如,假设SleepMessages
示例中的中央消息循环在线程的Runnable
对象的run
方法中。那么可以修改如下以支持中断:
for (int i = 0; i < importantInfo.length; i++) { // Pause for 4 seconds try { Thread.sleep(4000); } catch (InterruptedException e) { // We've been interrupted: no more messages. return; } // Print a message System.out.println(importantInfo[i]); }
许多抛出InterruptedException
的方法,如sleep
,设计为在接收到中断时取消当前操作并立即返回。
如果一个线程长时间不调用抛出InterruptedException
的方法会怎样?那么它必须定期调用Thread.interrupted
,如果接收到中断则返回true
。例如:
for (int i = 0; i < inputs.length; i++) { heavyCrunch(inputs[i]); if (Thread.interrupted()) { // We've been interrupted: no more crunching. return; } }
在这个简单的例子中,代码只是检测中断并在接收到中断时退出线程。在更复杂的应用程序中,抛出InterruptedException
可能更合理:
if (Thread.interrupted()) { throw new InterruptedException(); }
这使得中断处理代码可以集中在catch
子句中。
Java 中文官方教程 2022 版(九)(3)https://developer.aliyun.com/article/1486344