Java7技术系列:try-with-resource
Java7技术系列:int与二进制的转换优化
Java7技术系列:MultiCatchException
Java7技术系列:NIO.2异步IO
Java7技术系列:DI依赖注入
Java7技术系列:Queue
Java7技术系列:Java并发
1 NIO.2相关API介绍
NIO.2把位置(由Path表示)的概念和物理文件系统的处理(比如复制一个文件)分得很清楚,物理文件系统通常是由Files辅助类实现的【即Path不一定代表真实的文件或目录,你可以随心所欲地操作Path,用Files中的功能来检查文件是否存在,并对它进行处理】
【个人理解:Path是虚拟的,不对实际的文件进行操作处理,只读取Path指定目录或文件的信息;
Files是会对实际的文件进行操作处理,比如复制、读写等】
1.1 NIO.2关键基础类
API | 描述 |
---|---|
NIO2 | Path类中的方法可以用来获取路径信息,访问该路径中的各元素,将路径转换为其他形式,或提取路径中的一部分。有的方法还可以匹配路径字串以及移除路径中的冗余项 |
Paths | 工具类,提供返回一个路径的辅助方法,比如get(String first, String… more)和get(URI uri) |
FileSystem | 与文件系统交互的类,无论是默认的文件系统,还是通过其统一资源标识(URI)获取的可选文件系统 |
FileSystems | 工具类,提供各种方法,比如其中用于返回默认文件系统的FileSystems.getDefault() |
相对路径与绝对路径(以访问 /java7developer/src/main
目录为例):
比如程序在路径 /java7developer/src/test
目录下运行,代码要读取位于/java7developer/src/main目录的文件名。
为了进入 /java7developer/src/main
目录,可以用相对路径 ../main
。
如果程序在路径 /java7developer/src/test/java/com/java7developer
运行,用相对路径 ../main
则无法进入目标目录,而是会访问到不存在的目录 /java7developer/src/test/java/com/main
,只能用绝对路径 /java7developer/src/main
。
【总结:使用相对路径,相当于将当前运行程序的目录回退一个目录,然后再进入对应的目标目录,也就是说,要访问的目录与程序运行目录,它们的上一级目录是相同的,此时才能使用相对路径正确访问】
1.2 文件处理的基础类
API | 描述 |
---|---|
Files | 实现复制、移动、删除或处理文件的工具类 |
WatchService | 用来监视文件或目录的核心类,不管它们有没有变化 |
2 Path
2.1 获取Path
private static void operate1() {
// NIO2.get(String first, String... more),第二个参数一般用不到,它仅仅用来把额外的字符串合并起来形成Path字符串
// 注意:NIO.2的API中,Path或Paths中的各种方法抛出的受检异常只有IOException。这有时会存在掩藏潜在的问题,
// 如果想处理IOException的显式子类,则需要额外编写异常处理代码。
java.nio.file.Path path = Paths.get("/usr/bin/zip");
java.nio.file.Path samePath = FileSystems.getDefault().getPath("/usr/bin/zip"); // 与上面代码等价
java.nio.file.Path absolutePath = path.toAbsolutePath(); // 转换为绝对路径
}
2.2 从Path中获取信息
private static void operate2() {
java.nio.file.Path path = Paths.get("/usr/bin/zip");
java.nio.file.Path filename = path.getFileName(); // 获取文件名,zip
int nameCount = path.getNameCount(); // 获取文件名字符数,3
java.nio.file.Path parent = path.getParent(); // 获取父目录,/usr/bin
java.nio.file.Path root = path.getRoot(); // 获取根目录,/
java.nio.file.Path subpath = parent.subpath(0, 2); // 根据索引获取子路径,usr/bin
}
2.3 去除冗余项
private static void operate3() {
// ①在处理Path时可能会有一个或两个点:
// .表示当前目录 ..表示父目录
// 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行,
// 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录
// normalize()去除掉冗余项./,结果为Listing_2_1.java
java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize();
// ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt,
// 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置,
// 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。
// toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能
try {
java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
e.printStackTrace();
}
}
2.4 转换Path
private static void operate4() {
// 合并Path,调用resolve()方法,合并表示/uat/conf/application.properties的完整Path
java.nio.file.Path prefix = Paths.get("/uat/");
java.nio.file.Path completePath = prefix.resolve("conf/application.properties");
// 计算两个目录之间的路径,通过relativize()方法
String java7developerPath = "/java7developer/src/test/java/com/java7developer";
String mainPath = "/java7developer/src/main";
java.nio.file.Path java7developerDir = Paths.get(java7developerPath);
java.nio.file.Path mainDir = Paths.get(mainPath);
java.nio.file.Path java7developerRelativizePath = java7developerDir.relativize(mainDir);
java.nio.file.Path mainRelativizePath = mainDir.relativize(java7developerDir);
System.out.println(java7developerRelativizePath); // 结果:..\..\..\..\main
System.out.println(mainRelativizePath); // 结果:..\test\java\com\java7developer
}
2.5 java7的Path与java7之前的File之间转换
private static void operate5() {
File file = new File("../Listing_2_1.java");
java.nio.file.Path path = file.toPath(); // 通过toPath()转换为Path
path.toAbsolutePath();
file = path.toFile(); // 通过toFile()转换为File
}
2.6 文件属性
private static void operate3() {
// ①在处理Path时可能会有一个或两个点:
// .表示当前目录 ..表示父目录
// 假设程序在/java7developer/src/main/java/com/java7developer/chapter2/目录运行,
// 所在目录和Listing_2_1.java一样,如果传给你的Path是./Listing_2_1.java,其中./部分即为正在运行的当前目录
// normalize()去除掉冗余项./,结果为Listing_2_1.java
java.nio.file.Path normalizePath = Paths.get("./Listing_2_1.java").normalize();
// ②符号链接冗余处理,在*nix系统中/usr/logs目录下,你想寻找日志文件log1.txt,
// 但其实.usr/logs只是一个指向/application/logs的符号链接,那里才是存放日志文件的真正位置,
// 要得到这个位置,就需要去掉冗余符号信息。这些冗余项都会导致Path指向的不是你认为它应该指向的位置。
// toRealPath()也可以去除冗余项,它是融合了toAbsolutePath()和normalize()两个方法的功能
try {
java.nio.file.Path realPath = Paths.get("/usr/logs/log1.txt").toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
e.printStackTrace();
}
}
3 文件操作
3.1 创建文件
// 创建文件,通常处于安全考虑,要定义所创建的文件是用于读、写、执行,或者三者权限的某种组合时,要指明该文件的某些FileAttributes,这取决于文件系统,所以需要使用与文件系统相关的文件权限类
// 创建一个在POSIX系统上为属主、属组内用户和所有用户设置读写权限的文件
// 注意:如果在创建文件时要指定访问许可,不要忽略其父目录强加给该文件的umask限制或受限许可。
// 比如说,你会发现即使为新文件指定了rw-rw-rw许可,但由于目录的掩码,实际上文件最终访问许可为rw-r--r--
java.nio.file.Path path = Paths.get("D:\\Backup\\MyStuff.txt");
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-rw-");
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
Files.createFile(path, attr);
3.2 删除文件
// 删除文件,要删除文件需要有删除文件的权限
java.nio.file.Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.delete(target);
3.3 复制文件
// 复制文件
// StandardCopyOption有三种选项:
// [1] REPLACE_EXISTING:覆盖即替换已有文件
// [2] COPY_ATTRIBUTES:复制文件
// [3] ATOMIC_MOVE:确保在两边都操作成功,否则回滚
java.nio.file.Path sourcePath = Paths.get("C:\\MyDocuments\\Stuff.txt");
java.nio.file.Path targetPath = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
3.4 移动文件
// 移动文件
// 在移动源文件时保留其属性,并且覆盖目标文件(如果存在的话)
java.nio.file.Path sources = Paths.get("C:\\MyDocuments\\Stuff.txt");
java.nio.file.Path targets = Paths.get("D:\\Backup\\MyStuff.txt");
Files.move(sources, targets, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
4 文件读写数据
4.1 读文件
// 注意:在处理String时,要处理字符编码问题StandardCharsets.UTF_8,防止不可预料的字符编码问题
java.nio.file.Path path = Paths.get("/tmp/app.log");
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
4.2 写文件
// 写文件,对应的StandardOpenOptions可以确保文件的读写有正确的访问许可
// Files.newInputStream(path,OpenOptions)新建一个输入流
// Files.newOutputStream(path, OpenOptions)新建一个输出流
// StandardOpenOptions.WRITE
// StandardOpenOptions.READ
// StandardOpenOptions.APPEND
java.nio.file.Path logPath = Paths.get("/tmp/app.log");
try (BufferedWriter writer = Files.newBufferedWriter(logPath, StandardOpenOption.WRITE)) {
writer.write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
4.3 读写文件的简化
// 读写简化
java.nio.file.Path logFile = Paths.get("/tmp/app.log");
try {
List<String> lines = Files.readAllLines(logFile); // 读取文件所有行
byte[] bytes = Files.readAllBytes(path); // 读取文件所有字节
} catch (IOException e) {
e.printStackTrace();
}
4.4 拥有文件寻址的FileChannel读写文件
// SeekableByteChannel接口,是为了让开发人员能够改变字节通道的位置和大小。
// 它的实现类FileChannel,这个类可以在文件读取或写入时保持当前位置
// 读取日志文件中的最后1000个字符,FileChannel具有寻址能力
java.nio.file.Path file = Paths.get("/temp/app.log");
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileChannel channel;
try {
channel = FileChannel.open(file, StandardOpenOption.READ);
channel.read(buffer, channel.size() - 1000);
} catch (IOException e) {
e.printStackTrace();
}
4.5 文件修改监听通知WatchService
// 文件修改通知
// WatchService类检测文件或目录变化,该类用客户线程监视注册文件或目录的变化
// 并且检测到变化时返回一个事件。是轮询机制的一个理想替代品
// 检测目录变化,打印消息
// StandardWatchEventKinds.ENTRY_MODIFY:监听事件修改
// StandardWatchEventKinds.ENTRY_CREATE:监听事件创建
// StandardWatchEventKinds.ENTRY_DELETE:监听事件删除
// StandardWatchEventKinds.OVERFLOW:监听事件已经丢失或丢弃
try {
boolean isShutdown = false;
WatchService watcher = FileSystems.getDefault().newWatchService();
java.nio.file.Path dir = Paths.get("/usr/vincent");
WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);// 检测变化
while (!isShutdown) {
key = watcher.take(); // 得到下一个key及其事件
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { // 检查是否为变化事件
System.out.println("dir changed!");
isShutdown = true;
}
}
key.reset(); // 重置监听key,等待下一个事件
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
5 处理目录与目录树
5.1 目录树查找文件过滤
// 目录树查找文件,过滤指定目录中所有.properties文件
java.nio.file.Path dir = Paths.get("C:\\workspace\\java7developer");
try {
// Files.newDirectoryStream()返回指定目录中过滤后的DirectoryStream
DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(dir, "*.properties");
for (java.nio.file.Path path : stream) {
System.out.println(path.getFileName());
}
} catch (IOException e) {
e.printStackTrace();
}
5.2 遍历目录树
// 遍历目录树,walkFileTree可以实现递归移动、复制和删除操作
// API
// 关键方法:Files.walkFileTree(NIO2 startingDir, FileVisitor<? super NIO2> isitor);
// FileVisitor接口有五个方法(T一般就是Path):
// [1]FileVisitResult preVisitDirectory(T dir)
// [2]FileVisitResult preVisitDirectoryFailed(T dir, IOException e)
// [3]FileVisitResult visitFile(T file, BasicFileAttributes attrs)
// [4]FileVisitResult visitFileFailed(T file, IOException e)
// [5]FileVisitResult postVisitDirectory(T dir, IOException e)
// java7已经提供了一个默认实现类,SimpleFileVisitor<T>
// 注意:为了确保递归等操作的安全性,walkFileTree()不会自动跟随符号链接。如果需要跟随符号链接,需要使用 normalize()或toRealPath()检查。
// 遍历C:\workspace\java7developer\src目录下及其子目录内的所有.java文件
java.nio.file.Path startingDir = Paths.get("C:\\workspace\\java7developer\\src");
try {
Files.walkFileTree(startingDir, new FindJavaVisitor());
} catch (IOException e) {
e.printStackTrace();
}
private static final class FindJavaVisitor extends SimpleFileVisitor<java.nio.file.Path> {
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".java")) {
System.out.println(file.getFileName());
}
return FileVisitResult.CONTINUE;
}
}
6 异步IO
privatestatic void operate10() {
// java7中有三个新的异步通道:
// AsynchronousFileChannel:用于文件I/O
// AsynchronousSocketChannel:用于套接字I/O,支持超时
// AsynchronousServerSocketChannel:用于套接字接受异步连接
// 新的异步I/O主要有两种形式:将来式和回调式
// 将来式:表明使用java.util.concurrent.Future接口。
// Future接口提供了几个方法:
// ①get():用来获取结果。如果结果还没有准备好,get()会被阻塞直到它能取得结果。
// ②get(int second, TimeUnit unit):获取结果,可以设置超时时间。
// ③cancel():在运算结束前取消。
// ④isDone():调用者用它来判断运算是否结束。
// 当希望由主线程发起I/O操作并轮询等待结果时,一般都会用将来式异步处理
// 流程:将来式用现有的java.util.concurrent技术声明一个Future,用来保存异步操作的处理结果。
// 这很关键,因为这意味着当前线程不会因为比较慢的I/O操作而停滞。相反,有一个单独的
// 线程发起I/O操作,并在操作完成时返回结果。与此同时,主线程可以继续执行其他需要完成的任务。
// 在其他任务结束后,如果I/O操作还没有完成,主线程会一直等待。
// 实现方式:AsynchronousFileChannel会关联线程池,它的任务是接收I/O处理事件,并分发给负责处理通道中I/O操作结果
// 的结果处理器。跟通道中发起的I/O操作关联的结果处理器确保是由线程池中的某个线程产生的。
// 如果在创建AsynchronousFileChannel时没有为其指明线程池,那就会为其分配一个系统默认的线程池(可能会和其他通道共享)。
// 默认线程池是由AsynchronousChannelGroup类定义的系统属性进行配置的。
try {
java.nio.file.Path file = Paths.get("/usr/vincent/foobar.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000); // 在该文件中读取字节大小
Future<Integer> result = channel.read(byteBuffer, 0);
// 这里手动调用了isDone()判断是否读取完成
// 通常情况下,result或结束(主线程会继续执行),或等待后台I/O完成
while (!result.isDone()) {
// 在Future并发异步执行文件读取操作时,主线程可以继续做其他操作
System.out.println("do something in main thread");
}
Integer bytesRead = result.get(); // 获取读取结果
System.out.println("Bytes read[" + bytesRead + "]");
} catch (IOException | InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 回调式:其基本思想是主线程会派一个侦察员CompletionHandler到独立的线程中执行I/O操作。
// 这个侦察员将带着I/O操作的结果返回到主线程中,这个结果会触发它自己的completed或failed方法。
// CompletionHandler<V, A>:V是结果类型,A是提供的附着对象
try {
java.nio.file.Path files = Paths.get("/usr/vincent/foobar.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(files);
ByteBuffer byteBuffer = ByteBuffer.allocate(100_0000);
channel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("Bytes read [" + result + "]");
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println(exc.getMessage());
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
7 Socket和Channel整合
private static void operate11() {
// java7提供了NetworkChannel,把Socket和Channel结合到一起
// java.nio.channel包:定义通道,表示连接到执行I/O操作的实体,比如文件和套接字。定义用于多路传输、非阻塞I/O操作的选择器。
// java.nio.Socket包:该类实现了客户端套接字。套接字是两个机器间通信的端点。
// 在旧版java中,为了执行I/O操作,比如向TCP端口中写入数据,你需要将通道绑定到Socket的实现类上,但存在问题:
// ①在旧版java中,为了配置套接字选项和绑定在套接字上,必须把通道和套接字的API整合在一起。
// ②在旧版java中,不能利用平台特定的套接字行为。
// NetworkChannel:代表一个连接到网络套接字通道的映射。它定义了一组实用的方法,比如查看及设置通道上可用的套接字选项等。
// 输出互联网套接字地址在端口3080上所支持的选项,设置IP服务条款选项以及确认套接字通道上的SO_KEEPALIVE选项
SelectorProvider provider = SelectorProvider.provider();
try {
SocketChannel channel = provider.openSocketChannel();
SocketAddress address = new InetSocketAddress(3080);
channel = channel.bind(address); // 将端口绑定到3080
Set<SocketOption<?>> socketOptions = channel.supportedOptions(); // 获取该端口所支持选项
System.out.println(socketOptions.toString());
channel.setOption(StandardSocketOptions.IP_TOS, 3); // 设置IP服务条款选项TOS
Boolean keepAlive = channel.getOption(StandardSocketOptions.SO_KEEPALIVE);// 获取SO_KEEPALIVE选项
} catch (IOException e) {
e.printStackTrace();
}
// NetworkChannel让多播操作称为可能
// 多播(组播):表示一对多的网络通讯,通常用来指代IP多播。其基本前提是将一个包发送到一个组播地址,
// 然后网络对该包进行复制,分发给所有接收端(注册到组播地址中)
// 为了让NetworkChannel加入多播组,java7提供了一个新接口java.nio.channels.MulticastChannel及默认实现类DatagramChannel
// 加入IP地址为180.90.4.12的多播组,并对其发送和接收系统状态信息
try {
NetworkInterface networkInterface = NetworkInterface.getByName("net1"); // 选择网络接口
DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET); // 打开DatagramChannel
// 将通道设置为多播
dc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
dc.bind(new InetSocketAddress(8080));
dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
// 加入多播组
InetAddress group = InetAddress.getByName("180.90.4.12");
MembershipKey key = dc.join(group, networkInterface);
} catch (IOException e) {
e.printStackTrace();
}
}