文件编程
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);
}
});
}
}