一、前言
文件拷贝(传输)涉及到Java中的输入和输出流(InputStream,OutputStream),FileChannel等知识点,把文件拷贝学明白了,IO流的相关知识点在头脑中也会更加清晰。这篇博客介绍几种文件拷贝的方法,其中有一些方法是较为底层的(说白了就是自己手写的方法)在实际生产环境中不推荐使用,但是通过这些较为底层的方法才能更好的理解 “流” 的操作过程,有一些方法是JDK提供的、还有一些方法是各种工具类提供的。JDK提供的和各种工具类提供的,建议在生产环境中使用。
此篇博客主要分为传统的阻塞IO(Blocking I/O)实现的文件拷贝和基于NIO(No-Blocking I/O)的FileChannel方式实现的文件拷贝。
题外话:既然别人已经提供了优秀的文件拷贝方法,我们没有必要去重复造轮子,但是!如果想用好这些工具类,前提是你要明白底层原理。只有这样,当你使用那些优秀的工具类时才能游刃有余地去使用,并且看别人的源码时还可以学习一些编码的新知识,如果只是囫囵吞枣的用别人提供的工具,当时是解决了你的问题,但是总感觉用的一头雾水】
二、BIO拷贝文件的几种方式
1. 拷贝文件 - V1【生产环境不推荐使用】
最原始的文件拷贝方式(使用BIO的方式),也没有使用 try-with-resources 方式管理资源,生产环境不推荐使用。
java"> private static void copyFileV1() throws IOException {// 源文件File sourceFile = new File("test.txt");// 目标文件File targetFile = new File("test_copy.txt");FileInputStream fis = new FileInputStream(sourceFile);FileOutputStream fos = new FileOutputStream(targetFile);System.out.println("fis.available(): " + fis.available());byte[] buffer = new byte[1024];int length;// int total = 0;// int number = 0;while ((length = fis.read(buffer)) != -1) {// total += length;// number++;fos.write(buffer, 0, length);}// System.out.println("total bytes: " + total);// System.out.println("number: " + number);fos.close();fis.close();}
2. 拷贝文件 - V2 【生产环境不推荐使用】
使用BIO的方式,使用try-with-resources的方式释放资源,生产环境不推荐使用。
java"> private static void copyFileV2() throws IOException {// 源文件File sourceFile = new File("test.txt");// 目标文件File targetFile = new File("test_copy.txt");try (FileInputStream fis = new FileInputStream(sourceFile);FileOutputStream fos = new FileOutputStream(targetFile)) {byte[] buffer = new byte[1024];int length;while ((length = fis.read(buffer)) != -1) {fos.write(buffer, 0, length);}}}
3. 拷贝文件 - V3. 使用JDK提供的Files.copy方法 【生产环境推荐使用】
使用JDK提供的Files.copy工具类。JDK 提供的 Files.copy()
方法本身并没有特定的文件大小限制。这个方法是用于从源通道复制到目标通道,通常用于文件拷贝。拷贝操作的效率和限制更多地取决于底层的文件系统、可用内存、JVM堆大小以及操作系统的限制等因素。
然而,对于大文件的拷贝,使用 Files.copy()
方法可能不是最高效的方式,因为它可能会涉及将整个文件内容加载到内存中。对于大文件,推荐的做法是使用带缓冲区的分块读取和写入,这样可以减少内存消耗并提高性能,参考【3. 拷贝文件 - V2】。
java"> private static void copyFileV3() throws IOException {Path sourceFile = Paths.get("test.txt");Path targetFile = Paths.get("test_copy.txt");Files.copy(sourceFile, targetFile);//Files.copy(sourceFile, targetFile, StandardCopyOption.COPY_ATTRIBUTES);}
4. 拷贝文件 - V4. 使用Hutool工具提供的文件拷贝方法 【生产环境推荐使用】
需要导入hutool工具包依赖,如下。其【底层使用的是JDK提供的Files工具类】。
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.8</version> </dependency>
java"> private static void copyFileV4() {// 源文件File sourceFile = new File("test.txt");// 目标文件File targetFile = new File("test_copy.txt");FileUtil.copy(sourceFile, targetFile, false);}
5. 拷贝文件 - V5. 使用google提供的 guava 中的工具类 【生产环境推荐使用】
需要导入 guava工具类。如下。【底层使用的和 copyFileV2 一样的方式,但是使用这个工具类的好处是可以帮我们自动释放流资源, 底层创建的缓冲数组大小是8192个字节】
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>19.0</version> </dependency>
java"> private static void copyFileV5() throws IOException {// 源文件File sourceFile = new File("test.txt");// 目标文件File targetFile = new File("test_copy.txt");com.google.common.io.Files.copy(sourceFile, targetFile);}
三、基于NIO的FileChannel 拷贝文件的几种方式
1. 拷贝文件 - V1. 【生产环境不推荐使用】
使用FileChannel拷贝文件(最基本的使用方式),没有考虑文件大小问题 。
java"> private static void v1() throws IOException {// 获取文件输入流File file = new File("test.txt");FileInputStream fileInputStream = new FileInputStream(file);// 从文件输入流获取通道FileChannel inputStreamChannel = fileInputStream.getChannel();// 获取文件输出流FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");// 从文件输出流获取通道FileChannel outputStreamChannel = fileOutputStream.getChannel();// 创建ByteBuffer, 文件内容不大,这里的演示就一次性读取,部分多次读取了ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());// 把输入流通道的数据读取到数据缓冲区inputStreamChannel.read(byteBuffer);// 切换成读模式byteBuffer.flip();// 把缓冲区(ByteBuffer)中的数据写入到输出流通道outputStreamChannel.write(byteBuffer);// 关闭资源fileOutputStream.close();fileInputStream.close();outputStreamChannel.close();inputStreamChannel.close();}
2. 拷贝文件 - V2. 【生产环境不推荐使用】
相比于上面的示例,这个示例代码考虑了文件的大小,使用缓冲区一次读取1024个字节的数据。
java"> private static void v2_1() {try (FileInputStream fileInputStream = new FileInputStream("test.txt");FileChannel inputStreamChannel = fileInputStream.getChannel();FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");FileChannel outputStreamChannel = fileOutputStream.getChannel()) {ByteBuffer byteBuffer = ByteBuffer.allocate(1024);while ((inputStreamChannel.read(byteBuffer)) != -1) {byteBuffer.flip(); // Prepare the buffer for writingoutputStreamChannel.write(byteBuffer);byteBuffer.clear(); // Prepare the buffer for reading again}System.out.println("Large file copied successfully.");} catch (IOException e) {throw new RuntimeException(e);}}
这个示例和上面的v2_1方法是一样的,只不过获取FileChannel的方式不一样。
java"> private static void v2_2() {Path source = Paths.get("test.txt");Path target = Paths.get("test_copy.txt");try (FileChannel sourceChannel = FileChannel.open(source, StandardOpenOption.READ);FileChannel targetChannel = FileChannel.open(target, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {ByteBuffer buffer = ByteBuffer.allocate(8 * 1024); // 8 KB bufferwhile (sourceChannel.read(buffer) != -1) {buffer.flip(); // Prepare the buffer for writingtargetChannel.write(buffer);buffer.clear(); // Prepare the buffer for reading again}System.out.println("Large file copied successfully.");} catch (IOException e) {e.printStackTrace();}}
3. 拷贝文件 - V3. 【生产环境推荐使用】
使用FileChannel的transferTo方法拷贝文件(文件传输,效率比自己写的更高,JDK中方法transferTo的底层都会使用零拷贝进行优化) 【transferTo方法一次性传递的文件大小上限是2G,所以此代码考虑了如果要拷贝的文件大小超过2G的问题。】
java"> private static void v3() {try (FileInputStream fileInputStream = new FileInputStream("test.txt");FileChannel inputStreamChannel = fileInputStream.getChannel();FileOutputStream fileOutputStream = new FileOutputStream("test_copy.txt");FileChannel outputStreamChannel = fileOutputStream.getChannel()) {long size = inputStreamChannel.size();// left变量表示还剩余多少字节要传递for (long left = size; left > 0; ) {log.info("position:{},left:{}", (size - left), left);left = left - inputStreamChannel.transferTo((size - left), left, outputStreamChannel);}} catch (IOException e) {throw new RuntimeException(e);}}