Java NIO教程(下)

来源:互联网 发布:免费聊天相亲软件 编辑:程序博客网 时间:2024/04/29 03:55

由于原文比较长,我分成了上、中、下三部分介绍,各个部分链接如下:
Java NIO 教程(上)

Java NIO 教程(中)

Java NIO 教程(下)

原文:Java NIO Tutorial,作者:Jakob Jenkov,译文版本:version 1.0

12.Java NIO之DatagramChannel

Java NIO的DatagramChannel是一个发送和接收UDP数据包的Channel。因为UDP是无连接网络协议,所以你不能像从其他Channel那样默认地向DatagramChannel中读写数据。它收发的是数据包。


12.1打开DatagramChannel

打开DatagramChannel如下:
DatagramChannel channel = DatagramChannel.open();channel.socket().bind(new InetSocketAddress(9999));

上例是打开从UDP的9999端口接收数据包的DatagramChannel。


12.2接收数据

通过调用DatagramChannel的receive()方法从DatagramChannel接收数据:
ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();channel.receive(buf);

receive()方法把接收的数据包内容拷贝到指定的Buffer。如果接收的数据包中的数据超过了Buffer的容量,那么剩下的数据将被丢弃。


12.3发送数据

通过调用DatagramChannel的send()方法发送数据:
String newData = "New String to write to file..."                    + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();buf.put(newData.getBytes());buf.flip();int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

上例通过UDP和80端口向“jenkov.com”服务器发送字符串。因为没有监听80端口,所以不会有任何事发生。你不会被通知发送的数据包是否被收到,因为UDP不确保数据的传送。


12.4连接指定的地址

将DatagramChannel连接到网络上指定的地址是可能的。因为UDP是无连接的,所以连接到一个地址是不能建立真正的连接,像TCP Channel那样的连接。然而,它可以锁住DatagramChannel,让其只能从一个指定的地址发送和接收数据包。
channel.connect(new InetSocketAddress("jenkov.com", 80));   

当连接之后,就可以像使用传统的Channel一样使用read()和write()方法。只是它不会保证发送数据的传送。

int bytesRead = channel.read(buf); int bytesWritten = channel.write(buf);


13.Java NIO之管道

Java NIO管道是两个线程间的单向数据连接。Pipe有一个Source Channel和一个Sink Channel。数据被写到Sink Channel,然后这个数据从Source Channel中读取:

13.1创建Pipe

调用Pipe.open()方法打开Pipe。
Pipe pipe = Pipe.open();

13.2向Pipe写数据

为了向Pipe写数据,需要访问Sink Channel。
Pipe.SinkChannel sinkChannel = pipe.sink();

通过调用Sink Channel的write()方法向SinkChannel写数据。

String newData = "New String to write to file..." + System.currentTimeMillis();ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();buf.put(newData.getBytes());buf.flip();while(buf.hasRemaining()) {    sinkChannel.write(buf);}


13.3从Pipe中读数据

从Pipe中读数据,需要访问Source Channel。
Pipe.SourceChannel sourceChannel = pipe.source();

为了从Source Channel中读数据,需要调用它的read()方法。

ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf);

read()方法的int类型返回值,表示向Buffer中读了多少字节。


14.Java NIO vs IO

当学习完Java NIO和IO之后,一个问题会迅速的浮现在脑海中:什么时候使用IO?设么时候使用NIO?
在这一章中,我将会详细的讲述Java NIO和IO之间的区别,它们的使用案例,以及它们如何影响你的代码设计。


14.1Java NIO和IO之间的主要区别

下表中总结了Java NIO和IO之间的主要区别。我在这一节中进一步详细介绍表中的每一个区别。
IONIOStream orientedBuffer orientedBlocking IONon blocking IO Selector

14.2面向流vs面向缓冲

Java NIO和IO之间第一个不同点是,IO是面向流的,NIO是面向缓冲的。所以,这意味着什么呢?
Java IO面向流是指从一个Stream中一次可以读一个或多个字节,直到读完所有字节。这些字节不会缓存到任何地方。此外,不能向前或向后移动Stream中的数据。如果需要向前或向后移动从Stream读来的数据,那么需要先把数据缓存到一个缓冲区里。
Java NIO的缓冲导向方法略有不同。数据读到一个稍后处理缓冲区中。如果需要,可以在缓冲区中向前或向后移动数据。这在处理期间是更加灵活的。但是,你需要检查这个缓冲区中是否包含了需要的所有数据,以便于完全处理它。同时,你需要确保当向这个缓冲区读入更多数据时,不要覆盖这个缓冲区中还未处理的数据。

14.3阻塞vs非阻塞IO

Java IO的各种Stream都是阻塞的。这意味着当一个线程调用read()或write(),这个线程就会阻塞直到有数据可读,或者数据全部被写入。这期间这个线程不做其他任何事。
Java NIO的非阻塞模式,使一个线程发送请求从Channel读数据,并且只能得到当前可用的数据,如果当前没有可用数据,那么就什么都不获取。而不是一直阻塞到有数据可读,所以这个线程可以继续做其他的事。
非阻塞的写也是一样的。一个线程请求一些数据写到Channel,但不会等着数据全部写完。这个线程期间可以继续做其他的事情。
线程将非阻塞IO的空闲时间花在其他Channel的IO操作上。所以单个线程可以管理多个Channel的输入和输出。

14.4Selectors

Java NIO的Selector允许单个线程监听多个输入Channel。可以向一个Selector注册多个Channel,然后使用这一个线程选择Channel,有可以处理的输入的Channel,或者选择准备好写入的Channel。这个选择机制使得单个线程管理多个Channel变得简单。


14.5NIO和IO怎么影响应用程序设计

无论你选择NIO还是IO,都会影响应用程序设计的以下几个方面:
  1. 调用NIO或IO类的API
  2. 数据的处理
  3. 处理数据使用的线程数量

14.5.1API的调用

当然,当使用NIO的API调用和使用IO是不同的。这并不奇怪。而不只是从如InputStream中一个字节一个字节的读数据,而是数据必须先读到缓冲区中,然后在进行处理。


14.5.2数据的处理

当使用纯NIO设计或IO设计,数据的处理也是有影响的。
在IO的设计中,从InputStream或Reader中一个字节一个字节的读数据。想象一下正在处理基于行的文本文件数据的流。
Name: AnnaAge: 25Email: anna@mailserver.comPhone: 1234567890

这个文件行的Stream如下处理:

InputStream input = ... ; // get the InputStream from the client socketBufferedReader reader = new BufferedReader(new InputStreamReader(input));String nameLine   = reader.readLine();String ageLine    = reader.readLine();String emailLine  = reader.readLine();String phoneLine  = reader.readLine();

注意处理状态时通过程序执行了多久决定的。换句话说,一旦第一个reader.readLine()方法返回,你可以确定文本的一整行已经读完。这就是问什么readLine()会阻塞直到一整行被读取。你也知道这行包含的那个名字。同样地,当第二个readLine()返回时,你知道这行包含的那个年龄等。
如你所见,这种程序处理仅仅当有新的数据可以读,并且你知道每一步的数据是什么。一旦在代码中这个执行的线程已经处理过读入的某个数据,该线程不能再回退数据(大多如此)。如:

NIO的实现看起来是不同的。

ByteBuffer buffer = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buffer);

注意第二行从Channel读字节到ByteBuffer。当那个方法返回,你不知道是需要的全部数据是否都在这个Buffer中。你所知道的是这个Buffer包含了一些字节。这就使得处理更加困难。
想想一下如果第一个read(buffer)调用之后,读入到这个Buffer的全部数据只有半行,例如,“Name: An”。可以处理那个数据吗?显然不能。你需要等到至少数据的一整行已经在Buffer中,在此之前,任何处理都是无意义的。
所以,怎么知道Buffer是否包含了足够的数据,使得处理有意义呢?你不知道。唯一能发现的方法是,查看Buffer中的数据。其结果就是,你不得不在知道是否所有数据都在Buffer里之前,检查几次Buffer里的数据。这不仅不高效,而且可能使程序设计变得混乱。

ByteBuffer buffer = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buffer);while(! bufferFull(bytesRead) ) {    bytesRead = inChannel.read(buffer);}

bufferFull()方法可以追踪Buffer中读入了多少数据,并且依据Buffer是否填满,返回true或false。换句话说,如果Buffer准备好处理了,那么它就是填满的。
bufferFull()方法扫描Buffer,但是必须保证在bufferFull()方法调用前后Buffer的状态一样。如果不一样,下一个读入到这个Buffer中的数据可能不能读到正确的位置。这不是不可能的,但是也是另一个需要注意的问题。
如果Buffer满了,它就能被处理。如果不是满的,且要在你的实际案例中有意义,那么你可以处理其中部分的数据。许多情况下并非如此。
下面是Buffer数据循环就绪图:

14.6总结

NIO运行你仅用一个(少量)线程管理多个Channel(网络连接或者文件),但代价是解析数据比从阻塞Stream中读数据更复杂。
如果需要同时管理几千个打开的连接,而这几千个连接每个都只发送少量的数据,如聊天服务器,用NIO实现这样的服务器很可能是有优势的。同样,如果需要有很多打开的连接连在其他计算机上,如P2P网络,使用一个线程管理所有的出站连接是有优势的。一个线程、多个连接的设计描述如下:

如果有很少的高带宽的连接,一次发生很多数据,可能典型的IO服务器实现是最合适的。下图描述了典型的IO服务器设计:


15.Java NIO之Path

Java Path接口是Java NIO 2的一部分,Java NIO 2是Java NIO在java 6和java 7中的更新。Java Path接口在Java 7版本添加到Java NIO中。Path接口位于java.nio.file包中,所以Java Path的全路径名是java.nio.file.Path。
Java Path实例在文件系统中代表一个路径。路径可能指定一个文件或一个目录。路径可以是相对的,也可以是绝对的。绝对路径包含从根目录到指定的文件或目录的完整路径。相对路径包含相对于其他路径到指定文件或目录的路径。相对路径听起来可能有点困惑。不用担心,我将在本节后续详细解释。
不过疑惑一些操作系统中使用path环境变量的文件系统。java.nio.file.Path接口和path环境变量没有关系。
在很多方面,java.nio.file.Path接口和java.io.File类相似,但也有一些不同。在很多情况下,可以用File类代替Path接口。


15.1创建Path实例

为了使用java.nio.file.Path实例,你必须创建Path实例。使用Paths类中的静态方法Paths.get()创建Path实例。如:
import java.nio.file.Path;import java.nio.file.Paths;public class PathExample {    public static void main(String[] args) {        Path path = Paths.get("c:\\data\\myfile.txt");    }}

注意例子顶端的两个import声明。为了使用Path接口和Paths类,必须先import它们。
第二,注意Paths.get("C:\\data\\myfile.txt")方法调用。这是在调用Paths.get()方法创建Path实例。换句话说,Paths.get()方法是Path实例的工厂方法。


15.1.1创建绝对路径

通过调用Paths.get()方法和绝对文件作为参数创建绝对路径。如下例:
Path path = Paths.get("c:\\data\\myfile.txt");

这个绝对路径是C:\data\myfile.txt。双“\”符在Java字符串中是必须的,因为“\”是一个转移字符,意思是紧跟着的字符代表什么字符是此字符串中该位置的真正值。通过写“\\”告诉Java编译器在该字符串中写一个“\”。
上面的路径是Windows文件系统的路径。在Unix系统(Linux,Mac OS,FreeBSD等)上面的绝对路径是这样的:

Path path = Paths.get("/home/jakobjenkov/myfile.txt");

现在的绝对路径是/home/jakobjenkov/myfile.txt
如果你在Windows(路径以“/”开始)上使用这种路径,这个路径会被解释成相对于当前驱动器的路径。如下:
/home/jakobjenkov/myfile.txt

如果在C盘驱动器上会解释成全路径:
C:/home/jakobjenkov/myfile.txt


15.1.2创建相对路径

相对路径是指定从一个路径(基路径)到一个目录或文件的路径。相对路径的全路径(绝对路径)通过合并基路径和相对路径得到。
Java NIO的Path类也可以使用相对路径。通过使用Paths.get(basePath, relativePath)方法创建相对路径。如Java中下面两个相对路径的例子:
Path projects = Paths.get("d:\\data", "projects");Path file     = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");

第一个例子创建一个Java Path实例,指向d:\data\projects目录。第二个例子创建一个Path实例,指向d:\data\projects\a-project\myfile.txt文件。
当使用相对路径时,有两个特殊的符号可以用在路径字符串中。这两个符号是:
  • .
  • ..
“.”符代表当前目录。例如,如果你创建的相对路径像这样:
Path currentDir = Paths.get(".");System.out.println(currentDir.toAbsolutePath());

Java Path实例相应的绝对路径是,上面的代码被执行的目录。
如果“.”放在路径字符串中间,意味着相同的目录路径指向在这一点上。如:
Path currentDir = Paths.get("d:\\data\\projects\\.\\a-project");

该路径对应的路径是:

d:\data\projects\a-project

“..”符表示父目录或者上一层目录。如:
Path parentDir = Paths.get("..");

上例中的Path实例是指该应用程序执行的目录的父目录。
如果在路径字符串中间使用“..”符,它将表示该路径的上一层目录。如:
String path = "d:\\data\\projects\\a-project\\..\\another-project";Path parentDir2 = Paths.get(path);

上例中创建的Java Path实例对应的绝对路径是:
d:\data\projects\another-project

a-project目录后面的“..”符是路径指向它的上一层目录projects,然后该路径引用projects目录下的another-project目录。
“.”和“..”符也可以和Paths.get()方法的两个字符串一起使用。如下面的例子:
Path path1 = Paths.get("d:\\data\\projects", ".\\a-project");Path path2 = Paths.get("d:\\data\\projects\\a-project",                       "..\\another-project");


15.2Path.normalize()

Path接口的normalize()方法可以标准化路径。标准化是指移除路径字符串中的“.”和“..”符,同时重定向这个路径字符串的指向路径。如下Java Path.normalize()的例子:
String originalPath =        "d:\\data\\projects\\a-project\\..\\another-project";Path path1 = Paths.get(originalPath);System.out.println("path1 = " + path1);Path path2 = path1.normalize();System.out.println("path2 = " + path2);

上面的Path例子先创建了一个中间带有“..”符的路径字符串。然后,用该路径字符串创建Path实例,同时打印该Path实例(准确的讲是打印Path.toString())。
上例在创建Path实例时调用normalize()方法,使得返回一个新的Path实例。这个新的标准化的Path实例也被打印出来。
打印结果如下:
path1 = d:\data\projects\a-project\..\another-projectpath2 = d:\data\projects\another-project

如你所见,这个标准化的路径没有包含a-project\..部分,这部分被遗弃了。移除的部分不再在最后的绝对路径中添加任何东西。


16.Java NIO之Files

Java NIO的Files类(java.nio.file.Files)提供了几个文件系统中操作文件的方法。本章将涵盖最常用的这些方法。Files类包含了许多方法,如果你需要的方法这里没有,也可以查看JavaDoc。
java.nio.file.Files类和java.nio.file.Path实例一起使用,所以在使用Files类前需要先理解Path类。


16.1Files.exists()

Files.exists()方法检查文件系统中指定的路径是否存在。
在文件系统中创建一个不存在Path实例是可能的。例如,如果你计划创建一个目录,首先你得创建相应的Path实例,然后才能创建这个目录。
因为Path实例在文件系统中可能存在,也可能不存在,你需要使用Files.exists()方法确定(如果你需要检查)。
下面是Java Files.exists()的例子:
Path path = Paths.get("data/logging.properties");boolean pathExists =        Files.exists(path,            new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});

这个例子先创建了一个Path实例,指向一个你想检查是否存在的路径。然后,该例调用Files.exist()方法,用Path实例作为第一个参数。
注意Files.exists()方法的第二个参数。这个是一个选项数组,这个数组影响Files.exists()怎么确定该路径是否存在。上面例子中的数组包含LinkOption.NOFOLLOW_LINKS,它是指在文件系统中Files.exists()方法不应该依据符号链接来确定路径是否存在。


16.2Files.createDirectory()

Files.createDirectory()方法根据Path实例创建一个新的目录。下面是Files.createDirectory()的例子:
Path path = Paths.get("data/subdir");try {    Path newDir = Files.createDirectory(path);} catch(FileAlreadyExistsException e){    // the directory already exists.} catch (IOException e) {    //something else went wrong    e.printStackTrace();}

第一行创建一个目录的Path实例。在try-catch块里,Files.createDirectory()方法被调用,且那个路径作为参数。如果该目录创建成功,返回的Path实例指向这个新创建的路径。
如果这个目录已经存在,java.nio.file.FileAlreadyExistsException将会被抛出。如果是其他问题,IOException被抛出。例如,如果新目录的父目录不存在,IOException被抛出。这个父目录是你想创建的新目录的上级目录。因此,它是新目录的父目录。


16.3Files.copy()

Files.copy()方法把一个文件从一个路径拷贝到另一个路径。下面是Java NIO的Files.copy()的例子:
Path sourcePath      = Paths.get("data/logging.properties");Path destinationPath = Paths.get("data/logging-copy.properties");try {    Files.copy(sourcePath, destinationPath);} catch(FileAlreadyExistsException e) {    //destination file already exists} catch (IOException e) {    //something else went wrong    e.printStackTrace();}

首先,这个例子创建了一个源Path实例和一个目的Path实例。然后,这个例子调用Files.copy()方法,且以两个Path实例作为参数。其结果是被源路径引用的文件被拷贝到被目的路径引用的文件。
如果目的文件已经存在,java.nio.file.FileAlreadyExistsException被抛出。如果是其他错误,IOException被抛出。例如,如果拷贝文件的目录不存在,IOException被抛出。


16.3.1重写已存在的文件

强制Files.copy()重写一个已存在的文件是可能的。下面是用Files.copy如何从写已存在文件的例子:
Path sourcePath      = Paths.get("data/logging.properties");Path destinationPath = Paths.get("data/logging-copy.properties");try {    Files.copy(sourcePath, destinationPath,            StandardCopyOption.REPLACE_EXISTING);} catch(FileAlreadyExistsException e) {    //destination file already exists} catch (IOException e) {    //something else went wrong    e.printStackTrace();}

注意Files.copy()方法中的第三个参数。这个参数告诉copy()方法如果目的文件已经存在,那么就重写这个已存在文件。


16.4Files.move()

Java NIO的Files类把文件从一个路径移动到另一个路径的方法。移动文件和重命名文件是一样的,除了移动文件是可以同时把文件移动到其他目录中和改变文件的名字。没错,用java.io.File类的renameTo()方法也可以做到,但是现在在java.nio.file.Files类中也有文件移动方法。
下面是Java File.move()的例子:
Path sourcePath      = Paths.get("data/logging-copy.properties");Path destinationPath = Paths.get("data/subdir/logging-moved.properties");try {    Files.move(sourcePath, destinationPath,            StandardCopyOption.REPLACE_EXISTING);} catch (IOException e) {    //moving file failed.    e.printStackTrace();}

首先,创建源路径和目的路径。源路径表示移动的文件,目的路径表示这个文件应该往哪移。然后,调用Files.move()方法。其结果是文件被移动。
注意Files.move()的第三个参数。这个参数告诉Files.move()方法重写目的路径中已存在的文件。这个参数是可选的。
如果移动文件失败,Files.move()方法抛出IOException异常。例如,如果目的路径中的文件已存在,而你又没有使用StandardCopyOption.REPLACE_EXISTING选项,或者需要移动的文件不存在等。


16.5Files.delete()

Files.delete()方法用于删除一个文件或一个目录。下面是Java Files.delete()的例子:
Path path = Paths.get("data/subdir/logging-moved.properties");try {    Files.delete(path);} catch (IOException e) {    //deleting file failed    e.printStackTrace();}

首先,创建要删除的文件的Path。然后,调用Files.delete()方法。如果由于一些原因Files.delete()删除失败(如该文件或目录不存在),IOException会被抛出。


16.6Files.walkFileTree()

Files.walkFileTree()方法用于递归遍历建立目录树。walkFileTree()方法用一个Path实例和一个FileVisitor作为参数。这个Path实例是你想遍历的目录。这个FileVisitor在遍历期间被调用。
在解释怎么遍历之前,先看下FileVisitor接口:
public interface FileVisitor {    public FileVisitResult preVisitDirectory(        Path dir, BasicFileAttributes attrs) throws IOException;    public FileVisitResult visitFile(        Path file, BasicFileAttributes attrs) throws IOException;    public FileVisitResult visitFileFailed(        Path file, IOException exc) throws IOException;    public FileVisitResult postVisitDirectory(        Path dir, IOException exc) throws IOException {}

你必须自己实现FileVisitor接口,并且把你实现的实例作为walkFileTree()方法的参数。你的FileVisitor实现的每一个方法将在遍历目录期间的不同时间点被调用。如果你不需要进入到所有的这些方法,你可以继承SimpleFileVisitor类,它包含了FileVisitor接口的全部方法的默认实现。
下面是walkFileTree()的例子:
Files.walkFileTree(path, new FileVisitor<Path>() {  @Override  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {    System.out.println("pre visit dir:" + dir);    return FileVisitResult.CONTINUE;  }  @Override  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {    System.out.println("visit file: " + file);    return FileVisitResult.CONTINUE;  }  @Override  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {    System.out.println("visit file failed: " + file);    return FileVisitResult.CONTINUE;  }  @Override  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {    System.out.println("post visit directory: " + dir);    return FileVisitResult.CONTINUE;  }});

FileVisitor实现的每一个方法在遍历期间的不同时间点被调用。
preVisitDirectory()方法在访问目录之前调用。postVisitDirectory()方法在访问完目录之后调用。
visitFile()方法在每个文件访问后调用。它只针对文件,而不针对目录。visitFileFailed()方法在访问文件失败时调用。例如,如果没有权限,或其他的错误。
这四个方法都返回一个FileVisitResult的枚举实例。FileVisitResult枚举包含以下四种:
  • CONTINUE
  • TERMINATE
  • SKIP_SIBLNGS
  • SKIP_SUBTREE
通过调用方法返回的这些值,可以决定文件的遍历应该怎么继续进行。
CONTINUE:指文件遍历应该正常继续。
TERMINATE:指现在文件遍历应该终止。
SKIP_SIBLINGS:指文件遍历应该继续,但是不再访问这个文件或目录的任何同级文件或目录。
SKIP_SUBTREE:指文件遍历应该继续,但是不再访问这个目录中的项。这个值如果从preVisitDirectory()返回,就只有一个功能。如果从其他方法返回,它将被解释为CONTINUE。


16.6.1搜索文件

下面是继承SimpleFileVisitor的walkFileTree()寻找一个叫README.txt的文件:
Path rootPath = Paths.get("data");String fileToFind = File.separator + "README.txt";try {  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {        @Override    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {      String fileString = file.toAbsolutePath().toString();      //System.out.println("pathString = " + fileString);      if(fileString.endsWith(fileToFind)){        System.out.println("file found at path: " + file.toAbsolutePath());        return FileVisitResult.TERMINATE;      }      return FileVisitResult.CONTINUE;    }  });} catch(IOException e){    e.printStackTrace();}

16.6.2递归地删除目录

Files.walkFileTree()也可以用于删除一个目录中的所有文件和子目录。Files.delete()方法只能删除一个空目录。通过遍历所有目录同时删除每个目录中的所有文件(在visitFile()中),并且之后再删除这些目录本身(在postVisitDirectory()中),你可以删除一个目录及其子目录和它的文件。下面是递归删除目录的例子:
Path rootPath = Paths.get("data/to-delete");try {  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {    @Override    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {      System.out.println("delete file: " + file.toString());      Files.delete(file);      return FileVisitResult.CONTINUE;    }    @Override    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {      Files.delete(dir);      System.out.println("delete dir: " + dir.toString());      return FileVisitResult.CONTINUE;    }  });} catch(IOException e){  e.printStackTrace();}

16.7Files类中的其他方法

java.nio.file.Files类包含许多有用的方法,如创建符号链接、确定文件大小、设置文件权限等。关于java.nio.file.Files类中这些方法的更多信息,请参考JavaDoc。


17.Java NIO之AsynchronousFileChannel

在Java 7中,AsynchronousFileChannel添加到Java NIO。AsynchronousFileChannel使异步地从文件读数据和向文件写数据成为可能。本节将介绍怎么使用AsynchronousFileChannel。

17.1创建AsynchronousFileChannel

通过AsynchronousFileChannel的静态方法open()创建AsynchronousFileChannel。下面是创建AsynchronousFileChannel的例子:
Path path = Paths.get("data/test.xml");AsynchronousFileChannel fileChannel =    AsynchronousFileChannel.open(path, StandardOpenOption.READ);

open()的第一个参数是Path实例,这个Path实例指向AsynchronousFileChannel关联到的文件。
第二个参数是一个或多个打开选项,它(们)告诉AsynchronousFileChannel对底层文件执行什么操作。在这个例子中,使用StandardOpenOption.READ表示打开这个文件将要读数据。


17.2读数据

你可以用两种方法从一个AsynchronousFileChannel中读数据。每个读数据的方法都要调用AsynchronousFileChannel的一种read()方法。这两种读数据的方法在下面的小节中介绍。


17.2.1通过Future读数据

第一种从AsynchronousFileChannel读数据的方法是,调用返回值是Future的read()方法。read()方法的调用如下:
Future<Integer> operation = fileChannel.read(buffer, 0);

这个版本的read()方法用ByteBuffer作为第一个参数。从AsynchronousFileChannel读出的数据被读入到这个ByteBuffer。第二个参数是文件中开始读数据的起始字节位置。
即使读操作还没完成,这个read()方法也会立即返回。通过调用这个read()方法返回的Future实例的isDone()方法,你可以检查读操作什么时候完成。
下面是这种read()方法的使用例子:
AsynchronousFileChannel fileChannel =     AsynchronousFileChannel.open(path, StandardOpenOption.READ);ByteBuffer buffer = ByteBuffer.allocate(1024);long position = 0;Future<Integer> operation = fileChannel.read(buffer, position);while(!operation.isDone());buffer.flip();byte[] data = new byte[buffer.limit()];buffer.get(data);System.out.println(new String(data));buffer.clear();

这个例子创建一个AsynchronousFileChannel,然后创建一个ByteBuffer,这个ByteBuffer传给read()方法作为参数,并且起始读数据的位置是0。调用read()方法后,这个例子开始循环直到返回的Future的isDone()方法返回值是true。当然,这样是很不高效的使用CPU,但是你可以用其他方式等到读操作完成。
一旦读操作完成,数据读入到ByteBuffer,然后转换成一个字符串打印出来。


17.2.2通过CompletionHandler读数据

第二种从AsynchronousFileChannel读数据的方式是,调用以CompletionHandler作为参数的read()方法。下面是这种read()方法的使用:
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {    @Override    public void completed(Integer result, ByteBuffer attachment) {        System.out.println("result = " + result);        attachment.flip();        byte[] data = new byte[attachment.limit()];        attachment.get(data);        System.out.println(new String(data));        attachment.clear();    }    @Override    public void failed(Throwable exc, ByteBuffer attachment) {    }});

一旦读操作完成,CompletionHandler的completed()方法将被调用。传给comple()方法的Integer参数表示读了多少字节,“attachment”参数被传给read()方法。“attachment”是read()方法的第三个参数。这种情况下,它是数据被读入的ByteBuffer。你可以自由选择用什么对象来附加。
如果读操作失败,CompletionHandler的failed()方法将被调用。


17.3写数据

就像读操作,你有两种方式可以像AsynchronousFileChannel中写数据。每种写数据的方法都要调用AsynchronousFileChannel的一种write()方法。这两种写数据方式将在下面小节介绍。


17.3.1通过Future写数据

AsynchronousFileChannel可以让你异步地写数据。下面是完整的Java AsynchronousFileChannel写的例子:
Path path = Paths.get("data/test-write.txt");AsynchronousFileChannel fileChannel =     AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);ByteBuffer buffer = ByteBuffer.allocate(1024);long position = 0;buffer.put("test data".getBytes());buffer.flip();Future<Integer> operation = fileChannel.write(buffer, position);buffer.clear();while(!operation.isDone());System.out.println("Write done");

首先,AsynchronousFileChannel以写模式打开。然后,创建一个ByteBuffer,并且向它写入数据。接着,这个ByteBuffer里的数据被写入到文件。最后,该例子检查返回的Future看什么时候写操作完成。
注意,在这个代码执行之前,这个文件必须已经存在。如果该文件不存在,write()方法将抛出java.nio.file.NoSuchFileException。
用下面代码,你可以确保该文件Path存在。
if(!Files.exists(path)){    Files.createFile(path);}

17.3.2通过CompletionHandler写数据

你也可以用CompletionHandler代替Future告诉你什么时候写操作完成,以便来向AsynchronousFileChannel写数据。下面是用CompletionHandler向AsynchronousFileChannel写数据的例子:
Path path = Paths.get("data/test-write.txt");if(!Files.exists(path)){    Files.createFile(path);}AsynchronousFileChannel fileChannel =     AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);ByteBuffer buffer = ByteBuffer.allocate(1024);long position = 0;buffer.put("test data".getBytes());buffer.flip();fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {    @Override    public void completed(Integer result, ByteBuffer attachment) {        System.out.println("bytes written: " + result);    }    @Override    public void failed(Throwable exc, ByteBuffer attachment) {        System.out.println("Write failed");        exc.printStackTrace();    }});

当写操作完成时,CompletionHandler的completed()方法被调用。如果由于一些原因写操作失败,failed()方法被调用。
注意ByteBuffer怎么作为附件使用的——这个对象被传给CompletionHandler的方法。



0 0
原创粉丝点击