三、文件编程
3.1、FileChannel
FileChannel:只能工作在阻塞模式下。
获取
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法
通过 FileInputStream 获取的 channel 只能读
通过 FileOutputStream 获取的 channel 只能写
通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取
会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾
int readBytes = channel.read(buffer);
写入
写入的正确姿势如下, SocketChannel
ByteBuffer buffer = ...; buffer.put(...); // 存入数据 buffer.flip(); // 切换读模式 while(buffer.hasRemaining()) { channel.write(buffer); }
在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel
关闭
channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法
位置
获取当前位置
long pos = channel.position();
设置当前位置
long newPos = ...; channel.position(newPos);
设置当前位置时,如果设置为文件的末尾
这时读取会返回 -1
这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
大小
使用 size 方法获取文件的大小
强制写入
操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘
注意:只有当channel关闭的时候才会同步到磁盘上去,也可以通过编程式直接写入,不过对性能会有影响!
3.2、两个Channel传输数据(transferTo())
目的:进行文件的复制拷贝。
底层:使用操作系统的零拷贝。
方式:使用Channel的transferTo()方法,需要注意的是每次方法调用只能拷贝2GB容量,所以对于大于2G大小的需要多次重复调用。
代码说明:两个demo复制拷贝方法,一个是>2G的,另一个是<=2G。
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; /** * @ClassName TestTransferTo * @Author ChangLu * @Date 2021/12/17 19:26 * @Description 测试FileChannel的transferTo方法:底层为零拷贝 */ public class TestTransferTo { public static void main(String[] args) { String FILE_FROM = "C:\\Users\\93997\\Desktop\\新建文件夹\\这个杀手不太冷1994.mp4"; String FILE_TO = "C:\\Users\\93997\\Desktop\\新建文件夹\\新建文件夹\\这个杀手不太冷1994.mp4"; final long begin = System.currentTimeMillis(); System.out.println("复制中..."); //传输<=2G // transferTo_2G(FILE_FROM, FILE_TO); //>2G transferTo_t2G(FILE_FROM, FILE_TO); System.out.println("耗时:" + (System.currentTimeMillis() - begin) / 1000 + "s"); } /** * 传输超过<=2G容量大小,默认transferTo传输一次上限为2G容量 */ public static void transferTo_2G(String FILE_FROM, String FILE_TO){ try ( FileChannel from = new FileInputStream(FILE_FROM).getChannel(); FileChannel to = new FileOutputStream(FILE_TO).getChannel(); ) { final long size = from.size(); //注意:传输默认上限最大为2G from.transferTo(0, size, to); } catch (IOException e) { e.printStackTrace(); } } /** * 传输超过2G容量大小 */ public static void transferTo_t2G(String FILE_FROM, String FILE_TO){ try ( FileChannel from = new FileInputStream(FILE_FROM).getChannel(); FileChannel to = new FileOutputStream(FILE_TO).getChannel(); ) { //通过传输容量与原始容量比对来解决一次传输>2G文件的情况 long leftSize = from.size(); while (true) { leftSize -= from.transferTo(from.size() - leftSize, leftSize, to); System.out.println("已拷贝:" + (from.size() - leftSize)/1024.0/1024.0 + "MB,剩余:" + leftSize/1024.0/1024.0 + "MB"); if (leftSize < 2 * 1024 * 1024){ break; } } } catch (IOException e) { e.printStackTrace(); } } }
3.3、Path类、Paths工具类
jdk7 引入了 Path 和 Paths 类
Path 用来表示文件路径
Paths 是工具类,用来获取 Path 实例
基本的Paths.get()路径设置:
//基本的Path.get()格式 public static void test01(){ // 相对路径 使用 user.dir 环境变量来定位 1.txt Path path1 = Paths.get("1.txt"); System.out.println(path1);//1.txt // 绝对路径 代表了 d:\1.txt Path path2 = Paths.get("E:\\资源\\电影资源\\1.txt"); System.out.println(path2);//E:\资源\电影资源\1.txt // 绝对路径 同样代表了 d:\1.txt Path path3 = Paths.get("d:/1.txt"); System.out.println(path3); // 代表了 d:\data\projects Path path4 = Paths.get("d:\\data", "changlu.txt"); System.out.println(path4); }
较特殊的路径格式:可转义…
//调用normalize()可对路径进行转义:..表示上级 public static void test02(){ Path path = Paths.get("d:\\data\\projects\\a\\..\\b"); System.out.println(path); System.out.println(path.normalize()); }
3.4、Files工具类(自带传输方法copy)
3.4.1、基本API使用
检查文件是否存在
Path path = Paths.get("helloword/data.txt"); System.out.println(Files.exists(path));
创建目录
创建一级目录:
Path path = Paths.get("helloword/d1"); Files.createDirectory(path);
如果目录已存在,会抛异常 FileAlreadyExistsException
不能一次创建多级目录,否则会抛异常 NoSuchFileException
创建多级目录:
Path path = Paths.get("helloword/d1/d2"); Files.createDirectories(path);
拷贝文件
Path source = Paths.get("helloword/data.txt"); Path target = Paths.get("helloword/target.txt"); Files.copy(source, target);//底层使用的操作系统的拷贝,性能也很好!与transferTo相似。
如果文件已存在,会抛异常 FileAlreadyExistsException
如果希望用 source 覆盖掉 target,需要用 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("helloword/target.txt"); Files.delete(target);
如果文件不存在,会抛异常 NoSuchFileException
删除目录:
Path target = Paths.get("helloword/d1"); Files.delete(target);
如果目录还有内容,会抛异常 DirectoryNotEmptyException
3.4.2、walk、walkFileTree(递归遍历文件目录工具方法)
方法介绍
//递归遍历文件目录,返回的是一个文件Path Stream流 public static Stream<Path> walk(Path start, FileVisitOption... options) throws IOException { return walk(start, Integer.MAX_VALUE, options); }
//递归遍历文件目录,访问者模式,重写接口方法 public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException { return walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor); } //访问接口 public interface FileVisitor<T> { //访问文件目录前 FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; //访问文件时 FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; //访问文件失败 FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; //访问文件目录后 FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException; }
使用场景:
walk():返回的是stream流,流中包含所有的文件Path对象,可以进行过滤、统计、复制一系列操作。
walkFileTree():在递归过程中,可以通过接口重写的方式在不同的文件或目录访问时刻进行一些操作。
功能1:遍历目录文件 /** * 单独功能点1:遍历目录文件,打印出文件夹数量、文件数。 * 说明:这里将root文件也算入文件夹的数量中,所以内部文件夹数量应该-1 */ private static void testViewAllFiles() throws IOException { final Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_201"); //由于匿名实现类中重写方法不能直接对局部变量进行改变值操作所以使用AtomicInteger AtomicInteger dirCount = new AtomicInteger(); AtomicInteger fileCount = new AtomicInteger(); //递归遍历文件夹 Files.walkFileTree(path,new SimpleFileVisitor<Path>() { //进入目录前 @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("进入文件目录=》" + dir); dirCount.getAndIncrement(); return super.preVisitDirectory(dir, attrs); } //访问当前文件 @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(" =》" + file); fileCount.getAndIncrement(); return super.visitFile(file, attrs); } }); System.out.println("文件夹数量:" + dirCount); System.out.println("文件数量:" + fileCount); }
功能2:统计JDK8目录中jar包的个数
/** * 单独功能点2:统计JDK8目录中jar包的个数 */ private static void testJarFileCount() throws IOException { final Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_201"); AtomicInteger jarCount = new AtomicInteger(); //递归遍历文件夹 Files.walkFileTree(path,new SimpleFileVisitor<Path>() { //访问当前文件 @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { //注意:file.getFileName()返回值是Path if (file.toFile().getName().endsWith(".jar")){ System.out.println(file.getFileName()); jarCount.getAndIncrement(); } return super.visitFile(file, attrs); } }); System.out.println("jar包数量:" + jarCount); }
功能3:删除多级目录(不走回收站,谨慎删除)
/** * 单独功能点3:删除多级目录(不走回收站,谨慎删除) */ private static void testFileMultDirs() throws IOException { final Path path = Paths.get("C:\\Users\\93997\\Desktop\\新建文件夹\\工作室网站"); //注意点:删除多级目录时,首先需要先将某个文件夹中的文件内容删除,之后才可以删除文件目录 Files.walkFileTree(path,new SimpleFileVisitor<Path>() { //1、先删除文件 @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return super.visitFile(file, attrs); } //2、后删除目录(后置文件访问器) @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return super.postVisitDirectory(dir, exc); } }); }
####功能4、拷贝文件夹到指定目录
/** * 单独功能点4、拷贝文件夹到指定目录 */ private static void testCopyMultDir() throws IOException { String COPY_ORIGIN = "C:\\Users\\93997\\Desktop\\工作室网站"; //复制到的文件目录为:指定路径\\工作室网站1 String COPY_TARGET = "C:\\Users\\93997\\Desktop\\新建文件夹\\工作室网站1"; //递归返回所有的文件Path集合stream流 Files.walk(Paths.get(COPY_ORIGIN)).forEach(path->{ final String copyFileName = path.toFile().getAbsolutePath().replace(COPY_ORIGIN, COPY_TARGET); System.out.println(copyFileName); //若是文件先创建文件 try { if (Files.isDirectory(path)){ Files.createDirectory(Paths.get(copyFileName)); } else { Files.copy(path, Paths.get(copyFileName)); } } catch (IOException e) { e.printStackTrace(); } }); }