NIO-文件编程

Source

文件编程

FileChannel

工作模式

FileChannel只能在阻塞模式下工作,所以无法搭配Selector

获取

不能直接打开FileChannel,必须通过FileInputStream、FileOutputStream或者RandomAccessFile来获取FileChannel,它们都有getChannel方法

  • 通过FileInputStream获取的channel只能读
  • 通过FileOutputStream获取的channel只能写
  • 通过RandomAccessFile获取的channel能否读写需要根据构造RandomAccessFile时的读写模式决定
读取

通过 FileInputStream 获取channel,通过read方法将数据写入到ByteBuffer中

read方法的返回值表示读到了多少字节,若读到了文件末尾则返回-1

int readBytes = channel.read(buffer);

可根据返回值判断是否读取完毕

while(channel.read(buffer) > 0) {
    
      
    // 进行对应操作
    ...
}
写入

因为channel也是有大小的,所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入

// 通过hasRemaining()方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {
    
      
	channel.write(buffer);
}
关闭

通道需要close,一般情况通过try-with-resource进行关闭,最好使用以下方法获取stream以及channel,避免某些原因使得资源未被关闭

public class TestChannel {
    
      
    public static void main(String[] args) throws IOException {
    
      
        try (FileInputStream fis = new FileInputStream("xx.txt");
             FileOutputStream fos = new FileOutputStream("xxx.txt");
             FileChannel inputChannel = fis.getChannel();
             FileChannel outputChannel = fos.getChannel()) {
    
      
            ...
        }
    }
}
位置

position

channel也拥有一个保存读取数据位置的属性,即position

long pos = channel.position();

可以通过position(int pos)设置channel中position的值

long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
强制写入

操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘,而是等到缓存满了以后将所有数据一次性的写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

两个Channel传输数据

transferTo方法

使用transferTo方法可以快速、高效的将一个channel中的数据传输到另一个channel中,但是一次只能传输2g的内容

transferTo底层使用了零拷贝技术

public class TestChannelTransferTo {
    
      
    public static void main(String[] args) {
    
      
        try (FileChannel channel = new RandomAccessFile("b.txt", "r").getChannel();
             FileChannel channel1 = new RandomAccessFile("g.txt", "rw").getChannel()
        ) {
    
      
            channel.transferTo(0,channel.size(),channel1);
        } catch (IOException e) {
    
      
            e.printStackTrace();
        }
    }
}

当传输的文件大于2G时,需要通过以下方法进行多次传输

/**
 * @apiNote 解决transferTo方法一次只能写入2G数据
 */
@Slf4j
public class TestChannelTransferToGt2G {
    
      
    public static void main(String[] args) {
    
      
        try (FileChannel channel = new RandomAccessFile("b.txt", "r").getChannel();
             FileChannel channel1 = new RandomAccessFile("d.txt", "rw").getChannel()
        ) {
    
      
            long size = channel.size();
            long hasNotReadSize = size;
            while (hasNotReadSize > 0) {
    
      
                hasNotReadSize -= channel.transferTo((size - hasNotReadSize), hasNotReadSize, channel1);
                log.info("has not read size:{} post:{}", hasNotReadSize, (size - hasNotReadSize));
            }
        } catch (IOException e) {
    
      
            e.printStackTrace();
        }
    }
}

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
  • .代表当前路径
  • …代表了上一级路径
Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
System.out.println(path.normalize()); // 正常化路径 会去除 . 以及 ..

Files

查找

检查文件是否存在

Path path = Paths.get("a.txt");
System.out.println(Files.exists(path));
创建

创建一级目录

Path path= Path.get("C:/files");
Files.createDirectory(path);
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录

Path path = Paths.get("C:/f1/f2");
Files.createDirectories(path);
拷贝和移动

拷贝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望覆盖掉文件,需要使用StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING)

移动文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE)
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
删除

删除文件

Path target = Paths.get("t.txt");
Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

删除目录

Path target = Paths.get("d/d1");
Files.delete(target);
  • 如果目录还有内容,会抛异常 DirectoryNotEmptyException
遍历

可以使用Files工具类中的walkFileTree(Path, FileVisitor)方法,其中需要传入两个参数

  • Path:文件起始路径
  • FileVisitor:文件访问器,使用访问者模式,可以使用实现类SimpleFileVisitor
    • preVisitDirectory:访问目录前的操作
    • visitFile:访问文件的操作
    • visitFileFailed:访问文件失败时的操作
    • postVisitDirectory:访问目录后的操作

统计文件数量

@Slf4j
public class TestFilleWalkFileTree {
    
      
    public static void main(String[] args) throws IOException {
    
      
        AtomicInteger dirCount=new AtomicInteger();
        AtomicInteger fileCount=new AtomicInteger();
        Files.walkFileTree(Paths.get("C:\\Java\\jdk1.8.0_291"),new SimpleFileVisitor<Path>(){
    
      
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    
      
                dirCount.incrementAndGet();
                log.info("=====> dir:{}",dir);
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    
      
                log.info("file:{}",file);
                fileCount.incrementAndGet();
                return super.visitFile(file, attrs);
            }
        });
        log.info("dir_count:{} file_count:{}",dirCount,fileCount);
    }
}

批量删除文件

@Slf4j
public class TestFilesWalkFileTree {
    
      
    //执行顺序 1.preVisitDirectory  2.visitFile  3.postVisitDirectory
    public static void main(String[] args) throws IOException {
    
      
        Files.walkFileTree(Paths.get("E:\\by_delete"),new SimpleFileVisitor<Path>(){
    
      
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    
      
                log.info("enter:{}",dir);
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    
      
                log.info("delete file:{}",file);
                Files.delete(file);
                return super.visitFile(file, attrs);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    
      
                log.info("delete dir:{}",dir);
                Files.delete(dir);
                return super.postVisitDirectory(dir, exc);
            }
        });
    }
}