NIO 体系
NIO 跟传统的 IO 一样都用于输入/输出,但 NIO 采用了内存映射文件的方式来处理输入/输出。NIO 将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件,相比于传统 IO 要快得多。
Java 中与 NIO 有关的包:
- java.nio:主要包含各种与 Buffer 相关的类。
- java.nio.channels:主要包含与 Channel 和 Selector 相关的类。
- java.nio.charset:主要包含与字符集相关的类。
- java.nio.channels.spi:主要包含与 Channel 相关的服务提供者编程接口。
- java.nio.charset.spi:包含于字符集相关的服务提供者编程接口。
Channel(通道) 和 Buffer(缓冲)
Channel 是对传统的输入/输出系统的模拟,在 NIO 中所有数据都需要通过 Channel 传输。Channel 提供了一个 map() 方法,可以直接将 “一块数据” 映射到内存中。
Buffer(缓冲) 本质是一个数组,是一个容器。发送到 Channel 中的所有对象都必须首先放到 Buffer 中,而从 Channel 中读取的数据也必须先放到 Buffer 中。
除了 Channel 和 Buffer 之外,新 IO 还提供了用于将 Unicode 字符映射成字节序列以及逆映射操作的 Charset 类,也提供了用于支持非阻塞式 输入/输出 的 Selector 类。
Buffer
Buffer 是一个抽象类,对应于其他基本数据类型都有相应的 Buffer 类:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。通过 static XxxBuffer allocate(int capacity) 静态方法来获取一个容量为 capacity 的 XxxBuffer 对象。
ByteBuffer 类还有一个子类:MappedByteBuffer,它用于表示 Channel 将文件的部分或全部内容映射到内存中后得到的结果,由 Channel 的 map() 方法返回对应文件的 MappedByteBuffer。
Buffer 中有三个重要的概念:容量(capacity)、界限(limit) 和 位置(position):
- 容量(capacity):缓冲区的 容量(capacity) 表示该 Buffer 的最大数据容量。缓冲区的容量不可能为负值,创建后不能改变。
- 界限(limit):第一个不应该被读出或写入的缓冲区位置索引。也就是说,位于 limit 后的数据既不可被读,也不可被写。
- 位置(position):用于指明下一个可以被读出或者写入的缓冲区位置索引。当使用 Buffer 从 Channel 中读取数据时,position 的值恰好等于已经读到了多少数据。当新建一个 Buffer 对象时,其中 position 为 0;如果从 Channel 中读取了 2 个数据到该 Buufer 中,则 position 为 2,指向 Buffer 中第3个(第1个位置索引为0)位置。
除此之外,Buffer 还支持一个可选的标记 mark,Buffer 允许直接将 position 的定位到 mark 处。这些值存在以下关系:1
0 <= mark <= position <= limit <= capacity
Buffer 的主要作用就是装入数据,然后输出数据。开始的 Buffer 的 position 为 0,limit 为 capacity,mark=-1。程序可以通过 put() 方法向 Buffer 中放入一些数据(或者从 Channel 中获取一些数据),每放入一些数据,Buffer 的 position 相应地向后移动一些位置。
当 Buffer 装入数据后,调用 Buffer 的 flip() 方法,该方法将 limit 设置为 position 所在位置,并将 position 设为 0,这就使得 Buffer 的读写指针又移动到开始位置。也就是说,Buffer 调用 flip() 方法之后,Buffer 为输出数据做好准备;当 Buffer 输出数据结束后,Buffer 调用 clear() 方法,clear() 方法并非清空 Buffer 的数据,它仅仅将 position 置为 0,将 limit 置为 capacity,这样再次向 Buffer 中装入数据做好准备。
Buffer 还有如下一些常用方法:
- int capacity():返回 Buffer 的 capacity 大小。
- boolean hasRemaing():判断当前 position 和 limit 之间是否还有元素可供处理。
- int limit():返回 Buffer 的 limit 的位置。
- Buffer limit(int newLt):重新设置 limit 的值,并返回一个具有新的 limit 的缓冲区对象。
- Buffer mark():设置 Buffer 的 mark 位置,它只能在 0 和 position 之间做 mark。
- int position():返回 Buffer 中的 position 值。
- Buffer position(int newPs):设置 Buffer 的 position,并返回 position 被修改后的 Buffer 对象。
- int remaining():返回当前 position 和 limit 之间的元素个数。
- Buffer reset():将 position 转到 mark 所在的位置。
- Buffer rewind():将 position 设置成 0,取消设置的 mark。
Buffer 的所有子类还提供了两个重要的方法:put() 和 get() 方法,用于向 Buffer 中放入数据和从 Buffer 中取出数据。当使用 put() 和 get() 方法放入、取出数据时,Buffer 既支持对单个数据的访问,也支持对批量数据的访问(以数组作为参数)。
当使用 put() 和 get() 来访问 Buffer 中的数据时,分为相对和绝对两种:
- 相对:从 Buffer 的当前 position 处开始读取或写入数据,然后将 position 的值按处理元素的个数增加。
- 绝对:直接根据索引向 Buffer 中读取或写入数据,使用绝对方式访问 Buffer 里的数据时,并不会影响 position 的值。
Channel
Channel 类似于传统的流对象,但与传统的流对象有两个主要区别:
- Channel 可以直接将指定文件的部分或全部直接映射成 Buffer。
- 程序不能直接访问 Channel 中的数据,包括读取、写入都不行,Channel 只能与 Buffer 进行交互。
Java 为 Channel 接口按功能提供了以下常用实现类:
- Pipe.SinkChannel 和 Pipe.SourceChannel:用于支持线程之间通信管道。
- FileChannel:用于对文件进行操作。
- SelectableChannel
- ServerSocketChannel 和 SocketChannel:用于支持 TCP 网络通信。
- DatagramChannel:用于支持 UDP 网络通信。
所有的 Channel 都不应该通过构造器来直接创建,而是通过传统的节点 InputStream、OutputStream 的 getChanne() 方法来返回对应的 Channel,不同的节点流获得的 Channel 不一样。例如,FileInputStream、FileOutputStream 的getChannel() 返回的是 FileChannel,而 PipeInputStream 和 PipeOutputStream 的 getChannel() 返回的是 Pipe.SinkChannel 和 Pipe.SourceChannel。
Channel 中最常用的三类方法是 map()、read() 和 write()。其中 map() 方法用于将 Channel 对应的部分或全部数据映射成 ByteBuffer;而 read() 或 write() 方法都有一系列重载形式,这些方法用于从 Buffer 中读取数据或向 Buffer 中写入数据。
map() 方法的签名为:MappedByteBuffer map(FileChannel.MapMode mode,long position,long size),第一个参数执行映射时的模式,有 只读、读写等 模式;而第二个、第三个参数用于控制将 Channel 的哪些数据映射成 ByteBuffer。
直接将 FileChannel 的全部数据映射成 ByteBuffer:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24File file=new File("D:\\FileChannel.java");
try(
//根据文件输入流创建Channel
FileChannel inputFileChannel=new FileInputStream(file).getChannel();
//根据文件输出流创建Channel
FileChannel outputFileChannel=new FileOutputStream("D:\\a.txt").getChannel()
)
{
//将 FileChannel 里的全部数据映射成 ByteBuffer
MappedByteBuffer buffer=inputFileChannel.map(FileChannel.MapMode.READ_ONLY,0,f.length());
//使用 UTF-8 的字符集来创建解码器
Charset charset=Charset.forName("UTF-8");
//直接将 buffer 里的数据全部输出到 OutputChannel
outputFileChannel.write(buffer);
//调用 buffer 的 clear() 方法,重置 limit、position的位置
buffer.clear();
//根据前面定义的 charset 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder=charset.newDecoder();
//使用解码器将 ByteBuffer 转换成 CharBuffer
CharBuffer charBuffer=decoder.decode(buffer);
//CharBuffer 的 toString()方法可以获取对应的字符串
System.out.println(charBuffer.toString());
}catch(IOException e){}
}
不仅 InputStream、OutputStream 包含了 getChannel() 方法,在 RandomAccessFile 中也包含了一个 getChannel()方法,至于 RandomAccessFile 返回的 FileChannel() 是只读的还是读写的,取决于 RandomAccessFile 打开文件的模式。
以下代码实现了文件内容的追加复制:1
2
3
4
5
6
7
8
9
10
11
12
13
14File file=new File("D:\\randomFile.txt");
try(
//创建一个RandomAccessFile对象
RandomAccessFile raf=new RandomAccessFile(file,"rw");
//获取RandomAccessFile对应的Channel
FileChannel randomChannel=raf.getChannel()
)
{
//将Channel中的所有数据映射成ByteBuffer
ByteBuffer buffer=randomChannel.map(READ_ONLY,0,file.length());
//把Channel的记录指针移到最后
randomChannel.position(file.length());
//将buffer中的所有数据输出
randomChannl.write(buffer);
如果习惯了传统IO的”用竹筒多次重复取水”的过程,或者担心 Channel 对应的文件过大,使用 map() 一次将所有的文件内容映射到内存中引起性能下降,也可以使用 Channel 和 Buffer 传统的 “用竹筒多次重复取水” 的方式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24try(
//创建文件输入流
FileInputStream fis=new FileInputStream("readFile.java");
//创建FileChannel
FileChannel fileChannel=fis.getChannel();
)
{
//定义一个ByteBuffer对象,用于重复取水
ByteBuffer buffer=ByteBuffer.allocate(1024);
//将FileChannel中的数据放入ByteBuffer
while(fileChannel.read(buffer) != -1)
{
//锁定buffer的空白区,避免读取到null的区域
buffer.flip();
//根据指定编码字符集创建解码器
Charset charset=Charset.forName("UTF-8");
CharsetDecoder decoder=charset.newDecoder();
//将ByteBuffer的内容转码
CharBuffer charBuffer=decoder.decode(buffer);
System.out.print(charBuffer);
//将ByteBuffer对象重新初始化,为下一次读取数据做准备
buffer.clear();
}
}catch(IOException e);
文件锁
如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用 文件锁 可以有效地阻止多个进程并发修改同一个文件,所以现在的大部分操作系统都提供了 文件锁 功能。
文件锁控制文件的全部或部分字节的访问,但文件锁在不同的操作系统中差别较大,从 NIO 开始 java 提供了 FileLock 来支持文件锁功能,在 FileChannel 中提供的 lock()/tryLock() 方法可以获得文件锁 FileLock 对象,从而锁定文件。
lock() 和 tryLock() 方法存在区别:
当 lock() 试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;
而 tryLock() 是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,该方法则返回文件锁,否则返回 null。
如果 FileChannel 只想锁定文件的部分内容,而不是锁定全部内容,则可以使用如下的 lock() 或 tryLock() 方法:
- lock(long position,long size,boolean shared):对文件从 position 开始,长度为 size 的内容加锁,该方法是阻塞式的。
- tryLock(long position,long size,boolean shared):非阻塞式的加锁方法。参数的作用与上面一样。
当参数 shared 为 true 时,表明锁时一个共享锁,它将允许多个进程来读取该文件,但阻止其他进程获得对该文件的排他锁。当 shared 为 false 时,表明该锁是一个排他锁,它将锁住对该文件的读写。程序可以通过调用 FileLock() 的 isShared() 来判断它是否获得共享锁。直接使用无参数的 lock() 或 tryLock() 获得的是排他锁。当处理完文件后通过 FileLock 的 release() 方法释放文件锁。
1 | try( |
NIO2
Java7 对原有的 NIO 进行了重大改进:
- 提供了全面的文件 IO 和文件系统访问支持。
- 基于异步 Channel 的 AIO。
第一个改进表现为 Java7 新增的 java.nio.file 包以及各个子包;第二个改进表现为 Java7 在 java.nio.channels 包下增加了多个以 Asynchronous 开头的 channel 接口和类。
Path、Paths 和 Files 核心 API
传统的 IO 提供了一个 File 类来访问文件系统,但 File 缺陷较多,功能有限,现在尽量不用。
NIO2 为了取代 File 类,引入了 Path 接口,Path 接口代表一个平台无关的平台路径。除此之外,NIO2 还提供了 Files、Paths 两个工具类,其中 Files 类包含了大量静态的工具方法来操作文件;Paths 则包含了两个返回 Path 的静态工厂方法。
Path 通过 Paths 的 get(String first,String…more) 或 get(URI uri) 方法来获取 Path 对象。要注意,创建 Path 并不会创建物理文件或者目录。
Files 是一个操作文件的工具类,它提供了大量便捷的工具方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//复制文件
Files.copy(Paths.get("FilesTest.java"),Paths.get("a.txt"));
//判断 FilesTest.java 文件是否为隐藏文件
Files.isHidden(Paths.get("FilesTest.java"));
//一次性读取 FilesTest.java 文件的所有行
List<String> lines=Files.readAllLines(Paths.get("FilesTest.java"),Charset.forName("gbk"));
//判断指定文件的大小
Files.size(Paths.get("FilesTest.java"));
//使用 Java8 新增的 Stream API 列出当前目录下所有文件和子目录
Files.list(Paths.get(".")).forEach(path->System.out.println(path));
//使用 Java8 新增的 Stream API 读取文件内容
Files.lines(Paths.get("FilesTest.java"),Charset.forName("gbk")).forEach(line->System.out.println(lint));
FileStore cStore=Files.getFileStore(Paths.get("C:"));
//判断C盘的总空间、可用空间
System.out.println("C:总空间: "+ cStore.getTotleSpace());
System.out.println("C:可用空间:"+ cStore.getUsableSpace());
使用 FileVisitor 遍历文件和目录
早期版本中,程序要遍历指定目录下的所有文件和子目录,只能使用递归进行遍历,但这种方式不仅复杂,而且灵活性不高。现在在 Files 工具类中提供了如下两个方法来遍历文件和子目录:
- walkFileTree(Path start,FileVisitor<? super Path> visitor:遍历 start 路径下的所有文件和子目录。
- walkFileTree(Path start,Set
option,int maxDepth,FileVisitor<? super Path> visitor) :与上个方法功能类似。该方法最多可以遍历 maxDepth 深度的文件。
上述两个方法中的 FileVisitor 参数代表一个文件访问器,walkFileTree() 方法会自动遍历 star 路径下的所有文件和子目录,遍历文件和子目录都会出发 FileVisitor 中相应的方法。
FileVisitor 中定义了如下方法:
- FileVisitResult postVisitDirectory(T dir,IOException exc):访问子目录之后触发该方法。
- FileVisitResult preVisitDirectory(T dir,BasicFileAttributes attrs):访问子目录之前触发该方法。
- FileVisitResult visitFile(T file,BasicFileAttributes attrs):访问 file 文件时触发该方法。
- FileVisitResult visitFileFailed(T file,IOException exc):访问 file 文件失败时触发该方法。
上面4个方法都返回一个 FileVisitResult 对象,它是一个枚举类,代表了访问之后的后续行为:
- CONTINUE:代表”继续访问”的后续行为。
- SKIP_SIBLINGS:代表”继续访问”的后续行为,但不访问该文件或目录的兄弟文件或目录。
- SKIP_SUBTREE:代表”继续访问”的后续行为,但不访问该文件或目录的子目录。
- TERMINATE:代表”中止访问”的后续行为。
可以通过继承 SimpleFileVisitor类(FileVisitor的实现类)来实现自己的”文件访问器”,根据需要、选择性地重写指定方法。
下面示范了如何使用 FileVisitor 来遍历文件和子目录:
1 | //遍历 D:/ 下的所有文件和目录 |
使用 WatchService 监控文件变化
如果要监控文件的变化,Path 类提供了如下一个方法:
- register(WatchService watcher,WatchEvent.Kind<?>…events):用 watcher 监听该 path 代表的目录下的文件变化。events 参数指定要监听哪些类型的事件。
在这个方法中,WhatchService 代表一个文件系统监听服务,它负责监听 path 代表的目录下的文件变化。一旦使用 register() 方法完成注册之后,接下来就可以调用 WhatchService 的如下三个方法来监听目录的文件变化事件:
- WatchKey poll():获取下一个 WatchKey,如果没有 WatcheKey 发生就立即返回 null。
- WatchKey poll(long timeout,TimeUnit unit):尝试等待 timeout 时间去获取下一个 WatchKey。
- WatchKey take():获取下一个 WatchKey,如果没有 WatchKey 发生就一直等待。
(待补充)
访问文件属性
从 Java7 开始,java.nio.file.attribute 包下提供了大量的工具类,可以非常简单地读取、修改文件属性。这些工具类主要分为如下两类:
- XxxAttributeView:代表某种文件属性的 “视图”。
- XxxAttributes:代表某种文件属性的 “集合”,程序一般通过 XxxAttributeView 对象来获取 XxxAttributes。
(待补充)