第二章 新I/O
内容提要
- Java7的新I/O API(即NIO.2)
- Path———基于文件和目录的I/O新基础
- Files应用类及其它的各种辅助方法
- 如何实现常见的I/O应用场景
- 介绍异步I/O
2.1 JavaI/O简史
2.1.1 Java 1.0到1.3
在Java的早期(1.0~1.3) 中没有完整的I/O支持。会遇到如下问题。
- 没有数据缓冲区或通道的概念,开发人员要编程处理很多底层细节。
- I/O操作会被阻塞,扩展能力受限。
- 所支持的字符集编码有限,需要进行很多手工编码工作来支持特定类型的硬件。
- 不支持正则表达式,数据处理困难。
在Java 1.4发布之前,Java一直没能在服务器端开发领域得到重用,我们认为主要原因就是缺乏对非阻塞I/O的支持。
2.1.2 在Java 1.4中引入的NIO
为了解决以上问题,Java开始实现对非阻塞I/O的支持,其中有两次主要改进:
- 在Java 1.4中引入非阻塞I/O;
- 在Java 7中对非阻塞I/O进行修改。
以下特性为2002年Java 1.4新增:
- 为I/O操作抽象出缓冲区和通道层;
- 字符集的编码和解码能力;
- 提供了能够将文件映射为内存数据的接口;
- 实现非阻塞I/O的能力;
- 基于流行的Perl实现的正则表达式类库。
NIO的局限性:
- 在不同的平台中对文件名的处理不一致;
- 没有统一的文件属性模型。(比如读写访问模型)
- 遍历目录困难。
- 不能使用平台/操作系统的特性。
- 不支持文件系统的非阻塞操作。(Java 1.4的确支持网络套接字的非阻塞操作。)
2.1.3 下一代I/O-NIO.2
JSR-203最终变成Java 7中见到的NIO.2 API。它有三个主要目标:
- 一个能批量获取文件属性的文件系统接口,去掉和特定文件系统相关的API,还有一个用于引入标准文件系统实现的服务提供者接口。
- 提供一个套接字和文件都能够进行异步(与轮询、非阻塞相对)I/O操作的API。
- 完成JSR-51中定义的套接字——通道功能,包括额外对绑定、选项配置和多播数据报的支持。
2.2 文件I/O的基石:Path
文件系统中的几个概念:
- 目录树
- 根路径
- 绝对路径
- 相对路径
学习文件I/O的关键基础类
类 | 说明 |
---|---|
Path | Path类中的方法可以用来获取路径信息,访问该路径中的各元素,将路径转换为其他形式,或提取路径中的一部分。有的方法还可以匹配路径字串以及移除路径中的冗余项 |
Paths | 工具类,提供返回一个路径的辅助方法,比如get(String first, String... more)和get(URI uri) |
FileSystem | 与文件系统交互的类,无论是默认的文件系统,还是通过其统一资源标识(URI)获取的可选文件系统 |
FileSystems | 工具类,提供各种方法,比如其中用于返回默认文件系统的FileSystems.getDefault() |
2.2.1 创建一个Path
用
Path listing = Paths.get("/usr/bin/zip");
等同于
Path listing = FileSystems.getDefault().getPath("/usr/bin/zip");
2.2.2 从Path中获取信息
import java.nio.file.Path;
import java.nio.file.Paths;
public class Listing_2_1 {
public static void main(String[] args){
Path listing = Paths.get("/usr/bin/zip");//创建绝对路径
System.out.println("File Name [" + listing.getFileName() +"]");//获取文件名
/* 获取名称元素的数量*/
System.out.println("Number of Name Elemen in the Path [" + listing.getNameCount() +"]");
/*获取Path的信息*/
System.out.println("Parent Path [" + listing.getParent() + "]");
System.out.println("Root of Path" + listing.getRoot() + "]");
System.out.println("SubPath form Root, 2 elements deep [" + listing.subpath(0,2) +"]");
}
}
2.2.3 移除冗余项
首先可以用normalize()去掉Path中的冗余信息
Path notmalizePatch = Paths.get("./Listing_2_1.java").normalize();
也可以通过调用toRealPath(),得到表示/application/logs/log1.txt的真正Path。
Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath();
2.2.4 转换Path
Path合并
Path prefix = Paths.get("/uat/");
Path completePath = prefix.resolve("conf/application.properties");
取得两个Path之间的路径
String logging = args[0];
String configuration = args[1];
Path logDir = Paths.get(logging);
Path confDir = Path.get(configuration);
Path pathToConfDir = logDir.relativize(confDir);
2.2.5 NIO.2 Path和Java已有的File类
- java.io.File类中新增了toPath()方法,它可以马上把已有的File转化为为新的Path。
- Path类中有个toFile()方法,它可以马上把已有的Path转化为File。
File file = new File("../Listing_2_1.java");
Path listing = file.toPath();
listing.toAbsolutePath();
file = listing.toFile();
2.3 处理目录和目录树
- 循环遍历目录中的子项,比如查找目录中的文件;
- 用glob表达(比如Foobar)进行目录子项匹配和基于MIME的内容检测(比如text/xml);
- 用walkFileTree方法实现递归移动、复制和删除操作。
2.3.1 在目录中查找文件
代码清单2-2 列出目录下的properties文件
Path dir = Paths.get("C:\\workspace\\java7developer");//①设定起始路径
try(DirectoryStream stream
= Files.newDirectoryStream(dir, "*.properties")){//②声明过滤流
//③ 找出所有.properties 文件并输出
for (Path entry: stream)
{
System.out.println(entry.getFileName());
}
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
过滤流中用到的模式匹配称为glob模式匹配,它和Perl正则表达式类似,但稍有不同。
2.3.2 遍历目录树
关键方法是:
Files.walkFileTree(path startingDir, FileVisitor superPath> visitor);
代码清单2-3 列出子目录下的所有Java源码文件
public class Find{
public static void main(String[] args) throws IOException{
Path startingDir =
Paths.get("C:\\workspace\\java7developer\\src");//设置起始目录
Files.walkFileTree(startingDir, new FindJavaVisitor());//调用walkFileTree
}
private static class FindJavaVisitor
extends SimpleFileVisitor//扩展SimpleFileVisitor
{
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)// 重写visitFile方法
{
if(file.toString().endsWith(".java")) {
System.out.println(file.getFileName());
}
return FileVisitResult.CONTINUE;
}
}
}
2.4 NIO.2的文件系统I/O
对于文件系统的操作任务,比如移动文件、修改文件属性、以及处理文件内容等,在NIO.2中都有所改善。这些操作的支持主要是由Files类提供的。
表2-2 文件处理的基础类
类 | 说明 |
---|---|
Files | 让你轻松复制、移动、删除或处理文件的工具类,有你需要的所有方法 |
WatchService | 用来见识文件或目录的核心类,不管它们有没有变化 |
本节提要:
- 创建和删除文件;
- 移动、复制、重命名和删除文件;
- 文件属性的读写;
- 文件内容的读取和写入;
- 处理符号链接;
- 用WatchService发出文件修改通知;
- 使用SeekableByteChannel————一个可以指定位置及大小的增强型字节通道。
2.4.1 创建和删除文件
基本的文件创建操作
Path target = Paths.get("D:\\backUp\\MyStuff.txt");
Path file = Files.createFile(target);
通常处于安全考虑,要定义创建的文件的读、写、执行权限,需要指明该文件的某些FileAttributes。
下面是一个在POSIX文件系统上为属主、属主组内用户和所有用户设置读/写许可的例子。这种方法允许所有用户对即将创建的文件D:\Backup\MyStuff.txt进行读写操作。
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Set perms =
PosixFilePermission.fromString("rw-rw-rw-");
FileAttribute> attr = PosixFilePermission.asFileAttribute(perms);
Files.createFile(target, attr);
删除比较简单
Path target = Paths.get("D:\\backUp\\MyStuff.txt");
Files.delete(target);
2.4.2文件的复制和移动
基本的复制操作
Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(source, target);
下面这个例子用到了覆盖即替换已有文件的选项
import static java.nio.file.StandardCopyOption.*;
Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(source, target, REPLACE_EXISTING);
其他的复制选项包括COPY_ATTRIBUTES(复制文件属性)和ATOMIC_MOVE(确保在两边的操作都成功,否则回滚)。
移动和复制很像, 移动文件时保留属性,并且覆盖目标文件的示例
import static java.nio.file.StandardCopyOption.*;
Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.move(source, target, REPLACE_EXISTING, COPY_ATTRIBUTES);
2.4.3 文件的属性
1.基本文件属性支持
Files就可以回答与文件相关的各种问题,比如:
- 最后修改时间
- 文件多大
- 是符号连接吗
- 是目录吗
代码清单2-4 通用的文件属性
try{
Path zip = Paths.get("/usr/bin/zip");//获取Path
/**输出属性**/
System.out.println(Files.getLastModifiedTime(zip));
System.out.println(Files.size(zip));
System.out.println(Files.isSymbolicLink(zip));
System.out.println(Files.isDirectory(zip));
System.out.println(Files.readAttributes(zip,"*"));//执行批量读取
} catch(IOException ex){
System.out.println("Exception [" + ex.getMessage() + "]");
}
2.特定文件属性支持
为了支持文件系统特定的文件属性, Java 7允许文件系统提供者实现FileAttributeView和BasicFileAttributes接口。 代码清单2-5 Java 7 对文件属性的支持
import static java.nio.file.attribute.PosixFilePermission.*;
try
{
Path profile = Paths.get("/user/Admin/.profile");
PosixFileAttributes attrs =
Files.readAttributes(profile,
PosixFileAttributes.class);//获取属性视图
Set posixPermissions =
attrs.permissions();//读取访问许可
posixPermissions.clear(); //清除访问许可
/**日志信息*/
String owner = attrs.owner().getName();
String perms =
PosixFilePermissions.toString(posixPermissions);
System.out.format("%s %s%n", owner, perms);
/**设置新的访问许可*/
posixPermissions.add(OWNER_READ);
posixPermissions.add(GROUP_READ);
posixPermissions.add(OTHER_READ);
posixPermissions.add(OWNER_WRITE);
Files.setPosixFilePermissions(profile, posixPermissions);
}
catch(IOException e)
{
System.out.println(e.getMessage());
}
代码清单2-6 符号链接
Path file = Paths.get("/opt/platform/java");
try
{
if(Files.isSymbolicLink(file)) //检查符号链接
{
file = Files.readSymbolicLink(file); //读取符号链接
}
Files.readAttributes(file, BasicFileAttributes.class);//读取文件属性
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
NIO.2 API默认会跟随符号链接。如果不想跟随,需要用LinkOption.NOFOLLOW_LINKS选项。
2.4.4 快速读写数据
1.打开文件
下面的代码演示了Java 7如何用Files.newBufferedReader方法打开文件并按行读取其中的内容。
Path logFile = Paths.get("/tmp/app.log");
try(BUfferedReader reader = Files.newBufferedReader(logFile,
StandardCharsets.UTF_8)){
String line;
while((line = reader.readLine()) !=null ){
...
}
}
打开一个用于写入的文件
Path logFile = Path.get("/tmp/app.log");
try(BufferedWriter writer = Files.newBufferWriter(logFile,
StandardCharsets.UTF_8,
StandardOpenOption.WRITE)){
writer.write("Hello World!");
...
}
2.简化读取和写入
Path logFile = Paths.get("/tmp/app.log");
List lines = Files.readAllLines(logFile, StandardCharsets.UTF_8);
byte[] bytes = Files.readAllBytes(logFile);
2.4.5 文件修改通知
在Java 7中可以用java.nio.file.WatchService类监测文件或目录的变化。是现在某些应用 程序中常用的轮询机制的理想替代品。
代码清单2-7 使用WatchService
import static java.nio.file.StandardWatchEventKinds.*;
try{
WatchService watcher = FileSystem.getDefault().newWatchService();
Path dir = FileSystem.getDefault().getPath("/usr/karianna");
WatchKey key = dir.register(watcher, ENTRY_MODIFY) // 检测变化
while(!shutdown){//检查shutdown标志
/**得到下一个 key及事件*/
key = watcher.take();
for(WatchEvent> event: key.pollEvents()){
if(event.kind() == ENTRY_MODIFY){//检查是否为变化时间
System.out.println("Home dor changed!");
}
}
key.reset();//重置检测key
}
}
catch (IOException | InterruptedException e)
{
System.out.println(e.getMessage());
}
2.4.6 SeekableByteChannel
下面的代码展示了如何运用FileChannel的寻址能力读取日志文件中的最后1000个字符。
Path logFile = Paths.get("c:\\temp.log");
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileChannel channel = FileChannel.open(logFile, StandardOpenOption.READ);
channel.read(buffer, channel.size() - 1000);
2.5 异步 I/O操作
Java 7中有三个新的异步通道:
- AsynchronousFileChannel——用于文件I/O;
- AsynchronousSocketChannel——用于套接字I/O,支持超时;
- AsynchronousServerSocketChannel——用于套接字接受异步连接。
使用新的异步I/O时,主要有两种形式,将来式和回调式。
2.5.1 将来式
代码清单2-8 异步I/O——将来式
try{
Path file = Paths.get("/usr/karianna/foobar.txt");
AsynchromousFileChannel channel =
AsynchromousFileChannel.open(file);//异步打开文件
/**读取100 000字节*/
ByteBuffer buffer = ByteBuffer.allocate(100_000);
Future result = channel.read(buffer,0);
while(!result.isDone()){
ProfitCalculator.calculateTax();//干点别的事
}
Integer byteRead = result.get();//获取结果
System.out,println("Bytes read [" + bytesRead + "]");
}
catch(IOException | ExecutionException | InterruptedException e)
{
System.out.println(e.getMessage());
}
2.5.2 回调式
代码清单2-9 异步 I/O——回调式
try{
Path file = Paths.get("/usr/karianna/foobar.txt")
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(file); //以异步方式打开文件
ByteBuffer buffer = ByteBuffer.allocate(100_000);
/**从通道中读取数据*/
channel.read(buffer, 0, buffer, new CompletionHandler(){
/**读取完成时的回调方法*/
public void completed(Integer result,
ByteBuffer attachment)
{
System.out.println("Bytes read [" + result + "]");
}
public void failed(Throwable exception, ByteBuffer attachment)
{
System.out.println(exception.getMessage());
}
});
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
2.6 Socket和Channel的整合
接触不多,跳读。
- NetworkChannel
- MulticastChannel
2.7 小节
NIO.2 的使用能明显觉得便利,并且不需要花太多心思出像以前一样处理套路式且易错的代码