原创

InputStream.transferTo 复制结束以后,两端的流仍由调用方关闭

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。输入已经被读取过几个字节时,复制的就是剩余内容;前面为了猜格式而读取的字节,必须明确缓存、回放或重新打开。否则生成文件少一段头部,问题很可能在复制前就已经发生。

这个方法适合减少搬运循环里的低价值代码,但不该抹掉资源协议。接口应该让读者知道谁打开、谁关闭,允许多大内容,失败后哪些状态已经不可撤销,以及重试会从哪里开始。有了这些约定,一行复制才真的比一段循环更清楚。

正文到此结束
评论插件初始化中...
Loading...
本文目录