NIO前奏之Path、Files、AsynchronousFileChannel

  Java 1.4加入了nio包,Java 1.7 加入了真正的AIO(异步IO),AsynchronousFileChannel就是一个典型的可以异步处理文件的类。

  之前我们处理文件时,只能阻塞着,等待文件写入完毕之后才能继续执行,读取也是一样的道理,系统内核没有准备好数据时,进程只能干等着数据过来而不能做其他事。AsynchronousFileChannel则可以异步处理文件,然后做其他事,等到真正需要处理数据的时候再处理。

Path

  在引入新的nio的同时,专门添加了很多新的类来取代原来的IO操作方式。

  Path和Files就是其中比较基础的两个类,这里先简单介绍下。

创建Path

//单个路径创建,注意这里是Paths静态工厂方法
Path path = Paths.get("data.txt");//以“/”和盘符开头的为绝对路径,“/”会被认为是C盘,相当于“C:/”
//多路径创建,basePath是基础路径,relativePath是相对第一个参数的相对路径,后面可以添加多个相对路径
Path path2 = Path.get(basePath, relativePath, ...);//

Path, File, URI 互转

File file = path.toFile();
Path path = file.toPath();
URI uri = path.toUri();
URI uri = file.toURI();

路径正常化、绝对路径、真实路径

  路径中可以使用.表示当前路径,../表示父级路径,但是这种表示有时会造成路径冗余,这是可以使用下面的几个方法来处理。

Path path = Paths.get("./src");
System.out.println("path = " + path);//path = .\src
System.out.println("normalize : "+ path.normalize());//normalize : src
System.out.println("toAbsolutePath : "+path.toAbsolutePath());//toAbsolutePath : C:\Users\Dell\IdeaProjects\test\.\src
System.out.println("toRealPath : "+path.toRealPath());//toRealPath : C:\Users\Dell\IdeaProjects\test\src

这几个方法返回的还是Path

normalize():压缩路径

toAbsolutePath():绝对路径

toRealPath():真实路径,相当于同时调用上面两个方法。(需要注意调用此方法时路径需要存在,否则会抛出异常)

Files

  Files虽然看起来像File的工具类,但是实际却在java.nio.file包下面,是后来引入的工具类,一般配合Path使用。

这里介绍几个常用的方法:

  • 复制

    • copy(Path source, Path target)Path到Path复制,还有第三个可选参数为复制选项,是否覆盖之类的
    • copy(InputStream in, Path target)从流到Path
    • copy(Path source, OutputStream out)从Path到流
  • 创建文件(夹)
    • createFile(Path path)创建文件
    • createDirectory(Path dir)创建文件夹,父级目录不存在则报错
    • createDirectories(Path dir)创建文件夹,父级目录不存在则自动创建父级目录
  • 移动:move(Path source, Path target)可以移动或者重命名文件(注意这里必须是文件,不能是文件夹),同样也可选是否覆盖
  • 删除:delete(Path path)删除文件
  • 文件是否存在:exists(Path path)
  • 写文本:Files.write(path, Arrays.asList("落霞与孤鹜齐飞,","秋水共长天一色"), StandardOpenOption.APPEND);

具体方法可以查看API文档,这里不再一一赘述。

Files.walkFileTree()

  这是个比较强大的方法,之前我们遍历在一个文件夹里搜索文件或者删除一个非空文件夹的时候只能使用递归(或者自己手动维护一个栈),但是递归效率非常低并且容易爆栈,使用walkFileTree()方法可以很优雅地遍历文件夹。

首先来看看这个方法的其中一种重载的定义:

walkFileTree(Path start, FileVisitor<? super Path> visitor)

第一个参数是遍历的根目录,第二个参数是控制遍历行为的接口,我们来看看这个接口的定义:

public interface FileVisitor<T> {
//访问目录前做什么
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; //访问文件时做什么
FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; //访问文件失败做什么
FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; //访问目录后做什么
FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}

  接口中定义了四个方法,分别规定了我们访问一个文件(夹)前中后失败分别做什么,我们自己访问文件时也就这么几个时间,使用这个接口就不用自己去递归遍历文件夹了。

  FileVisitor接口定义了四个抽象方法,有时候我们只是想要访问文件时做点什么,不关心访问前、访问后做什么,但是却必须实现其功能,这样显得臃肿。

  此时我们可以使用它的适配器实现类:SimpleFileVisitor,该类提供了四个抽象方法的平庸实现,使用的时候只需要重写特定方法即可。

放上个删除非空文夹的Demo:

删除非空文件夹
Path path = Paths.get("D:/dir/test");
Files.walkFileTree(path,new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("删除文件:"+file);
Files.delete(file);
return FileVisitResult.CONTINUE;
} @Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("删除文件夹:"+dir);
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});

上面的抽象方法的返回值是一个枚举,用来决定是否继续遍历:

  • CONTINUE:继续遍历
  • SKIP_SIBLINGS:继续遍历,但忽略当前节点的所有兄弟节点直接返回上一层继续遍历
  • SKIP_SUBTREE:继续遍历,但是忽略子目录,但是子文件还是会访问;
  • TERMINATE:终止遍历

因此如果是用来所搜文件的话,在找到文件之后可以终止遍历,具体实现这里就不赘述了。

AsynchronousFileChannel

阅读本节之前请先看另一篇文章:浅析Java NIO

异步的通道有好几个:

  • AsynchronousFileChannel:lock,read,write
  • AsynchronousSocketChannel:connect,read,write
  • AsynchronousDatagramChannel:read,write,send,receive
  • AsynchronousServerSocketChannel:accept

  分别对应文件IO、TCP IO、UDP IO、服务端TCP IO。和非异步通道正好是对应的。

  这里就只说文件异步IO :AsynchronousFileChannel

异步文件通道的创建

  AsynchronousFileChannel是通过静态工厂的方法创建,通过open()方法可以打开一个Path的异步通道:

Path path = Paths.get("data.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);

第一个参数是关联的Path,第二个参数是操作的方式(或者叫权限,该参数可以省略)

  AsynchronousFileChannel通道的读写分别都有两种方式,一种是Futrue方式,另一种是注册回调函数CompletionHandler的方式。这里稍微演示一下。

Future方式读写

读:

Path path = Paths.get("data.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path,StandardOpenOption.READ);//第二个参数是操作方式
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
//立即返回,不会阻塞
Future<Integer> future = channel.read(buffer, 0);//第二个参数是从哪开始读
//主线程可以继续处理
System.out.println("主线程继续处理..."); //需要处理数据时先判断是否读取完毕
while (!future.isDone()){
System.out.println("还未完成...");
}
byte[] data = new byte[buffer.limit()];
buffer.flip();//切换成读模式
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
channel.close();

写:

Path path = Paths.get("data2.txt");
if (!Files.exists(path)) {
Files.createFile(path);
}
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("为美好的世界献上祝福".getBytes());
buffer.flip();
Future<Integer> future = channel.write(buffer, 0);
//主线程继续处理
System.out.println("主线程继续..."); //需要处理数据时
while (!future.isDone()){
System.out.println("写入未完成");
Thread.sleep(1);
}
System.out.println("写入完成!");

  很可惜,上面open方法的第二个参数不能设置为StandardOpenOption.APPEND,也就是说这种方式的异步写出只能写入一个新文件,写入已有数据的文件的时候源数据会被覆盖。(Stack Overflow上好像有人给出了解决方式,但是我没看太明白)

回调函数CompletionHandler方式读写

读:

Path path = Paths.get("data.txt");
if (!Files.exists(path)) {
Files.createFile(path);
}
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
//这里,使用了read()的重载方法
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("读取完成,读取了"+result+"个字节");
byte[] bytes = new byte[attachment.position()];
attachment.flip();
attachment.get(bytes);
System.out.println(new String(bytes));
attachment.clear();
} @Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("读取失败...");
}
});
System.out.println("主线继续运行...");

  read()的重载方式,可以添加一个回调函数CompletionHandler,当读取成功的时候会执行completed方法,读取失败执行failed方法。

  这个read方法的第一个参数和第二个参数是要读入的缓冲位置,第三个参数是一个附件,可以理解为传入completed方法的参数一般用来传递上下文,比如下面的异步读取大文件就是这么做的),可以为null,第四个参数则是传入的回调函数CompletionHandler,完成或失败的时候会执行这个函数的特定方法。

  需要指出的是在异步读取完成之前不要操作缓冲,也就是read方法的第一个参数。

  回调函数CompletionHandler的第一个泛型代表读取的字节数,第二个泛型就是read方法的第三个参数的类型,例子中我使用了Buffer。

写:

Path path = Paths.get("data2.txt");
if (!Files.exists(path)){
Files.createFile(path);
}
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("为美好的世界献上祝福".getBytes());
buffer.flip();
channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
@Override
public void completed(Integer result, Path attachment) {
System.out.println("写入完毕...");
} @Override
public void failed(Throwable exc, Path attachment) {
System.out.println("写入失败...");
}
}); System.out.println("主线程继续执行...");

至此,四种方式的读写已展示完毕。

  不过你有没有发现,读文件时,不管是Future的方式还是回调的方式,都需要把整个文件加载到内存中来,也就是Buffer的尺寸必须比文件大,有时文件比较大的时候肯定会内存暴涨甚至溢出,那么有没有一种方法可以在一个1000 byte大小的Buffer下读取大文件呢?

神奇的Stack Overflow告诉我们:有!不废话,直接上源码:

import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.IOException; public class TryNio implements CompletionHandler<Integer, AsynchronousFileChannel> { //读取到文件的哪个位置
int pos = 0;
ByteBuffer buffer = null; public void completed(Integer result, AsynchronousFileChannel attachment) {
//如果result为-1代表未读取任何数据
if (result != -1) {
pos += result; //防止读取相同的数据 //操作读取的数据,这里直接输出了
System.out.print(new String(buffer.array(),0,result)); buffer.clear(); //清空缓冲区来继续下一次读取
}
//启动另一个异步读取
attachment.read(buffer, pos , attachment, this ); }
public void failed(Throwable exc,
AsynchronousFileChannel attachment) {
System.err.println ("Error!");
exc.printStackTrace();
} //主逻辑方法
public void doit() {
Path file = Paths.get("data.txt");
AsynchronousFileChannel channel = null;
try {
channel = AsynchronousFileChannel.open(file);
} catch (IOException e) {
System.err.println ("Could not open file: " + file.toString());
System.exit(1);
}
buffer = ByteBuffer.allocate(1000); // 开始异步读取
channel.read(buffer, pos , channel, this );
// 此方法调用后会直接返回,不会阻塞
} public static void main (String [] args) {
TryNio tn = new TryNio();
tn.doit();
//因为doit()方法会直接返回不会阻塞,并且异步读取数据不能让虚拟机保持运行,所以这里添加一个输入来防止程序结束。
try { System.in.read(); } catch (IOException e) { }
}
}

  Stack Overflow上的答主选择直接实现CompletionHandler接口,而不是使用匿名内部类,他给出的原因是:

The magic happens when you initiate another asynchronous read during the complete method. This is why I discarded the anonymous class and implemented the interface itself.

翻译过来就是:

当您在complete方法期间启动另一个异步读取时,会发生魔法。这就是为什么我放弃了匿名类并实现了接口本身。

不过我自己试了试匿名内部类却并么有发生魔法:

Path path = Paths.get("data.txt");
if (!Files.exists(path)) {
Files.createFile(path);
}
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(100); channel.read(buffer, 0, channel, new CompletionHandler<Integer, AsynchronousFileChannel>() {
int pos = 0;
@Override
public void completed(Integer result, AsynchronousFileChannel attachment) {
//如果result为-1代表未读取任何数据
if (result != -1) {
pos += result; //防止读取相同的数据 //操作读取的数据,这里直接输出了
System.out.print(new String(buffer.array(),0,result)); buffer.clear(); //清空缓冲区来继续下一次读取
}
//启动另一个异步读取
attachment.read(buffer, pos , attachment, this );
} @Override
public void failed(Throwable exc, AsynchronousFileChannel attachment) {
System.out.println("读取失败...");
}
}); System.out.println("主线继续运行...");
new Scanner(System.in).nextLine();

  所以还是不太明白为什么答主使用实现类而不是直接使用匿名内部类。

  用上面的方法虽然实现了异步读取大文件,但也不是没有缺点,因为这种方法的原理是在异步中递归调用异步读取,也就是说每次读取1000个字节都需要建立新异步,所以效率并没有理想中的高(不过异步的开销还是比线程低就是了)。

  还有一个小瑕疵就是读取中文的时候会乱码,因为UTF-8中中文一般是3个字节,生僻字会是4个字节,换行是1个字节,也就是说一个字有可能会被分成两半,接着就乱码了

NIO前奏之Path、Files、AsynchronousFileChannel的更多相关文章

  1. Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

  2. Java NIO之拥抱Path和Files

    Java面试通关手册(Java学习指南)github地址(欢迎star和pull):https://github.com/Snailclimb/Java_Guide 历史回顾: Java NIO 概览 ...

  3. NIO.2中Path,Paths,Files类的使用

    Java NIO Java NIO概述 Java NIO(New IO(新io),Non-Blocking IO(非阻塞的io))是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的 ...

  4. Java NIO.2 使用Path接口来监听文件、文件夹变化

    Java7对NIO进行了大的改进,新增了许多功能: 对文件系统的访问提供了全面的支持 提供了基于异步Channel的IO 这些新增的IO功能简称为 NIO.2,依然在java.nio包下. 早期的Ja ...

  5. JAVA nio 2 定义 Path 类

    一旦确认了文件系统上的一个文件或目录,那么就可以定义一个 Path 类来指向它.定义 Path 类可以使用绝对路径.相对路径.路径中带有一个点号“.”(表示当前目录).路径中带有两个点“..”(表示上 ...

  6. JAVA nio 2 和 Path 类简介

    想要初步了解 NIO.2 API,也就是通常所说的“JSR203: More New I/O APIs for the Java Platform”,最好的切入点就是新的抽象类 java.nio.fi ...

  7. Java NIO学习(Path接口、Paths和Files工具类的使用)

    NIO学习:Paths和Files工具类的使用 JDK1.7引入了新的IO操作类.在java.nio.file包下,Java NIO Path接口和Files类. Path接口:Path表示的是一个目 ...

  8. NIO.2中Path、 Paths、Files类的使用

  9. Path,Files巩固,题目:从键盘接收两个文件夹路径,把其中一个文件夹中(包含内容)拷贝到另一个文件夹中

    这个题目用传统的File,InputStream可以做,但是如果用Files,Path类做,虽然思路上会困难一些,但是代码简洁了很多,以下是代码: import java.io.IOException ...

随机推荐

  1. Keras入门——(3)生成式对抗网络GAN

    导入 matplotlib 模块: import matplotlib 查看自己版本所支持的backends: print(matplotlib.rcsetup.all_backends) 返回信息: ...

  2. android 动态壁纸开发

    转:http://www.eoeandroid.com/thread-100389-1-1.html android 动态壁纸开发参考:http://www.ophonesdn.com/article ...

  3. Core Data 基本数据操作 增删改查 排序

    所有操作都基于Core Data框架相关 API,工程需要添加CoreData.framework支持 1.增  NSEntityDescription insertNewObjectForEntit ...

  4. sklearn中实现多分类任务(OVR和OVO)

    sklearn中实现多分类任务(OVR和OVO) 1.OVR和OVO是针对一些二分类算法(比如典型的逻辑回归算法)来实现多分类任务的两种最为常用的方式,sklearn中专门有其调用的函数,其调用过程如 ...

  5. greenplum 存储过程 返回结果集多列和单列

    参考: http://francs3.blog.163.com/blog/static/4057672720125231223786/

  6. POJ 3233:Matrix Power Series 矩阵快速幂 乘积

    Matrix Power Series Time Limit: 3000MS   Memory Limit: 131072K Total Submissions: 18450   Accepted:  ...

  7. C++编程学习(十二) STL

    一.简介 标准模板库STL,是一组模板类和函数.提供了: 1.容器.用于存储信息. 2.迭代器.用于访问容器中的信息. 3.算法.操作容器内容. 1.容器 STL有两种类型的容器类: (1)顺序容器 ...

  8. ROS大型工程学习(三) ROS常用命令行

    1.rosbag 对ros包进行操作的命令. (1)录制包: rosbag record -a //录制数据包,所有topic都录制 rosbag record /topic_name1 /topic ...

  9. Spring源码深度解析-《源码构建》

    1.gradle构建eclipse项目时,gradle-5.0版本构建失败,gradle-3.3构建成功!Why 2.导入spring-framework-3.2.x/spring-beans之前先导 ...

  10. Metasploit学习笔记——Web应用渗透技术

    1.命令注入实例分析 对定V公司网站博客系统扫描可以发现,它们安装了zingiri-web-shop这个含有命令注入漏洞的插件,到www.exploit-db.com搜索,可以看到2011.11.13 ...