Java的NIO系统(5) - NIO的用法简介

发表于2019-12-28,长度6329, 1489个单词, 18分钟读完
Flag Counter

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的原理和源码比较简单,就是使用栈深度优先的记录访问节点,栈元素引用了节点的迭代流,每次都从流中取下一个节点迭代处理。如果是节点不是目录或流已经结束,就弹出元素。

Written on December 28, 2019
分类: dev, 标签: java
如果你喜欢,请赞赏! davelet