将Java InputStream的内容写入OutputStream的简单方法

506

今天我惊讶地发现,在Java中找不到将InputStream的内容写入OutputStream的简单方式。显然,字节缓冲区代码并不难编写,但我认为我可能只是错过了一些可以使我的生活更轻松(并且代码更清晰)的东西。

那么,对于给定的InputStream inOutputStream out,是否有更简单的方法来编写以下内容呢?

byte[] buffer = new byte[1024];
int len = in.read(buffer);
while (len != -1) {
    out.write(buffer, 0, len);
    len = in.read(buffer);
}

1
你在评论中提到这是为移动应用程序而设计的。它是原生的Android吗?如果是,让我知道,我会发布另一个答案(在Android中可以用一行代码完成)。 - Jabari
24个回答

414
如WMR所述,来自Apache的org.apache.commons.io.IOUtils有一个名为copy(InputStream,OutputStream)的方法,正是您要寻找的。
因此,您需要:
InputStream in;
OutputStream out;
IOUtils.copy(in,out);
in.close();
out.close();

...在你的代码中。

你避免使用IOUtils的原因是什么?


187
我正在开发一款移动应用程序,因为将它加入会使应用程序的大小增加五倍,只为节省区区五行代码,所以我正在避免使用它。 - Jeremy Logan
24
或者使用带资源的 try 块。 - Warren Dew
1
如果您已经在使用Guava库,Andrejs推荐下面的ByteStreams类。它类似于IOUtils的功能,但避免了将Commons IO添加到您的项目中。 - Jim Tough
@fiXedd 你可以使用 Maven Shade从最终的 .jar 文件中去除不需要的类,从而只会引起文件大小的适度增加。 - Sled
如果您正在使用类似于Maven的依赖管理器,并且出于大小原因避免使用IOUtils和Guava,那么您可能需要查看您的依赖层次结构。也许其中一个依赖项在没有通知的情况下过渡地包含了其中之一,毕竟它们非常有名。如果是这种情况,您最好自己使用它们,反正您已经包含它们了。 - user3792852
显示剩余2条评论

343
如果您使用的是Java 7,那么标准库中的Files是最佳选择:
/* You can get Path from file also: file.toPath() */
Files.copy(InputStream in, Path target)
Files.copy(Path source, OutputStream out)

编辑:当您从文件创建InputStream或OutputStream之一时,这当然只有用处。使用file.toPath()从文件获取路径。

要写入现有文件(例如使用File.createTempFile()创建的文件),您需要传递REPLACE_EXISTING副本选项(否则将抛出FileAlreadyExistsException):

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

29
我认为这并不能解决问题,因为一端是路径。虽然你可以获得文件的路径,但据我所知,你无法获取任何通用流(例如网络上的流)的路径。 - Matt Sheppard
4
CopyOptions 是任意的!如果你想要的话,可以将它放在这里。 - user1079877
4
现在这个就是我正在寻找的!JDK来解救了,不需要另一个库。 - Don Cheadle
9
FYI,“Files”在Android的Java 1.7中不可用。我被这个问题困扰了:https://dev59.com/rGAf5IYBdhLWcg3wQQq4。 - Joshua Pinter
26
有趣的是,JDK还有一个Files.copy()函数,它接受两个流,并且所有其他Files.copy()函数都会将其转发以执行实际的复制工作。但是,因为它在那个阶段实际上并没有涉及路径或文件,所以它是私有的,并且与OP问题中的代码(加上一条返回语句)完全相同——没有打开,没有关闭,只有一个复制循环。 - Ti Strga
显示剩余5条评论

244

Java 9

自从Java 9版本,InputStream提供了一个名为transferTo的方法,它具有以下签名:

public long transferTo(OutputStream out) throws IOException
根据文档transferTo 方法将会执行以下操作:

从输入流中读取所有字节并按读取顺序将它们写入给定的输出流。返回时,此输入流将位于流的末尾。此方法不会关闭任何一个流。

该方法可能会无限期地阻塞读取输入流或写入输出流。当输入和/或输出流异步关闭或传输过程中线程被中断时的行为高度取决于特定的输入和输出流,并且因此未指定。

因此,要将Java的InputStream的内容写入OutputStream,可以编写如下代码:

input.transferTo(output);

18
尽可能优先选择使用Files.copy。它是使用本地代码实现的,因此可能更快。只有在两个流都不是FileInputStream/FileOutputStream时,才应使用transferTo - ZhekaKozlov
5
很遗憾,Files.copy不处理任何输入/输出流,而是专门为文件流设计的。 - The Impaler
1
仅适用于API 26及以上版本。 - Prof
4
似乎Files.copy(in, out)方法在底层也使用了transferTo方法。因此,除非JVM为Files.copy(in, out)提供了内置函数,否则似乎没有本地代码。 - Ali Dehghani
这是正确的答案,谢谢。 - Ermiya Eskandary

110

我认为这个方案可行,但请务必测试一下……虽然只是个小“改进”,但可能会牺牲一些易读性。

byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
    out.write(buffer, 0, len);
}

27
我建议至少使用10KB到100KB的缓冲区。这并不多,但可以极大地加快拷贝大量数据的速度。 - Aaron Digulla
7
建议改用 while(len > 0) 而非 != -1,因为当使用 read(byte b[], int off, int len) 方法时,后者也可能返回0,从而在 out.write 处引发异常。 - phil294
13
这是错误的,因为根据“InputStream”合同,read方法返回0的次数可以是任意的。而根据“OutputStream”合同,write方法必须接受长度为0,并且只有当“len”为负时才应该抛出异常。 - Christoffer Hammarström
1
你可以通过将 while 改为 for 并将其中一个变量放在 for 的初始化部分来节省一行代码,例如:for (int n ; (n = in.read(buf)) != -1 ;) out.write(buf, 0, n);。=) - ɲeuroburɳ
1
@Andreas:谢谢!我从未意识到这一点。不过我可以想象出使用情况,你可能需要传递一个零长度的缓冲区,或者由于其他原因而发生。所以就像我总是系安全带一样,我仍然会检查“-1”。停止读取并丢弃剩余数据至少与传递空缓冲区一样错误。 - Christoffer Hammarström
显示剩余5条评论

58

12
之后不要忘记关闭流! - WonderCsabo
如果您已经在使用Guava,那么这就是最好的答案,因为对我来说,Guava已经变得不可或缺了。 - Hong
1
@Hong 你应该尽可能使用Files.copy。仅在两个流都不是FileInputStream/FileOutputStream时,才使用ByteStreams.copy - ZhekaKozlov
@ZhekaKozlov 谢谢你的提示。在我的情况下,输入流来自于 Android 应用程序的资源(drawable)。 - Hong

31

简单功能

如果您只需要将InputStream写入File,那么您可以使用此简单函数:

private void copyInputStreamToFile( InputStream in, File file ) {
    try {
        OutputStream out = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int len;
        while((len=in.read(buf))>0){
            out.write(buf,0,len);
        }
        out.close();
        in.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

4
非常好的功能,谢谢。不过您是否需要将 close() 调用放在 finally 块中呢? - Joshua Pinter
@JoshPinter 这不会有什么伤害。 - Jordan LaPrise
3
在实际的实现中,你可能需要同时包含finally块和不吞咽异常。此外,关闭传递给方法的InputStream有时会让调用方法感到意外,因此应该考虑这是否是他们想要的行为。 - Cel Skeggs
2
为什么要捕获异常,当IOException就足够了呢? - Prabhakar
如果我们使用这段代码可能会导致漏洞问题。在此处找到了一种最佳实践[https://bytemeagain.wordpress.com/2013/09/03/sanitizing-a-byte-steam-in-java/],请相应地进行修改。 - 09Q71AO534

21

JDK 使用相同的代码,因此似乎没有“更容易”的方法,而不使用笨重的第三方库(这些库可能也没有任何不同)。以下内容直接从 java.nio.file.Files.java 中复制:

// buffer size used for reading and writing
private static final int BUFFER_SIZE = 8192;

/**
  * Reads all bytes from an input stream and writes them to an output stream.
  */
private static long copy(InputStream source, OutputStream sink) throws IOException {
    long nread = 0L;
    byte[] buf = new byte[BUFFER_SIZE];
    int n;
    while ((n = source.read(buf)) > 0) {
        sink.write(buf, 0, n);
        nread += n;
    }
    return nread;
}

2
唉。很遗憾这个特定的调用是私有的,没有其他选择,只能将其复制到您自己的实用程序类中,因为您可能不是在处理文件,而是同时处理两个套接字。 - Dragas

21

对于使用Spring框架的人来说,有一个有用的StreamUtils类:

StreamUtils.copy(in, out);

以上代码并未关闭流。如果您想在复制后关闭流,请改用FileCopyUtils类:

FileCopyUtils.copy(in, out);

18

PipedInputStreamPipedOutputStream 应该仅在存在多个线程的情况下使用,正如Javadoc中所指出的

此外,请注意,输入流和输出流不会使用 IOException包装任何线程中断...因此,您应该考虑将中断策略纳入您的代码:

byte[] buffer = new byte[1024];
int len = in.read(buffer);
while (len != -1) {
    out.write(buffer, 0, len);
    len = in.read(buffer);
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
}

如果您预计使用该API来复制大量数据或从长时间卡住的流中复制数据,则这将是一个有用的补充。


8

使用Java7和try-with-resources,可以得到一个简化且易读的版本。

try(InputStream inputStream = new FileInputStream("C:\\mov.mp4");
    OutputStream outputStream = new FileOutputStream("D:\\mov.mp4")) {

    byte[] buffer = new byte[10*1024];

    for (int length; (length = inputStream.read(buffer)) != -1; ) {
        outputStream.write(buffer, 0, length);
    }
} catch (FileNotFoundException exception) {
    exception.printStackTrace();
} catch (IOException ioException) {
    ioException.printStackTrace();
}

4
在循环内部刷新是非常低效的。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接