从JDK1.4开始,Java提供了一系列改进的输入/输出流处理的新功能,这些功能被统称为新IO,即NIO。这些类都被放在java.nio包以及子包下。
Java新IO概述
新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。
Java中与新IO相关包如下:
- java.nio包:主要包含各种与Buffer相关的类;
- java.nio.channels:主要包含与Channel和Selector相关的类;
- java.nio.charset:主要包含与字符集相关的类;
- java.nio.channels.spi包:主要包含与Channel相关的服务器提供者编程接口;
- java.nio.charset.spi:主要包含与字符集相关的服务器提供者编程接口;
Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统字节流相比最大的区别在于它提供了一个map()方法,通过map方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出流系统是面向流的处理,则新IO则是面向块的处理。
Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先方到Buffer中。除此之外,新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作Charset类,也提供了用于支持非阻塞式输入/输出的Selector类。
使用Buffer
从内部结构来看,Buffer就像一个数组,他可以保存多个类型相同的数据。Buffer是一个抽象类,它可以在底层字节数组上进行get/set操作。
static XxxBuffer allocate (int capacity)
:创建一个容量为capacity的XxxBuffer对象;
其中使用最多的是ByteBuffer和CharBuffer,其他的Buffer子类则较少使用。其中ByteBuffer类还有一个MappedByteBuffer子类,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果。通常MappedByteBuffer对象由Channel的map方法返回。
Buffer中的三个重要概念
- 容量(capacity):表示该Buffer的最大数据容量;
- 界限(limit):位于limit后的数据既不可被读,也不可被写;
- 位置(position):用于指明下一个可以被读取的活着写入的缓冲区位置索引。
- 标记(mark):允许直接将position定位到mark处。
Buffer的作用及用法
Buffer的主要作用就是装入数据,然后输出数据。开始时Buffer的position设为0,limit为capacity,程序可通过put方法向Buffer中放入一些数据,每放入一些数据,Buffer的position相应向后移动一些位置。
当Buffer装入数据结束后,调用Buffer的flip方法,该方法将limit设置为position所在位置,并将position设置为0,也就是说Buffer调用flip之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear方法,将position置为0,将limit置为capacity,这样再次向Buffer中装入数据做好准备。
除此之外,Buffer还包含一些常用方法:
int capacity()
:返回Buffer的capacity大小;boolean hasRemaining()
:判断当前位置和界限之间是否还有元素可供处理;int limit()
:返回Buffer的界限的位置;Buffer limit()
:重新设置界限的值,并返回一个具有新的limit的缓冲区对象;Buffer mark()
:设置Buffer的mark位置,它只能在0和position之间做mark;int position()
:返回Buffer中的position值;Buffer position (int newPs)
:设置Buffer的position,并返回position被修改后的Buffer对象。int remaining()
:返回当前位置和界限之间的元素个数;Buffer reset()
:将位置转到mark所造的位置;Buffer rewind()
:将位置设置为0,取消设置的mark。
-put()
:存放数据;get()
:读取数据;
当读取数据的时候,分为相对和绝对两种:
- 相对(Relative):从Buffer的当前position出开始读取或写入数据,然后将位置的值按处理元素的个数增加;
- 绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer时,不会影响到位置的值。
package org.westos.demo6;
import java.nio.CharBuffer;
public class BufferTest {
public static void main(String[] args) {
//创建Buffer
CharBuffer buff = CharBuffer.allocate(8);
System.out.println("capacity:"+buff.capacity()+"\n" + //capacity:8
"limit:"+buff.limit()+"\n" + //limit:8
"position:"+buff.position()); //position:0
//放入元素
buff.put('a');
buff.put('b');
buff.put('c');
System.out.println("加入三个元素后,position="+buff.position());//加入三个元素后,position=3
//调用flip方法
buff.flip();
System.out.println("执行flip后,limit="+buff.limit());//执行flip后,limit=3
System.out.println("position="+buff.position());//position=0
//取出第一个元素
System.out.println("第一个元素:"+buff.get());//第一个元素:a
System.out.println("取出一个元素后,position="+buff.position());//取出一个元素后,position=1
//调用clear方法
buff.clear();
System.out.println("执行clear后,limit="+buff.limit());//执行clear后,limit=8
System.out.println("执行clear后,position="+buff.position());//执行clear后,position=0
System.out.println("执行clear后,缓冲区内容并没有被清除,第三个元素为:"+buff.get(2));//执行clear后,缓冲区内容并没有被清除,第三个元素为:c
}
}
ByteBuffer还提供了一个allocateDirect方法来创建直接Buffer,直接Buffer的创建成本比普通Buffer的创建成本高,但是效率更高。
使用Channel
- channel可以直接将指定文件的部分或全部直接映射成Buffer;
- 程序不能直接访问Channel中的数据,Channel只能和Buffer进行交互。
所有的Channel都不应该通过构造器来直接创建,而是通过传统节点字节流的getChannel()方法来返回对应的Channel,Channel中最常用的三类方法是map(),read(),write(),其中map方法用于将Channel对应的部分或全部数据映射成ByteBuffer;而read()或write()方法都有一系列重载形式,这些方法用于从Buffer中读取或写入数据。
实例:
直接将Filechannel的全部数据映射成ByteBuffer。
package org.westos.demo6;
import java.io.*;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class FileChannelTest {
public static void main(String[] args) {
File f = new File("FileChannelTest.java");
try (
//创建FileInputStream,以该文件的输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
//以文件输出流创建FileChannel,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
)
{
//将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
//使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
//直接将buffer里的数据全部输出
outChannel.write(buffer);
//复原limit、position位置
buffer.clear();
//使用解码器将ByteBuffer转换成CharBuffer
// CharBuffer decoder = charset.decode(buffer);
//CharBuffer的toString方法可以获取对应的字符串
System.out.println(buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
除此之外,RandomAccessFile也包含了一个getChannel方法。
对Channel和Buffer的理解
使用的目的是将文件内容从磁盘映射到内存,访问内存肯定效率高。Buffer相当于内存中映射内容存储的地方,而Channel是映射的通道也就是内容从磁盘到内存的通道。
字符集和Charset
计算机底层不管是图片、音频还是文件都是以二进制形式进行存储的。
把明文字符序列转换成计算机理解的二进制序列成为编码(Encode);
把二进制序列转换成普通人能看懂的明文字符串称为解码(Decode);
Java默认使用Unicode字符集,但很多操作系统并不使用Unicode字符集,所以会出现乱码情况。
JDK1.4提供了Charset来处理字节序列和字符序列之间的转换关系该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,该类是不可变类。
chatset提供了availableCharset静态方法来获取当前JDK所支持的所有字符集。
public class CharsetTest {
public static void main(String[] args) {
//获取Java支持的全部字符集
SortedMap<String, Charset> map = Charset.availableCharsets();
for (String alias:map.keySet()){
System.out.println(alias+"——>"+map.get(alias));
}
}
}
一旦知道了字符集的别名之后,程序就可以调用Charset的forName方法来创建对应的Charset对象,forName方法的参数就是相应字符集的别名。
获取了对象之后,就可以通过该对象的newDecoder(),newEncoder两个方法分别返回CharsetDecoder和CharsetEncoder对象,代表Chatset的解码器和编码器。调用charsetDecoder的decode方法就可以将ByteBuffer转换成CharBuffer。调用encode则相反。
实例:
package org.westos.demo6;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
public class CharsetTransform {
public static void main(String[] args) throws CharacterCodingException {
//创建简体中文对应的charset
Charset cn = Charset.forName("GBK");
//获取编码器和译码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
//创建一个charBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put("孙");
cbuff.put("悟");
cbuff.put("空");
cbuff.flip();
//将字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
for (int i = 0; i <bbuff.capacity() ; i++) {
System.out.println(bbuff.get(i)+" ");
}
//将数据解码成字符序列
System.out.println("\n"+cnDecoder.decode(bbuff));
}
}
文件锁
如果多个运行的程序需要并发访问同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一文件。
在NIO中Java提供了FileLock来支持文件锁功能,在FileChannel中提供的lock/tryLock方法可以获得文件锁FileLock对象,从而锁定文件。两者的区别在于:当Lock试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock时尝试锁定文件,它将直接返回而不是阻塞,如果得到了文件锁,该方法则返回该文件锁,否则将返回null。
如果Filechannel只想锁定文件的部分内容,而不是锁定全部文件内容,则可以使用:
lock(long position,long size, boolean shared )
:从文件的position开始,长度为size的内容加锁,该方法是阻塞式的。trylock(long position,long size, boolean shared )
:非阻塞式加锁法。
当参数shared为true时,表明该锁是一个共享锁,它将允许多个进程来读取文件,但阻止其他进程获得对该文件的排他锁。当shared为false时,表明它是一个排他锁。
package org.westos.demo6;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockTest {
public static void main(String[] args) throws IOException, InterruptedException {
try(
FileChannel channel = new FileOutputStream("a.txt").getChannel();
)
{
FileLock lock = channel.tryLock();
//程序暂停10秒
Thread.sleep(10000);
//释放锁
lock.release();
}
}
}
Java7的NIO.2
早期的File类的功能有限,不能利用特定文件系统特性,方法性能也不高。NIO.2为了弥补这种不足,引入了一个Path接口,Path接口代表一个与平台无关的平台路径。除此之外,NIO.2还提供了Files、Paths两个工具类,其中Files包含了大量静态的工具方法来操作文件;paths则包含了两个返回Path的静态方法。
package org.westos.demo6;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathTest {
public static void main(String[] args) {
//以当前路径来创建Path对象
Path path = Paths.get(".");
System.out.println("path里包含路径数量:"+path.getNameCount());//path里包含路径数量:1
//获取path的绝对路径
System.out.println(path.toAbsolutePath());
//C:\Users\WIN10\Desktop\java\day14\20181110-JavaSE\代码\20181110-MyJavaDemo-IO流-字符流-下午\.
//获取绝对根路径
System.out.println(path.toAbsolutePath().getRoot());//C:\
System.out.println("绝对路径里包含的路径数量:"+path.toAbsolutePath().getNameCount());
//绝对路径里包含的路径数量:9
//以多个String来创建Path对象
Path path2 = Paths.get("g:", "publish", "codes");
System.out.println(path2);//g:\publish\codes
}
}
File类是一个操作文件的工具类,它提供了大量便捷的工具方法
package org.westos.demo6;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import static java.nio.charset.Charset.*;
import static java.nio.file.Paths.*;
public class FilesTest {
public static void main(String[] args) throws IOException {
//复制文件
Files.copy(get("FilesTest.java"),new FileOutputStream("a.txt"));
//判断FilesTest.java是不是隐藏文件
System.out.println("FilesTest是否为隐藏文件:"+Files.isHidden(get("FilesTest.java")));
//一次性读取FilesTest.java文件的所有行
List<String> lines = Files.readAllLines(get("FilesTest.java"));
System.out.println(lines);
//判断指定文件大小
System.out.println(Files.size(get("FilesTest.java")));
//
ArrayList<Object> poem = new ArrayList<>();
poem.add("落霞与孤鹜齐飞");
poem.add("秋水共长天一色");
//直接将多个字符串写入指定文件
java.nio.file.Path write = Files.write(Paths.get("a.txt"), poem,Charset.forName("gbk"));
}
}
使用FileVisitor遍历文件和目录
使用Files工具类可以优雅的遍历文件和子目录。
walkFileTree(Path start,FileVisitor<? super Path>visitor)
:遍历start路径下的所有文件和子目录;walkFileTree(Path start,Set<FilevisitOption>option ,int maxDepth,FileVisitor<? super Path> visitor)
:与上一个方法功能类似,该方法最多遍历maxDepth深度的文件。
这两个方法都需要FileVistor参数,FileVisitor代表一个文件访问器,walkFileTree方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会触发FileVisitor中相应的方法。
-
FileVisitorResult postVisitDirectory(T dir,IOException exc)
:访问子目录之后触发该方法; -
FileVisitorResult postVisitDirectory(T dir,BasicFileAttributes attrs)
:访问子目录之前触发该方法; -
FileVisitorResult postVisitFile(T file,BasicFileAttributes attrs)
:访问file文件时触发该方法; -
FileVisitorResult postVisitFileFailed(T file,IOException exc)
:访问file文件失败时触发该方法;
FileVisitorResult是一个枚举类,代表访问之后的后续行为。 -
CONTINUE:继续访问
-
SKIP_SIBLINGS:继续访问,但不访问该文件或目录的兄弟文件或目录;
-
SKIP_SUBTREE;继续访问,但不访问该文件或目录的子文件;
-
TERMINATE:终止后续访问;
可以通过继承SimpleFileVisitor(FileVisitor的实现类)
来实现自己的文件访问器。
实例:
package org.westos.demo7;
import javafx.beans.binding.SetExpression;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
//C:\Users\WIN10\Desktop\java\day14\20181110-JavaSE\代码\20181110-MyJavaDemo-IO流-字符流-下午
public class FileVisitorTest {
public static void main(String[] args) throws IOException {
//遍历目录下的所有文件和子目录
Files.walkFileTree(Paths.get("C:","Users","WIN10","Desktop","java","day14",
"20181110-JavaSE","代码","20181110-MyJavaDemo-IO流-字符流-下午"),
new SimpleFileVisitor<Path>(){
//访问文件时触发该方法
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("正在访问"+file+"文件");
if(file.endsWith("FileVisitorTest.java")){
System.out.println("已找到目标文件");
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
//开始访问目录时触发该方法
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("正在访问"+dir+"路径");
return FileVisitResult.CONTINUE;
}
});
}
}
遍历指定路径下所有文件和子目录,如果找到FileVisitorTest.java结尾的文件就停止。
使用WatchService监控文件变化
NIO.2的Path类提供了如下方法来监听文件系统的变化:
-
register(WatchService watcher,WatchEvent.Kind<?>...events)
:用watcher监听该path代表的目录下的文件变化。events参数指定要监听哪些类型的参数;
WatchService代表一个文件系统监听服务,它负责监听path代表的目录下的文件变化。一旦使用register方法完成注册之后,接下来就可调用WatchService的如下方法来获取被监听目录的文件变化事件: -
WatchKey poll()
:获取下一个WatchKey,如果没有WathKey发生就立即返回null; -
WathKey poll(long timeout,TimeUnit unit)
:尝试等待timeout时间去获取下一个WathKey; -
WathKey take()
:获取下一个WathKey,如果没有WathKey发生就一直等待。
package org.westos.demo7;
import java.io.IOException;
import java.nio.file.*;
public class WatchServiceTest {
public static void main(String[] args) throws IOException, InterruptedException {
//获取文件系统的WatchService对象
WatchService watchService = FileSystems.getDefault().newWatchService();
//为C:盘根路径注册监听
Paths.get("C:").register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true){
//获取下一个文件变化事件
WatchKey key = watchService.take();
for (WatchEvent<?> event:key.pollEvents()){
System.out.println(event.context()+"文件发生了变化"+event.kind()+"事件!");
}
//重设WatchKey
boolean vaild = key.reset();
//如果重设失败,退出监听
if(!vaild){
break;
}
}
}
}
可以在C盘创建删除文件来检测。
访问文件属性
java7NIO.2在java.nio.file.attribute包下提供了大量的工具类,通过这些工具类,开发者可以非常简单地读取、修改文件属性。
- XxxAttributeView:代表某文件属性的“视图”;
- XxxAttributes:代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取XxxAttributes;
在这些类中FileAttributeView是其他类的父接口。
- AclFileAttributeView:通过AclFileAttributeView,开发者可以为特定文件设置ACL及文件所有者属性。通过getAcl方法返回该文件的权限集,setAcl可以修改该文件的ACL;
- BasicFileAttributeView:可以获取或修改文件的基本属性,包括文件的最后修改时间、最后访问时间、创建时间、大小、是否为目录、是否为链接符号等。
- DosFileAttributeView:它主要用获取或修改文件DOS相关属性,比如是否为只读、是否隐藏、是否为系统文件等。
- FileOwnerAttributeview:主要用于获取文件或修改文件的所有者。
- UserDefinedFileAttributeView:可以让开发者为文件设置一些自定义属性;
package org.westos.demo7;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.util.Date;
import java.util.List;
public class AttributeViewTest {
public static void main(String[] args) throws IOException {
//获取将要操作的文件
Path testPath = Paths.get("AttributeViewTest.java");
//获取访问基本属性的BasicFileAttributeView
BasicFileAttributeView basicView = Files.getFileAttributeView(testPath, BasicFileAttributeView.class);
//多去访问基本属性的BasicFileAttribute
BasicFileAttributes basicFileAttributes = basicView.readAttributes();
//访问基本属性
System.out.println("创建时间:"+new Date(basicFileAttributes.creationTime().toMillis()));
System.out.println("最后访问时间:"+new Date(basicFileAttributes.lastAccessTime().toMillis()));
System.out.println("最后修改时间:"+new Date(basicFileAttributes.lastModifiedTime().toMillis()));
System.out.println("文件大小:"+basicFileAttributes.size());
//获取访问文件属主信息FileOwnerAttributeView
FileOwnerAttributeView ownerView = Files.getFileAttributeView(testPath, FileOwnerAttributeView.class);
//获取该文件所属用户
System.out.println(ownerView.getOwner());
//获取系统中对应的用户
UserPrincipal user = FileSystems.getDefault()
.getUserPrincipalLookupService()
.lookupPrincipalByName("DESKTOP-TGIQ28G\\WIN10 (User)");
//修改用户
ownerView.setOwner(user);
//获取访问自定义属性的
UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath,
UserDefinedFileAttributeView.class);
List<String> attrName = userView.list();
//遍历所有的自定义属性
for (String name:attrName) {
ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
userView.read(name,buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();
System.out.println(name+"---->"+value);
}
//添加一个自定义属性
userView.write("发行者",Charset.defaultCharset().encode("java"));
}
}