Java 1.4加入了nio包,Java 1.7 加入了真正的AIO(异步IO),AsynchronousFileChannel就是一个典型的能够异步处理文件的类。html
以前咱们处理文件时,只能阻塞着,等待文件写入完毕以后才能继续执行,读取也是同样的道理,系统内核没有准备好数据时,进程只能干等着数据过来而不能作其余事。AsynchronousFileChannel则能够异步处理文件,而后作其余事,等到真正须要处理数据的时候再处理。java
在引入新的nio的同时,专门添加了不少新的类来取代原来的IO操做方式。
Path和Files就是其中比较基础的两个类,这里先简单介绍下。网络
建立Pathapp
//单个路径建立,注意这里是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();
路径正常化、绝对路径、真实路径async
路径中可使用.
表示当前路径,../
表示父级路径,可是这种表示有时会形成路径冗余,这是可使用下面的几个方法来处理。ide
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虽然看起来像File的工具类,可是实际却在java.nio.file包下面,是后来引入的工具类,通常配合Path使用。工具
这里介绍几个经常使用的方法:post
copy(Path source, Path target)
Path到Path复制,还有第三个可选参数为复制选项,是否覆盖之类的copy(InputStream in, Path target)
从流到Pathcopy(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; } });
上面的抽象方法的返回值是一个枚举,用来决定是否继续遍历:
所以若是是用来所搜文件的话,在找到文件以后能够终止遍历,具体实现这里就不赘述了。
阅读本节以前请先看另外一篇文章:浅析Java NIO
异步的通道有好几个:
AsynchronousFileChannel
:lock,read,writeAsynchronousSocketChannel
:connect,read,writeAsynchronousDatagramChannel
:read,write,send,receiveAsynchronousServerSocketChannel
: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的方式。这里稍微演示一下。
读:
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上好像有人给出了解决方式,可是我没看太明白)
读:
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个字节,也就是说一个字有可能会被分红两半,接着就乱码了😆
此文仅是为另外一篇文章铺路,请结合两篇文章同时阅读。
文章地址:浅析Java NIO
至于网络NIO相关能够参考这篇文章:《在 Java 7 中体会 NIO.2 异步执行的快乐》