1. 认识NIO
之前介绍了基于InputStream、OutputStream、Reader、Writer 的输入输出,
对于高阶输入输出处理,Java 从JDK1.4 开始提供了NIO(New IO),
而Java SE 7 之后又提供了NIO2,认识与善用这些高阶输入输出处理API,
对于输入输出的处理效率会有很大的帮助。
1.1 NIO 概念
InputStream、OutputStream 的输入输出,基本上是以字节数组为单位进行低层处理,
虽然你得直接面对字节数组,但实际上多半是对字节数组阵列中整个区块进行处理。
例如,下面的方法。
实际上是整块资料读入后又整块资料写出,然而你必须处理byte[],必须记录读取的字节数,必须指定写出的byte[]起点与字节数:
public static void dump(InputStream src, OutputStream dest) throws IOException {
try (src; dest) {
byte[] data = new byte[1024];
int length;
while ((length = src.read(data)) != -1) {
dest.write(data, 0, length);
}
}
}
虽然java.io API中也有一些装饰( Decorator ) 类型,
像DataInputStream、DataOutputStream、 BufferedReader 与BufferedWriter 等。
不过,若只要对感兴趣的区块进行处理,这些类别就不见得适合,必须自行撰写API 或寻找相关的类库来处理索引、标记等细节。
相对于串流输入输出使用InputStream、OutputStream 来衔接资料来源与目的地,
NIO 使用频道(Channel)来衔接文档节点,在处理资料时,NIO 可以让你设定缓冲区(Buffer)容量,在缓冲区中对感兴趣的资料区块进行标记,像是标记读取位置、资料有效位置。
对于这些区块标记,提供了clear()、 rewind()、flip()、compact()等高阶操作。
举例来说,上面的 dump()方法, 若使用 NIO 的话,可以这样写:
public static void dump(ReadableByteChannel src, WritableByteChannel dest) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (src; dest) {
while (src.read(buffer) != -1) {
buffer.flip();
dest.write(buffer);
buffer.clear();
}
}
}
之后会说明API 的细节,现阶段你可以先了解的是,在这段代码中,
你只要确认有将资料从Channel 中读入Buffer(read()方法不传回-1),
使用高阶filp()方法标记Buffer 中读入资料的所在区块,
然后Buffer 中的资料写到另一个Channel,
最后使用clear()方法清除Buffer 中的标记,
这个过程中,你不用接触byte[]的相关细节。
1.2 Channel 架构与常见操作
NIO 中Channel 相关接口与类型,都在java.nio.channels 包中,
Channel 接口是AutoCloseable 的子接口,因此都可以使用JDK7 之后的自动关闭资源的语法,
Channel 接口上主要新增了isOpen()方法,用来确认Channel 是否开启,
对NIO 入门来说,主要可以先认识以下的Channel 继承架构:
ReadableByteChannel 定义了read()方法, 负责将ReadableByte Channel 中的资料读取至ByteBuffer,
WritableByteChannel 定义了write() 方法, 负责将ByteBuffer 的资料写到WritableByteChannel 中,
ScatteringByteChannel 定义的read()方法,可以将ScatteringByte Channel分配到ByteBuffer 阵列中,
GatheringByteChannel 定义的write() 方法可以将ByteBuffer数组的资料写到GatheringByteChannel。
ByteChannel 没有定义任何方法,单纯继承了 ReadableByteChannel 与 WritableByteChannel 的 行 为 ,
ByteChannel 的子接口SeekableByte Channel 可以读取与改变下一个要存取资料的位置。
我们在API 文件上可以看到Channel 的实际类型,都是抽象类别,不能直接实例化,想要取得Channel 的实例,可以使用Channels 类别,它定义了静态方法newChannel(),
可以让你从InputStream、OutputStream 分别建立ReadableByteChannel 、 WritableByteChannel ,
有些InputStream、OutputStream 实例本身也有方法可以取得Channel 实例,
比如,FileInputStream、FileOutputStream 都有个getChannel()方法可 以 分 别 取 得 FileChannel 实 例 ( 实现了SeekableByteChannel 、 GatheringByteChannel, ScatteringByteChannel 接口)。如果你已经有相关的 Channel实例,也可以透过 Channels上其他 newXXX() 静态方法,取得 InputStream、OutputStream、Reader、Writer 实例。
1.4 Buffer 架构与常见操作
在NIO 设计中,资料都是在java.nio.Buffer 中处理,Buffer 是个抽象类别,定义了clear()、flip()、reset()、rewind()等对资料区块的高阶操作,这类操作返回值都是Buffer,实际上传回this,因此在需要连续高阶操作时,可以形成管线操作风格,Buffer 的类型继承架构如下,它们都是抽象类别:
1.4.1 容量、界限与存取位置
根据不同的资料型态处理需求,你可以选择不同的Buffer子类别,然而它们都是抽象类别,因此你不能直接实例化,然而Buffer 的直接子类别们都有个allocate()静态方法,可以让你指定Buffer 容量(Capacity),
如果是ByteBuffer,容量是指内部实作时使用的byte[]长度,
如果是CharBuffer, 容量是指char[]长度,
如果是FloatBuffer,容量是指float[]长度,
依此类推,Buffer 的容量大小可以使用capacity()方法取得,
如果想取得Buffer 内部的数组,可以使用array()方法,
如果你有个数组想要转为某个Buffer 子类实例,每个Buffer子类别实例都有wrap()静态方法可以提供这项服务。
如果你使用 ByteBuffer,它还有一个 allocateDirect()方法,相较于 allocate()方法配置的内存是由 JVM 管理,allocateDirect()会直接利用操作系统的原生I/O 操作,试着避免JVM 的中介转接,理论上会比allocate() 配置的内存更有效率,不过allocateDirect()在配置内存时会耗用较多系统资源,因此建议只用在大型、存活长的ByteBuffer 物件,并能观察出明显效能差异的场合,想要知道Buffer 是否为直接配置,可以透过isDirect() 得知。
Buffer 是个容器,你填装的资料不会超过它的容量,实际可读取或写入的资料界限(Limit)索引值可以由limit()方法获得或设定,
举例来说,容量为1024 字节的ByteBuffer, ReadableByteChannel 对其写入了512 字节, 那么limit()应该设为512,至于下一个可读取资料的位置(Position)索引值, 可以使用position()方法获得或设定。
1.4.2 clear()、flip()与rewind()
Buffer 的操作可以先从clear()、flip()与rewind()开始学习,当一个缓冲区刚被配置或调用clear()方法后,limit()会等于capacity(), position()会是0,
例如配置了容量为32 位字节的ByteBuffer 时,内部的字节数据容量、资料界限与可读写位置分别是
若 ReadableByteChannel 对 BufferBuffer 写入了 16 个字节,那么 position()就是 16:
现在如果要对ByteBuffer 中已写入的16 字节进行读取,position 必须设回0 , 为了不读取到索引16 , limit 必须设为16 , 虽然可以使用buffer.position(0).limit(16)
来完成这项任务,不过,你可以直接调用flip()
方法,它会将limit 值设为position 目前值,而position 设为0。
下面我们写一个完整的例子:
public class NIOUtil {
public static void dump(ReadableByteChannel src, WritableByteChannel dest) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try (src; dest) {
while (src.read(buffer) != -1) {
buffer.flip();
dest.write(buffer);
buffer.clear();
}
}
}
public static void main(String[] args) throws Exception {
URL url = new URL("http://www.ripjava.com");
ReadableByteChannel src = Channels.newChannel(url.openStream());
try (FileOutputStream fileops = new FileOutputStream("index.html");) {
WritableByteChannel dest = fileops.getChannel();
NIOUtil.dump(src, dest);
}
}
}
这段代码可以从网站下载首页, 并自动在工作资料夹下存为index.html,
在dump()方法中,destCH.write(buffer)会将buffer 中从position 至limit 前的资料写到WritableByteChannel中,
最后position 会等于limit,因此调用clear()将position 设为0,limit 设为等于容量的值,以便下一次循序,
让ReadableByteChannel 将资料写到buffer 中。调用 rewind()方法的话,会将 position 设为 0,而 limit 不变,
这个方法通常用在想要重复读取 Buffer 中某段资料时使用,作用相当于调用 Buffer 的 position(0)方法。
1.4.3 mark()、reset()、remaining()
Buffer上还有个mark()方法,可以在目前position 上标记,在存取Buffer 之后,若调用reset()方法,会将position设回被mark( )标记的位置。
position 与 limit 之间的为剩余可存取的资料,可以使用 remaining()方法得知还有多少 长度,使用 hasRemaining()可以测试是否还有剩余可存取的资料。
2. NIO2文件系统
JDK7提出了NIO2文件系统API,在java.nio.file、java.nio.file.attribute与java.nio.file.spi包中,提供了存取默认文件系统进行各种输入/输出的API,既可简化现有文档输入/输出API的操作,也增加了许多过去没有提供的文件系统存取功能。
2. 1 API架构概述
现今世界上存在着各式各样文件系统,不同文件系统会提供不同的存取方式、文件属性、权限控制等操作。在JDK6之前,常要针对特定文件系统撰写特定程序,不仅撰写方式没有标准,针对特定功能撰写程序也会增加应用程序开发者负担。
NIO2文件系统API提供一组标准接口与类,应用程序开发者只要基于这些标准接口与类进行文件系统操作,底层实际如何进行文件系统操作,是由文件系统提供者负责(由厂商操作)。
应用程序开发者主要使用java.nio.file与java.nio.file.attribute,包中必须操作的抽象类或接口,由文件系统提供者操作,应用程序开发者无须担心底层实际如何存取文件系统;
通常只有文件系统提供者才需关心java.nio.file.spi包。
NIO2文件系统的中心是java.nio.file.spi.FileSystemProvider,本身为抽象类,是文件系统提供者才要操作的类,作用是产生java.nio.file与java.nio.file.attribute中各种抽象类或接口的操作对象。
对应用程序开发者而言,只要知道有FileSystemProvider的存在即可。应用程序开发者可以通过java.nio.file包中FileSystems、Paths、Files等类提供的静态方法,取得相关操作对象或进行各种文件系统操作,这些静态方法内部会运用FileSystemProvider来取得所需的操作对象,完成应有的操作。
比如想要取得java.nio.file.FileSystem操作对象,可以通过FileSystems.getDefault()取得:
FileSystem fileSystem = FileSystems.getDefault();
FileSystems.getDefault()内部会使用FileSystemProvider操作对象的getFileSystem()方法,取得默认的FileSystem操作对象。如果有其他厂商操作的FileSystemProvider类,可以使用系统属性java.nio.file.spi.DefaultFileSystemProvider指定该厂商操作的类名称,这样就会使用指定的FileSystemProvider操作对象。
一旦更换FileSystemProvider操作对象,通过FileSystems.getDefault(),就会取得该厂商的FileSystem操作对象。当然,FileSystems、Paths、Files等类静态方法使用到的操作对象也会一并更换为该厂商的操作对象。
想要操作文档,就得先指出文档路径。Path实例是在JVM中路径的代表对象,也是NIO2文件系统API操作的起点,NIO2文件系统API中有许多操作,都必须使用Path指定路径。
想要取得Path实例,可以使用Paths.get()
方法。
最基本的使用方式,就是使用字符串路径,可使用相对路径或绝对路径。
例如:
Path path = Paths.get("C:\\workspace");
Paths.get()的第二个参数开始接受不定长度自变量,因此可指定起始路径,之后的路径分段指定。
例如,以下可指定用户目录下的Documents\Downloads:
Path path2 = Paths.get(System.getProperty("user.home"), "Documents", "Downloads");
System.out.println(path2.toString());
如果用户目录是/Users/jundongpei
,那么以上Path实例代表的路径就是/Users/jundongpei/Documents/Downloads
。
Path实例仅代表路径信息,该路径实际对应的文档或文件夹(也是一种文档)不一定存在。
Path提供一些方法取得路径的各种信息。例如:
Path path = Paths.get(System.getProperty("user.home"), "Documents", "Downloads");
System.out.printf("toString: %s%n", path.toString());
System.out.printf("getFileName: %s%n", path.getFileName());
System.out.printf("getName(0): %s%n", path.getName(0));
System.out.printf("getNameCount: %d%n", path.getNameCount());
System.out.printf("subpath(0,2): %s%n", path.subpath(0, 2));
System.out.printf("getParent: %s%n", path.getParent());
System.out.printf("getRoot: %s%n", path.getRoot());
输出如下:
toString: /Users/jundongpei/Documents/Downloads
getFileName: Downloads
getName(0): Users
getNameCount: 4
subpath(0,2): Users/jundongpei
getParent: /Users/jundongpei/Documents
getRoot: /
Path实现了Iterable接口,可以循序取得Path中分段的路径信息,当然也可以使用增强式for循环语法。
例如:
Path path = Paths.get(System.getProperty("user.home"),
"Documents", "Downloads");
for(Path p : path) {
System.out.println(p);
}
输出如下:
Users
jundongpei
Documents
Downloads
路径中若有冗余信息,可以使用normalize()方法移除。
Path p1 = Paths.get("/Users/jundongpei/./Documents/Downloads").normalize();
Path p2 = Paths.get("/Users/jundongpei/../Documents/Downloads").normalize();
System.out.println(p1);
System.out.println(p2);
输出如下:
/Users/jundongpei/Documents/Downloads
/Users/Documents/Downloads
Path的toAbsolutePath()
方法可以将相对路径Path转为绝对路径Path;
如果路径是符号链接(Symbolic link),toRealPath()
可以转换为真正的路径,
若是相对路径则转换为绝对路径,若路径中有冗余信息也会移除。
路径与路径可以使用resolve()
结合。
如果有两个路径,想知道如何从一个路径切换至另一个路径,则可以使用relativize()
方法。
可以使用equals()
方法比较两个Path实例的路径是否相同,
使用startsWith()
比较路径起始是否相同,
使用endsWith()
比较路径结尾是否相同。
如果文件系统支持符号链接,两个路径不同的Path实例,有可能是指向同一文档,
可以使用Files.isSameFile()
测试看看是否如此。
如果想确定Path代表的路径,实际上是否存在文档,可以使用Files.exists()
或Files.notExists()
。
Files.exists()
仅在文档存在时返回true,如果文档不存在或无法确认存不存在(例如没有权限存取文档)则返回false。
Files.notExists()
会在文档不存在时返回true,如果文档存在或无法确认存不存在则返回false。
对于文档的一些基本属性,可以使用Files的isExecutable()
、isHidden()
、isReadable()
、isRegularFile()
、isSymbolicLink()
、isWritable()
等方法来得知。
如果需要更多文件属性信息,则必须通过BasicFileAttributes或搭配FileAttributeView来取得。
2.3 属性读取与设定
在过去并无标准方式取得不同文件系统支持的不同属性,在JDK7中,可以通过BasicFileAttributes、DosFileAttributes、PosixFileAttributes,针对不同文件系统取得支持的属性信息。
BasicFileAttributes顾名思义,就是取得各文件系统中都支持的属性,可以通过Files.readAttributes()取得BasicFileAttributes实例。
Path file = Paths.get("/Users/jundongpei/file.txt");
BasicFileAttributes attrs =
Files.readAttributes(file, BasicFileAttributes.class);
System.out.printf("creationTime: %s%n", attrs.creationTime());
System.out.printf("lastAccessTime: %s%n", attrs.lastAccessTime());
System.out.printf("lastModifiedTime: %s%n", attrs.lastModifiedTime());
System.out.printf("isDirectory: %b%n", attrs.isDirectory());
System.out.printf("isOther: %b%n", attrs.isOther());
System.out.printf("isRegularFile: %b%n", attrs.isRegularFile());
System.out.printf("isSymbolicLink: %b%n", attrs.isSymbolicLink());
System.out.printf("size: %d%n", attrs.size());
输出如下:
creationTime: 2019-06-10T13:43:27Z
lastAccessTime: 2019-10-08T11:51:14.48452Z
lastModifiedTime: 2019-06-10T13:43:28.23568Z
isDirectory: false
isOther: false
isRegularFile: true
isSymbolicLink: false
size: 40
creationTime()
、lastAccessTime()
、lastModifiedTime()
返回的是FileTime
实例,也可以通过Files.getLastModifiedTime()
取得最后修改时间。
若想设定最后修改时间,可以通过Files.setLastModifiedTime()
指定代表修改时间的FileTime实例:
// 修改最后修改时间
long currentTimeMillis = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTimeMillis);
Files.setLastModifiedTime(file, ft);
System.out.printf("lastModifiedTime: %s%n", attrs.lastModifiedTime());
实际上,Files.setLastModifiedTime()只是个简便方法,属性设定主要可通过Files.setAttribute()方法。
Files.setAttribute()第二个自变量必须指定FileAttributeView子接口规范的名称,
格式为[view-name:]attribute-name。
view-name可以从FileAttributeView子接口操作对象的name()方法取得,
如果省略就默认为"basic",attribute-name可在FileAttributeView各子接口的API文件中查询。
例如,同样设定最后修改时间,改用Files.setAttributes()可以这样撰写:
long currentTimeMillis = System.currentTimeMillis();
FileTime ft = FileTime.fromMillis(currentTimeMillis);
Files.setAttribute(file, "lastModifiedTime", ft);
类似地,可以通过Files.getAttribute()方法取得各种文件属性,使用方式类似setAttributes(),
也可从通过Files.readAttributes()另一版本取得Map<String, Object>对象,键部分指定属性名称,就可以取得属性值。例如:
Map<String, Object> readAttributes = Files.readAttributes(file, "size,lastModifiedTime,lastAccessTime");
DosFileAttributes继承BasicFileAttributes,新增了isArchive()、isHidden()、isReadOnly()、isSystem()等方法,
可以这样取得DosFileAttributes实例:
DosFileAttributes dosAttributes = Files.readAttributes(file,DosFileAttributes.class);
PosixFileAttributes继承BasicFileAttributes,新增了owner()、group()方法,可取得UserPrincipal(java.security.Principal子接口)、GroupPrincipal(UserPrincipal子接口)实例,可分别取得文档的群组(Group)与拥有者(Owner)信息;permissions()会以Set返回Enum类型的PosixFilePermission实例,代表文档拥有者、群组与其他用户的读写权限信息。
如果想取得储存装置本身的信息,可以利用Files.getFileStore()
方法取得指定路径的FileStore实例,
或通过FileSystem的getFileStores()
方法取得所有储存装置的FileStore实例。
以下是利用FileStore计算磁盘使用率的范例:
public static void main(String[] args) throws IOException {
if (args.length == 0) {
FileSystem fs = FileSystems.getDefault();
for (FileStore store: fs.getFileStores()) {
print(store);
}
}
else {
for (String file: args) {
FileStore store = Files.getFileStore(Paths.get(file));
print(store);
}
}
}
public static void print(FileStore store) throws IOException {
long total = store.getTotalSpace();
long used = store.getTotalSpace() - store.getUnallocatedSpace();
long usable = store.getUsableSpace();
DecimalFormat formatter = new DecimalFormat("#,###,###");
System.out.println(store.toString());
System.out.printf("\t- 总容量\t%s\t字节%n", formatter.format(total));
System.out.printf("\t- 可用空間\t%s\t字节%n", formatter.format(used));
System.out.printf("\t- 已用空間\t%s\t字节%n", formatter.format(usable));
}
FileSystem的getFileStores()方法会以Iterable
一个参考的执行结果如下所示:
/Volumes/PEIJD-01T1 (/dev/disk2s2)
- 总容量 999,860,912,128 字节
- 可用空間 842,038,857,728 字节
- 已用空間 157,822,054,400 字节
2.4 操作文档与目录
如果想要删除Path代表的文档或目录,可以使用Files.delete()
方法,
如果不存在,会抛出NoSuchFileException
,
如果因目录不为空而无法删除文档,会抛出DirectoryNotEmptyException
。
使用Files.deleteIfExists()
方法也可以删除文档,这个方法在文档不存在时调用,并不会抛出异常。
如果想要复制来源Path的文档或目录至目的地Path,可以使用Files.copy()
方法,
这个方法的第三个选项可以指定CopyOption
接口的操作对象,CopyOption操作类有以Enum类型的StandardCopyOption
与LinkOption
。
例如,指定StandardCopyOption
的REPLACE_EXISTING
实例进行复制时,
若目标文档已存在就会予以覆盖,COPY_ATTRIBUTES
会尝试复制相关属性,
LinkOption的NOFOLLOW_LINKS
则不会跟随符号链接。
一个使用Files.copy()的范例如下:
Path srcPath = Paths.get("/Users/jundongpei/file.txt");
Path dest = Paths.get("/Users/jundongpei/file2.txt");
Files.copy(srcPath, dest, StandardCopyOption.REPLACE_EXISTING);
Files.copy()还有两个重载版本,
一个是接受InputStream作为来源,可直接读取数据,并将结果复制至指定的Path中;
另一个Files.copy()版本是将来源Path复制至指定的OutputStream。
例如:
URL url = new URL("http://www.ripjava.com");
Files.copy(url.openStream(), Paths.get("/Users/jundongpei/ripjava.txt"), StandardCopyOption.REPLACE_EXISTING);
若要进行文档或目录移动,可以使用Files.move()方法,使用方式与Files.copy()方法类似,可指定来源Path、目的地Path与CopyOption。
如果文件系统支持原子移动,可在移动时指定StandardCopyOption.ATOMIC_MOVE选项。
如果要建立目录,可以使用Files.createDirectory()方法,如果调用时父目录不存在,会抛出NoSuchFileException。
Files.createDirectories()会在父目录不存在时一并建立。
如果要建立暂存目录,可以使用Files. createTempDirectory()方法,这个方法有可指定路径与使用默认路径建立暂存目录两个版本。
对于java.io中的基本输入/输出API,NIO2也做了封装。
例如,如果Path实例是个文档,可使用Files.readAllBytes()读取整个文档,然后以byte[]返回文档内容;
如果文档内容都是字符,则可使用Files.readAllLines()指定文档Path与编码,读取整个文档,将文档中每行收集在List<String>
中返回。
如果文档内容都是字符,则需要在读取或写入时使用缓冲区,也可以使用Files.newBufferedReader()、Files.newBufferedWriter()指定文档Path与编码,它们分别会返回BufferedReader、BufferedWriter实例,可以使用它们来进行文档读取或写入。
例如:
// java.io
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/Users/jundongpei/ripjava.txt"), "UTF-8"));
// NIO2的封装
BufferedReader bufferedReader = Files.newBufferedReader(Paths.get("/Users/jundongpei/ripjava.txt"), "UTF-8");
在使用Files.newBufferedWriter()时,还可以指定OpenOption接口的操作对象,其操作类为StandardOpenOption与LinkOption(操作了CopyOption与OpenOption),可以指定开启文档时的行为,可以查看StandardOpenOption与LinkOption的API,了解有哪些选项可以使用。
如果想要以InputStream、OutputStream处理文档,也有对应的Files.newInputStream()、Files.newOutputStream()可以使用。