这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战
FileChannel
FileChannel是一个对文件进行操作的通道,需要注意的是,FileChannel只能在阻塞模式下工作,因此不能够搭配Selector。
创建FileChannel
FileChannel没有相应的构造函数,不过通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法,但各自使用情况不同:
- 通过 FileInputStream 获取的 FileChannel只能读
- 通过 FileOutputStream 获取的 FileChannel只能写
- 通过 RandomAccessFile 获取的FileChannel 是否能读写根据构造 RandomAccessFile 时的读写模式决定
FileChannel f1 = new FileInputStream("data.txt").getChannel();
FileChannel f2 = new FileOutputStream("data.txt").getChannel();
FileChannel f3 = new RandomAccessFile("data.txt", "rw").getChannel();
读取内容
可以通过 FileInputStream 获取文件的 channel,通过read方法将数据写入到ByteBuffer中,read方法的返回值表示读到了多少字节,若读到了文件末尾则返回-1,在实际使用中,可以通过read方法的返回值判断是否读取完毕。
public static void main(String[] args) throws IOException {
FileChannel f1 = new FileInputStream("data.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(10000);
int value = 0;
while((value = f1.read(buffer))>0)
{
System.out.println("\n============="+value);
buffer.flip();
while(buffer.hasRemaining())
System.out.print(((char) buffer.get()));
buffer.flip();
}
f1.close();
}
写入内容
可以通过 FileOutputStream 获取文件的 channel,通过write方法将buffer中的数据写入到指定文件中,需要注意的是,因为channel也是有大小的,所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入:
// 通过hasRemaining()方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {
channel.write(buffer);
}
其次,一定要使用buffer的flip方法将buffer的模式转换,否则会写乱码。
public static void main(String[] args) throws IOException {
FileChannel f1 = new FileOutputStream("data2.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("hello wolrd!".getBytes(StandardCharsets.UTF_8));
buffer.flip();
while(buffer.hasRemaining())
{
f1.write(buffer);
}
f1.close();
}
关闭
通道需要close,一般情况通过try-with-resource进行关闭,最好使用以下方法获取stream以及channel,避免某些原因使得资源未被关闭
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("stu.txt");
FileOutputStream fos = new FileOutputStream("student.txt");
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel())
{
// 执行对应操作
}
}
FileChannel的位置
在之前Netty编程(二)—— nio.ByteBuffer基础操作 - 掘金 (juejin.cn)介绍了buffer有个变量position指示的是下一个读写位置的索引,在FileChannel中拥有一个保存读取数据位置的属性position
long pos = channel.position();
可以通过position(int pos)设置channel中position的值,设置当前位置时,如果设置为文件的末尾
- 这时读取会返回 -1
- 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
强制写入
操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘,而是等到缓存满了以后将所有数据一次性的写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘。
两个FileChannel传输数据
transferTo方法
使用transferTo方法可以快速、高效地将一个channel中的数据传输到另一个channel中,但一次只能传输2G的内容。transferTo要传入三个参数:传输channel的起始position,传输大小,目的channel
public static void main(String[] args) throws IOException {
try(
FileChannel f1 = new FileInputStream("data.txt").getChannel();
FileChannel f2 = new FileOutputStream("data2.txt").getChannel();){
System.out.println("传输前的大小: f1大小: "+f1.size()+" f2大小: "+f2.size());
f1.transferTo(0,f1.size(),f2);
System.out.println("传输后的大小: f1大小: "+f1.size()+" f2大小: "+f2.size());
}catch (IOException e) { }
}
结果展示:
传输超过2G
上面说到transferTo方法一次只能传输不超过2G大小的内容,但是超过2G内容,可以使用循环的方式进行多次传输,知道全部传输完毕:
try(
FileChannel f1 = new FileInputStream("data.txt").getChannel();
FileChannel f2 = new FileOutputStream("data2.txt").getChannel();){
long size = f1.size();
long capacity = f1.size();
while(capacity>0)
{
long thisTransfor = f1.transferTo(size - capacity , capacity, f2);
capacity -= thisTransfor;
}
}catch (IOException e) { }
Path和Paths
- Path 用来表示文件路径
- Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt"); // 相对路径 不带盘符 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了 d:\1.txt 反斜杠需要转义
Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了 d:\1.txt
Path projects = Paths.get("d:\\data", "projects"); // 拼接,代表了 d:\data\projects
Files
查找
可以使用Files工具类检查文件是否存在
Path path = Paths.get("data.txt");
System.out.println(Files.exists(path));
创建
如果想要创建一级目录:
Path path = Paths.get("study/java");
Files.createDirectory(path);
需要注意以下两点
- 如果目录已存在,会抛异常 FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛异常 NoSuchFileException
如果要创建多级目录:
Path path = Paths.get("study/java/nio");
Files.createDirectories(path);
拷贝移动
拷贝文件可以使用Files工具类里的copy函数,需要注意的是,如果文件已存在,会抛异常 FileAlreadyExistsException
Path source = Paths.get("java/data.txt");
Path target = Paths.get("java/data1.txt");
Files.copy(source, target);
如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
移动文件可以使用Files工具类里的move函数,如果第三个参数使StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
Path source = Paths.get("java/data.txt");
Path target = Paths.get("study/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
删除
删除文件可以使用Files工具类里的delete方法,需要注意的是,如果文件不存在,会抛异常 NoSuchFileException。
Path target = Paths.get("study/data.txt");
Files.delete(target);
删除目录与删除文件一样,都是使用Files工具类里的delete方法,但是需要注意的是,如果目录还有内容,会抛异常 DirectoryNotEmptyException,所以只能删除空目录,如果想要删除非空目录,可以遍历目录里的所有文件并且删除这些文件以及子目录,然后删除这个目录。不过Files工具类还提供了一个更方便的方法——walkFileTree方法,下一部分会介绍。
Path target = Paths.get("study/java");
Files.delete(target);
walkFileTree方法
使用Files工具类中的walkFileTree(Path, FileVisitor)方法可以更加方便地操作一个目录下的子目录以及文件,他其中需要传入两个参数
-
Path:文件起始路径
-
FileVisitor:文件访问器,这里使用了使用访问者模式,可以传入新建的SimpleFileVisitor并且重写这个类的有四个方法:
- preVisitDirectory:访问目录前的操作
- visitFile:访问文件的操作
- visitFileFailed:访问文件失败时的操作
- postVisitDirectory:访问目录后的操作
使用walkFileTree统计
使用walkFileTree来统计一个目录下所有目录数以及文件数:
public static void main(String[] args) throws IOException {
Path path = Paths.get("E:\\ppt\\");
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
dicAccount.incrementAndGet();
System.out.println("===> "+dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
fileAccount.incrementAndGet();
File f = new File(file.toString());
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return super.visitFileFailed(file, exc);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println(dir+" <===");
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
System.out.println("dicAccount " + dicAccount.get());
System.out.println("fileAccount "+ fileAccount.get());
}
结果展示:
使用walkFileTree方法还可以删除一个目录下所有的文件和子目录,但需要注意的是,刚刚说到Files.delete方法删除目录时只能删除空目录,因此需要在visiteFile中先删除文件,然后在postVisitDirecory方法删除该子目录。
拷贝多级目录
Files.walk(Paths.get(source)).forEach(path1 -> {
try{
String name = path1.toString().replace(source,target);
if(Files.isDirectory(path1))
{
Files.createDirectories(Paths.get(name));
}
else if(Files.isRegularFile(path1))
{
Files.copy(path1,Paths.get(target));
}
}catch (IOException e)
{
e.printStackTrace();
}
});