为什么PrintStream.close()最终会被调用两次?

7
有点出乎意料的是,以下代码会打印两次“Close”。通过调试器运行,似乎MyPrintStream.close() 调用了 super.close(),这最终导致再次调用了MyPrintStream.close()
    
import java.io.*;

public class PrintTest
{
    static class MyPrintStream extends PrintStream
    {
        MyPrintStream(OutputStream os)
        {
            super(os);
        }
@Override public void close() { System.out.println("Close"); super.close(); } }
public static void main(String[] args) throws IOException { PrintStream ps = new MyPrintStream(new FileOutputStream(File.createTempFile("temp", "file"))); ps.println("Hello"); ps.close(); } }
为什么会发生这种情况呢?我不应该扩展PrintStream吗?

这是调试器的一个好用之处。在close方法中设置一个断点,你就能够看到为什么它被调用了。 - Peter Lawrey
2个回答

11

如果你在调试器中查看代码,并在close()方法中设置断点,它将显示调用close()方法的函数的堆栈跟踪信息:

  1. 你的主方法
  2. sun.nio.cs.StreamEncoder$CharsetSE.implClose()第431行

后者的完整堆栈跟踪信息如下:

PrintTest$MyPrintStream.close() line: 20    
sun.nio.cs.StreamEncoder$CharsetSE.implClose() line: 431 [local variables unavailable]  
sun.nio.cs.StreamEncoder$CharsetSE(sun.nio.cs.StreamEncoder).close() line: 160 [local variables unavailable]    
java.io.OutputStreamWriter.close() line: 222 [local variables unavailable]  
java.io.BufferedWriter.close() line: 250 [local variables unavailable]  
PrintTest$MyPrintStream(java.io.PrintStream).close() line: 307  
PrintTest$MyPrintStream.close() line: 20    
PrintTest.main(java.lang.String[]) line: 27 

不过很遗憾,我不能解释为什么StreamEncoder会回调到你的PrintStream,因为我的IDE没有sun.nio.cs.StreamEncoder的源代码附件:( 这是JDK 6,如果有关系的话。

顺便说一下,如果你之所以问这个问题是因为注意到你的close()方法中的自定义代码运行了两次,你应该确实检查一下this.closingPrintStream.close()将其设置为true(并且该类的注释指出 /* To avoid recursive closing */ )。


1
+1 鼓励用户今后自己解决这个问题的方法 - Aaron Digulla
1
“closing”实例变量是PrintStream私有的,所以我无法检查它,但当然我可以使用自己的变量。 - Simon Nickerson
2
在jdk中有几个类做了类似的事情。我相信这是因为A和B类互相引用的情况存在,用户可能会引用A或B中的任何一个,并且关闭其中任何一个都应该关闭另一个。正如提到的那样,通常应该保护您的close方法免受多次调用(尽管递归调用是一种更隐蔽和不太预期的情况)。 - james
很好的答案。simonn - 我认为建议是如果需要,添加自己的关闭实例变量。 - Kevin Day

1

看一下PrintStream的源代码。

它有两个对基础Writer textOutcharOut的引用,一个是基于字符的,另一个是基于文本的(无论什么意思)。此外,它还继承了对基于字节的OutputStream的第三个引用,称为out

/**
 * Track both the text- and character-output streams, so that their buffers
 * can be flushed without flushing the entire stream.
 */
private BufferedWriter textOut;
private OutputStreamWriter charOut;

close()方法中,它会关闭所有的输出流(textOut基本上与charOut相同)。
 private boolean closing = false; /* To avoid recursive closing */

/**
 * Close the stream.  This is done by flushing the stream and then closing
 * the underlying output stream.
 *
 * @see        java.io.OutputStream#close()
 */
public void close() {
synchronized (this) {
    if (! closing) {
    closing = true;
    try {
        textOut.close();
        out.close();
    }
    catch (IOException x) {
        trouble = true;
    }
    textOut = null;
    charOut = null;
    out = null;
    }
}
}

现在,有趣的部分是charOut包含对PrintStream本身的引用(请注意构造函数中的 init(new OutputStreamWriter(this))
private void init(OutputStreamWriter osw) {
   this.charOut = osw;
   this.textOut = new BufferedWriter(osw);
}

/**
 * Create a new print stream.
 *
 * @param  out        The output stream to which values and objects will be
 *                    printed
 * @param  autoFlush  A boolean; if true, the output buffer will be flushed
 *                    whenever a byte array is written, one of the
 *                    <code>println</code> methods is invoked, or a newline
 *                    character or byte (<code>'\n'</code>) is written
 *
 * @see java.io.PrintWriter#PrintWriter(java.io.OutputStream, boolean)
 */
public PrintStream(OutputStream out, boolean autoFlush) {
this(autoFlush, out);
init(new OutputStreamWriter(this));
}

所以,对于 close() 的调用将会调用 charOut.close(),而后者又会再次调用原始的 close(),这就是为什么我们需要关闭标志来截断无限递归的原因。

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