Java 编程问题:六、Java I/O 路径、文件、缓冲区、扫描和格式化2

简介: Java 编程问题:六、Java I/O 路径、文件、缓冲区、扫描和格式化

134 遍历


对于遍历(或访问)路径有不同的解决方案,其中一种由 NIO.2API 通过FileVisitor接口提供。


此接口公开了一组方法,这些方法表示访问给定路径的递归过程中的检查点。通过覆盖这些检查点,我们可以干预这个过程。我们可以处理当前访问的文件/文件夹,并通过FileVisitResult枚举决定应该进一步执行的操作,该枚举包含以下常量:


   CONTINUE:遍历过程应该继续(访问下一个文件、文件夹、跳过失败等)

   SKIP_SIBLINGS:遍历过程应继续,而不访问当前文件/文件夹的同级

   SKIP_SUBTREE:遍历过程应继续,而不访问当前文件夹中的条目

   TERMINATE:遍历应该残酷地终止


FileVisitor公开的方法如下:


   FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException:对每个访问的文件/文件夹自动调用


   FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException:在访问文件夹内容前自动调用文件夹


   FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException:在目录(包括子目录)中的内容被访问后,或在文件夹的迭代过程中,发生 I/O 错误或访问被编程中止后自动调用


   FileVisitResult visitFileFailed(T file, IOException exc) throws IOException:由于不同原因(如文件属性无法读取或文件夹无法打开)无法访问(访问)文件时自动调用

好的,到目前为止,很好!让我们继续几个实际的例子。




琐碎的文件夹遍历


实现FileVisitor接口需要覆盖它的四个方法。然而,NIO.2 附带了这个接口的一个内置的简单实现,称为SimpleFileVisitor。对于简单的情况,扩展这个类比实现FileVisitor更方便,因为它只允许我们覆盖必要的方法。


例如,假设我们将电子课程存储在D:/learning文件夹的子文件夹中,我们希望通过FileVisitorAPI 访问每个子文件夹。如果在子文件夹的迭代过程中出现问题,我们只会抛出报告的异常。


为了塑造这种行为,我们需要覆盖postVisitDirectory()方法,如下所示:


class PathVisitor extends SimpleFileVisitor<Path> {
  @Override
  public FileVisitResult postVisitDirectory(
      Path dir, IOException ioe) throws IOException {
    if (ioe != null) {
      throw ioe;
    }
    System.out.println("Visited directory: " + dir);
    return FileVisitResult.CONTINUE;
  }
}



为了使用PathVisitor类,我们只需要设置路径并调用其中一个Files.walkFileTree()方法,如下所示(这里使用的walkFileTree()的风格得到起始文件/文件夹和相应的FileVisitor

Path path = Paths.get("D:/learning");
PathVisitor visitor = new PathVisitor();
Files.walkFileTree(path, visitor);



通过使用前面的代码,我们将收到以下输出:

Visited directory: D:\learning\books\ajax
Visited directory: D:\learning\books\angular
...




按名称搜索文件


在计算机上搜索某个文件是一项常见的任务。通常,我们依赖于操作系统提供的工具或其他工具,但如果我们想通过编程实现这一点(例如,我们可能想编写一个具有特殊功能的文件搜索工具),那么FileVisitor可以帮助我们以非常简单的方式实现这一点。本申请存根如下:

public class SearchFileVisitor implements FileVisitor {
  private final Path fileNameToSearch;
  private boolean fileFound;
  ...
  private boolean search(Path file) throws IOException {
    Path fileName = file.getFileName();
    if (fileNameToSearch.equals(fileName)) {
      System.out.println("Searched file was found: " +
        fileNameToSearch + " in " + file.toRealPath().toString());
      return true;
    }
    return false;
  }
}


让我们看看主要检查点和按名称搜索文件的实现:


   visitFile()是我们的主要检查站。一旦有了控制权,就可以查询当前访问的文件的名称、扩展名、属性等。需要此信息才能与搜索文件上的相同信息进行比较。例如,我们比较名字,在第一次匹配时,我们TERMINATE搜索。但是如果我们搜索更多这样的文件(如果我们知道不止一个),那么我们可以返回CONTINUE:

@Override
public FileVisitResult visitFile(
  Object file, BasicFileAttributes attrs) throws IOException {
  fileFound = search((Path) file);
  if (!fileFound) {
    return FileVisitResult.CONTINUE;
  } else {
    return FileVisitResult.TERMINATE;
  }
}


visitFile()方法不能用于查找文件夹。改用preVisitDirectory()或postVisitDirectory()方法。

   visitFileFailed()是第二个重要关卡。调用此方法时,我们知道在访问当前文件时出现了问题。我们宁愿忽略任何这样的问题和搜索。停止搜索过程毫无意义:


@Override
public FileVisitResult visitFileFailed(
  Object file, IOException ioe) throws IOException {
  return FileVisitResult.CONTINUE;
}


preVisitDirectory()和postVisitDirectory()方法没有任何重要的任务,因此为了简洁起见,我们可以跳过它们。


为了开始搜索,我们依赖另一种风格的Files.walkFileTree()方法。这一次,我们指定搜索的起点(例如,所有根)、搜索期间使用的选项(例如,跟随符号链接)、要访问的最大目录级别数(例如,Integer.MAX_VALUE)和FileVisitor(例如,SearchFileVisitor):

Path searchFile = Paths.get("JavaModernChallenge.pdf");
SearchFileVisitor searchFileVisitor 
  = new SearchFileVisitor(searchFile);
EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Iterable<Path> roots = FileSystems.getDefault().getRootDirectories();
for (Path root: roots) {
  if (!searchFileVisitor.isFileFound()) {
    Files.walkFileTree(root, opts,
      Integer.MAX_VALUE, searchFileVisitor);
  }
}

如果您查看本书附带的代码,前面的搜索将以递归方式遍历计算机的所有根(目录)。前面的例子可以很容易地通过扩展名、模式进行搜索,或者从一些文本中查看文件内部。



删除文件夹


在试图删除文件夹之前,我们必须删除其中的所有文件。这个语句非常重要,因为它不允许我们对包含文件的文件夹简单地调用delete()/deleteIfExists()方法。此问题的优雅解决方案依赖于从以下存根开始的FileVisitor实现:

public class DeleteFileVisitor implements FileVisitor {
  ...
  private static boolean delete(Path file) throws IOException {
    return Files.deleteIfExists(file);
  }
}

让我们看看主要检查点和删除文件夹的实现:

  • visitFile()是从给定文件夹或子文件夹删除每个文件的理想位置(如果文件不能删除,则我们只需将其传递到下一个文件,但可以随意调整代码以满足您的需要):
@Override
public FileVisitResult visitFile(
  Object file, BasicFileAttributes attrs) throws IOException {
  delete((Path) file);
  return FileVisitResult.CONTINUE;
}

只有当文件夹为空时,才可以删除它,因此postVisitDirectory()是执行此操作的最佳位置(我们忽略任何潜在的IOException,但可以随意调整代码以满足您的需要(例如,记录无法删除的文件夹的名称或引发异常以停止进程)):

@Override
public FileVisitResult postVisitDirectory(
    Object dir, IOException ioe) throws IOException {
  delete((Path) dir);
  return FileVisitResult.CONTINUE;
}


visitFileFailed()preVisitDirectory()中,我们只返回CONTINUE

删除文件夹时,在D:/learning中,我们可以调用DeleteFileVisitor,如下所示:

Path directory = Paths.get("D:/learning");
DeleteFileVisitor deleteFileVisitor = new DeleteFileVisitor();
EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Files.walkFileTree(directory, opts, 
  Integer.MAX_VALUE, deleteFileVisitor);

通过将SearchFileVisitorDeleteFileVisitor组合,我们可以得到一个搜索删除应用。



复制文件夹


为了复制一个文件,我们可以使用Path copy(Path source, Path target, CopyOption options) throws IOException方法。此方法使用指定如何执行复制的参数options将文件复制到目标文件。

通过将copy()方法与自定义FileVisitor相结合,我们可以复制整个文件夹(包括其所有内容)。本次定制FileVisitor存根代码如下:


public class CopyFileVisitor implements FileVisitor {
  private final Path copyFrom;
  private final Path copyTo;
  ...
  private static void copySubTree(
      Path copyFrom, Path copyTo) throws IOException {
    Files.copy(copyFrom, copyTo, 
      REPLACE_EXISTING, COPY_ATTRIBUTES);
  }
}

让我们看一下主要的检查点和复制文件夹的实现(注意,我们将通过复制任何可以复制的内容来进行操作,并避免抛出异常,但可以随意调整代码以满足您的需要):

   在从源文件夹复制任何文件之前,我们需要复制源文件夹本身。复制源文件夹(空或不空)将导致目标文件夹为空。这是用preVisitDirectory()方法完成的完美任务:


@Override
public FileVisitResult preVisitDirectory(
  Object dir, BasicFileAttributes attrs) throws IOException {
  Path newDir = copyTo.resolve(
    copyFrom.relativize((Path) dir));
  try {
    Files.copy((Path) dir, newDir, 
      REPLACE_EXISTING, COPY_ATTRIBUTES);
  } catch (IOException e) {
    System.err.println("Unable to create "
      + newDir + " [" + e + "]");
    return FileVisitResult.SKIP_SUBTREE;
  }
  return FileVisitResult.CONTINUE;
}

visitFile()方法是复制每个文件的最佳场所:

@Override
public FileVisitResult visitFile(
  Object file, BasicFileAttributes attrs) throws IOException {
  try {
    copySubTree((Path) file, copyTo.resolve(
      copyFrom.relativize((Path) file)));
  } catch (IOException e) {
    System.err.println("Unable to copy " 
      + copyFrom + " [" + e + "]");
  }
  return FileVisitResult.CONTINUE;
}

或者,我们可以保留源目录的属性。只有将文件复制到postVisitDirectory()方法后才能完成(例如,保留上次修改的时间):

@Override
public FileVisitResult postVisitDirectory(
    Object dir, IOException ioe) throws IOException {
  Path newDir = copyTo.resolve(
    copyFrom.relativize((Path) dir));
  try {
    FileTime time = Files.getLastModifiedTime((Path) dir);
    Files.setLastModifiedTime(newDir, time);
  } catch (IOException e) {
    System.err.println("Unable to preserve 
      the time attribute to: " + newDir + " [" + e + "]");
  }
  return FileVisitResult.CONTINUE;
}


如果无法访问文件,则调用visitFileFailed()。现在是检测循环链接并报告它们的好时机。通过以下链接(FOLLOW_LINKS),我们可以遇到文件树与父文件夹有循环链接的情况。这些病例通过visitFileFailed()中的FileSystemLoopException异常报告:


@Override
public FileVisitResult visitFileFailed(
    Object file, IOException ioe) throws IOException {
  if (ioe instanceof FileSystemLoopException) {
    System.err.println("Cycle was detected: " + (Path) file);
  } else {
    System.err.println("Error occured, unable to copy:"
      + (Path) file + " [" + ioe + "]");
  }
  return FileVisitResult.CONTINUE;
}


D:/learning/packt文件夹复制到D:/e-courses

Path copyFrom = Paths.get("D:/learning/packt");
Path copyTo = Paths.get("D:/e-courses");
CopyFileVisitor copyFileVisitor 
  = new CopyFileVisitor(copyFrom, copyTo);
EnumSet opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Files.walkFileTree(copyFrom, opts, Integer.MAX_VALUE, copyFileVisitor);


通过组合CopyFileVisitor和DeleteFileVisitor,我们可以很容易地形成一个移动文件夹的应用。在本书附带的代码中,还有一个移动文件夹的完整示例。基于我们迄今为止积累的专业知识,代码应该是非常容易访问的,没有更多的细节。


在记录有关文件的信息(例如,处理异常的情况)时要注意,因为文件(例如,它们的名称、路径和属性)可能包含敏感信息,这些信息可能会被恶意利用。



JDK8,Files.walk()


从 JDK8 开始,Files类用两个walk()方法进行了丰富。这些方法返回一个由Path惰性填充的Stream。它通过使用给定的最大深度和选项遍历以给定起始文件为根的文件树来实现这一点:

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

例如,让我们显示从D:/learning开始并以D:/learning/books/cdi开头的所有路径:

Path directory = Paths.get("D:/learning");
Stream<Path> streamOfPath = Files.walk(
  directory, FileVisitOption.FOLLOW_LINKS);
streamOfPath.filter(e -> e.startsWith("D:/learning/books/cdi"))
  .forEach(System.out::println);


现在,让我们计算一个文件夹的字节大小(例如,D:/learning):

long folderSize = Files.walk(directory)
  .filter(f -> f.toFile().isFile())
  .mapToLong(f -> f.toFile().length())
  .sum();

此方法为弱一致。它不会在迭代过程中冻结文件树。对文件树的潜在更新可能会反映出来,也可能不会反映出来。



135 监视路径


监视路径的变化只是可以通过 JDK7nio.2(低级的WatchServiceAPI)实现的线程安全目标之一。


简而言之,可以通过以下两个主要步骤来观察路径的变化:


   为不同类型的事件类型注册要监视的文件夹。


   当WatchService检测到注册的事件类型时,它在单独的线程中处理,因此监视服务不会被阻塞。


在 API 级别,起点是WatchService接口。对于不同的文件/操作系统,这个接口有不同的风格。


这个接口与两个主要类一起工作。它们一起提供了一种方便的方法,您可以实现这种方法来将监视功能添加到特定上下文(例如,文件系统):


   Watchable:实现此接口的任何对象都是可观察对象,因此可以观察其变化(例如Path)


   StandardWatchEventKinds:这个类定义了标准的事件类型(这些是我们可以注册通知的事件类型:


       ENTRY_CREATE:创建目录条目

       ENTRY_DELETE:删除目录条目


       ENTRY_MODIFY:目录条目已修改;被认为是修改的内容在某种程度上是特定于平台的,但实际上修改文件的内容应该始终触发此事件类型


       OVERFLOW:一种特殊事件,表示事件可能已经丢失或丢弃


WatchService被称为观察者,我们说观察者观察可观察对象。在下面的示例中,WatchService将通过FileSystem类创建,并监视已注册的Path。



监视文件夹的更改

让我们从一个桩方法开始,该方法获取应该监视其更改的文件夹的Path作为参数:

public void watchFolder(Path path) 
    throws IOException, InterruptedException {
  ...
}


当给定文件夹中出现ENTRY_CREATE、ENTRY_DELETE和ENTRY_MODIFY事件类型时,WatchService将通知我们。为此,我们需要遵循以下几个步骤:


   创建WatchService以便我们可以监视文件系统这是通过FileSystem.newWatchService()完成的,如下所示:

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

注册应通知的事件类型这是通过Watchable.register()完成的:

path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
  StandardWatchEventKinds.ENTRY_MODIFY,
  StandardWatchEventKinds.ENTRY_DELETE);


对于每个可监视对象,我们接收一个注册令牌作为WatchKey实例(监视键)。我们在注册时收到这个监视键,但是每次触发事件时WatchService都返回相关的WatchKey。


   现在,我们需要等待传入事件。这是在无限循环中完成的(当事件发生时,观察者负责将相应的观察键排队等待以后检索,并将其状态更改为已发射:

while (true) {
  // process the incoming event types
}


现在,我们需要检索监视键——检索监视键的方法至少有三种:


   poll():返回队列中的下一个键并将其移除(或者,如果没有键,则返回null)。

   poll(long timeout, TimeUnit unit):返回队列中的下一个键并将其删除;如果没有键,则等待指定的超时并重试。如果键仍然不可用,则返回null。

   take():返回队列中的下一个键并将其删除;如果没有键,则等待某个键排队或无限循环停止:


WatchKey key = watchService.take();


接下来,我们需要检索监视键的未决事件。一个已发射状态的监视键至少有一个挂起事件,我们可以通过WatchKey.pollEvents()方法检索并删除某个监视键的所有事件(每个事件由一个WatchEvent实例表示):

for (WatchEvent<?> watchEvent : key.pollEvents()) {
  ...
}



然后,我们检索关于事件类型的信息。对于每个事件,我们可以获得不同的信息(例如,事件类型、出现次数和上下文特定的信息(例如,导致事件的文件名),这对于处理事件很有用):

Kind<?> kind = watchEvent.kind();
WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
Path filename = watchEventPath.context();


接下来,我们重置监视键。监视键的状态可以是就绪(创建时的初始状态)、已发射或无效。一旦发出信号,一个监视键就会保持这样,直到我们调用reset()方法,尝试将其放回就绪状态,接受事件的状态。如果已发射到就绪(恢复等待事件)转换成功,则reset()方法返回true;否则返回false,表示监视键可能无效。一个监视键如果不再处于活动状态,可能会处于无效状态(显式调用监视键的close()方法、关闭监视程序、删除目录等都会导致不活动):


boolean valid = key.reset();
if (!valid) {
  break;
}

当有一个监视键处于无效状态时,就没有理由停留在无限循环中。只需调用break即可跳出循环。



  1. 最后,我们关闭监视器。具体调用WatchServiceclose()方法或依赖资源尝试,可实现如下:


try (WatchService watchService
    = FileSystems.getDefault().newWatchService()) {
  ...
}



本书附带的代码将所有这些代码片段粘在一个名为FolderWatcher的类中。结果将是一个观察者,它能够报告在指定路径上发生的创建、删除和修改事件。

为了观察路径,即D:/learning/packt,我们只调用watchFolder()方法:

Path path = Paths.get("D:/learning/packt");
FolderWatcher watcher = new FolderWatcher();
watcher.watchFolder(path);

运行应用将显示以下消息:

Watching: D:\learning\packt


现在,我们可以直接在此文件夹下创建、删除或修改文件,并检查通知。例如,如果我们简单地复制粘贴一个名为resources.txt的文件,那么输出如下:

ENTRY_CREATE -> resources.txt
ENTRY_MODIFY -> resources.txt


最后,不要忘记停止应用,因为它将无限期地运行(理论上)。


从这个应用开始,本书附带的源代码附带了另外两个应用。其中一个是视频捕获系统的模拟,而另一个是打印机托盘观察器的模拟。依靠我们在本节中积累的知识,理解这两个应用应该非常简单,无需进一步的细节。

相关文章
|
21天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
102 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
29天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
1月前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
103 34
|
1月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
66 12
|
2天前
|
前端开发 Java 开发工具
Git使用教程-将idea本地Java等文件配置到gitte上【保姆级教程】
本内容详细介绍了使用Git进行版本控制的全过程,涵盖从本地仓库创建到远程仓库配置,以及最终推送代码至远程仓库的步骤。
12 0
|
29天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
160 2
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
64 3
|
16天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
74 17
|
27天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者