We copy an InputStream to an OutputStream in Java with in.transferTo(out), which reads every byte from the input stream, writes it to the output stream and returns the number of bytes copied. The method exists since Java 9 and closes neither stream.
We copy an InputStream to an OutputStream when bytes must move from one place to another without being read as text, for example a file download sent to an HTTP response, an upload saved to disk, a database Blob written into a ZIP entry or a resource copied out of a JAR file.
The following example copies an image from one in-memory stream to another and counts the bytes.
InputStream in = new ByteArrayInputStream(new byte[] {(byte) 0x89, 'P', 'N', 'G'});
ByteArrayOutputStream out = new ByteArrayOutputStream();
long copied = in.transferTo(out); // 4
byte[] result = out.toByteArray();
int firstByte = result[0] & 0xFF; // 137
Notice that transferTo() returns a long, so it reports copies larger than 2 GB correctly. Next, we look at what the method does inside, write the same loop ourselves for progress and limits, copy to and from files with Files.copy(), and compare the Guava and Commons IO helpers.
1. Using InputStream.transferTo()
The method transferTo(OutputStream) reads a chunk of bytes into a buffer, writes the chunk to the output stream and repeats until read() returns -1. In JDK 25, the buffer is 16 KB. Some stream classes override the method with a faster path, for example a FileInputStream that copies to a FileOutputStream delegates the copy to the operating system through a FileChannel.

Say a Spring MVC controller lets customers download their invoices as PDF files. The controller gets an InputStream from the file storage and the OutputStream of the HTTP response, and copies one into the other. The following example does the same with two files, so it runs anywhere.
Path invoice = Files.write(Files.createTempFile("invoice", ".pdf"), new byte[20_000]);
Path download = Files.createTempFile("download", ".pdf");
long bytes;
try (InputStream in = Files.newInputStream(invoice);
OutputStream out = Files.newOutputStream(download)) {
bytes = in.transferTo(out);
}
long copiedBytes = bytes; // 20000
long targetSize = Files.size(download); // 20000
The method transferTo() does not close or flush either stream, so we open both in one try-with-resources statement. Closing an OutputStream flushes it first, so the last chunk reaches the file before the handle is released. The article on downloading files with Spring MVC shows the controller side.
If an I/O error happens halfway, transferTo() throws an IOException and both streams may hold partial data. In that case the target file contains only the first part of the source, so code that writes important files copies into a temporary file and moves it into place after a successful copy.
2. Writing the Copy Loop Ourselves
The loop behind transferTo() is short, and writing it ourselves still makes sense when we need to act on every chunk. Typical reasons are a progress bar for a large upload, a limit on the number of bytes and a checksum computed while copying.
The following example copies a stream and stops with an exception when it grows past a limit, as an upload endpoint that accepts files up to a fixed size would do.
static long copyWithLimit(InputStream in, OutputStream out, long maxBytes) throws IOException {
byte[] buffer = new byte[8192];
long total = 0;
int read;
while ((read = in.read(buffer)) != -1) {
total += read;
if (total > maxBytes) {
throw new IOException("Upload larger than " + maxBytes + " bytes");
}
out.write(buffer, 0, read);
}
return total;
}
InputStream small = new ByteArrayInputStream(new byte[3_000]);
long copied = copyWithLimit(small, OutputStream.nullOutputStream(), 5_000); // 3000
InputStream big = new ByteArrayInputStream(new byte[9_000]);
long rejected = copyWithLimit(big, OutputStream.nullOutputStream(), 5_000); // IOException: Upload larger than 5000 bytes
The loop checks for -1, the end-of-stream value, and writes only the read bytes of the buffer, because the last chunk rarely fills the whole array. The stream OutputStream.nullOutputStream() (Java 11) discards everything, which is useful for counting bytes or in tests.
3. Copying Between Streams and Files With Files.copy()
When one side of the copy is a file, the class Files has two methods that open and close the file for us. The method Files.copy(InputStream, Path, CopyOption…) writes a stream into a file, and Files.copy(Path, OutputStream) writes a file into a stream.
Path target = Files.createTempDirectory("uploads").resolve("photo.png");
InputStream upload = new ByteArrayInputStream(new byte[500]);
long saved = Files.copy(upload, target); // 500
InputStream again = new ByteArrayInputStream(new byte[700]);
long failed = Files.copy(again, target); // FileAlreadyExistsException
InputStream replacement = new ByteArrayInputStream(new byte[700]);
long replaced = Files.copy(replacement, target, StandardCopyOption.REPLACE_EXISTING); // 700
ByteArrayOutputStream response = new ByteArrayOutputStream();
long sent = Files.copy(target, response); // 700
By default, Files.copy() refuses to overwrite an existing file and throws a FileAlreadyExistsException, which protects us from replacing a file by mistake. With REPLACE_EXISTING, it replaces the existing file. For copies where both sides are files, Files.copy(Path, Path) is the better choice, as the article on copying files in Java explains.
4. Using Guava ByteStreams.copy()
Guava’s ByteStreams.copy(InputStream, OutputStream) copies all bytes and returns the count as a long. It behaves like transferTo() and does not close or flush the streams, so on Java 9 and later it adds nothing over the JDK method.
InputStream in = new ByteArrayInputStream(new byte[1_024]);
ByteArrayOutputStream out = new ByteArrayOutputStream();
long copied = ByteStreams.copy(in, out); // 1024
5. Using Commons IO IOUtils.copy()
Apache Commons IO’s IOUtils.copy(InputStream, OutputStream) also buffers internally, so we do not wrap the input in a BufferedInputStream. Its return type is int, and for copies larger than 2 GB it returns -1 even though the copy succeeds. The method IOUtils.copyLarge() returns a long, and an overload copies only a range of bytes.
InputStream in = new ByteArrayInputStream(new byte[2_048]);
ByteArrayOutputStream out = new ByteArrayOutputStream();
int copied = IOUtils.copy(in, out); // 2048
InputStream video = new ByteArrayInputStream(new byte[10_000]);
long firstPart = IOUtils.copyLarge(video, OutputStream.nullOutputStream(), 0, 4_000); // 4000
The range overload is useful for HTTP range requests, where a video player asks for a part of a file. The examples use commons-io 2.22.0.
6. Closing, Flushing and Buffering
Most bugs in stream copying come from data that stays in a buffer or from a stream that is closed too early. The rules differ slightly between the methods.
| Method | Closes streams | Flushes output | Return type |
|---|---|---|---|
| in.transferTo(out) | No | No | long |
| Files.copy(in, path) | Closes the file, not in | Yes, for the file | long |
| Files.copy(path, out) | Closes the file, not out | No | long |
| ByteStreams.copy(in, out) | No | No | long |
| IOUtils.copy(in, out) | No | No | int, -1 above 2 GB |
If the output is a BufferedOutputStream or a GZIPOutputStream that must stay open, we call flush() or finish() after the copy. If we own the output stream, closing it with try-with-resources covers both.
7. InputStream to OutputStream FAQs
Stream copies fail in small ways, such as a missing flush or a stream closed by the wrong owner, and these questions come from those failures.
7.1. Does transferTo() Close the Streams?
No. Both streams stay open after the copy, and the Javadoc recommends closing both promptly if an I/O error occurs. When a framework owns the output stream, such as the servlet container for an HTTP response, we close only the input stream.
7.2. Do I Need a BufferedInputStream With transferTo()?
No. The method reads into its own buffer in large chunks, so an extra BufferedInputStream adds a second copy of the data without any gain. Buffering helps only code that reads or writes one byte at a time.
7.3. How Do I Copy Only the First N Bytes of a Stream?
For small amounts, we call in.readNBytes(n) (Java 11) and write the array. For large amounts, we use the limit loop from section 2, or Commons IO’s copyLarge(in, out, 0, n).
InputStream in = new ByteArrayInputStream(new byte[] {1, 2, 3, 4, 5});
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(in.readNBytes(3));
int size = out.size(); // 3
7.4. Is transferTo() or Files.copy() Faster for Files?
For file-to-file copies, Files.copy(Path, Path) lets the operating system copy the data and also copies attributes on request, so it is the better choice. In JDK 25, FileInputStream.transferTo() to a FileOutputStream also uses a FileChannel, so the speed difference is small, but Files.copy() needs less code. For channels in general, the article on transferring data between channels covers transferTo() and transferFrom().
7.5. Why Is the Output File Empty or Shorter Than the Input?
The output stream was not flushed or closed before the file was read. A BufferedOutputStream keeps up to 8 KB in memory, so a small file can stay at 0 bytes until close() or flush() runs. Opening the output stream in try-with-resources fixes it.
8. Conclusion
Copying an InputStream to an OutputStream in Java takes one call to transferTo() on Java 9 and later. The method copies in chunks, returns the byte count as a long and leaves closing to us, so both streams belong in a try-with-resources statement.
A hand-written loop is useful for progress, limits or checksums, and Files.copy() is shorter when one side is a file. Guava’s ByteStreams.copy() and Commons IO’s IOUtils.copy() do the same job for projects that already use those libraries, with the 2 GB caveat for IOUtils.copy(). The Java IO tutorial groups the other stream conversions, such as OutputStream to InputStream.
9. References
Happy Learning !!