Java的NIO系统(5) - NIO的用法简介
NIO 除了可以基于通道进行输入输出,还可以基于流,还可以基于文件系统。这一篇里通过文件操作简单说一下NIO的这三种用法。
一、基于通道的IO
这是NIO使用最广泛的场景,在网上搜到的基本都是讲这个。这里简单说一下。
1. 通道读
try (SeekableByteChannel sc = Files.newByteChannel("文件路径")){
ByteBuffer buffer = ByteBuffer.allocate(128);
while (true){
int read = sc.read(buffer);
if (read == -1) break;
buffer.rewind();
int temp = 0;
while (temp< read) {
System.out.print((char)buffer.get());
temp++;
}
}
} catch (IOException e) {
e.printStackTrace();
}
代码比较简短,就是把文件每次读取128个字节打印出来。buffer读写模式转换使用了rewind(),它会把当前位置置为0。所以使用flip()也可以,而且更符合实际。但是由于下面限制了读取次数(temp < read)所以不会越界,而rewind比flip每次少设置一次值(flip会改变limit,rewind不会)。
熟悉前面文章的读者看到这里应该会提出一个疑问:既然是按字节输出,那中文怎么办?
运气好的话,我们可以使用缓冲区转码的方式:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
// 将byteBuffer转为charBuffer
CharBuffer charBuffer = decoder.decode(buffer);
buffer.compact();
charBuffer.rewind();
while (charBuffer.position() < charBuffer.limit()) {
char c = charBuffer.get();
System.out.print(c);
}
看起来似乎可以,测试的话也有很大可能正常输出。但是有时候还是不行,因为128个字节的末尾可能是半个汉字。
另外可能还有读者会说:前面说过有一个基础通道叫MappedByteBuffer,是专门用于处理文件的,为什么不用呢?下面我们就用一下:
try (FileChannel fc = (FileChannel) Files.newByteChannel(path)){
long size = fc.size();
MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
int tem = 0;
while (tem < size) {
System.out.print((char)map.get());
tem++;
}
} catch (IOException e) {
e.printStackTrace();
}
代码更加简短了,因为文件内容被一次性读入了内存(所以能拿到size)。在使用默认文件系统的前提下,Files.newByteChannel(path)返回的是FileChannel,所以可以强制类型转换;而MappedByteBuffer是通过FileChannel的map方法生成的。注意下map的第一个参数,可以看下源码怎么用。
这时候可以测试,中文还是全部乱码,因为依然是按照字节输出的
因为文件被一次性读完,就不会存在末尾是半个汉字的问题了。所以我们把MappedByteBuffer和CharsetDecoder结合起来:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
try (FileChannel fc = (FileChannel) Files.newByteChannel(path)){
long size = fc.size();
MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
CharBuffer charBuffer = decoder.decode(map);
charBuffer.rewind();
while (charBuffer.position() < charBuffer.limit()) {
System.out.print(charBuffer.get());
}
} catch (IOException e) {
e.printStackTrace();
}
这样就不会有中文乱码了!
2. 通道写
也先使用缓冲区:
Path path = Paths.get("t.txt");
try (FileChannel fc = (FileChannel) Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)){
ByteBuffer b = ByteBuffer.allocate(60);
for (int a = 0; a< 60; a++) {
b.put((byte)('0' + a));
}
b.rewind();
fc.write(b);
} catch (IOException e) {
e.printStackTrace();
}
代码很简单,就说一点:创建文件通道的时候指定了打开模式。读的时候没有指定就默认只读(当然也可以指定),现在指定为可写和创建,会在文件不存在的时候生成文件先。
另外注意一点,这样写入的时候并非追加而是覆盖,会从头开始覆盖文件,但和流覆盖不同的是没覆盖的地方并不会删除。所以如果更开始文件很长,写入内容很少,文件还是那么长,只是文件开头变了
下面同样使用文件映射写入文件:
Path path = Paths.get("t.txt");
try (FileChannel fc = (FileChannel) Files.newByteChannel(
path,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.READ)) {
MappedByteBuffer b = fc.map(FileChannel.MapMode.READ_WRITE, 0, 60);
for (int a = 0; a < 60; a++) {
b.put((byte) ('0' + a));
}
} catch (IOException e) {
e.printStackTrace();
}
注意打开模式要是可读写的,因为缓冲区没有只写模式,只有读写模式。而读写模式要求通道也是读写的。
写入不会出现乱码问题。比如下面的例子会把字符串内容完整记入文件
Path path = Paths.get("t.txt");
try (FileChannel fc = (FileChannel) Files.newByteChannel(
path,
StandardOpenOption.WRITE,
StandardOpenOption.READ)) {
String s = "啊啊明快";
byte[] bytes = s.getBytes();
MappedByteBuffer b = fc.map(FileChannel.MapMode.READ_WRITE, 0, bytes.length);
b.put(bytes);
} catch (IOException e) {
e.printStackTrace();
}
二、基于流的NIO
下面的功能是Java7开始提供的。
1 输入流
这个就简单了,直接调用Files.newInputStream(Paths.get(“路径”))即可拿到老IO流对象:
try (InputStream is = Files.newInputStream(path)) {
while (true) {
int read = is.read();
if (read == -1)
break;
System.out.print((char) read);
}
} catch (IOException e) {
e.printStackTrace();
}
2 输出流
这个也简单,看代码:
try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(path), 1)) {
os.write("111两两".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
三、文件系统使用NIO
早在《IO概述》中就说过File可以操作目录、文件元信息等,Java7开始也提供了同样的能力。NIO2的文件系统处理能力更好用,开始支持符号链接,目录遍历更加方便。
升级tip:File提供了toPath方法用于转为新API
下面简单演示一下,更多方法的定义在上一篇也说过。
Path path = Paths.get("t.txt");
System.out.println(path.isAbsolute());
System.out.println(path.resolve("/Users/sheldon/IdeaProjects/css-agreement-center").getName(2));
System.out.println(path.resolve("/Users/sheldon/IdeaProjects/css-agreement-center").isAbsolute());
System.out.println(Files.isExecutable(path));
try {
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println(attributes.isDirectory());
System.out.println(attributes.isSymbolicLink());
System.out.println(attributes.fileKey());
} catch (IOException e) {
e.printStackTrace();
}
输出是:
false
IdeaProjects
true
false
false
false
(dev=1000004,ino=34728099)
在看一个目录遍历的例子:
try {
DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get("."));
stream.forEach(dir -> {
try {
BasicFileAttributes attributes = Files.readAttributes(dir, BasicFileAttributes.class);
if (attributes.isDirectory()) {
System.out.print("文件夹:");
} else {
System.out.print(" 文件: ");
}
System.out.println(dir.getName(1));
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
输出:
文件夹:css-agreement-center-dao
文件: pom.xml
文件: t.txt
文件夹:logs
文件: .gitignore
文件夹:.idea
文件夹:sql
目录遍历可以使用通配符,也可以使用过滤器,这里不演示了。
最后介绍一个大招:深度遍历目录。上面仅仅显示当前目录下的内容,现在我们想要多级显示,使用Files.walkFileTree:
try {
Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
注意这里实现了一个类SimpleFileVisitor,这是一个定义了如何遍历目录的实现,但是要使用必须重新实现,最简单的就是继承啥也不干,这样遍历完啥也看不出来。上面我们重写了visitFile方法,将路径打印出来。SimpleFileVisitor还有其他方法可以重新,分别是访问前、访问后、访问失败时,类似于AOP。比如可以在访问前判断目录是否要跳过,跳过就返回FileVisitResult.SKIP_SUBTREE 而非 FileVisitResult.CONTINUE。walkFileTree的原理和源码比较简单,就是使用栈深度优先的记录访问节点,栈元素引用了节点的迭代流,每次都从流中取下一个节点迭代处理。如果是节点不是目录或流已经结束,就弹出元素。
