4、Java的NIO概览(二)——DatagramChannel、Pipe、Path、File(Windows环境下)
一、DatagramChannel
DatagramChannel 是一种能够发送和接收 UDP 数据包的Channel ,由于 UDP 是一种无连接的网络协议,所以默认情况下,使用者不能像对其它Channel(比如ServerSocketChannel和SocketChannel)那样直接对 DatagramChannel 进行读写操作,而是要通过数据包来发送和接收数据。
1.1、创建DatagramChannel 的方式
伪代码如下:
//创建了一个UDP数据通道,该通道可在 UDP 端口 9999 上接收数据包。
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
1.2、接收数据
receive()函数会将接收到的数据包的内容复制到指定的Buffer中。如果接收到的数据包中的数据量超过了Buffer所能容纳的容量,多余的数据将会被丢弃。伪代码如下:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
1.3、发送数据
使用者可以通过调用 DatagramChannel.class::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));
1.4、假连接并发送数据
可以将DatagramChannel与网络中的特定地址进行假连接。由于 UDP 是无连接的,这种通过地址进行连接的方式并不会像使用 TCP 中的Channel(ServerSocketChannel和SocketChannel)那样创建真正的连接。相反,它会锁定你的DatagramChannel,使得你只能从一个特定的地址发送和接收数据包。伪代码如下:
channel.connect(new InetSocketAddress("jenkov.com", 80));
假连接成功后,使用者还可以使用 read() 和 write() 函数,就像使用传统Channel一样。不过,DatagramChannel无法保证发送的数据能够成功送达。伪代码如下:
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);
二、Pipe
NIO 中的Pipe是两个线程之间的一条单向数据连接。管道包含一个源Channel和一个接收Channel。您将数据写入接收通道。然后,可以从源通道读取这些数据。如下所示:

下面是一个使用Pipe.SinkChannel和Pipe.SourceChannel在两个线程之间传输数据的示例:
package com.xxx.pipe;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
public class PipeForTransferInThread {
static Pipe pipe;
public static void main(String[] args) throws IOException, InterruptedException {
pipe = Pipe.open();
new Thread(() -> {
try {
Pipe.SinkChannel sinkChannel = pipe.sink();//可以向通过sink()函数获得的Channel写入数据
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);
}
} catch (IOException e) {
e.printStackTrace();
}
}, "sourceDataThread").start();
Thread.sleep(1000);//主线程阻塞1s后再启动destinationDataThread线程,然后在该线程中从pipe读数据
new Thread(() -> {
try {
Pipe.SourceChannel sourceChannel = pipe.source();//可以从通过source()函数获得的Channel读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
buf.flip();//读写模式反转
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println(Thread.currentThread().getName() + "线程收到数据 :" + body);
} catch (Exception e) {
e.printStackTrace();
}
}, "destinationDataThread").start();
}
}
程序运行结果,如下所示:

三、Path
Path.interface接口是在 Java 7 中添加到 NIO 中的。Path.interface接口位于 java.nio.file 包中,因此 Path.interface接口的完整限定名称是 java.nio.file.Path。
实现Path.interface接口的实例代表了文件系统中的一个路径。路径可以指向文件或文件夹。路径可以是绝对路径,也可以是相对路径:
①、绝对路径指的是从文件系统的根目录一直延伸到其所指向的文件或文件夹的完整路径;
②、相对路径指的是相对于其它路径的文件或文件夹的路径。(在本文关于 Java NIO 路径的教程中,我将在稍后的地方更详细地讲解相对路径)
不要将文件系统路径与某些操作系统中关于路径的环境变量搞混。java.nio.file.Path 接口与路径环境变量毫无关系。
在很多方面,实现java.nio.file.Path.interface 接口的实例与 java.io.File.class 类相似,但存在一些细微的差别。不过在大多数情况下,使用者可以将 File 类的使用替换为 Path 接口的使用。
3.1、生成实现Path.interface接口的实例和绝对路径
- Windows操作系统
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathExample {
public static void main(String[] args) {
//“Paths.get()”函数就是用于生成“Path”实例的工厂方法
Path path = Paths.get("c:\\data\\myfile.txt");
}
}
- Linux、MacOS、FreeBSD 等操作系统
//伪代码
Path path = Paths.get("/home/jakobjenkov/myfile.txt");
如果在 Windows 系统中使用以 "/" 开头的路径的话,该路径将被视为相对于当前驱动器(逻辑盘符C盘)的相对路径。例如:
Windows 系统中的这种路径/home/jakobjenkov/myfile.txt将被翻译为:
C:/home/jakobjenkov/myfile.txt
3.2、生成实现Path.interface接口的实例和相对路径(待完善)
相对路径是指从一个路径(基础路径)指向一个目录或文件的路径。相对路径的完整路径(绝对路径)是通过将基础路径与相对路径相结合而得出的。NIO 中的路径类也可用于处理相对路径。您可以通过以下方式创建一个相对路径:
//伪代码
Path projects = Paths.get("d:\\data", "projects");//创建了一个指向“d:\data\projects”目录的 Java Path 实例
Path file = Paths.get("d:\\data", "projects\\a-project\\myfile.txt");//创建了一个指向“d:\data\projects\a-project\myfile.txt”文件的 Path 实例
在使用相对路径时,路径字符串中可以使用
.
..
【.】 的意思是“当前目录”。例如,如果创建一个相对路径如下:
package com.xxx.path;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathTest {
public static void main(String[] args) {
Path currentDir = Paths.get(".");
System.out.println(currentDir.toAbsolutePath());
}
}
3.3、relativize()函数
relativize() 函数可以创建一个新的 Path,该新 Path 表示第二个 Path 在第一个 Path 之下的相对位置。例如,对于路径 "/data" 和 "/data/subdata/subsubdata/myfile.txt",第二个路径相对于第一个路径可以表示为 "/subdata/subsubdata/myfile.txt"。如下所示:
package com.xxx.path;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathTest {
public static void main(String[] args) {
Path basePath = Paths.get("/data");
Path path = Paths.get("/data/subdata/subsubdata/myfile.txt");
Path basePathToPath = basePath.relativize(path);
Path pathToBasePath = path.relativize(basePath);
System.out.println(basePathToPath);
System.out.println(pathToBasePath);
}
}
程序运行结果,如下所示:

不能同时使用相对路径和绝对路径。否则在使用relativize()函数报IllegalArgumentException,如下所示:
package com.xxx.path;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathTest {
public static void main(String[] args) {
Path basePath = Paths.get("/data");
Path path = Paths.get("myfile.txt");
Path basePathToPath = basePath.relativize(path);
}
}
程序运行结果,如下所示:

3.4、normalize()函数
normalize() 函数可以对路径进行规范化处理。规范化意味着会移除路径字符串中间的所有 . 和 .. 标记,并确定该路径字符串所指代的具体路径,如下所示:
package com.xxx.path;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathTest {
public static void main(String[] args) {
String originalPath =
"d:\\data\\projects\\a-project\\..\\another-project";
Path path1 = Paths.get(originalPath);
System.out.println("path1 = " + path1);//实际上它输出的是 Path.toString() 的结果
Path path2 = path1.normalize();
System.out.println("path2 = " + path2);//实际上它输出的是 Path.toString() 的结果
}
}
程序运行结果,如下所示:

四、Files
NIO中的Files有以下2个特点:
①、java.nio.file.Files 类是基于 java.nio.file.Path 类实现的,因此在使用 Files 类之前,您需要先了解 Path 类的用法。
②、NIO 中的Files(java.nio.file.Files)提供了多种用于操作文件系统中文件的函数。
4.1、Files.exists()函数
File.exists() 函数用于检查给定的路径是否存在于文件系统中。可以创建不存在于文件系统中的路径实例。例如,如果您打算创建一个新的目录,您首先会创建相应的路径实例,然后创建该目录。由于路径实例可能指向也可能不指向文件系统中的实际路径,因此使用者可以使用 Files.exists() 函数来判断这些路径是否存在。如下所示:
package com.xxx.file;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FilesTest {
public static void main(String[] args) {
Path path = Paths.get("D:\\nio-data.txt");
//第二个参数会影响Files.exists()函数判断路径是否存在的方式
//此处的new LinkOption[]{LinkOption.NOFOLLOW_LINKS}表示在确定路径是否存在时不应遵循文件系统中的符号链接
boolean pathExists =
Files.exists(path,
new LinkOption[]{LinkOption.NOFOLLOW_LINKS});
System.out.println(pathExists);
}
}
我的windows操作系统的D盘根目录下有nio-data.txt文件,程序运行结果,如下所示:

4.2、Files.createDirectory()函数
Files的 createDirectory()函数能够根据一个 Path 实例创建一个新的文件(或文件夹)。如下所示(该例子为创建文件夹的示例,创建文件的方式与之类同):
package com.xxx.file;
import java.io.IOException;
import java.nio.file.*;
public class FilesTest {
public static void main(String[] args) {
Path path = Paths.get("D:\\NIODirectory");
try {
//返回一个指向新创建路径的 Path 实例
Path newDir = Files.createDirectory(path);
} catch(FileAlreadyExistsException e){
// 如果该文件夹已存在,则会抛出 java.nio.file.FileAlreadyExistsException 异常
} catch (IOException e) {
//其它IO异常,如:要创建的新文件夹的父目录不存在(此例子中则为可能不存在逻辑D盘)
e.printStackTrace();
}
}
}
我的windows操作系统的D盘根目录下没有NIODirectory文件夹,程序运行后,有了NIODirectory文件夹。
4.3、Files.copy()函数
Files的 copy()函数能够将一个文件(或文件夹)从一个路径复制到另一个路径。如下所示(该例子为复制文件的示例,复制文件夹的方式与之类同)
package com.xxx.file;
import java.io.IOException;
import java.nio.file.*;
public class FilesTest {
public static void main(String[] args) {
Path sourcePath = Paths.get("D:\\nio-data.txt");
Path destinationPath = Paths.get("E:\\nio-data.txt");
try {
Files.copy(sourcePath, destinationPath);
} catch(FileAlreadyExistsException e) {
//如果目标文件已存在(此例子为执行该程序前,已存在了E:\\nio-data.txt文件),则会抛出 java.nio.file.FileAlreadyExistsException 异常。
} catch (IOException e) {
//其它IO异常,如:被复制的源文件不存在(此例子为不存在D:\\nio-data.txt文件)
e.printStackTrace();
}
}
}
我的windows操作系统的D盘根目录下有nio-data.txt文件,程序运行后,我的windows操作系统的E盘根目录下也有了nio-data.txt文件。
4.3.1、覆盖目标文件
只需要在上述实例中Files的 copy()函数中增加一个参数,就可以强制覆盖一个已存在的文件,如下所示:
//第3个参数会影响Files.copy()函数复制策略
//StandardCopyOption.REPLACE_EXISTING表示如果目标文件已经存在,则可以覆盖现有文件
Files.copy(sourcePath, destinationPath,
StandardCopyOption.REPLACE_EXISTING);
package java.nio.file;
/**
* Defines the standard copy options.
*
* @since 1.7
*/
public enum StandardCopyOption implements CopyOption {
/**
* Replace an existing file if it exists.
*/
REPLACE_EXISTING,
/**
* Copy attributes to the new file.
*/
COPY_ATTRIBUTES,
/**
* Move the file as an atomic file system operation.
*/
ATOMIC_MOVE;
}
4.4、Files.move()函数
Files的 move()函数与java.io.File.class::renameTo()函数的功能完全相同,主要功能是将一个文件(或文件夹)从一个路径复制到另一个路径,重命名一个文件(或文件夹)也可以使用这个函数。
该函数和Files的copy()函数的区别是:
①、copy()函数不会删除被复制的源文件,move()函数会删除被复制(移动)的源文件;
4.5、Files.delete()函数
Files的 delete()函数能够删除一个文件(或文件夹),如下所示:
package com.xxx.file;
import java.io.IOException;
import java.nio.file.*;
public class FilesTest {
public static void main(String[] args) {
Path path = Paths.get("E:\\move_nio-data.txt");
try {
Files.delete(path);
} catch (IOException e) {
//某些原因未能删除该文件(例如文件或文件夹不存在),则抛出一个IOException。
e.printStackTrace();
}
}
}
我的windows操作系统的E盘根目录下有move_nio-data.txt文件,程序运行后,该文件被删除。
4.6、Files.walkFileTree()函数
Files的walkFileTree()函数包含用于递归遍历目录树的功能。walkFileTree() 函数接受2个参数:
①、Path.interface类型的实例,指向要遍历的目录;
②、FileVisitor.interface类型的实例,遍历过程中会调用实现了该接口实例的preVisitDirectory()函数、visitFile()函数、visitFileFailed()函数、postVisitDirectory()函数;
FileVisitor.interface源码如下:
package java.nio.file;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
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;
}
每个函数被调用的时间:
①、preVisitDirectory() 函数在访问任何文件夹之前会被调用;
②、postVisitDirectory() 函数则在访问完一个文件夹之后被调用;
③、 visitFile() 函数在文件遍历(如果是文件夹,不会调用该函数)过程中,对于每个被访问的文件都会调用。
④、visitFileFailed() 函数会在访问文件失败时被调用。比如,没有相应的权限等。
preVisitDirectory()函数、visitFile()函数、visitFileFailed()函数、postVisitDirectory()函数的返回值都是一个枚举类型的FileVisitResult实例,共有4种枚举类型:
package java.nio.file;
public enum FileVisitResult {
CONTINUE,
TERMINATE,
SKIP_SUBTREE,
SKIP_SIBLINGS;
}
每个枚举类型的含义如下:
①、CONTINUE:文件或文件夹遍历操作应按正常方式进行;
②、TERMINATE:文件或文件夹遍历操作现在应该停止了;
③、SKIP_SUBTREE:表示文件或文件夹遍历操作应继续进行,但不访问此文件或文件夹的任何同级文件或文件夹。
④、SKIP_SIBLINGS:文件或文件夹遍历操作应当继续进行,但不会访问此文件夹的任何子文件或者子文件夹,只有当该枚举值从 preVisitDirectory() 函数中返回时,它才有作用。如果从其它任何3个函数中返回,则会被解释为CONTINUE操作。
4.6.1、利用SimpleFileVisitor.class递归寻找windows操作系统的D:\maven_repository文件夹下的所有名称为_remote.repositories的文件
package com.xxx.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
public class FilesTest {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
Path rootPath = Paths.get("D:\\","maven_repository");
String fileToFind = File.separator + "_remote.repositories";
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)){
linkedList.add(fileString);
}
return FileVisitResult.CONTINUE;
}
});
} catch(IOException e){
e.printStackTrace();
}
for (String s : linkedList) {
System.out.println(s);
}
}
}
我的windows操作系统有D:\maven_repository文件夹,程序运行结果,如下所示:

4.6.2、利用SimpleFileVisitor.class递归删除windows操作系统的D:\delete文件夹下的所有名称为testdelete_remote.repositories的文件夹和名称为testdelete_remote.repositories.txt的文件
package com.xxx.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
public class FilesTest {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
Path rootPath = Paths.get("D:\\","delete");
String fileToFind = File.separator + "testdelete_remote.repositories.txt";
String dirToFind = File.separator + "testdelete_remote.repositories";
try {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileString = file.toAbsolutePath().toString();
if (fileString.endsWith(fileToFind)){
Files.delete(file);
linkedList.add(fileString);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
String dirString = dir.toAbsolutePath().toString();
if (dirString.endsWith(dirToFind)){
Files.delete(dir);
linkedList.add(dirString);
}
return FileVisitResult.CONTINUE;
}
});
} catch(IOException e){
e.printStackTrace();
}
for (String s : linkedList) {
System.out.println(s);
}
}
}
我的windows操作系统有D:\delete文件夹,该文件夹中的文件如下所示:

程序运行结果,如下所示:

D:\delete文件夹下的所有名称为testdelete_remote.repositories的文件夹和名称为testdelete_remote.repositories.txt的文件全部被递归删除。

浙公网安备 33010602011771号