探究 NIO
从1.4版本,Java提供了另一套I/O系统,称为NIO即New I/O的缩写。NIO支持面向缓冲区的、基于通道的I/O操作。随着JDK7的发布,Java对NIO系统进行了极大扩展,增强了对文件和文件系统特性的支持。事实上,这些修改是如此重要,以至于经常使用术语NIO.2。缘于NIO文件类提供的功能,NIO预期会成为文件处理中越来越重要的部分。本篇将研究NIO系统的一些关键特性,包括新的文件处理功能。
1 NIO 类
包含NIO类的包如表1所示。
方 法 | 描 述 |
---|---|
java.nio | NIO系统的顶级包,用于封装各种类型的缓冲区,这些缓冲区包含NIO系统所操作的数据 |
java.nio.channels | 支持通道,通道本质上是打开的I/O连接 |
java.nio.channels.spi | 支持通道的服务提供者 |
java.nio.charset | 封装字符集,另外还支持分别将字符转换成字节以及将字节转换成字符的编码和解码器 |
java.nio.charset.spi | 支持字符集的服务停供者 |
java.nio.file | 提供对文件的支持 |
jjava.nio.file.attribute | 提供对文件属性的支持 |
java.nio.file.spi | 支持文件系统的服务提供者 |
NIO系统并非用于替换java.io中基于流的I/O类。
2 NIO 的基础知识
NIO系统构建于两个基础术语之上:缓冲区和通道。缓冲区用于容纳数据,通道表示打开的到I/O设备(例如文件或套接字)的连接。通常,为了使用NIO系统,需要获取用于连接I/O设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区。根据需要输入和输出数据。接下来就更加详细地分析缓冲区和通道。
2.1 缓冲区
缓冲区是在java.nio包中定义的。所有缓冲区都是Buffer类的子类,Buffer类定义了对所有缓冲区都通用的核心功能:当前位置、界限和容量。当前位置是缓冲区下一次发生读取和写入操作的索引,当前位置通过大多数读或写操作向前推进。界限是缓冲区中最后一个有效位置之后下一个位置的索引值。容量是缓冲区能容纳的元素的数量。通常界限等于缓冲区的容量。Buffer类还支持标记和重置。Buffer类定义了一些方法,表2显示了这些方法。
方 法 | 描 述 |
---|---|
abstract Object array() | 如果调用缓冲区是基于数组的,就返回数组的引用,否则抛出UnsupportedOperationException异常;如果数组是只读的,就抛出ReadOnlyBufferException异常 |
abstract int arrayOffset() | 如果调用缓冲区是基于数组的,就返回第一个元素的索引,否则就抛出UnsupportedOperationException异常;如果数组是只读的,就抛出ReadOnlyBufferException异常 |
final int capacity() | 返回调用缓冲区能够容纳的元素的数量 |
final Buffer clear() | 清空调用缓冲区并返回对缓冲区的引用 |
final Buffer flip() | 将调用缓冲区的界限设置为当前位置,并将当前位置重置为0,返回对缓冲区的引用 |
fabstract boolean hasArray() | 如果调用缓冲区是基于读/写数组的,就返回true;否则返回false |
final boolean hasRemaining() | 如果调用缓冲区中还剩余元素,就返回true;否则返回false |
abstract boolean isDirect() | 如果调用缓冲区是定向的,就返回true,这以为着可以直接对缓冲区进行I/O操作;否则返回false |
abstract boolean isReadOnly() | 如果调用缓冲区是只读的,就返回true;否则返回false |
final int limit() | 返回调用缓冲区的界限 |
final Buffer limit(int n) | 将调用缓冲区的界限设置为n。返回缓冲区的引用 |
final Buffer mark() | 对调用缓冲区设置标记并返回对调用缓冲区的引用 |
final int position() | 返回当前位置 |
final Buffer position(int n) | 将调用缓冲区的当前位置设置为n。返回对缓冲区的引用 |
int remaining() | 返回在到达界限之前可用元素的数量。即返回界限减去当前位置后的值 |
final Buffer reset() | 将调用缓冲区的当前位置重置为以前设置的标记。返回对缓冲区的引用 |
final Buffer rewind() | 将调用缓冲区的位置设置为0。返回对缓冲区的引用 |
下面这些特定的缓冲区类派生自Buffer,这些类的名称暗含了它们所能容纳的数据的类型:
ByteBuffer | CharBuffer | DoubleBuffer | FloatBuffer |
IntBuffer | LongBuffer | MappedByteBuffer | ShortBuffer |
MappedByteBuffer是ByteBuffer的子类。用于将文件映射到缓冲区。
前面提到的所有缓冲区类都提供了不同的get()和put()方法,这些方法可以从缓冲区获取数据或将数据放入缓冲区中(如果缓冲区是只读的,就不能使用put()操作)。表3显示了ByteBuffer类定义的get()和put()方法。其他缓冲区类具有类似的方法。所有缓冲区类都支持用于执行各种缓冲区操作的方法。例如,可以使用allocate()方法手动分配缓冲区,使用wrap()方法在缓冲区中封装数组,使用slice()方法创建缓冲的子序列。
方 法 | 描 述 |
---|---|
abstract byte get() | 返回当前位置的字节 |
ByteBuffer get(byte vals[]) | 将调用缓冲区复制到vals引用的数组中,返回对缓冲区的引用。如果缓冲区中剩余元素的数量小于vals.length,就会抛出BufferUnderflowException异常 |
ByteBuffer get(byte vals[],int start,int num) | 从调用缓冲区复制num个元素到vals引用的数组中,返回对缓冲区的引用。如果缓冲区中剩余元素的数量小于num,就会抛出BufferUnderflowException异常 |
abstract byte get(int ids) | 返回调用缓冲区中由ids指定的索引位置的字节 |
abstract ByteBuffer put(byte b) | 将b复制到调用缓冲区的当前位置。返回对缓冲区的引用,如果缓冲区已满,就会排除BufferOverflowException异常 |
final ByteBuffer put(byte vals[]) | 将vals中的所有元素赋值到调用缓冲区中,从当前位置开始。返回对缓冲区的引用,如果缓冲区不能容纳所有元素。就会抛出BufferOverflowException异常 |
ByteBuffer put(byte vals[],int start,int num) | 将vals中从start开始的num个元素复制到调用缓冲区中。返回对缓冲区的引用。如果缓冲区不能容纳全部元素,就会抛出BufferOverflowException异常 |
ByteBuffer put(ByteBuffer bb) | 将bb中的元素复制到调用缓冲区中。从当前位置开始,如果缓冲区不能容纳全部元素,就会抛出BufferOverflowException异常。返回对缓冲区的引用 |
abstract ByteBuffer put(int idx,byte b) | 将b复制到调用缓冲区中idx指定的位置,返回对缓冲区的引用 |
2.2 通道
通道是由java.nio.channels包定义的,通道表示到I/O源或目标的打开的连接。通道实现了Channel接口并扩展了Console接口和AutoCloseable接口。通过实现AutoCloseable接口,可以使用带资源的try语句管理通道。如果使用带资源的try语句,那么当通道不再需要时会自动关闭。
获取通道的一种方式是对支持通道的对象调用getChannel()方法。例如,以下I/O类支持getChannel()方法:
DatagramSocket | FileInputStream | FileOutputStream | |
RandomAccessFile | ServerSocket | Socket |
根据调用getChannel()方法的对象的类型返回特定类型的通道。例如,当对FileInputStream、FileOutoutStream或RandomAccessFile对象调用getChannel()方法时,会返回SocketChannel类型的通道。
获取通道的另外一种方式是使用Files类定义的静态方法。例如,使用Files类,可以通过newByteChannel()方法获取字节通道。该方法返回一个SeekableByteChnanel对象,SeekableByteChannel是FileChannel实现的一个接口。
FileChannel和SocketChannel这类通道支持各种read()和write()方法,使用这些方法可以通过通道执行I/O操作。例如,表4中是为FileChannel定义的一些read()和write()方法。
方 法 | 描 述 |
---|---|
abstract int read(ByteBuffer bb) throws IOException | 从调用通道读取字节到bb中,直到缓冲区已满或者不再有输入内容为止。返回实际读取的字节数。如果读到流的末尾,就返回-1 |
abstract int read(ByteBuffer bb,long start) throws IOException | 从start指定的文件位置开始,从调用通道读取字节到bb中,直到缓冲区已满或者不再有输入内容为止。不改变当前位置。返回实际读取的字节数,如果start超出文件的末尾,就返回-1 |
abstract int write(ByteBuffer bb) throws IOException | 将bb的内容写入调用通道,从当前位置开始。返回写入的字节数 |
abstract int write(ByteBuffer bb,long start) throws IOException | 从start指定的文件位置开始,将bb中的内容写入调用通道,不改变当前位置。返回写入的字节数 |
所有通道都支持一些额外的方法,通过这些方法可以访问和控制通道。例如,FileChannel支持获取或设置当前位置的方法、在文件通道之间传递信息的方法、获取当前通道大小的方法以及锁定通道的方法,等等。FileChannel还提供了静态的open()方法,该方法打开文件并返回指向文件的通道。这提供了获取通道的另外一种方法。FileChannel还提供了map()方法,通过该方法可以将文件映射到缓冲区。
2.3 字符集和选择器
NIO使用的另外两个实体是字符集和选择器。字符集定义了将字节映射为字符的方法。可以使用编码器将一系列字符编码成字节,使用解码器将一系列字节解码成字符。字符集、编码器和解码器由java.nio.charset包中定义的类支持。因为提供了默认的编码器和解码器,所以通常不需要显式地使用字符集进行工作。
选择器支持基于键的、非锁定的多通道I/O。换句话说,使用选择器可以通过多个通道执行I/O。选择器由java.nio.channels包中定义的类支持。选择器最适合用于基于套接字的通道。
3 JDK 7 对 NIO 的增强
从JDK7开始,Java对NIO系统进行了充分扩展和增强。除了支持带资源的try语句外,JDK7对NIO的改进包括:3个新包(java.nio.file,java.nio.file.attribute和java.nio.file.spi);一些新类、接口和方法;以及对基于流的I/O的定向支持。这些增强的内容极大扩展了NIO的使用方式,特别是文件。下面是一些关键的新增内容。
3.1 Path接口
对NIO系统最重要的新增内容可能是Path接口,因为该接口封装了文件的路径。Path接口是NIO.2中将基于文件的许多特性捆绑在一起的黏合剂,描述了目录结构中文件的位置。Path接口被打包到java.nio.file中,并且继承自下列接口:Watchable,Iterable和Comparable接口。
Path接口声明了操作路径的大量方法。表5显示了其中的一下方法。请特别注意getName()方法,该方法用于获取路径中的元素并使用索引进行工作。在0索引位置,也就是路径中最靠近根路径的部分,是路径中最左边的元素。后续索引标识根路径右侧的元素。通过调用getNameCount()方法可以获取路径中元素的数量。如果希望获取整个路径的字符串表示,可简单地调用toString()方法。注意可以使用resolve()方法将相对路径解析为绝对路径。
方 法 | 描 述 |
---|---|
boolean endsWith(String path) | 如果调用Path对象以path指定的路径结束,就返回true;否则返回false |
boolean endsWith(Path path) | 如果调用Path对象以path指定的路径结束,就返回true;否则返回false |
Path getFileName() | 返回调用Path对象关联的文件名 |
Path getName(int idx) | 返回的Path对象包含调用对象中由idx指定的路径元素的名称,最左边的元素位于0索引位置,这是离根路径最近的元素,最右边的元素位于getNameCount()-1索引位置 |
int getCount() | 返回调用Path对象中根目录后面元素的数量 |
Path getParent() | 返回的Path对象包含整个路径,但是不包含由调用Path对象指定的文件的名称 |
Path getRoot() | 返回调用Path对象的根路径 |
boolean isAbsolute() | 如果调用Path对象是绝对路径,就返回true;否则返回false |
Path resolve(Path path) | 如果Path是绝对路径,就返回path;否则,如果path不包含根路径,就在path前面加上由调用Path对象指定的根路径,并返回结果。如果path为空,就返回调用Path对象;否则不对行为进行指定 |
Path resolve(String path) | 如果path是绝对路径,就返回path;否则,如果path不包含根路径,就在path前面加上由调用Path对象指定的根路径,并返回结果。如果path为空,就返回调用Path对象,否则不对行为进行指定 |
boolean startsWith(String path) | 如果调用Path对象以path指定的路径开始,就返回true;否则返回false |
Path toAbsolutePath() | 作为绝对路径返回Path对象 |
String toString() | 返回Path对象的字符串表示形式 |
当更新那些使用File类(在java.io包中定义)的遗留代码时,可以通过File对象调用toPath()方法,将File实例转换成Path实例。此外,可以通过Path接口定义的toFile()方法来获取File实例。
3.2 Files 类
对文件执行的许多操作都是由Files类中的静态方法提供的。要进行操作的文件是由文件的Path对象指定的;因此,Files类的方法使用Path对象指定将要进行操作的文件。Files类提供了广泛的功能。例如,提供了打开或创建具有特定路径的文件的方法。可以获取关于Path对象的信息,例如是否可执行、是隐藏的还是只读的。Files类还支持赋值和移动文件的方法。
表6中显示了Files类提供的一些方法。除了可能抛出IOException异常外,也可能抛出其他异常。JDK8在Files类中添加了以下4个方法:list()、walk()、lines()和find()。它们都返回一个Stream对象。这些方法帮助把NIO与JDK8定义的新的流API集成起来。
方 法 | 描 述 |
---|---|
static Path copy(Path src,Path dest,CopyOption …how) | 将src制定的文件复制到dest指定的位置,参数how指定了复制是如何发生的 |
static Path createDirectory(Path path,FileAttribute<?>…attribs) throws IOException | 创建一个目录,path指定了该目录的路径。目录属性是由attribs指定的 |
值 | 含 义 |
---|---|
TRUNCATE_EXISTING | 将为输出操作而代开的、之前就存在的文件的长度减少到0 |
WRITE | 为输出操作打开文件 |
3.3 Path 接口
因为Path是接口,而不是类,所以不能通过构造函数直接创建Path实例。但是,可以通过调用方法来返回Path实例。通常,使用Path接口定义的get()方法来完成该工作,get()方法有两种形式。在本篇中使用的形式如下所示:
static Path get(String pathname,String ... parts)
该方法返回一个封装指定路径的Path对象。可以通过两种形式指定路径。第一种,如果没有使用parts,就必须通过pathname以整体来指定路径。如果使用了parts,那么可以分块传递路径,使用pathname传递第1部分,通过parts可变长度参数指定后续部分。对于这两种情况,如果指定的路径在语法上无效,get()方法会抛出InvalidPathException异常。
get()方法的第二种形式根据URI来创建Path对象,如下所示:
static Path get(URI uri)
这种形式返回与uri对象的Path对象。
创建链接到文件的Path对象不会导致打开或创建文件。这仅仅创建了封装文件目录的对象而已。
3.4 文件属性接口
与文件关联的是一套属性。这些属性包括文件的创建时间、最后一次修改时间、文件是否是目录以及文件的大小等内容。NIO将文件属性组织到几个接口中。 属性是通过在java.nio.file.attribute包中定义的接口层次表示的。顶部是BasicFileAttributes,该接口封装了在各种文件系统中都通用的一组属性。表21-8显示了BasicFileAttributes接口定义的方法。
方 法 | 描 述 |
---|---|
FileTime creationTime() | 返回文件的创建时间。如果文件系统没有提供创建时间,就返回一个依赖于实现的时间值 |
Object fileKey() | 返回文件键,如果不支持,就返回null |
boolean isDirectory() | 如果文件表示目录就返回true |
boolean isOther() | 如果文件不是文件、符号链接或目录,就返回true |
boolean isRegularFile() | 如果文件是常规文件,而不是目录或符号链接,就返回true |
boolean isSymbolicLink() | 如果文件是符号链接,就返回true |
FileTime lastAccessTime() | 返回文件的最后一次访问时间。如果文件系统没有提供最后访问时间,就返回一个依赖实现的时间值 |
FileTime lastModifiedTime() | 返回文件的最后一次修改时间。如果文件系统没有提供最后一次修改时间,就返回一个依赖于实现的时间值 |
long size() | 返回文件的大小 |
有两个接口派生自BasicFileAttributes:DosFileAttributes和PosixFileAttributes。DosFileAttributes描述了与FAT文件系统相关的那些属性,FAT文件系统最初是由DOS定义的。DOSFileAttributes接口定义的方法如表9所示。
方 法 | 描 述 |
---|---|
boolean isArchive() | 如果文件被标记为存档文件,就返回true;否则返回false |
boolean isHidden() | 如果文件是隐藏的,就返回true;否则返回false |
boolean isSystem() | 如果文件被标记为系统文件,就返回true;否则返回false |
PosixFileAttributes封装了POSIX标准定义的属性(POSIX代表Portable Operating System Interface,即轻便型操作系统接口),该接口定义的方法如表10所示。
方 法 | 描 述 |
---|---|
GroupPrincipal group() | 返回文件的组拥有者 |
UserPrincipal owner() | 返回文件的拥有者 |
Set<PosixFilePermission permission()> | 返回文件的权限 |
返回文件属性的方式有多重。第一种方式,可以通过readAttributes()方法获取用于封装文件属性的对象,该方法是由Files类定义的静态方法,它的其中一种形式如下所示:
static <A extends BasicFileAttributes> A readAttributes(Path path,Class<A> attrType,LinkOption ... opts) throws IOException
这个方法返回一个指定对象的引用,该对象标识了与path传递的文件相关的属性。使用attrType参数作为Class对象指定特定类型的属性。例如,为了获取基本文件属性,向attrType传递BasicFileAttributes.class;对于DOS属性,使用DosFileAttributes.class;对于POSIX属性,使用PosixFileAttributes.class。可选的链接选项是通过opts传递的。如果没有指定该选项,就使用一个符号链接。该方法返回指向所有请求属性的引用。如果请求属性类型不可用,就会抛出UnsupportedOperationException异常。可以使用返回的对象访问文件属性。
访问文件属性的第二种方式是调用Files类定义的getFileAttributeView()方法。NIO定义了一些属性属视图接口,包括AttributeView、BasicFileAttributeView、DosFileAttributeView、PosixFileAttributeView等。
在有些情况下,不需要直接使用文件属性接口,因为Files类提供了一些访问文件属性的静态的便利方法。例如File类提供了isHidden()和isWritable()这类方法。
并不是所有的文件系统都支持所有可能的属性。例如,DOS文件属性应用于最初由DOS定义的FAT文件系统。广泛应用于各种文件系统的属性是由BasicFileAttributes接口描述的。因此,本篇的例子中使用这些属性。
3.5 FileSystem、FileSystems和FileStore类
通过打包到java.nio.file中的FileSystem和FileSystems类,很容易访问文件系统。实际上,使用FileSystem类定义的newFileSystem()方法,甚至可以获取新的文件系统。FileStore类封装了文件存储系统。
4 使用 NIO 系统
下面将演示如何应用NIO系统完成各种任务。随着JDK7的发布,Java对NIO系统进行了极大扩展。因此,NIO系统的应用也被极大扩展了。这个增强版本有时被称为NIO.2。因为NIO.2添加的属性是如此丰富,以至于它们改变了许多基于NIO的代码的编写方式,并且增强了使用NIO可以完成的任务类型。本篇剩余的大部分内容和例子都将使用NIO.2的新特性,因此需要JDK7、JDK8或更新的版本。
在过去,NIO的主要目的是进行基于通道的I/O,这到目前仍然是一个非常重要的应用,然而,可以为基于流的I/O以及执行文件操作系统使用NIO。因此,对NIO使用的讨论分为以下3个部分:
- 为基于通道的I/O使用NIO。
- 为基于流的I/O使用NIO。
- 为路径和文件系统操作使用NIO。
因为最常用的I/O设备是磁盘文件。所以本篇剩余部分在示例中使用磁盘文件。因为所有文件通道操作都是基于字节的,所以我们将使用的缓冲区类是ByteBuffer。
在为了通过NIO系统进行访问而打开文件之前,必须获取描述文件的Path对象。完成该工作的一种方式是调用Paths.get()工厂方法。在前面介绍过该方法。在示例中使用的get()方法如下所示:
static Path get(String pathname,String ... parts)
指定路径的第一种方式:可以分块传递,第1部分使用pathname传递,后续部分通过parts可变参数指定。另外一种方式,可以使用pathname指定整个路径,而不是用parts,示例程序将使用这种形式。
4.1 为基于通道的I/O使用NIO
NIO的重要应用是通过通道和缓冲区访问文件。下面演示了使用通道读取文件以及写入文件的一些技术。
1. 通过通道读取文件
使用通道从文件读取数据有多种方式。最常用的方式可能是手动分配缓冲区,然后执行显式的读取操作,读取操作使用来自文件的数据加载缓冲区。下面首先介绍这种方式。
在能够从文件读取数据之前必须打开文件。为此,首先创建描述文件的Path对象,然后使用Path对象打开文件。根据使用文件的方式,有各种打开文件的方式。在这个例子中,将为基于字节的输入打开文件,通过显式的输入操作进行字节输入。所以,这个例子中,将为基于自己的输入打开文件,通过显式的输入操作进行字节输入。所以,这个例子将通过调用Files.newByteChannel()来打开文件并建立链接到文件的通道。newByteChannel()方法的一般形式如下:
static SeekableByteChannel newByteChannel(Path path,OpenOption ... how)throws IOException
该方法返回的SeekableByteChannel对象封装了文件操作的通道。描述文件的Path对象时通过path传递的。参数how指定了打开文件的方式,因为是可变长度参数,所以可以指定0个或多个由逗号隔开的参数。如果没有指定参数,将为输入操作打开文件。SeekableByteChannel是接口,用于描述能够用于文件操作的通道。FileChannel类实现了该接口。如果使用的是默认文件系统,那么可以返回对象强制转换成FileChannel类型。通道使用完之后必须关闭。既然所有通道——包括FileChannel,都实现了AutoCloseable接口,那么可以使用带资源的try语句自动关闭文件,而不必显式地调用close()方法。
接下来,必须通过封装已经存在的数组或通过动态分配缓冲区,缓冲区将由通道使用。示例程序将动态分配缓冲区,但是可以自行选择任何一种方式。因为文件通道提供操作字节数组,所以将使用ByteBuffer定义的allocate()方法获取缓冲区。该方法的一般形式如下所示:
static ByteBuffer allocate(int cap)
其中,cap指定了缓冲区的容量。该方法返回对缓冲区的引用。
创建缓冲区后,在通道上调用read()方法,传递指向缓冲区的引用。在此使用的read()版本如下所示:
int read(Buffer buf) throws IOException
每次调用read()方法时,都使用来自文件的数据填充buf指定的缓冲区。读取是连续的,这以为着每次调用read()方法都会从文件读取后续字节以填充缓冲区。read()方法返回实际读取的字节数量。当试图在文件末尾读取时,该方法返回-1.
下面的程序将应用前面讨论的技术,使用显式的输入操作通过通道读取文件test.txt:
//Use Channel I/O read a file.Requires JDK 7 or later.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
class ExplicitChannelRead {
public static void main(String[] args) {
int count;
Path filePath = null;
//First,obtain a path to the file.
try {
filePath = Paths.get("test.txt");
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
return;
}
try (SeekableByteChannel fChan = Files.newByteChannel(filePath)) {
//Allocate a buffer.
ByteBuffer mBuf = ByteBuffer.allocate(128);
do {
//Read a buffer.
count = fChan.read(mBuf);
//Stop when end of file is reached.
if (count != -1) {
//Rewind the buffer so that it can be read.
mBuf.rewind();
//Read bytes from the buffer and show
//them on the screen as characters.
for (int i = 0; i < count; i++) {
System.out.print((char) mBuf.get());
}
}
} while (count != -1);
} catch (IOException e) {
System.out.println("I/O Error " + e);
}
}
}
下面是该程序的工作原理。首先获取一个Path对象,其中包含test.txt文件的相对路径。将指向该对象的引用赋值给filePath。接下来调用newByteChannel()方法,并传递filePath最为参数,获取链接到文件的通道。因为没有指定打开方式,所以为读取操作打开文件。注意这个通道是由带资源的try语句管理的对象。因此,当代码块结束时会自动关闭通道。然后该程序调用ByteBuffer的allocate()方法分配缓冲区,当读取文件时,缓冲区将容纳文件的内容。指向缓冲区的引用保存在mBuf中。然后调用read()方法将文件内容读取到mBuf中,读取的字节数量保存在count中。接下来调用rewind()方法回绕缓冲区。这个调用是必须的,因为在调用read()方法之后,当前位置位于缓冲区的末尾。为了通过get()方法读取mBuf中的字节,必须将当前位置重置到缓冲区的开头(get()方法是由ByteBuffer定义的)。因为mBuf是字节缓冲区,所以get()方法返回的值时字节。将它们强制转换char类型,从而可以作为文本显示(也可以创建将字节编码成字符的缓冲区,然后读取缓冲区)。当到达文件末尾时,read()方法返回的值将是-1。当到达文件末尾时,自动关闭通道,结束程序。
注意程序在一个try代码块中获取Path,然后使用另外一个try代码块获取并管理与这个路径链接的通道。尽管使用这种方式没有什么错误,但是在许多情况下,可以对其进行简化,从而只使用一个try代码块。在这种方式中,Paths.get()和newByteChannel()方法调用被连接到一起。例如,下面是对该程序进行改写后的版本,该版本使用这种方式:
//A more compact way to open a channel. Requires JDK 7 or later.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
class ExplicitChannelRead {
public static void main(String[] args) {
int count;
//Here,the channel is opened on the Path returned by Paths.get()
//There is no need for the filePath variable.
try (SeekableByteChannel fChan = Files.newByteChannel(Paths.get("test.txt"))) {
//Allocate a buffer.
ByteBuffer mBuf = ByteBuffer.allocate(128);
do {
//Read a buffer.
count = fChan.read(mBuf);
//Stop when end of file is reached.
if (count != -1) {
//Rewind the buffer so that it can be read.
mBuf.rewind();
//Read bytes from the buffer and show
//them on the screen as character.
for (int i = 0; i < count; i++) {
System.out.print((char) mBuf.get());
}
}
} while (count != -1);
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error " + e);
}
}
}
在这个版本中,不再需要filePath变量,并且两个异常都通过相同的try语句进行处理,因为这种方式更紧凑。当不能将Path对象的创建和通道的获取放到一起时,可以使用前一种方式。
读取文件的另外一种方式是将文件映射到缓冲区。这种方式的优点是缓冲区自动包含文件的内容,不需要显示的读操作。为了映射和读取文件内容,需要遵循以下一般过程。首先获取用于封装文件的Path对象,接下来调用Files.newByteChannel()方法,并传递获取的Path对象作为参数,然后将返回的对象转换成FileChannel类型,获取链接到文件的通道。如前所述,newByteChannel()方法返回SeekableByteChannel类的对象。当使用默认文件系统时,可以将这个对象换成FileChannel类型。然后,在通道上调用map()方法,将通道映射到缓冲区。map()方法是由FileChannel定义的,所以需要将返回的对象转换成FileChannel类型。map()方法如下所示:
MappedByteBuffer map(FileChannel.MapMode how,long pos,long size) throws IOException
map()方法导致将文件中的数据映射到内存中的缓冲区。参数how的值决定了允许的操作类型,它必须是以下这些值的一个:
MapMode.READ_ONLY | MapMode.READ_WRITE | MapMode.PRIVATE |
对于读取文件,使用MapMode.READ_ONLY。要读取并写入文件,使用MapMode.READ_WRITE。MapMode.PRIVATE导致创建文件的私有副本,并且对缓冲区的修改不会影响底层的文件。文件中开始进行映射的位置是由pos指定的,并且映射的字节数量是由size指定的。作为MappedByteBuffer返回指向缓冲区的引用,MappedByteBuffer是ByteBuffer的子类。一旦将文件映射到缓冲区,就可以从缓冲区读取文件了。下面是演示这种方式的一个例子:
//Use a mapped file to read a file. Requires JDK 7 or later.
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
class MappedChannelRead {
public static void main(String[] args) {
//Obtain a channel to a file within a try-with-resources block.
try (FileChannel fChan = (FileChannel) Files.newByteChannel(Paths.get("test.txt"))) {
//Get the size of the file.
long fSize = fChan.size();
//Now,map the file into a buffer
MappedByteBuffer mBuf = fChan.map(FileChannel.MapMode.READ_ONLY, 0, fSize);
//Read and display bytes from buffer.
for (int i = 0; i < fSize; i++) {
System.out.print((char) mBuf.get());
}
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error " + e);
}
}
}
在这个程序中,首先创建链接到文件的Path对象,然后通过newByteChannel()方法打开文件。通道被转换成FileChannel类型并保存在fChan中。接下来,调用size()方法以获取文件的大小。然后,对fChan调用map()方法将整个文件映射到内存,并将指向缓冲区的引用保存到mBuf中。注意mBuf被声明为指向MappedByteBuffer的引用。最后通过get()方法读取mBuf中的字节。
2. 通过通道写入文件
与读取文件一样,使用通道将数据写入文件的方式有多种。首先介绍最常用的一种。在这种方式中,手动分配缓冲区,将数据写入缓冲区,然后执行显式的写操作,将数据写入文件。
在向文件中写入数据之前,必须打开文件。为此,首先获取描述文件的Path对象,然后使用Path对象打开文件。在这个例子中,将为进行基于字节的输出打开文件,通过显式的输出操作写入数据。所以,这个例子调用Files.newByteChannel()方法来打开文件,并建立链接到文件的数据。正如前面所显示的,newByteChannel()方法的一般形式如下:
static SeekableByteChannel newByteChannel(Path path,OpenOption...how) throws IOException
该方法返回的SeekableByteChannel对象中封装了用于文件操作的通道。为了针对输入操作打开文件。how参数必须为StandardOpenOption.WRITE。当文件不存在时,如果希望创建文件,还必须指定StandardOpenOption.CREATE(也可以使用表7中显示的其他选项)。SeekableByteChannel是接口,用于描述能够用于文件操作的通道。FileChannel类实现了接口。如果使用的是默认文件系统,可以将返回对象转换成FileChannel类型。当通道使用之后必须关闭。
下面是通过通道写入文件的一种方式,这种方式显式调用write()方法。首先,调用newByteChannel()方法以获取与文件链接的Path对象,然后打开文件,将返回的结果转换成FileChannel类型。接下来分配字节缓冲区,并将数据写入缓冲区。在将数据写入文件之前,在缓冲区上调用rewind()方法,将当前文职设置为0(在缓冲区上的每次输出操作都会增加当前位置。因此在写入文件之前,必须重置当前位置)。然后,对通道调用write()方法,传递缓冲区。下面的程序演示了这个过程。该程序将字母表写入test.txt文件。
//Write to a file using NIO. Requires JDK 7 or later.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
class ExplicitChannelWrite {
public static void main(String[] args) {
//Obtain a channel to a file within a try-with-resources block.
try (FileChannel fChan = (FileChannel) Files.newByteChannel(Paths.get("test.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
//Create a buffer.
ByteBuffer mBuf = ByteBuffer.allocate(26);
//Write some bytes to the buffer.
for (int i = 0; i < 26; i++) {
mBuf.put((byte) ('A' + i));
}
//Reset the buffer so that it can be written
mBuf.rewind();
//Write the buffer to the output file.
fChan.write(mBuf);
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error: " + e);
System.exit(1);
}
}
}
该程序有一个重要方面值得强调。如前所述,在将数据写入mBuf之后,写入文件之前,对mBuf调用rewind()方法。在将数据写入mBuf之后,为了将当前位置重置为0,这是必须做的。请记住,每次对mBuf调用put()方法都会向前推进当前位置。所以在调用write()方法之前,需要将当前位置重置到缓冲区的开头。如果没有这么做,write()方法会认为缓冲区中没有数据。
在输入和输出操作之间,处理缓冲区重置的另外一种方式是调用flip()方法而不是调用rewind()方法。flip()方法将当前位置设置为0,并将界限设置为前一个当前位置。然而,并不是在所有情况下都可以互换这两个方法。
通常在读和写操作之间必须重置缓冲区。例如,对于前面的例子下面的循环会将字面表写入文件3此。请特别注意rewind()方法的调用。
for(int h=0;h<3;h++){
//Write some bytes to the buffer.
mBuf.put((byte)('A'+i));
//Rewind the buffer so that it can be written.
mBuf.rewind();
//Write the buffer to the ouput file
fChan.write(mBuf);
//Write the buffer so that it can be written to again.
mBuf.rewind();
}
注意在每次读和写操作之间都要调用rewind()方法。
关于该程序需要注意的另外一点是:当将缓冲区写入文件时,文件中的钱26个字节将包含输出。如果文件test.txt先前就已经存在,那么在执行程序后,test.txt中的前26个字节将包含字母表,但是文件的剩余部分会保持不变。
写入文件的另外一种方式是将文件映射都到缓冲区。这种方式的优点是:写入缓冲区的数据会被自动写入文件,不需要显示的写操作。为了映射和写入文件内容,需要使用以下一般过程。首先,获取封装文件的Path对象,然后调用Files.newByteChannel()方法,传递获取的Path对象作为参数,创建链接到文件的通道。将newByteChannel()方法返回的引用转换成FileChannel类型。接下来对通道调用map()方法,将通道映射到缓冲区。此前提到过的,map()方法的一般形式:
MappedByteBuffer map(FileChannel.MapMode how,long pos,long size) throws IOException
map()方法导致文件中的数据被映射到内存中的缓冲区。how的值决定了允许的操作类型。为了写入文件,how必须是MapMode.READ_WRITE。文件中开始映射的位置是由pos指定的,映射的字节数量是由size决定的。map()方法返回指向缓冲区的引用。一旦将文件映射到缓冲区,就可以向缓冲区写入数据,并且这些数据会被自动写入文件。所以,不需要对通道执行显式的写入操作。
下面的程序对前面的程序进行了改写,从而使用映射文件。注意在newByteChannel()方法调用中,添加了StandardOpenOption.READ打开选项。这是因为映射缓冲区要么是只读的,要么是读/写的。因此,为了向映射缓冲区中写入数据,必须以读/写模式打开通道。
//Write to a mapped file. Requires JDK 7 or later.
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
class ExplicitChannelWrite {
public static void main(String[] args) {
//Obtain a channel to a file within a try-with-rwsources block.
try (FileChannel fChan = (FileChannel) Files.newByteChannel(Paths.get("test.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE)) {
//Then,map the file into a buffer.
MappedByteBuffer mBuf = fChan.map(FileChannel.MapMode.READ_WRITE, 0, 26);
//Write some bytes to the buffer.
for (int i = 0; i < 26; i++) {
mBuf.put((byte) ('A' + i));
}
}catch (InvalidPathException e){
System.out.println("Path Error "+e);
}catch (IOException e){
System.out.println("I/O Error "+e);
}
}
}
可以看出,对于通道自身没有显式的写操作。因为mBuf被映射到文件,所以对mBuf的修改会自动反映到底层的文件。
3. 使用NIO复制文件
NIO简化了好几种类型的文件操作。尽管不可能对所有这些操作进行分析,不过可以通过一个例子提供这一思想。下面的程序调用NIO的copy()方法来复制文件,该方法是由Files类定义的静态文件。copy()方法有好几种形式,下面是我们将使用的其中一种形式:
static Path copy(Path src,Path dest,CopyOption...how) throws IOException
这种形式将src指定的文件复制到dest指定的文件中。执行复制的方式是由how指定的。因为是可变长度参数,所以可以省略。如果指定how参数,那么参数值可以使下面这些值中的一个或多个,对于所有文件系统这些值都是合法的。
- StandardCopyOption.COPY_ATTRIBUTES:要求复制文件的属性
- StandardCopyOption.NOFOLLOW_LINKS:不使用符号链接
- StandardCopyOption.REPLACE_EXISTING:覆盖先前存在的文件
根据具体的实现,也可能支持其他选项。
下面的程序要是了copy()方法。源文件和目标文件都是在命令行上指定的,首先指定源文件。
//Copy a file using NIO.Requires JDK 7 or later
import java.io.IOException;
import java.nio.file.*;
class NIOCopy {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("Usage: Copy from to");
return;
}
try {
Path source = Paths.get(args[0]);
Path target = Paths.get(args[1]);
//Copy the file.
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error " + e);
}
}
}
4.2 为基于流的I/O使用NIO
从NIO.2开始,可以使用NIO打开I/O流。如果拥有Path对象,那么可以通过调用newInputStream()或newOutputStream()方法来打开文件,它们是Files类定义的静态方法。这些方法返回链接到指定文件的流。使用Path对象打开对象的优点是:NIO定义的所有特性都可以使用。
为了针对基于流的输入操作打开文件,可以使用Files.newInputStream()方法,该方法的一般形式如下所示:
static InputStream newInputStream(Path path,OpenOption ... how) throws IOException
其中,path指定了要打开的文件,how指定了打开文件的方式,how参数的值必须是一个或多个由StandardOpenOption定义的值,在前面描述了这些值(当然,只能应用与输入流相关的选项)。如果没有指定选项,那么文件的打开方式为StandradOpenOption.READ。
一旦文件打开,就可以使用InputStream定义的任何方法。例如,可以使用read()方法从文件读取字节。
下面的程序演示了基于NIO的流I/O的使用。使用NIO特性打开文件并获取流:
/*Display a text file using stream-based,NIO code.
Requires JDK 7 or later.
To use this program,specify the name
of the file that you want to see.
For example,to see a file called TEST.TXT,
use the following command line.
java ShowFile TEST.txt
*/
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
class ShowFile {
public static void main(String[] args) {
int i;
//First,confirm that a filename has been specified
if (args.length != 1) {
System.out.println("Usage: ShowFile filename");
return;
}//Open the file and obtain a stream linked to it.
try (InputStream fin = Files.newInputStream(Paths.get(args[0]))) {
do {
i = fin.read();
if (i != -1) {
System.out.print((char) i);
}
} while (i != -1);
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error " + e);
}
}
}
因为newInputStream()方法返回的流是常规流。所以可以像所有其他流那样使用。例如,为了提供缓冲,可以在缓冲流(如BufferInputStream)中等转流,如下所示:
new BufferedInputStream(Files.newInputStream(Paths.get(args[0])))
现在,读取的所有数据都会被自动缓冲。
为了针对输出操作打开文件,可以使用Files.newOutputStream()方法。该方法如下所示:
static OutputStream newOutputStream(Path path,OpenOption...how) throws IOException
其中,path指定了要打开的文件,how指定了打开文件的方式,how参数的值必须是StandardOpenOption定义的一个或多个值,在前面描述了这些值(只能应用那些与输出流相关的选项)。如果没有指定选项,那么使用StandardOpenOption.WRITE、StandardOpenOption.CREATE和StandardOpenOption.TRUNCATE_EXISTING打开文件。
使用newOutputStream()方法的方式与前面显示的使用newInputStream()方法的方式类似。一旦打开文件,就可以使用OutputStream定义的任何方法了。例如,可以使用write()方法将字节写入文件。为了缓冲流,还可以在BufferOutputStream对象中封装流。
下面的程序显示了newOutputStream()方法的用法。该程序将字母表写入test.txt文件。注意缓冲I/O的用法。
//Demonstrate NIO-based,stream output.Requires JDK 7 or later
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
class NIOStreamWrite {
public static void main(String[] args) {
//Open the file and obtain a stream linked to it.
try (OutputStream fout = new BufferedOutputStream(Files.newOutputStream(Paths.get(
"test.txt")))) {
//Write some bytes to the stream.
for (int i = 0; i < 26; i++) {
fout.write((byte) ('A' + i));
}
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (IOException e) {
System.out.println("I/O Error: " + e);
}
}
}
4.3 为路径和文件系统操作使用NIO
File类处理文件系统以及与文件关联的各种属性,例如文件是否是只读的、隐藏的,等等。还可以使用File类获取与文件路径相关的信息。虽然使用File类完全可以接受,但是NIO.2定义的接口和类为执行这些功能提供了更好的方式。其优点包括支持符号链接。能更好地支持目录树遍历以及改进的元素处理等。下面将显示两种通用文件系统操作的一些例子:获取与路径和文件相关的信息以及获取目录的内容。
请记住:
如果希望使用java.io.file的旧代码更新为使用Path接口,可以使用toPath()方法从File实例获取Path实例。
1. 获取与路径和文件相关的信息
可以使用Path定义的方法获取与路径相关的信息。有些与Path对象描述的文件相关的属性(例如文件是否是隐藏的),可以使用Files类定义我的方法来获取。在此使用的由Path接口定义的方法有getName()、toAbsolutePath()。Files类提供的那些方法是isExecutable()、isHidden()、isReadable()、isWritable()和exists()。如前所示,在表5和表6中对这些方法进行了总结。
警告
使用isExecutable()、isReadable()、isWritable()和exists()这类方法时必须小心,因为在调用这些方法之后,文件系统的状态可能会改变。在这情况下,程序可能会发生故障。这类情况可能暗示存在安全性问题。
其他文件属性可以通过调用Files.readAttributes()方法,请求文件属性列表来进行获取。在下面的程序中,调用这个方法获取与文件关联的BasicFileAttributes,但是也可以将这种通用方式应用到其他类型的文件属性。
下面的程序演示了Path接口和Files类定义的一些方法,以及BasicFileAttributes提供的一些方法。这个程序假定文件test.txt位于examples目录下,example目录必须是当前目录的子目录。
//Obtain information about a path and a file.
//Requires JDK 7 or later.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
class PathDemo {
public static void main(String[] args) {
Path filePath = Paths.get("example\\test.txt");
System.out.println("File Name: " + filePath.getName(1));
System.out.println("Path: " + filePath);
System.out.println("Absolute Path: " + filePath.toAbsolutePath());
System.out.println("Parent: " + filePath.getParent());
if (Files.exists(filePath)) {
System.out.println("File exists");
} else {
System.out.println("File does not exist");
}
try {
if (Files.isHidden(filePath)) {
System.out.println("File is hidden");
} else {
System.out.println("File is not hidden");
}
} catch (IOException e) {
System.out.println("I/O Error: " + e);
}
Files.isWritable(filePath);
System.out.println("File is writable");
Files.isReadable(filePath);
System.out.println("File is readable");
try {
BasicFileAttributes attribs = Files.readAttributes(filePath, BasicFileAttributes.class);
Files.readAttributes(filePath, BasicFileAttributes.class);
if (attribs.isDirectory()) {
System.out.println("This file is a directory");
} else {
System.out.println("This file is not a directory");
}
if (attribs.isRegularFile()) {
System.out.println("This file is a normal file");
} else {
System.out.println("This file is not a normal file");
}
if (attribs.isSymbolicLink()) {
System.out.println("This file is a symbolic link");
} else {
System.out.println("This file is not a symbolic link");
}
System.out.println("File last modified: " + attribs.lastModifiedTime());
System.out.println("File size: " + attribs.size() + " Bytes");
} catch (IOException e) {
System.out.println("Error reading attributes: " + e);
}
}
/**
* 输出:
* File Name: test.txt
* Path: example\test.txt
* Absolute Path: D:\inmoodsideaworkspace\Java8\example\test.txt
* Parent: example
* File exists
* File is not hidden
* File is writable
* File is readable
* This file is not a directory
* This file is a normal file
* This file is not a symbolic link
* File last modified: 2020-02-19T14:05:08.382Z
* File size: 26 Bytes
*/
}
如果使用的计算机支持FAT文件系统(比如DOS文件系统),可以尝试使用DosFileAttrubutes定义的方法。如果使用的是POSIX兼容的系统,那么可以尝试使用PosixFileAttributes。
2. 列出目录的内容
如果路径描述的是目录,那么可以使用Files类定义的静态方法来读取目录的内容。为此,首先调用newDirectoryStream()方法以获取目录流,传递描述目录的Path对象作为参数。newDirectoryStream()方法的其中一种形式如下所示:
static DirectoryStream<Path> newDirectoryStream(Path dirPath) throws IOException
其中,dirPath封装了目录的路径。该方法返回一个DirectoryStream<Path>对象,可以使用该对象获取目录的内容。如果发生I/O错误,该方法抛出IOException异常,并且如果指定的路径不是目录,那么会抛出NoDirectoryException异常(NoDirectoryException是IOException的子类)。如果不允许访问目录,还可能抛出SecurityException异常。
DirectoryStream<Path>实现了AutoCloseable接口,所以可以使用带资源的try语句进行管理。另外还实现了Iterable<Path>,这以为着可以通过DirectoryStream对象来获取目录的内容。进行迭代时,每个目录项都是由一个Path实例表示的。迭代DirectoryStream对象的一种简单方式是使用for-each风格的for循环。但是,DirectoryStream<Path>实现的迭代器,针对每个实例只能获取一次。因此,iterator()方法只能调用一次,并且for-each循环只能执行一次。
下面的程序显示了example目录的名称:
//Display a directory.Requires JDK 7 or later.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
class DirList {
public static void main(String[] args) {
String dirName ="example";
//obtain and manage a directory stream within a try block.
try(DirectoryStream<Path> dirstrm = Files.newDirectoryStream(Paths.get(dirName))){
System.out.println("Directory of "+dirName);
//Because DirectoryStream implements Iterable,wo
//can use a "foreach" loop to display the directory.
for (Path entry:dirstrm){
BasicFileAttributes attribs=Files.readAttributes(entry,BasicFileAttributes.class);
if(attribs.isDirectory()){
System.out.print("<DIR>");
}else{
System.out.print(" ");
}
System.out.println(entry.getName(1));
}
}catch (InvalidPathException e){
System.out.println("Path Error "+e);
}catch (NotDirectoryException e){
System.out.println(dirName+" is not a directory.");
}catch (IOException e){
System.out.println("I/O Error: "+e);
}
}
/**
* 输出:
* Directory of example
* test.txt
*/
}
可以使用两种方式过滤目录的内容,最简单的方式是使用下面这个版本的newDirectoryStream()方法:
static DirectoryStream<Path> newDirectoryStream(Path dirPath,String wildcard) throws IOException
在这个版本中,只能获取与通配符文件名想匹配的方法。其中,通配符文件名是由wildcard指定的。对于wildcard,可以指定完整的文件名,也可以指定glob。glob是定义通用模式的字符串,通用模式使用熟悉的"“和”?“通配符来匹配一个或多个文件,通配符”“匹配0个或多个任意字符,通配符”?"匹配任意一个字符。在glob中还能识别表11中的通配符。
通 配 符 | 描 述 |
---|---|
** | 匹配0个或多个跨越目录的任意字符 |
[chars] | 匹配chars中的任意 一个字符。chars中的*或 ?被看作常规字符而不是挺佩服。通过使用连字符,可以指定某个范围,例如[x-z] |
{globlist} | 匹配一组glob中的任意一个glob,这些glob是由globlist指定的由逗号隔开的glob列表 |
使用"\“和”\?“可以指定一个”“或”?“字符。为了指定”\“字符,需要使用”\",可以将下面这个newDirectoryStream()方法调用替换到前面的程序中试验glob:
Files.newDirectoryStream(Paths.get(dirname),"{Path,Dir}*.{java,class}")
这个调用获取的目录流值包含名称由"Path"或"Dir"开头,并且扩展名为"java"或"class"的文件。因此,能够匹配DirList.java和PathDemo.java这类名称,但是不能匹配MyPathDemo.java。
过滤目录的另外一种方式是使用下面这个版本的newDirectoryStream()方法:
static DiectoryStream<Path> newDirectoryStream(Path dirPath,DirectoryStream.Filter<? super Path> filefilter) throws IOException
其中,DirectoryStream.Filter是定义了以下方法的接口:
boolean accept(T entry) throws IOException
在这个方法中,T将会是Path类型。如果希望在列表中包含entry,就返回true;否则返回false。这种形式的newDirectoryStream()方法的优点是:可以基于文件名之外的其他内容过滤目录。例如,可以根据文件大小、创建日期、修改日期或属性进行过滤。
下面的程序演示了这个过程,该过程只列出可写的文件。
//Display a directory of only those files that are writable.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
class DirList {
public static void main(String[] args) {
String dirname = "example";
//Create a filter that returns true only for writable files.
DirectoryStream.Filter<Path> how = new DirectoryStream.Filter<Path>() {
@Override
public boolean accept(Path filename) throws IOException {
if (Files.isWritable(filename)) {
return true;
}
return false;
}
};
//Obtain and manage a directory stream of writable files.
try (DirectoryStream<Path> dirstrm = Files.newDirectoryStream(Paths.get(dirname), how)) {
System.out.println("Directory of " + dirname);
for (Path entry : dirstrm) {
BasicFileAttributes attribs = Files.readAttributes(entry, BasicFileAttributes.class);
if (attribs.isDirectory()) {
System.out.print("<DIR>");
} else {
System.out.print(" ");
}
}
} catch (InvalidPathException e) {
System.out.println("Path Error " + e);
} catch (NotDirectoryException e) {
System.out.println(dirname + " is not a directory.");
} catch (IOException e) {
System.out.println("I/O Error: " + e);
}
}
}
3. 使用walkFileTree()列出目录树
前面的例子值获取单个目录的内容。然而,有时会希望获取目录树中的文件列表。在过去,这项工作很复杂,但NIO.2使完成这项工作变得很容易,因为现在可以使用Files类定义的walkFileTree()方法来处理目录树。该方法有两种形式,在本篇使用的形式如下所示:
static Path walkFileTree(Path root,FileVisitor<? extends Path> fv) throws IOException
对于要遍历的目录,起始位置的路径是由root传递的。fv传递FileVisitor实例。FileVisitor的实现决定了如何遍历目录树,并且支持访问目录信息。如果发生I/O错误,会抛出IOException异常,也可能抛出SecurityException异常。
FileVisitor是定义遍历目录树时访问文件方式的接口。FileVisitor是泛型接口,其声明如下所示:
interface FileVisitor<T>
为了能在walkFileTree()中使用,T需要时Path类型(或派生自Path的任意类型)。FileViaitor接口定义了表12中所示的方法。
方 法 | 描 述 |
---|---|
FileVisitor postVisitDirectory(T dir,IOException exc) throws IOException | 在访问目录之后调用。目录被传递给dir,任何IOException异常都会被传递给exec。如果exec为null,就表示没有发生异常。结果被返回 |
FileVisitResult preVisitDirectory(T dir,BasicFileAttributes attribs) throws IOException | 在访问目录之前调用。目录被传递给dir,与目录关联的属性被传递给attribs。结果被返回。为了继续检查目录,返回FileVisitResult.CONTINUE |
FileVisitResult visitFile(T file,BasicFileAttributes attribs) throws IOException | 当访问文件时调用。文件被传递给file,与文件关联的属性被传递给attribs。结果被返回 |
FileVisitResult visitFileFailed(T file,IOException exc) throws IOException | 当尝试访问文件失败时调用,访问失败的文件由file传递,IOException异常由exc传递。结果被返回 |
注意每个方法都返回FileVisitResult枚举对象。这个枚举定义了以下值:
CONTINUE | SKIP_SIBLINGS | SKIP_SKIP_TREE | TERMINATE |
通常,为了继续遍历目录和子目录,方法应当返回CONTINUE。对于preVisitDirectory()为了绕过目录及其子兄弟目录并阻止调用postVisitDirectory()方法,会返回SKIP_SIBLINGS;为了只绕过目录及其子目录,返回SKIP_SUBTREE;为了停止目录遍历,返回TERMINATE。
尽管完全可以通过实现FileVisitor定义的这些方法来创建自己的访问器类,但是通常不会这么做,因为SimpleFileVisitor已经提供了一个简单实现。可以只重写感兴趣方法的默认实现。下面的简单示例演示了这个过程。该例显示目录树中以"example"作为根目录的所有文件。
//A simple example that uses walkFileTree() to display a directory tree.
//Requires JDK 7 or later.
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
class MyFileVisitor extends SimpleFileVisitor<Path> {
public FileVisitResult visitResult(Path path, BasicFileAttributes attribs) throws IOException{
System.out.println(path);
return FileVisitResult.CONTINUE;
}
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
class DirTreeList {
public static void main(String[] args) {
String dirname="example";
System.out.println("Directory tree starting with "+dirname+":\n");
try{
Files.walkFileTree(Paths.get(dirname),new MyFileVisitor());
}catch (IOException exc){
System.out.println("I/O Error");
}
}
}
在这个程序中,MyFileVisitor扩展了SimpleFileVisitor,并且只重写了visitFile()方法。在这个例子中,visitFile()方法简单地显示文件,但是更复杂的功能也很容易实现。例如,可以过滤文件或对文件执行操作,比如将它们复制到备份设备。为了清晰起见,使用命名的类重写visitFile()方法,但是也可以使用匿名的内部类。
最后一点:可以使用java.nio.file.WatchService来观察目录的变化。