- 一、Java NIO 概述
- 二、Java NIO Channel
- 三、Java NIO Buffer
- 四、Java NIO Selector
- 五、Java NIO 扩展
一、Pipe
1. 创建管道Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 Source 通道和一个 Sink 通道。数据会被写到 Sink 通道,从 Source 通道读取。
// 1. 获取管道 Pipe pipe = Pipe.open();2. 向管道写入数据
要向管道写数据,需要访问 Sink 通道:
// 2. 获取Sink通道,向管道写入数据 Pipe.SinkChannel sinkChannel = pipe.sink();
调用 SinkChannel 的 write() 方法,将数据写入 SinkChannel:
// 3. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("qs".getBytes());
buffer.flip();
// 4. 写入数据
sinkChannel.write(buffer);
buffer.flip();
// 3. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(84);
buffer.clear();
buffer.put("qshome".getBytes());
buffer.flip();
// 4. 写入数据
while (buffer.hasRemaining()) {
sinkChannel.write(buffer);
}
buffer.flip();
3. 从管道读取数据
从管道读取数据,需要访问 Source 通道:
// 5. 获取Source通道,从管道读取数据 Pipe.SourceChannel sourceChannel = pipe.source();
调用 SourceChannel 的 read() 方法来读取数据:read() 方法返回的 int 值会告诉我们多少字节被读进了缓冲区。
// 6. 创建缓冲区,读取数据 int read = sourceChannel.read(buffer); System.out.println(new String(buffer.array(), 0, read));4. Pipe示例
public class Pipe1 {
public static void main(String[] args) throws IOException {
// 1. 获取管道
Pipe pipe = Pipe.open();
// 2. 获取Sink通道,向管道写入数据
Pipe.SinkChannel sinkChannel = pipe.sink();
// 3. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("qs".getBytes());
buffer.flip();
// 4. 写入数据
sinkChannel.write(buffer);
buffer.flip();
// 5. 获取Source通道,从管道读取数据
Pipe.SourceChannel sourceChannel = pipe.source();
// 6. 创建缓冲区
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
// 7. 读取数据
int read = sourceChannel.read(buffer2);
System.out.println(new String(buffer2.array(), 0, read));
// 8. 关闭通道
sourceChannel.close();
sinkChannel.close();
}
}
二、FileLock 1. FileLock 简介
文件锁在 OS 中很常见,如果多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件,或者程序都只能读此文件,这就解决了同步问题。
文件锁是 进程级别 的,不是线程级别的。文件锁可以解决多个进程并发访问、修改同一个文件的问题,但不能解决多线程并发访问、修改同一文件的问题。使用文件锁时,同一进程内的多个线程,可以同时访问、修改此文件。
文件锁是当前程序所属的 JVM 实例持有的,一旦获取到文件锁(对文件加锁),要调用 release(),或者 关闭对应的 FileChannel 对象,或者当前 JVM 退出,才会释放这个锁。
2. 文件锁分类 2.1 排它锁一旦某个进程(比如说 JVM 实例)对某个文件加锁,则在释放这个锁之前,此进程不能再对此文件加锁,就是说 JVM 实例在同一文件上的 文件锁是不重叠的(进程级别不能重复在同一文件上获取锁)。
2.2 共享锁又叫独占锁。对文件加排它锁后,该进程可以对此文件进行读写,该进程独占此文件,其他进程不能读写此文件,直到该进程释放文件锁。
3. FileLock示例某个进程对文件加共享锁,其他进程也可以访问此文件,但这些进程都只能读此文件,不能写。线程是安全的。只要还有一个进程持有共享锁,此文件就只能读,不能写。
文件锁要通过 FileChannel 对象使用:
public class FileLock1 {
private static final String filePath = "E:\java\nio\FileLock1.txt";
@Test
public void exclusiveLock() throws IOException, InterruptedException {
Path path = Paths.get(filePath);
// 1. 创建 FileChannel 对象,文件锁只能通过 FileChannel 对象来使用
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
// 光标在文件末尾的位置
fileChannel.position(fileChannel.size() > 0 ? fileChannel.size() - 1 : 0);
// 2. 加锁,无参默认为排它锁,对整个文件加锁,阻塞方法
// FileLock lock = fileChannel.lock();
// 非阻塞方法,当文件锁不可用时,tryLock() 会得到 null 值
FileLock lock = fileChannel.tryLock();
System.out.printf("是否共享锁: %s", lock.isShared()).println();
// 3. 文件读写操作
writeFile(fileChannel, "排它锁");
// readFile(filePath);
// 等待20秒不释放锁
TimeUnit.SECONDS.sleep(20);
// 4. 释放锁
// lock.release();
}
@Test
public void sharedLock() throws IOException, InterruptedException {
Path path = Paths.get(filePath);
// 1. 创建 FileChannel 对象,文件锁只能通过 FileChannel 对象来使用
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.READ);
fileChannel.position(fileChannel.size() > 0 ? fileChannel.size() - 1 : 0);
// FileLock lock = fileChannel.lock(0, Long.MAX_VALUE, true);
// 非阻塞方法,当文件锁不可用时,tryLock() 会得到 null 值
FileLock lock = fileChannel.tryLock(0, Long.MAX_VALUE, true);
System.out.printf("是否共享锁: %s", lock.isShared()).println();
// 3. 文件读写操作
// writeFile(fileChannel, "共享锁");
readFile(filePath);
// 等待20秒不释放锁
TimeUnit.SECONDS.sleep(20);
// 4. 释放锁
// lock.release();
}
@Test
public void testRead() throws IOException {
readFile(filePath);
// 排他锁 java.io.IOException: 另一个程序已锁定文件的一部分,进程无法访问。
}
private void writeFile(FileChannel fileChannel, String input) throws IOException {
System.out.printf("input: %s", input).println();
ByteBuffer buffer = ByteBuffer.wrap(input.getBytes());
// 写入
fileChannel.write(buffer);
// 释放锁
// fileChannel.close();
System.out.println("写完成");
}
private void readFile(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String output = bufferedReader.readLine();
System.out.println("读取到内容: ");
while (output != null) {
System.out.println(output);
output = bufferedReader.readLine();
}
fileReader.close();
bufferedReader.close();
System.out.println("读完成");
}
}
4. 获取文件锁方法
有 4 种获取文件锁的方法:
// 对整个文件加锁,默认为`排它锁` lock(); lock(long position, long size, booean shared);
// 对整个文件加锁,默认为`排它锁` tryLock(); tryLock(long position, long size, booean shared);5. lock 与 tryLock 的区别
6. FileLock 的 isShared 和 isValidlock 是阻塞式的,如果未获取到文件锁,会一直阻塞当前线程,直到获取文件锁;tryLock 和 lock 的作用相同,只不过 tryLock 是非阻塞式的,tryLock 是尝试获取文件锁,获取成功就返回锁对象,否则返回 null,不会阻塞当前线程。
// 此文件锁是否是共享锁 boolean isShared();
// 此文件锁是否还有效 boolean isValid();
在某些 OS 上,对某个文件加锁后,不能对此文件使用通道映射。
三、Path 1. Path 简介
Java Path 接口是 Java NIO 更新的一部分,同 Java NIO 一起已经包括在 Java6 和 Java7 中。Java Path 接口是在 Java7 中添加到 Java NIO 的。Path 接口位于 java.nio.file 包中,所以 Path 接口的完全限定名称为 java.nio.file.Path。
Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。绝对路径 包含从文件系统的根目录到它指向的文件或目录的完整路径。相对路径 包含相对于其他路径的文件或目录的路径。
2. 创建绝对路径在许多方面,java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。不过,在许多情况下,可以使用 Path 接口来替换 File 类的使用。
在 Java 字符串中, 是一个转义字符,需要编写 \,告诉 Java 编译器在字符串中写入一个 字符。
// 创建绝对路径
Path path = Paths.get("E:\java\nio\Path1.txt");
System.out.printf("path: %s", path).println(); // path: E:javanioPath1.txt
在 Linux、MacOS 等操作系统上绝对路径如下:
Path path = Paths.get("/home/Path1.txt");
3. 创建相对路径如果在 Windows 机器上使用了从 / 开始的路径,那么路径将被解释为相对于当前驱动器。
// 创建相对路径
Path path1 = Paths.get("E:\java\nio\path1", "Path1.txt");
Path path2 = Paths.get("E:\java\nio", "path2\Path1.txt");
System.out.printf("path1: %s", path1).println(); // path1: E:javaniopath1Path1.txt
System.out.printf("path2: %s", path2).println(); // path2: E:javaniopath2Path1.txt
3. Path.normalize()
Path 接口的 normalize() 方法可以使路径标准化。标准化意味着它将移除所有在路径字符串的中间的 . 和 .. 代码,并解析路径字符串所引用的路径。
// normalize()路径标准化
Path path3 = Paths.get("E:\java\nio\path1\..\path2");
System.out.printf("path3: %s", path3).println(); // path3: E:javaniopath1..path2
Path path4 = path3.normalize();
System.out.printf("path4: %s", path4).println(); // path4: E:javaniopath2
四、Files
1. Files.createDirectory()Java NIO Files 类(java.nio.file.Files)提供了几种操作文件的方法。
Files.createDirectory() 创建一个新目录。
@Test
public void testCreateDirectory() {
try {
Path path = Paths.get("E:\java\nio\files");
Path directory = Files.createDirectory(path);
System.out.println(directory);
} catch (FileAlreadyExistsException e) {
System.out.printf("目录已经存在: %s", e.getMessage()).println();
} catch (IOException e) {
// 例如,如果新目录的父目录不存在,则可能会抛出 IOException
e.printStackTrace();
}
}
2. Files.copy()
Files.copy() 方法拷贝一个文件到另外一个目录。
@Test
public void testCopy() {
Path path = Paths.get("E:\java\nio\Files1.txt");
Path path2 = Paths.get("E:\java\nio\files\Files1.txt");
try {
// Path copy = Files.copy(path, path2);
// 覆盖已存在的文件
Path copy = Files.copy(path, path2, StandardCopyOption.REPLACE_EXISTING);
System.out.println(copy);
} catch (FileAlreadyExistsException e) {
System.out.printf("目录已经存在: %s", e.getMessage()).println();
} catch (IOException e) {
e.printStackTrace();
}
}
3. Files.move()StandardCopyOption.REPLACE_EXISTING 覆盖已存在的文件。
Files.move() 用于将文件从一个路径移动到另一个路径。移动文件与重命名相同,但是移动文件既可以移动到不同的目录,也可以在相同的目录中更改它的名称。
@Test
public void testMove() throws IOException {
// 文件重命名
Path path = Paths.get("E:\java\nio\files\Files1.txt");
Path path2 = Paths.get("E:\java\nio\files\Files1-back.txt");
Path move = Files.move(path, path2, StandardCopyOption.REPLACE_EXISTING);
System.out.println(move);
}
4. Files.delete()
Files.delete() 方法可以删除一个文件或者目录。
@Test
public void testDelete() {
Path path = Paths.get("E:\java\nio\files\Files1-back.txt");
try {
Files.delete(path);
} catch (IOException e) {
System.out.printf("文件或目录不存在: %s", e.getMessage()).println();
e.printStackTrace();
}
}
5. Files.walkFileTree()
Files.walkFileTree() 方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor 作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用。
FileVisitor 是一个接口,必须自己实现 FileVisitor 接口,并将实现的实例传递给 walkFileTree() 方法。在目录遍历过程中,您的 FileVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以扩展 SimpleFileVisitor 类,它包含 FileVisitor 接口中所有方法的默认实现。
FileVisitor 接口的方法中,每个都返回一个 FileVisitResult 枚举实例。FileVisitResult 枚举包含以下四个选项:
- ConTINUE 继续
- TERMINATE 终止
- SKIP_SIBLING 跳过同级
- SKIP_SUBTREE 跳过子级
@Test
public void testWalkFileTree() {
Path rootPath = Paths.get("E:\java\nio");
final String fileToFind = File.separator + "Files1.txt";
try {
Path path = Files.walkFileTree(rootPath, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String absolutePath = file.toAbsolutePath().toString();
if (absolutePath.endsWith(fileToFind)) {
System.out.printf("找到文件: %s", absolutePath).println();
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
});
System.out.println(path);
} catch (IOException e) {
e.printStackTrace();
}
}
五、AsynchronousFileChannel
1. 创建 AsynchronousFileChannel在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数据写入文件。
// 1. 创建AsyncFileChannel
Path path = Paths.get("E:\java\nio\AsyncFileChannel1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
2. 通过 Future 读取数据
两种方式从 AsynchronousFileChannel 读取数据。第一种方式是调用返回 Future 的 read() 方法:
@Test
public void readAsyncFileChannelFuture() throws IOException {
// 1. 创建AsyncFileChannel
Path path = Paths.get("E:\java\nio\AsyncFileChannel1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 2. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 调用channel的read方法
Future future = asynchronousFileChannel.read(buffer, 0);
// 4. 判断是否完成isDone,返回true读取完成
while (!future.isDone()) ;
// 5. 读取数据到Buffer
buffer.flip();
// while (buffer.remaining() > 0) {
// System.out.println(buffer.get());
// }
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(new String(bytes));
buffer.clear();
}
3. 通过 CompletionHandler 读取数据
第二种方式是调用 read() 方法,该方法将一个 CompletionHandler 作为参数:
@Test
public void readAsyncFileChannelComplete() throws IOException {
// 1. 创建AsyncFileChannel
Path path = Paths.get("E:\java\nio\AsyncFileChannel1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 2. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 调用channel的read方法
asynchronousFileChannel.read(buffer, 0, buffer, new CompletionHandler() {
// 读取操作完成,调用 completed() 方法(附件attachment == 参数三buffer)
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.printf("读取字节数: %s", result).println();
attachment.flip();
byte[] bytes = new byte[attachment.limit()];
attachment.get(bytes);
System.out.println(new String(bytes));
attachment.clear();
}
// 读取操作失败,调用 failed() 方法
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("读取失败");
}
});
}
4. 通过 Future 写入数据
文件必须已经存在。如果该文件不存在,那么 write() 方法将抛出一个 java.nio.file.NoSuchFileException。
@Test
public void writeAsyncFileChannelFuture() throws IOException {
// 1. 创建AsyncFileChannel
Path path = Paths.get("E:\java\nio\AsyncFileChannel1.txt");
// 写入模式
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
// 2. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("wy".getBytes());
buffer.flip();
// 3. 调用channel的write方法
Future future = asynchronousFileChannel.write(buffer, 0);
// 4. 判断是否完成isDone,返回true写入完成
while (!future.isDone());
System.out.println("write over");
}
5. 通过 CompletionHandler 写入数据
@Test
public void writeAsyncFileChannelComplete() throws IOException {
// 1. 创建AsyncFileChannel
Path path = Paths.get("E:\java\nio\AsyncFileChannel1.txt");
if (!Files.exists(path)) {
Files.createFile(path);
}
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
// 2. 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. write写方法
buffer.put("wyb".getBytes());
buffer.flip();
asynchronousFileChannel.write(buffer, 0, buffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.printf("写入字节数: %s", result).println();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("写入失败");
}
});
System.out.println("write over");
}
六、字符集(Charset)
1. Charset 常用静态方法java 中使用 Charset 来表示字符集编码对象。
| 方法 | 描述 |
|---|---|
| static Charset forName(String charsetName) | 通过编码类型获得 Charset 对象 |
| static SortedMap | 获得系统支持的所有编码方式 |
| static Charset defaultCharset() | 获得虚拟机默认的编码方式 |
| static boolean isSupported(String charsetName) | 判断是否支持该编码类型 |
@Test
public void testCharset1() {
// 1. 通过编码类型获得 Charset 对象
Charset charsetGBK = Charset.forName("GBK");
System.out.printf("charsetGBK: %s", charsetGBK).println();
// 2. 获得系统支持的所有编码方式
Map map = Charset.availableCharsets();
Set> set = map.entrySet();
for (Map.Entry entry : set) {
System.out.println(entry.getKey() + " = " + entry.getValue().toString());
}
// 3. 获得虚拟机默认的编码方式
Charset defaultCharset = Charset.defaultCharset();
System.out.printf("defaultCharset: %s", defaultCharset).println();
// 4. 判断是否支持该编码类型
boolean supported = Charset.isSupported("GBK");
System.out.printf("supported: %b", supported).println();
}
2. Charset 常用普通方法
| 方法 | 描述 |
|---|---|
| final String name() | 获得 Charset 对象的编码类型(String) |
| abstract CharsetEncoder newEncoder() | 获得编码器对象 |
| abstract CharsetDecoder newDecoder() | 获得解码器对象 |
@Test
public void testCharset2() {
// 1. 获得 Charset 对象的编码类型(String)
String charsetName = Charset.forName("GBK").name();
System.out.printf("charsetName: %s", charsetName).println();
}
public static void main(String[] args) throws CharacterCodingException {
// 1. 获取Charset对象
Charset charset = StandardCharsets.UTF_8;
// 2. 获取编码器对象
CharsetEncoder encoder = charset.newEncoder();
// 3. 创建缓冲区
CharBuffer buffer = CharBuffer.allocate(1024);
buffer.put("qs你好");
buffer.flip();
// 4. 编码
ByteBuffer byteBuffer = encoder.encode(buffer);
System.out.println("UTF-8编码结果: ");
for (int i = 0; i < byteBuffer.limit(); i++) {
System.out.println(byteBuffer.get());
}
// 5. 获取解码器对象
byteBuffer.flip();
CharsetDecoder decoder = charset.newDecoder();
// 6 解码
CharBuffer decode = decoder.decode(byteBuffer);
System.out.println("UTF-8解码结果: ");
System.out.println(decode.toString());
// 7. 指定GBK格式解码
Charset charsetGBK = Charset.forName("GBK");
byteBuffer.flip();
CharsetDecoder decoder2 = charsetGBK.newDecoder();
CharBuffer decode2 = decoder2.decode(byteBuffer);
System.out.println("GBK解码结果: ");
System.out.println(decode2.toString());
}



