InputStream.transferTo 复制结束以后,两端的流仍由调用方关闭
- 发布时间:2026-10-23 21:34:39
- 本文热度:浏览 3 赞 0 评论 0
- 文章标签: Java InputStream 资源管理
- 全文共1字,阅读约需1分钟
input.transferTo(output) 把循环读写压成了一行,但这行代码没有顺便接管两条流。复制成功以后,输入和输出都不会因为调用结束而自动关闭;失败时也没有“双方回到原位”的事务保证。
这对库方法尤其重要。方法里得到的流可能来自调用方,调用方还要继续写入别的内容;擅自close会破坏组合。也可能是方法自己打开的文件句柄,忘记close则会保留资源。能否关闭,应由所有权决定,而不是由代码行数决定。
下面使用Java 17的内存流与可观察包装器,核对关闭动作和一次受控输出失败。保存为 TransferOwnership.java,执行 java TransferOwnership.java。
import java.io.*;
public class TransferOwnership {
static final class In extends FilterInputStream {
boolean closed;
In(byte[] bytes) { super(new ByteArrayInputStream(bytes)); }
public void close() throws IOException { closed = true; super.close(); }
}
static final class Out extends ByteArrayOutputStream {
boolean closed;
public void close() throws IOException { closed = true; super.close(); }
}
public static void main(String[] args) throws Exception {
In in = new In(new byte[]{1, 2, 3}); Out out = new Out();
try (in; out) {
System.out.println("copied=" + in.transferTo(out));
System.out.println("closed_inside=" + in.closed + "," + out.closed);
}
System.out.println("closed_after=" + in.closed + "," + out.closed);
In source = new In(new byte[]{1, 2, 3, 4});
ByteArrayOutputStream received = new ByteArrayOutputStream();
OutputStream failing = new OutputStream() {
public void write(int value) throws IOException {
if (received.size() == 2) throw new IOException("injected output failure");
received.write(value);
}
};
try (source; failing) {
try { source.transferTo(failing); throw new AssertionError(); }
catch (IOException expected) {
System.out.println("partial_output=" + received.size());
System.out.println("source_left=" + source.available());
}
}
}
}
输出:
copied=3
closed_inside=false,false
closed_after=true,true
partial_output=2
source_left=0
transferred 不等于资源已经释放
第一段成功复制三个字节,两个closed标记仍为false。退出外层try-with-resources以后,才会看到它们都为true。程序通过包装器记录close,而不是因为内存流还能读取就推断资源状态;内存流本身的close通常没有文件流那样的效果。
若复制方法只负责搬运调用方传入的流,就应该说明不关闭两端,调用者将它们放进自己的资源作用域。若方法根据路径自行打开两端,则可以让方法负责关闭。这两种接口都合理,最难维护的是一部分路径关闭、一部分路径留给外部,而且没有写明协议。
输出端还可能需要flush。例如缓冲输出流必须把缓冲内容交给下游;transferTo不负责替所有可能的包装器完成业务上的提交动作。文件写入完毕是否还需强制落盘,更是另一层持久性要求,不能从返回复制字节数直接推断。
失败后输入和输出可能已经前进
第二段故意让输出端在收到两个字节后失败。它没有回滚先前输出,输入也已经经过读取。样本说明“抛了IOException”不等于“什么都没发生”。具体留下多少字节、输入位置到了哪里,取决于流实现和失败时点,不能把本次数字当成所有transferTo实现的通用规则。
尤其不要在同一个输入流上直接再调用一次transferTo,希望从头重试。除非明确支持重新定位并且已经恢复状态,否则重试可能只传剩余部分。输出如果是网络、对象存储或已存在文件,还可能形成重复前缀或损坏内容。
可重试的文件复制通常会打开一份新的输入,写到新的临时目标,验证完整性后再发布;网络协议需要自己的请求幂等或分片确认。复制函数提供字节流能力,业务必须补上成功与部分成功的定义。
返回 long,不代表适合无上限复制
transferTo会读到EOF,因此不受控输入如果一直不结束,调用也可能持续等待并占用输出资源。需要限额时,应在读取和写入路径设置明确字节预算,超限时关闭或中止本次资源作用域;单看声明的Content-Length不足以替代真实读取计数。
超时同样要看具体输入类型。文件、套接字、HTTP响应流和自定义流,对于中断或并发关闭的支持并不相同。把这一行放进Future再等待一秒,只说明等待方能返回,不等于底层复制已经停止。需要可控的终止机制时,应沿所用库的契约验证。
另外,它从输入流当前状态开始,不会自动rewind。输入已经被读取过几个字节时,复制的就是剩余内容;前面为了猜格式而读取的字节,必须明确缓存、回放或重新打开。否则生成文件少一段头部,问题很可能在复制前就已经发生。
这个方法适合减少搬运循环里的低价值代码,但不该抹掉资源协议。接口应该让读者知道谁打开、谁关闭,允许多大内容,失败后哪些状态已经不可撤销,以及重试会从哪里开始。有了这些约定,一行复制才真的比一段循环更清楚。